@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
|
@@ -0,0 +1,3746 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseArtifactRef,
|
|
3
|
+
validateCompetitionJudgeConfig
|
|
4
|
+
} from "./chunk-4HN6DF7V.js";
|
|
5
|
+
import {
|
|
6
|
+
getWorktreeManager
|
|
7
|
+
} from "./chunk-4IYIZQWV.js";
|
|
8
|
+
import {
|
|
9
|
+
isValidCheckPath,
|
|
10
|
+
validateCompletionContracts
|
|
11
|
+
} from "./chunk-R35PEUKH.js";
|
|
12
|
+
import {
|
|
13
|
+
getBucketPathByKind
|
|
14
|
+
} from "./chunk-HOYBYAOA.js";
|
|
15
|
+
import {
|
|
16
|
+
TokenEstimator
|
|
17
|
+
} from "./chunk-ZSKKMTQJ.js";
|
|
18
|
+
import {
|
|
19
|
+
WORKTREE_RECORD_RECOVERY_REASON,
|
|
20
|
+
getProjectRoot,
|
|
21
|
+
getWorktreeRecordsByStatus,
|
|
22
|
+
listManifests,
|
|
23
|
+
reconcileAndSaveManifest,
|
|
24
|
+
updateWorktreeRecordStatus
|
|
25
|
+
} from "./chunk-CQM3A35X.js";
|
|
26
|
+
import {
|
|
27
|
+
applyFlywheelModelDefaults
|
|
28
|
+
} from "./chunk-FH7WOJAE.js";
|
|
29
|
+
import {
|
|
30
|
+
getProviderHealth
|
|
31
|
+
} from "./chunk-UHRBTZYY.js";
|
|
32
|
+
import {
|
|
33
|
+
summarizeRepoPaths
|
|
34
|
+
} from "./chunk-H2AYQIHW.js";
|
|
35
|
+
import {
|
|
36
|
+
resolveDefaultProvider
|
|
37
|
+
} from "./chunk-XPU2XADL.js";
|
|
38
|
+
import {
|
|
39
|
+
getProviderReadiness
|
|
40
|
+
} from "./chunk-AURFMVCD.js";
|
|
41
|
+
import {
|
|
42
|
+
signalCoordinator
|
|
43
|
+
} from "./chunk-72YTZMAM.js";
|
|
44
|
+
import {
|
|
45
|
+
assertSafePathComponent
|
|
46
|
+
} from "./chunk-S2VQCZO4.js";
|
|
47
|
+
import {
|
|
48
|
+
WorktreeCleanupManager
|
|
49
|
+
} from "./chunk-DF5SLDE4.js";
|
|
50
|
+
import {
|
|
51
|
+
filterWorktreeRunSentinelStatusOutput,
|
|
52
|
+
isActiveRun,
|
|
53
|
+
withCleanupLock
|
|
54
|
+
} from "./chunk-I5WRT7ZR.js";
|
|
55
|
+
import {
|
|
56
|
+
WORKTREE_HYGIENE_MAX_MANIFESTS_PER_PASS,
|
|
57
|
+
WORKTREE_HYGIENE_MAX_RECOVERY_RECORDS_PER_PASS,
|
|
58
|
+
WORKTREE_HYGIENE_MAX_RETAINED_TOTAL_BYTES,
|
|
59
|
+
WORKTREE_HYGIENE_MAX_RETENTION_COLLATION,
|
|
60
|
+
WORKTREE_HYGIENE_MAX_RETENTION_PRUNE_OPS,
|
|
61
|
+
WORKTREE_HYGIENE_MAX_STALE_DIR_ENUM,
|
|
62
|
+
getPoeticFlywheelManifestsRoot,
|
|
63
|
+
getPoeticRepoWorktreePoolRoot,
|
|
64
|
+
getWorktreeBaseDir,
|
|
65
|
+
pruneGitWorktreeEntries
|
|
66
|
+
} from "./chunk-5EERLVZB.js";
|
|
67
|
+
import {
|
|
68
|
+
classifyBranchRole,
|
|
69
|
+
isProtectedPipelineBranch
|
|
70
|
+
} from "./chunk-XQD2B6BB.js";
|
|
71
|
+
import {
|
|
72
|
+
isPoeticOwnedBranch,
|
|
73
|
+
poeticVariantBranchPrefix,
|
|
74
|
+
sanitizeRefPart
|
|
75
|
+
} from "./chunk-34CNP2HB.js";
|
|
76
|
+
import {
|
|
77
|
+
execGit
|
|
78
|
+
} from "./chunk-KNDYMVWN.js";
|
|
79
|
+
import {
|
|
80
|
+
checkVerifyCommandSafety
|
|
81
|
+
} from "./chunk-LO23NABI.js";
|
|
82
|
+
import {
|
|
83
|
+
GIT_COMMAND_TIMEOUT_MS
|
|
84
|
+
} from "./chunk-LAMKBTXX.js";
|
|
85
|
+
import {
|
|
86
|
+
isJsonOutputRequested,
|
|
87
|
+
isQuietOutputRequested
|
|
88
|
+
} from "./chunk-FKAUPHMR.js";
|
|
89
|
+
import {
|
|
90
|
+
ProviderRegistry
|
|
91
|
+
} from "./chunk-AT4TNPWW.js";
|
|
92
|
+
import {
|
|
93
|
+
safeRmDir
|
|
94
|
+
} from "./chunk-G4THLFV3.js";
|
|
95
|
+
|
|
96
|
+
// src/automation/flywheel-preflight.ts
|
|
97
|
+
import { execFileSync } from "child_process";
|
|
98
|
+
import {
|
|
99
|
+
accessSync,
|
|
100
|
+
existsSync as existsSync2,
|
|
101
|
+
constants as fsConstants,
|
|
102
|
+
fstatSync,
|
|
103
|
+
readFileSync,
|
|
104
|
+
statSync,
|
|
105
|
+
writeFileSync,
|
|
106
|
+
writeSync
|
|
107
|
+
} from "fs";
|
|
108
|
+
import * as fs2 from "fs/promises";
|
|
109
|
+
import os2 from "os";
|
|
110
|
+
import * as path3 from "path";
|
|
111
|
+
import { fileURLToPath } from "url";
|
|
112
|
+
import chalk from "chalk";
|
|
113
|
+
import { execa as execa2 } from "execa";
|
|
114
|
+
|
|
115
|
+
// src/automation/flywheel-run-cleanup.ts
|
|
116
|
+
import * as fs from "fs";
|
|
117
|
+
import { opendir, stat } from "fs/promises";
|
|
118
|
+
import * as path2 from "path";
|
|
119
|
+
import { execa } from "execa";
|
|
120
|
+
|
|
121
|
+
// src/core/extensions/paths.ts
|
|
122
|
+
import { mkdirSync } from "fs";
|
|
123
|
+
import os from "os";
|
|
124
|
+
import * as path from "path";
|
|
125
|
+
function getDefaultExtensionsRootDir() {
|
|
126
|
+
return path.join(os.homedir(), ".poetic", "extensions");
|
|
127
|
+
}
|
|
128
|
+
function getExtensionsRootDir() {
|
|
129
|
+
return process.env.POETIC_EXTENSIONS_DIR?.trim() || getDefaultExtensionsRootDir();
|
|
130
|
+
}
|
|
131
|
+
function getExtensionDirs(extensionId) {
|
|
132
|
+
const safeId = assertSafePathComponent(extensionId, "extensionId");
|
|
133
|
+
const rootDir = path.join(getExtensionsRootDir(), safeId);
|
|
134
|
+
return {
|
|
135
|
+
id: safeId,
|
|
136
|
+
rootDir,
|
|
137
|
+
packageDir: path.join(rootDir, "package"),
|
|
138
|
+
configDir: path.join(rootDir, "config"),
|
|
139
|
+
dataDir: path.join(rootDir, "data"),
|
|
140
|
+
cacheDir: path.join(rootDir, "cache"),
|
|
141
|
+
logsDir: path.join(rootDir, "logs"),
|
|
142
|
+
stateDir: path.join(rootDir, "state")
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function getPoeticRepoWorktreePoolRoot2() {
|
|
146
|
+
return getPoeticRepoWorktreePoolRoot();
|
|
147
|
+
}
|
|
148
|
+
function getPoeticFlywheelWorkflowsRoot(projectRoot) {
|
|
149
|
+
return getPoeticFlywheelManifestsRoot(projectRoot);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/automation/flywheel-run-cleanup.ts
|
|
153
|
+
async function runAsyncWithOptionalTimeout(fn, timeoutMs, label) {
|
|
154
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
155
|
+
return fn();
|
|
156
|
+
}
|
|
157
|
+
let timeoutId;
|
|
158
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
159
|
+
timeoutId = setTimeout(() => {
|
|
160
|
+
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
|
|
161
|
+
}, timeoutMs);
|
|
162
|
+
});
|
|
163
|
+
try {
|
|
164
|
+
return await Promise.race([fn(), timeoutPromise]);
|
|
165
|
+
} finally {
|
|
166
|
+
if (timeoutId !== void 0) {
|
|
167
|
+
clearTimeout(timeoutId);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function resolvePrimaryWorktreePoolRoot() {
|
|
172
|
+
return path2.resolve(getPoeticRepoWorktreePoolRoot2());
|
|
173
|
+
}
|
|
174
|
+
function directoryHasUserContent(dir) {
|
|
175
|
+
try {
|
|
176
|
+
const entries = fs.readdirSync(dir);
|
|
177
|
+
return {
|
|
178
|
+
hasContent: entries.some((entry) => entry !== ".git" && entry !== ".DS_Store")
|
|
179
|
+
};
|
|
180
|
+
} catch (error) {
|
|
181
|
+
return { hasContent: true, error: normalizeGitErrorText(error) };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function resolveWorktreePoolRootViaFlywheelLayout() {
|
|
185
|
+
return path2.resolve(path2.join(getPoeticFlywheelWorkflowsRoot(), "..", "worktrees"));
|
|
186
|
+
}
|
|
187
|
+
function getWorktreePoolPathRootsForHygiene() {
|
|
188
|
+
const primary = resolvePrimaryWorktreePoolRoot();
|
|
189
|
+
const viaFlywheel = resolveWorktreePoolRootViaFlywheelLayout();
|
|
190
|
+
return primary === viaFlywheel ? [primary] : [primary, viaFlywheel];
|
|
191
|
+
}
|
|
192
|
+
function isPathUnderWorktreePoolRoots(candidatePath, roots) {
|
|
193
|
+
const abs = path2.resolve(candidatePath.trim());
|
|
194
|
+
return roots.some((root) => abs === root || abs.startsWith(`${root}${path2.sep}`));
|
|
195
|
+
}
|
|
196
|
+
function getWorktreeCleanupProtectedPaths(extraPaths = []) {
|
|
197
|
+
return Array.from(
|
|
198
|
+
new Set(
|
|
199
|
+
[
|
|
200
|
+
getProjectRoot(),
|
|
201
|
+
getWorktreeBaseDir(),
|
|
202
|
+
...getWorktreePoolPathRootsForHygiene(),
|
|
203
|
+
...extraPaths
|
|
204
|
+
].map((protectedPath) => path2.resolve(protectedPath))
|
|
205
|
+
)
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
function getWorktreeCleanupManagedRoots(extraRoots = []) {
|
|
209
|
+
return Array.from(
|
|
210
|
+
new Set(
|
|
211
|
+
[getWorktreeBaseDir(), ...getWorktreePoolPathRootsForHygiene(), ...extraRoots].map(
|
|
212
|
+
(managedRoot) => path2.resolve(managedRoot)
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
async function removeWorktreeDirectoryWithGuard(worktreePath, protectedPaths, managedRoots) {
|
|
218
|
+
const result = await safeRmDir(worktreePath, {
|
|
219
|
+
protectedPaths: Array.from(protectedPaths),
|
|
220
|
+
managedRoots: Array.from(managedRoots)
|
|
221
|
+
});
|
|
222
|
+
if (!result.deleted) {
|
|
223
|
+
throw new Error(result.reason ?? `Refusing to delete unsafe worktree path ${worktreePath}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async function roughDirectoryFootprintBytes(worktreePath) {
|
|
227
|
+
try {
|
|
228
|
+
const st = await stat(worktreePath);
|
|
229
|
+
const blocks = typeof st.blocks === "number" && st.blocks > 0 ? st.blocks * 512 : 0;
|
|
230
|
+
return st.size + blocks;
|
|
231
|
+
} catch {
|
|
232
|
+
return 0;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function normalizeGitErrorText(error) {
|
|
236
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
237
|
+
const stderr = error?.stderr ?? "";
|
|
238
|
+
return `${message}
|
|
239
|
+
${stderr}`;
|
|
240
|
+
}
|
|
241
|
+
function isWorktreeMetadataGone(error, worktreePath) {
|
|
242
|
+
const text = normalizeGitErrorText(error);
|
|
243
|
+
return !fs.existsSync(worktreePath) || /cwd option is invalid/i.test(text) || /no such file or directory/i.test(text) || /not a git repository/i.test(text) || /this operation must be run in a work tree/i.test(text) || /not a working tree/i.test(text) || /bad default revision/i.test(text) || /unknown revision or path/i.test(text) || /ambiguous argument.*HEAD/i.test(text);
|
|
244
|
+
}
|
|
245
|
+
function isNotAGitRepositoryError(error) {
|
|
246
|
+
const text = normalizeGitErrorText(error);
|
|
247
|
+
return /not a git repository/i.test(text) || /not a git repo/i.test(text) || /this operation must be run in a work tree/i.test(text);
|
|
248
|
+
}
|
|
249
|
+
function isBenignGitBranchDeleteError(error) {
|
|
250
|
+
const text = normalizeGitErrorText(error);
|
|
251
|
+
return /error: branch ['"][^'"]+['"] not found/i.test(text) || /couldn't find local branch/i.test(text) || /has already been deleted/i.test(text);
|
|
252
|
+
}
|
|
253
|
+
function isBenignGitWorktreeRemoveError(error, worktreePath) {
|
|
254
|
+
const text = normalizeGitErrorText(error);
|
|
255
|
+
return isWorktreeMetadataGone(error, worktreePath) || /not a valid worktree/i.test(text) || /is not a working tree/i.test(text) || /no such worktree/i.test(text) || /validation failed.*worktree/i.test(text);
|
|
256
|
+
}
|
|
257
|
+
function getCleanupWorktreeManager(gitCwd) {
|
|
258
|
+
return getWorktreeManager({
|
|
259
|
+
baseDir: getWorktreeBaseDir(),
|
|
260
|
+
gitCwd,
|
|
261
|
+
enablePooling: false
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
function isManagedCleanupWorktreePath(worktreePath, gitCwd) {
|
|
265
|
+
let abs = path2.resolve(worktreePath.trim());
|
|
266
|
+
try {
|
|
267
|
+
abs = fs.realpathSync(abs);
|
|
268
|
+
} catch {
|
|
269
|
+
}
|
|
270
|
+
const repoPool = path2.resolve(gitCwd, ".poetic", "tmp", "worktrees");
|
|
271
|
+
return isPathUnderWorktreePoolRoots(abs, getWorktreeCleanupManagedRoots([repoPool]));
|
|
272
|
+
}
|
|
273
|
+
async function safeUpdateWorktreeRecordStatus(workflowId, worktreePath, status, retentionReason, errors) {
|
|
274
|
+
try {
|
|
275
|
+
await updateWorktreeRecordStatus(workflowId, worktreePath, status, retentionReason);
|
|
276
|
+
return true;
|
|
277
|
+
} catch (error) {
|
|
278
|
+
errors.push({
|
|
279
|
+
path: worktreePath,
|
|
280
|
+
error: error instanceof Error ? error.message : String(error)
|
|
281
|
+
});
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async function listWorktreeDirectories(baseDir, maxEntries) {
|
|
286
|
+
const dir = baseDir ?? getWorktreeBaseDir();
|
|
287
|
+
const cap = Math.max(1, maxEntries ?? WORKTREE_HYGIENE_MAX_STALE_DIR_ENUM);
|
|
288
|
+
if (!fs.existsSync(dir)) {
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
const directories = [];
|
|
292
|
+
const handle = await opendir(dir);
|
|
293
|
+
try {
|
|
294
|
+
for await (const entry of handle) {
|
|
295
|
+
if (!entry.isDirectory()) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (directories.length >= cap) {
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
const worktreePath = path2.join(dir, entry.name);
|
|
302
|
+
try {
|
|
303
|
+
const stats = await stat(worktreePath);
|
|
304
|
+
directories.push({
|
|
305
|
+
path: worktreePath,
|
|
306
|
+
name: entry.name,
|
|
307
|
+
mtimeMs: stats.mtimeMs
|
|
308
|
+
});
|
|
309
|
+
} catch {
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} finally {
|
|
313
|
+
await handle.close().catch(() => {
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
return directories;
|
|
317
|
+
}
|
|
318
|
+
async function listGitWorktrees(repoCwd = getProjectRoot()) {
|
|
319
|
+
let stdout;
|
|
320
|
+
try {
|
|
321
|
+
({ stdout } = await execa("git", ["worktree", "list", "--porcelain"], {
|
|
322
|
+
cwd: repoCwd,
|
|
323
|
+
stdio: "pipe",
|
|
324
|
+
timeout: 1e4
|
|
325
|
+
}));
|
|
326
|
+
} catch (error) {
|
|
327
|
+
if (isNotAGitRepositoryError(error)) {
|
|
328
|
+
return [];
|
|
329
|
+
}
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
const entries = [];
|
|
333
|
+
let current;
|
|
334
|
+
for (const rawLine of stdout.split("\n")) {
|
|
335
|
+
const line = rawLine.trimEnd();
|
|
336
|
+
if (line.startsWith("worktree ")) {
|
|
337
|
+
if (current) entries.push(current);
|
|
338
|
+
current = { path: line.slice("worktree ".length).trim() };
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (line.startsWith("branch ") && current) {
|
|
342
|
+
const ref = line.slice("branch ".length).trim();
|
|
343
|
+
const prefix = "refs/heads/";
|
|
344
|
+
if (ref.startsWith(prefix)) {
|
|
345
|
+
current.branch = ref.slice(prefix.length);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (current) entries.push(current);
|
|
350
|
+
return entries;
|
|
351
|
+
}
|
|
352
|
+
async function listLocalBranches(repoCwd = getProjectRoot()) {
|
|
353
|
+
let stdout;
|
|
354
|
+
try {
|
|
355
|
+
({ stdout } = await execa("git", ["branch", "--format=%(refname:short)"], {
|
|
356
|
+
cwd: repoCwd,
|
|
357
|
+
stdio: "pipe",
|
|
358
|
+
timeout: 1e4
|
|
359
|
+
}));
|
|
360
|
+
} catch (error) {
|
|
361
|
+
if (isNotAGitRepositoryError(error)) {
|
|
362
|
+
return [];
|
|
363
|
+
}
|
|
364
|
+
throw error;
|
|
365
|
+
}
|
|
366
|
+
return stdout.split("\n").map((b) => b.trim()).filter(Boolean);
|
|
367
|
+
}
|
|
368
|
+
function buildRelatedBranchPrefixes(taskId) {
|
|
369
|
+
const safeTaskId = sanitizeRefPart(taskId, { fallback: "task", maxLen: 80 });
|
|
370
|
+
return [
|
|
371
|
+
poeticVariantBranchPrefix(taskId),
|
|
372
|
+
`poetic/substage/${safeTaskId}-`,
|
|
373
|
+
`poetic/st-${safeTaskId}-`,
|
|
374
|
+
`poetic/wt-${safeTaskId}-`,
|
|
375
|
+
`poetic/synth-${safeTaskId}-`,
|
|
376
|
+
`poetic/bg-${safeTaskId}-`
|
|
377
|
+
];
|
|
378
|
+
}
|
|
379
|
+
function matchesCleanupBranchPrefix(branch, prefix) {
|
|
380
|
+
if (!branch.startsWith(prefix)) {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
if (prefix.endsWith("/") || prefix.endsWith("-")) {
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
const next = branch.charAt(prefix.length);
|
|
387
|
+
return next === "/" || next === "-";
|
|
388
|
+
}
|
|
389
|
+
async function cleanupWorkflowRelatedBranchesAndWorktrees(options) {
|
|
390
|
+
const relatedTaskIds = (options.relatedTaskIds ?? []).map((t) => t.trim()).filter(Boolean);
|
|
391
|
+
const relatedSafeTaskIds = relatedTaskIds.map(
|
|
392
|
+
(taskId) => sanitizeRefPart(taskId, { fallback: "task", maxLen: 80 })
|
|
393
|
+
);
|
|
394
|
+
const trackedBranches = (options.trackedBranches ?? []).map((b) => b.trim()).filter(Boolean);
|
|
395
|
+
const trackedWorktrees = (options.trackedWorktrees ?? []).map((w) => w.trim()).filter(Boolean);
|
|
396
|
+
const enablePooledWorktreePathRecovery = options.enablePooledWorktreePathRecovery ?? true;
|
|
397
|
+
const childBranchPrefix = `${options.integrationBranch}-`;
|
|
398
|
+
const repoCwd = getProjectRoot();
|
|
399
|
+
const [branches, worktrees] = await Promise.all([
|
|
400
|
+
listLocalBranches(repoCwd),
|
|
401
|
+
listGitWorktrees(repoCwd)
|
|
402
|
+
]);
|
|
403
|
+
const prefixes = [
|
|
404
|
+
childBranchPrefix,
|
|
405
|
+
...relatedTaskIds.flatMap((taskId) => buildRelatedBranchPrefixes(taskId))
|
|
406
|
+
];
|
|
407
|
+
const branchesToDelete = Array.from(
|
|
408
|
+
new Set(
|
|
409
|
+
branches.filter(
|
|
410
|
+
(branch) => trackedBranches.includes(branch) || prefixes.some((prefix) => matchesCleanupBranchPrefix(branch, prefix))
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
);
|
|
414
|
+
const worktreesToRemove = Array.from(
|
|
415
|
+
new Set(
|
|
416
|
+
[
|
|
417
|
+
...trackedWorktrees,
|
|
418
|
+
...worktrees.filter((w) => {
|
|
419
|
+
if (trackedWorktrees.includes(w.path)) return true;
|
|
420
|
+
if (w.branch && branchesToDelete.includes(w.branch)) return true;
|
|
421
|
+
if (!enablePooledWorktreePathRecovery) return false;
|
|
422
|
+
const base = path2.basename(w.path);
|
|
423
|
+
return relatedSafeTaskIds.some((taskId) => base.startsWith(`${taskId}-`));
|
|
424
|
+
}).map((w) => w.path)
|
|
425
|
+
].filter(Boolean)
|
|
426
|
+
)
|
|
427
|
+
);
|
|
428
|
+
if (options.dryRun) {
|
|
429
|
+
return {
|
|
430
|
+
branchesToDelete,
|
|
431
|
+
worktreesToRemove,
|
|
432
|
+
skippedWorktreesMissingOnDisk: [],
|
|
433
|
+
skippedBranchesAlreadyAbsent: [],
|
|
434
|
+
cleanupWarnings: [],
|
|
435
|
+
cleanupErrors: []
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
const skippedWorktreesMissingOnDisk = worktreesToRemove.filter((p) => !fs.existsSync(p));
|
|
439
|
+
const skippedWorktreesMissing = new Set(skippedWorktreesMissingOnDisk);
|
|
440
|
+
const skippedBranchesAlreadyAbsent = [];
|
|
441
|
+
const cleanupWarnings = [];
|
|
442
|
+
const cleanupErrors = [];
|
|
443
|
+
const activeWorktreeBranches = /* @__PURE__ */ new Set();
|
|
444
|
+
const branchByWorktreePath = new Map(
|
|
445
|
+
worktrees.filter(
|
|
446
|
+
(worktree) => Boolean(worktree.branch)
|
|
447
|
+
).map((worktree) => [path2.resolve(worktree.path), worktree.branch])
|
|
448
|
+
);
|
|
449
|
+
for (const worktreePath of worktreesToRemove) {
|
|
450
|
+
if (!fs.existsSync(worktreePath)) {
|
|
451
|
+
skippedWorktreesMissing.add(worktreePath);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (await isActiveRun(worktreePath)) {
|
|
455
|
+
const resolvedWorktreePath = path2.resolve(worktreePath);
|
|
456
|
+
const attachedBranch = branchByWorktreePath.get(resolvedWorktreePath);
|
|
457
|
+
if (attachedBranch) {
|
|
458
|
+
activeWorktreeBranches.add(attachedBranch);
|
|
459
|
+
}
|
|
460
|
+
cleanupWarnings.push({
|
|
461
|
+
path: worktreePath,
|
|
462
|
+
message: "Preserved active worktree; its owning run is still active"
|
|
463
|
+
});
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (!isManagedCleanupWorktreePath(worktreePath, repoCwd)) {
|
|
467
|
+
cleanupWarnings.push({
|
|
468
|
+
path: worktreePath,
|
|
469
|
+
message: "Refusing to clean worktree path outside canonical Poetic worktree pools"
|
|
470
|
+
});
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
try {
|
|
474
|
+
const worktreeManager = getCleanupWorktreeManager(repoCwd);
|
|
475
|
+
await worktreeManager.cleanup(worktreePath, { preserve: false, gitCwd: repoCwd });
|
|
476
|
+
} catch (error) {
|
|
477
|
+
if (!fs.existsSync(worktreePath)) {
|
|
478
|
+
skippedWorktreesMissing.add(worktreePath);
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
cleanupWarnings.push({
|
|
482
|
+
path: worktreePath,
|
|
483
|
+
message: normalizeGitErrorText(error)
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
const branchesLive = await listLocalBranches(repoCwd);
|
|
488
|
+
const liveBranchSet = new Set(branchesLive);
|
|
489
|
+
for (const branch of branchesToDelete) {
|
|
490
|
+
if (activeWorktreeBranches.has(branch)) {
|
|
491
|
+
cleanupWarnings.push({
|
|
492
|
+
path: branch,
|
|
493
|
+
message: "Preserved branch attached to an active worktree"
|
|
494
|
+
});
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
if (!liveBranchSet.has(branch)) {
|
|
498
|
+
skippedBranchesAlreadyAbsent.push(branch);
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
const branchRole = classifyBranchRole(branch);
|
|
502
|
+
if (!isPoeticOwnedBranch(branch) || isProtectedPipelineBranch(branch) || branchRole !== "generated-artifact") {
|
|
503
|
+
cleanupWarnings.push({
|
|
504
|
+
path: branch,
|
|
505
|
+
message: `Refusing to delete non-generated or protected branch ${branch}`
|
|
506
|
+
});
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
await execa("git", ["branch", "-D", branch], {
|
|
511
|
+
cwd: repoCwd,
|
|
512
|
+
stdio: "pipe",
|
|
513
|
+
timeout: 1e4
|
|
514
|
+
});
|
|
515
|
+
} catch (error) {
|
|
516
|
+
if (isBenignGitBranchDeleteError(error)) {
|
|
517
|
+
skippedBranchesAlreadyAbsent.push(branch);
|
|
518
|
+
} else {
|
|
519
|
+
cleanupErrors.push({
|
|
520
|
+
path: branch,
|
|
521
|
+
error: normalizeGitErrorText(error)
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (worktreesToRemove.length > 0) {
|
|
527
|
+
await pruneGitWorktreeEntries({ cwd: repoCwd, timeoutMs: 1e4 });
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
branchesToDelete,
|
|
531
|
+
worktreesToRemove,
|
|
532
|
+
skippedWorktreesMissingOnDisk: Array.from(skippedWorktreesMissing),
|
|
533
|
+
skippedBranchesAlreadyAbsent,
|
|
534
|
+
cleanupWarnings,
|
|
535
|
+
cleanupErrors
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
539
|
+
async function cleanupStaleWorktrees(options = {}) {
|
|
540
|
+
const baseDir = options.baseDir ?? getWorktreeBaseDir();
|
|
541
|
+
const repoCwd = options.repoCwd ?? getProjectRoot();
|
|
542
|
+
const maxAgeMs = options.maxAgeMs ?? 7 * MS_PER_DAY;
|
|
543
|
+
const maxDirectoryEnum = Math.max(
|
|
544
|
+
1,
|
|
545
|
+
options.maxDirectoryEnum ?? WORKTREE_HYGIENE_MAX_STALE_DIR_ENUM
|
|
546
|
+
);
|
|
547
|
+
const dryRun = options.dryRun ?? false;
|
|
548
|
+
const protectedPaths = getWorktreeCleanupProtectedPaths([baseDir, repoCwd]);
|
|
549
|
+
const managedRoots = getWorktreeCleanupManagedRoots([baseDir]);
|
|
550
|
+
const report = {
|
|
551
|
+
cleaned: [],
|
|
552
|
+
skippedDirty: [],
|
|
553
|
+
skippedActive: [],
|
|
554
|
+
skippedAlreadyClean: [],
|
|
555
|
+
errors: [],
|
|
556
|
+
dryRun
|
|
557
|
+
};
|
|
558
|
+
if (!fs.existsSync(baseDir)) {
|
|
559
|
+
return report;
|
|
560
|
+
}
|
|
561
|
+
const runCleanup = async () => {
|
|
562
|
+
let directories;
|
|
563
|
+
try {
|
|
564
|
+
directories = await listWorktreeDirectories(baseDir, maxDirectoryEnum);
|
|
565
|
+
if (directories.length >= maxDirectoryEnum) {
|
|
566
|
+
report.directoryEnumCapHit = true;
|
|
567
|
+
}
|
|
568
|
+
} catch (error) {
|
|
569
|
+
report.errors.push({
|
|
570
|
+
path: baseDir,
|
|
571
|
+
error: error instanceof Error ? error.message : String(error)
|
|
572
|
+
});
|
|
573
|
+
return report;
|
|
574
|
+
}
|
|
575
|
+
const now = Date.now();
|
|
576
|
+
for (const entry of directories) {
|
|
577
|
+
const worktreePath = entry.path;
|
|
578
|
+
const ageMs = now - entry.mtimeMs;
|
|
579
|
+
if (ageMs <= maxAgeMs) {
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
try {
|
|
583
|
+
const gitMeta = path2.join(worktreePath, ".git");
|
|
584
|
+
if (await isActiveRun(worktreePath)) {
|
|
585
|
+
report.skippedActive.push(worktreePath);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
if (!fs.existsSync(gitMeta)) {
|
|
589
|
+
const contentProbe = directoryHasUserContent(worktreePath);
|
|
590
|
+
if (contentProbe.error) {
|
|
591
|
+
report.errors.push({
|
|
592
|
+
path: worktreePath,
|
|
593
|
+
error: contentProbe.error
|
|
594
|
+
});
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (contentProbe.hasContent) {
|
|
598
|
+
report.skippedDirty.push(worktreePath);
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
if (!dryRun) {
|
|
602
|
+
await removeWorktreeDirectoryWithGuard(worktreePath, protectedPaths, managedRoots);
|
|
603
|
+
}
|
|
604
|
+
report.cleaned.push(worktreePath);
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
let clean = false;
|
|
608
|
+
try {
|
|
609
|
+
const { stdout } = await execa("git", ["status", "--porcelain"], {
|
|
610
|
+
cwd: worktreePath,
|
|
611
|
+
stdio: "pipe",
|
|
612
|
+
timeout: GIT_COMMAND_TIMEOUT_MS
|
|
613
|
+
});
|
|
614
|
+
clean = filterWorktreeRunSentinelStatusOutput(stdout).trim().length === 0;
|
|
615
|
+
} catch (error) {
|
|
616
|
+
const text = normalizeGitErrorText(error);
|
|
617
|
+
const disappearedDuringStatus = isWorktreeMetadataGone(error, worktreePath);
|
|
618
|
+
if (disappearedDuringStatus) {
|
|
619
|
+
if (!fs.existsSync(worktreePath)) {
|
|
620
|
+
report.skippedAlreadyClean.push(worktreePath);
|
|
621
|
+
} else if (!dryRun) {
|
|
622
|
+
try {
|
|
623
|
+
await removeWorktreeDirectoryWithGuard(worktreePath, protectedPaths, managedRoots);
|
|
624
|
+
report.cleaned.push(worktreePath);
|
|
625
|
+
} catch (rmErr) {
|
|
626
|
+
report.errors.push({
|
|
627
|
+
path: worktreePath,
|
|
628
|
+
error: normalizeGitErrorText(rmErr)
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
report.skippedAlreadyClean.push(worktreePath);
|
|
633
|
+
}
|
|
634
|
+
} else {
|
|
635
|
+
report.errors.push({
|
|
636
|
+
path: worktreePath,
|
|
637
|
+
error: text
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
if (!clean) {
|
|
643
|
+
report.skippedDirty.push(worktreePath);
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
let branchName = null;
|
|
647
|
+
try {
|
|
648
|
+
const { stdout } = await execa("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
649
|
+
cwd: worktreePath,
|
|
650
|
+
stdio: "pipe",
|
|
651
|
+
timeout: GIT_COMMAND_TIMEOUT_MS
|
|
652
|
+
});
|
|
653
|
+
branchName = stdout.trim();
|
|
654
|
+
} catch (error) {
|
|
655
|
+
if (isWorktreeMetadataGone(error, worktreePath)) {
|
|
656
|
+
if (!fs.existsSync(worktreePath)) {
|
|
657
|
+
report.skippedAlreadyClean.push(worktreePath);
|
|
658
|
+
} else if (!dryRun) {
|
|
659
|
+
try {
|
|
660
|
+
await removeWorktreeDirectoryWithGuard(worktreePath, protectedPaths, managedRoots);
|
|
661
|
+
report.cleaned.push(worktreePath);
|
|
662
|
+
} catch (rmErr) {
|
|
663
|
+
report.errors.push({
|
|
664
|
+
path: worktreePath,
|
|
665
|
+
error: normalizeGitErrorText(rmErr)
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
} else {
|
|
669
|
+
report.skippedAlreadyClean.push(worktreePath);
|
|
670
|
+
}
|
|
671
|
+
} else {
|
|
672
|
+
report.errors.push({
|
|
673
|
+
path: worktreePath,
|
|
674
|
+
error: normalizeGitErrorText(error)
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
if (!branchName || !isPoeticOwnedBranch(branchName)) {
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
if (dryRun) {
|
|
683
|
+
report.cleaned.push(worktreePath);
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
let removedViaGit = false;
|
|
687
|
+
try {
|
|
688
|
+
await execa("git", ["worktree", "remove", "--force", worktreePath], {
|
|
689
|
+
cwd: repoCwd,
|
|
690
|
+
stdio: "pipe",
|
|
691
|
+
timeout: GIT_COMMAND_TIMEOUT_MS
|
|
692
|
+
});
|
|
693
|
+
removedViaGit = true;
|
|
694
|
+
} catch (error) {
|
|
695
|
+
if (isBenignGitWorktreeRemoveError(error, worktreePath) && !fs.existsSync(worktreePath)) {
|
|
696
|
+
report.skippedAlreadyClean.push(worktreePath);
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (!removedViaGit) {
|
|
701
|
+
if (!fs.existsSync(worktreePath)) {
|
|
702
|
+
report.skippedAlreadyClean.push(worktreePath);
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
try {
|
|
706
|
+
await removeWorktreeDirectoryWithGuard(worktreePath, protectedPaths, managedRoots);
|
|
707
|
+
} catch (rmErr) {
|
|
708
|
+
report.errors.push({
|
|
709
|
+
path: worktreePath,
|
|
710
|
+
error: normalizeGitErrorText(rmErr)
|
|
711
|
+
});
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
report.cleaned.push(worktreePath);
|
|
716
|
+
} catch (error) {
|
|
717
|
+
report.errors.push({
|
|
718
|
+
path: worktreePath,
|
|
719
|
+
error: error instanceof Error ? error.message : String(error)
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (!dryRun && report.cleaned.length > 0) {
|
|
724
|
+
try {
|
|
725
|
+
await pruneGitWorktreeEntries({ cwd: repoCwd });
|
|
726
|
+
} catch (error) {
|
|
727
|
+
report.errors.push({
|
|
728
|
+
path: repoCwd,
|
|
729
|
+
error: error instanceof Error ? `Failed to prune git worktree entries: ${error.message}` : `Failed to prune git worktree entries: ${String(error)}`
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return report;
|
|
734
|
+
};
|
|
735
|
+
if (dryRun) {
|
|
736
|
+
return runCleanup();
|
|
737
|
+
}
|
|
738
|
+
return withCleanupLock(baseDir, runCleanup);
|
|
739
|
+
}
|
|
740
|
+
async function recoverPendingWorktrees(options = {}) {
|
|
741
|
+
const {
|
|
742
|
+
dryRun = false,
|
|
743
|
+
verbose = false,
|
|
744
|
+
includeCleanupFailed = true,
|
|
745
|
+
maxManifests = WORKTREE_HYGIENE_MAX_MANIFESTS_PER_PASS,
|
|
746
|
+
maxRecords = WORKTREE_HYGIENE_MAX_RECOVERY_RECORDS_PER_PASS,
|
|
747
|
+
restrictToPoeticWorktreePool = false
|
|
748
|
+
} = options;
|
|
749
|
+
const manifestCap = Math.max(1, maxManifests);
|
|
750
|
+
const recordCap = Math.max(1, maxRecords);
|
|
751
|
+
const result = {
|
|
752
|
+
recovered: [],
|
|
753
|
+
skippedActive: [],
|
|
754
|
+
skippedDirty: [],
|
|
755
|
+
alreadyGone: [],
|
|
756
|
+
errors: []
|
|
757
|
+
};
|
|
758
|
+
const protectedPaths = getWorktreeCleanupProtectedPaths();
|
|
759
|
+
const managedRoots = getWorktreeCleanupManagedRoots();
|
|
760
|
+
const runRecovery = async () => {
|
|
761
|
+
let manifests;
|
|
762
|
+
try {
|
|
763
|
+
manifests = await listManifests();
|
|
764
|
+
} catch (error) {
|
|
765
|
+
result.errors.push({
|
|
766
|
+
path: "<manifests>",
|
|
767
|
+
error: error instanceof Error ? error.message : String(error)
|
|
768
|
+
});
|
|
769
|
+
return result;
|
|
770
|
+
}
|
|
771
|
+
const manifestsSlice = manifests.slice(0, manifestCap);
|
|
772
|
+
if (manifestsSlice.length < manifests.length) {
|
|
773
|
+
result.hygieneCapReached = true;
|
|
774
|
+
}
|
|
775
|
+
const poolRoots = getWorktreePoolPathRootsForHygiene();
|
|
776
|
+
let recordsExamined = 0;
|
|
777
|
+
const seenRecoveryKeys = /* @__PURE__ */ new Set();
|
|
778
|
+
for (let mi = 0; mi < manifestsSlice.length; mi++) {
|
|
779
|
+
const manifest = manifestsSlice[mi];
|
|
780
|
+
const reconciledManifest = dryRun ? manifest : await reconcileAndSaveManifest(manifest.workflowId).catch(() => manifest);
|
|
781
|
+
const statuses = ["pending"];
|
|
782
|
+
if (includeCleanupFailed) {
|
|
783
|
+
statuses.push("cleanup_failed");
|
|
784
|
+
}
|
|
785
|
+
const pending = statuses.flatMap(
|
|
786
|
+
(status) => getWorktreeRecordsByStatus(reconciledManifest, status)
|
|
787
|
+
);
|
|
788
|
+
for (const record of pending) {
|
|
789
|
+
const recoveryKey = `${reconciledManifest.workflowId}::${record.worktreePath}`;
|
|
790
|
+
if (seenRecoveryKeys.has(recoveryKey)) {
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
seenRecoveryKeys.add(recoveryKey);
|
|
794
|
+
if (recordsExamined >= recordCap) {
|
|
795
|
+
result.hygieneCapReached = true;
|
|
796
|
+
break;
|
|
797
|
+
}
|
|
798
|
+
recordsExamined += 1;
|
|
799
|
+
const worktreePath = record.worktreePath;
|
|
800
|
+
try {
|
|
801
|
+
if (restrictToPoeticWorktreePool && !isPathUnderWorktreePoolRoots(worktreePath, poolRoots)) {
|
|
802
|
+
if (verbose) {
|
|
803
|
+
console.log(`[recover] Skipping worktree outside pool roots: ${worktreePath}`);
|
|
804
|
+
}
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
const exists = fs.existsSync(worktreePath);
|
|
808
|
+
if (!exists) {
|
|
809
|
+
if (verbose) {
|
|
810
|
+
console.log(`[recover] Worktree already gone: ${worktreePath}`);
|
|
811
|
+
}
|
|
812
|
+
if (!dryRun) {
|
|
813
|
+
const ok = await safeUpdateWorktreeRecordStatus(
|
|
814
|
+
reconciledManifest.workflowId,
|
|
815
|
+
worktreePath,
|
|
816
|
+
"cleaned",
|
|
817
|
+
WORKTREE_RECORD_RECOVERY_REASON.PATH_ALREADY_ABSENT,
|
|
818
|
+
result.errors
|
|
819
|
+
);
|
|
820
|
+
if (!ok) {
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
result.alreadyGone.push(worktreePath);
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
if (await isActiveRun(worktreePath)) {
|
|
828
|
+
if (verbose) {
|
|
829
|
+
console.log(`[recover] Preserving active worktree: ${worktreePath}`);
|
|
830
|
+
}
|
|
831
|
+
result.skippedActive.push(worktreePath);
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
let dirty = false;
|
|
835
|
+
try {
|
|
836
|
+
const { stdout } = await execa("git", ["status", "--porcelain"], {
|
|
837
|
+
cwd: worktreePath,
|
|
838
|
+
stdio: "pipe",
|
|
839
|
+
timeout: GIT_COMMAND_TIMEOUT_MS
|
|
840
|
+
});
|
|
841
|
+
dirty = filterWorktreeRunSentinelStatusOutput(stdout).trim().length > 0;
|
|
842
|
+
} catch (error) {
|
|
843
|
+
const text = normalizeGitErrorText(error);
|
|
844
|
+
const disappearedDuringStatus = isWorktreeMetadataGone(error, worktreePath);
|
|
845
|
+
if (disappearedDuringStatus) {
|
|
846
|
+
if (verbose) {
|
|
847
|
+
console.log(
|
|
848
|
+
`[recover] Worktree path missing or not a git repository, marking cleaned: ${worktreePath}`
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
if (!dryRun) {
|
|
852
|
+
const ok = await safeUpdateWorktreeRecordStatus(
|
|
853
|
+
reconciledManifest.workflowId,
|
|
854
|
+
worktreePath,
|
|
855
|
+
"cleaned",
|
|
856
|
+
WORKTREE_RECORD_RECOVERY_REASON.PATH_INVALID_DURING_STATUS_PROBE,
|
|
857
|
+
result.errors
|
|
858
|
+
);
|
|
859
|
+
if (!ok) {
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
result.alreadyGone.push(worktreePath);
|
|
864
|
+
} else {
|
|
865
|
+
result.errors.push({
|
|
866
|
+
path: worktreePath,
|
|
867
|
+
error: text
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
if (dirty) {
|
|
873
|
+
if (verbose) {
|
|
874
|
+
console.log(`[recover] Skipping dirty worktree: ${worktreePath}`);
|
|
875
|
+
}
|
|
876
|
+
result.skippedDirty.push(worktreePath);
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
if (verbose) {
|
|
880
|
+
console.log(`[recover] Recovering clean worktree: ${worktreePath}`);
|
|
881
|
+
}
|
|
882
|
+
if (!dryRun) {
|
|
883
|
+
try {
|
|
884
|
+
await execa("git", ["worktree", "remove", "--force", worktreePath], {
|
|
885
|
+
cwd: getProjectRoot(),
|
|
886
|
+
stdio: "pipe",
|
|
887
|
+
timeout: GIT_COMMAND_TIMEOUT_MS
|
|
888
|
+
});
|
|
889
|
+
} catch {
|
|
890
|
+
}
|
|
891
|
+
if (fs.existsSync(worktreePath)) {
|
|
892
|
+
try {
|
|
893
|
+
await removeWorktreeDirectoryWithGuard(worktreePath, protectedPaths, managedRoots);
|
|
894
|
+
} catch (rmErr) {
|
|
895
|
+
result.errors.push({
|
|
896
|
+
path: worktreePath,
|
|
897
|
+
error: normalizeGitErrorText(rmErr)
|
|
898
|
+
});
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
if (fs.existsSync(worktreePath)) {
|
|
903
|
+
result.errors.push({
|
|
904
|
+
path: worktreePath,
|
|
905
|
+
error: "Recovery could not remove worktree directory; leaving manifest unchanged"
|
|
906
|
+
});
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
const updated = await safeUpdateWorktreeRecordStatus(
|
|
910
|
+
reconciledManifest.workflowId,
|
|
911
|
+
worktreePath,
|
|
912
|
+
"cleaned",
|
|
913
|
+
null,
|
|
914
|
+
result.errors
|
|
915
|
+
);
|
|
916
|
+
if (!updated) {
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
result.recovered.push(worktreePath);
|
|
921
|
+
} catch (error) {
|
|
922
|
+
result.errors.push({
|
|
923
|
+
path: worktreePath,
|
|
924
|
+
error: error instanceof Error ? error.message : String(error)
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
if (recordsExamined >= recordCap) {
|
|
929
|
+
if (mi < manifestsSlice.length - 1) {
|
|
930
|
+
result.hygieneCapReached = true;
|
|
931
|
+
}
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
if (!dryRun && (result.recovered.length > 0 || result.alreadyGone.length > 0)) {
|
|
936
|
+
try {
|
|
937
|
+
await pruneGitWorktreeEntries({ cwd: getProjectRoot() });
|
|
938
|
+
} catch {
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return result;
|
|
942
|
+
};
|
|
943
|
+
if (dryRun) {
|
|
944
|
+
return runRecovery();
|
|
945
|
+
}
|
|
946
|
+
return withCleanupLock(getWorktreeBaseDir(), runRecovery);
|
|
947
|
+
}
|
|
948
|
+
async function enforceRetentionBounds(options = {}) {
|
|
949
|
+
const {
|
|
950
|
+
maxAgeMs = 7 * 24 * 60 * 60 * 1e3,
|
|
951
|
+
maxCount = 20,
|
|
952
|
+
maxManifests = WORKTREE_HYGIENE_MAX_MANIFESTS_PER_PASS,
|
|
953
|
+
maxGitStatusProbes = WORKTREE_HYGIENE_MAX_RETENTION_PRUNE_OPS,
|
|
954
|
+
dryRun = false,
|
|
955
|
+
verbose = false
|
|
956
|
+
} = options;
|
|
957
|
+
const diskCapResolved = options.maxRetainedTotalDiskBytes !== void 0 ? options.maxRetainedTotalDiskBytes : WORKTREE_HYGIENE_MAX_RETAINED_TOTAL_BYTES;
|
|
958
|
+
const manifestCap = Math.max(1, maxManifests);
|
|
959
|
+
const probeCap = Math.max(1, maxGitStatusProbes);
|
|
960
|
+
const result = {
|
|
961
|
+
pruned: [],
|
|
962
|
+
preserved: [],
|
|
963
|
+
errors: []
|
|
964
|
+
};
|
|
965
|
+
const protectedPaths = getWorktreeCleanupProtectedPaths();
|
|
966
|
+
const managedRoots = getWorktreeCleanupManagedRoots();
|
|
967
|
+
const runRetention = async () => {
|
|
968
|
+
let manifests;
|
|
969
|
+
try {
|
|
970
|
+
manifests = await listManifests();
|
|
971
|
+
} catch (error) {
|
|
972
|
+
result.errors.push({
|
|
973
|
+
path: "<manifests>",
|
|
974
|
+
error: error instanceof Error ? error.message : String(error)
|
|
975
|
+
});
|
|
976
|
+
return result;
|
|
977
|
+
}
|
|
978
|
+
const manifestsSlice = manifests.slice(0, manifestCap);
|
|
979
|
+
if (manifestsSlice.length < manifests.length) {
|
|
980
|
+
result.hygieneCapReached = true;
|
|
981
|
+
}
|
|
982
|
+
const retainedEntries = [];
|
|
983
|
+
const retainedByWorkflowPath = /* @__PURE__ */ new Map();
|
|
984
|
+
for (const manifest of manifestsSlice) {
|
|
985
|
+
const retained = getWorktreeRecordsByStatus(manifest, "retained");
|
|
986
|
+
for (const record of retained) {
|
|
987
|
+
const key = `${manifest.workflowId}::${record.worktreePath}`;
|
|
988
|
+
const next = {
|
|
989
|
+
workflowId: manifest.workflowId,
|
|
990
|
+
worktreePath: record.worktreePath,
|
|
991
|
+
updatedAt: record.updatedAt
|
|
992
|
+
};
|
|
993
|
+
const existing = retainedByWorkflowPath.get(key);
|
|
994
|
+
if (!existing || new Date(next.updatedAt).getTime() > new Date(existing.updatedAt).getTime()) {
|
|
995
|
+
retainedByWorkflowPath.set(key, next);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
retainedEntries.push(...retainedByWorkflowPath.values());
|
|
1000
|
+
retainedEntries.sort(
|
|
1001
|
+
(a, b) => new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()
|
|
1002
|
+
);
|
|
1003
|
+
const diskCap = diskCapResolved > 0 ? diskCapResolved : 0;
|
|
1004
|
+
const footprintByPath = /* @__PURE__ */ new Map();
|
|
1005
|
+
let aggregateRetainedBytes = 0;
|
|
1006
|
+
let diskFootprintTruncated = false;
|
|
1007
|
+
if (diskCap > 0) {
|
|
1008
|
+
const maxStat = WORKTREE_HYGIENE_MAX_RETENTION_COLLATION;
|
|
1009
|
+
for (const [si, entry] of retainedEntries.entries()) {
|
|
1010
|
+
if (si >= maxStat) {
|
|
1011
|
+
diskFootprintTruncated = true;
|
|
1012
|
+
result.hygieneCapReached = true;
|
|
1013
|
+
break;
|
|
1014
|
+
}
|
|
1015
|
+
const p = entry.worktreePath;
|
|
1016
|
+
if (!fs.existsSync(p)) {
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
const bytes = await roughDirectoryFootprintBytes(p);
|
|
1020
|
+
footprintByPath.set(p, bytes);
|
|
1021
|
+
aggregateRetainedBytes += bytes;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
const now = Date.now();
|
|
1025
|
+
let remainingCountExcess = Math.max(0, retainedEntries.length - maxCount);
|
|
1026
|
+
let gitStatusProbes = 0;
|
|
1027
|
+
const consumeFootprintForPath = (wp) => {
|
|
1028
|
+
if (diskCap <= 0 || diskFootprintTruncated) {
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
const used = footprintByPath.get(wp);
|
|
1032
|
+
if (used !== void 0) {
|
|
1033
|
+
aggregateRetainedBytes -= used;
|
|
1034
|
+
footprintByPath.delete(wp);
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
for (let i = 0; i < retainedEntries.length; i++) {
|
|
1038
|
+
const entry = retainedEntries[i];
|
|
1039
|
+
if (!entry) continue;
|
|
1040
|
+
const worktreePath = entry.worktreePath;
|
|
1041
|
+
const ageMs = now - new Date(entry.updatedAt).getTime();
|
|
1042
|
+
const isOverAge = ageMs > maxAgeMs;
|
|
1043
|
+
const isOverCount = remainingCountExcess > 0;
|
|
1044
|
+
const isOverDisk = diskCap > 0 && !diskFootprintTruncated && aggregateRetainedBytes > diskCap;
|
|
1045
|
+
if (!isOverAge && !isOverCount && !isOverDisk) {
|
|
1046
|
+
result.preserved.push(worktreePath);
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
if (await isActiveRun(worktreePath)) {
|
|
1050
|
+
if (verbose) {
|
|
1051
|
+
console.log(`[retention] Preserving active retained worktree: ${worktreePath}`);
|
|
1052
|
+
}
|
|
1053
|
+
result.preserved.push(worktreePath);
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
let dirty = false;
|
|
1057
|
+
if (fs.existsSync(worktreePath)) {
|
|
1058
|
+
if (gitStatusProbes >= probeCap) {
|
|
1059
|
+
result.hygieneCapReached = true;
|
|
1060
|
+
for (const retainedEntry of retainedEntries.slice(i)) {
|
|
1061
|
+
result.preserved.push(retainedEntry.worktreePath);
|
|
1062
|
+
}
|
|
1063
|
+
break;
|
|
1064
|
+
}
|
|
1065
|
+
gitStatusProbes += 1;
|
|
1066
|
+
try {
|
|
1067
|
+
const { stdout } = await execa("git", ["status", "--porcelain"], {
|
|
1068
|
+
cwd: worktreePath,
|
|
1069
|
+
stdio: "pipe",
|
|
1070
|
+
timeout: GIT_COMMAND_TIMEOUT_MS
|
|
1071
|
+
});
|
|
1072
|
+
dirty = filterWorktreeRunSentinelStatusOutput(stdout).trim().length > 0;
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
const disappearedDuringStatus = isWorktreeMetadataGone(error, worktreePath);
|
|
1075
|
+
if (disappearedDuringStatus) {
|
|
1076
|
+
if (verbose) {
|
|
1077
|
+
console.log(
|
|
1078
|
+
`[retention] Worktree path missing or not a git repository, marking cleaned: ${worktreePath}`
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
if (!dryRun) {
|
|
1082
|
+
try {
|
|
1083
|
+
await updateWorktreeRecordStatus(entry.workflowId, worktreePath, "cleaned", null);
|
|
1084
|
+
} catch (updateError) {
|
|
1085
|
+
result.errors.push({
|
|
1086
|
+
path: worktreePath,
|
|
1087
|
+
error: updateError instanceof Error ? updateError.message : String(updateError)
|
|
1088
|
+
});
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
if (remainingCountExcess > 0) {
|
|
1093
|
+
remainingCountExcess--;
|
|
1094
|
+
}
|
|
1095
|
+
consumeFootprintForPath(worktreePath);
|
|
1096
|
+
result.pruned.push(worktreePath);
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
dirty = true;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
if (dirty) {
|
|
1103
|
+
if (verbose) {
|
|
1104
|
+
console.log(`[retention] Preserving dirty retained worktree: ${worktreePath}`);
|
|
1105
|
+
}
|
|
1106
|
+
result.preserved.push(worktreePath);
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
if (verbose) {
|
|
1110
|
+
console.log(`[retention] Pruning retained worktree: ${worktreePath}`);
|
|
1111
|
+
}
|
|
1112
|
+
if (!dryRun) {
|
|
1113
|
+
try {
|
|
1114
|
+
if (fs.existsSync(worktreePath)) {
|
|
1115
|
+
try {
|
|
1116
|
+
await execa("git", ["worktree", "remove", "--force", worktreePath], {
|
|
1117
|
+
cwd: getProjectRoot(),
|
|
1118
|
+
stdio: "pipe",
|
|
1119
|
+
timeout: GIT_COMMAND_TIMEOUT_MS
|
|
1120
|
+
});
|
|
1121
|
+
} catch {
|
|
1122
|
+
await removeWorktreeDirectoryWithGuard(worktreePath, protectedPaths, managedRoots);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
await updateWorktreeRecordStatus(entry.workflowId, worktreePath, "cleaned", null);
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
result.errors.push({
|
|
1128
|
+
path: worktreePath,
|
|
1129
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1130
|
+
});
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
if (remainingCountExcess > 0) {
|
|
1135
|
+
remainingCountExcess--;
|
|
1136
|
+
}
|
|
1137
|
+
consumeFootprintForPath(worktreePath);
|
|
1138
|
+
result.pruned.push(worktreePath);
|
|
1139
|
+
}
|
|
1140
|
+
if (!dryRun && result.pruned.length > 0) {
|
|
1141
|
+
try {
|
|
1142
|
+
await pruneGitWorktreeEntries({ cwd: getProjectRoot() });
|
|
1143
|
+
} catch {
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
return result;
|
|
1147
|
+
};
|
|
1148
|
+
if (dryRun) {
|
|
1149
|
+
return runRetention();
|
|
1150
|
+
}
|
|
1151
|
+
return withCleanupLock(getWorktreeBaseDir(), runRetention);
|
|
1152
|
+
}
|
|
1153
|
+
async function runFlywheelWorktreeHygiene(options) {
|
|
1154
|
+
const recoverResult = await recoverPendingWorktrees({
|
|
1155
|
+
dryRun: options.dryRun,
|
|
1156
|
+
verbose: options.verbose,
|
|
1157
|
+
maxManifests: options.maxManifests,
|
|
1158
|
+
maxRecords: options.maxRecoveryRecords,
|
|
1159
|
+
restrictToPoeticWorktreePool: options.restrictRecoveryToPoeticWorktreePool
|
|
1160
|
+
});
|
|
1161
|
+
const retentionResult = await enforceRetentionBounds({
|
|
1162
|
+
dryRun: options.dryRun,
|
|
1163
|
+
verbose: options.verbose,
|
|
1164
|
+
maxAgeMs: options.maxAgeMs,
|
|
1165
|
+
maxCount: options.maxCount,
|
|
1166
|
+
maxRetainedTotalDiskBytes: options.maxRetainedTotalDiskBytes,
|
|
1167
|
+
maxManifests: options.maxManifests,
|
|
1168
|
+
maxGitStatusProbes: options.maxGitStatusProbes
|
|
1169
|
+
});
|
|
1170
|
+
return {
|
|
1171
|
+
recoverPendingWorktrees: recoverResult,
|
|
1172
|
+
enforceRetentionBounds: retentionResult
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
async function getFlywheelCleanupDryRunWorktreeSummary() {
|
|
1176
|
+
const { recoverPendingWorktrees: recoverPendingWorktrees2, enforceRetentionBounds: enforceRetentionBounds2 } = await runFlywheelWorktreeHygiene({
|
|
1177
|
+
dryRun: true,
|
|
1178
|
+
verbose: false
|
|
1179
|
+
});
|
|
1180
|
+
return {
|
|
1181
|
+
recoverPendingWorktrees: recoverPendingWorktrees2,
|
|
1182
|
+
enforceRetentionBounds: enforceRetentionBounds2,
|
|
1183
|
+
retained: enforceRetentionBounds2.preserved.length,
|
|
1184
|
+
pruned: enforceRetentionBounds2.pruned.length,
|
|
1185
|
+
skipped: recoverPendingWorktrees2.skippedActive.length + recoverPendingWorktrees2.skippedDirty.length
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// src/automation/flywheel-path-safe-ids.ts
|
|
1190
|
+
var PATH_SAFE_IDENTIFIER_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
1191
|
+
function isPathSafeIdentifier(value) {
|
|
1192
|
+
return PATH_SAFE_IDENTIFIER_PATTERN.test(value);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// src/automation/flywheel-retry-policies.ts
|
|
1196
|
+
var VALID_ON_FAILURE_POLICIES = [
|
|
1197
|
+
"abort",
|
|
1198
|
+
"retry-with-feedback",
|
|
1199
|
+
"warn",
|
|
1200
|
+
"skip"
|
|
1201
|
+
];
|
|
1202
|
+
function isValidOnFailurePolicy(value) {
|
|
1203
|
+
return typeof value === "string" && VALID_ON_FAILURE_POLICIES.includes(value);
|
|
1204
|
+
}
|
|
1205
|
+
var DEFAULT_RETRY_CONFIG = {
|
|
1206
|
+
onFailure: "abort",
|
|
1207
|
+
maxRetries: 2,
|
|
1208
|
+
feedbackTemplate: void 0,
|
|
1209
|
+
// Use built-in template
|
|
1210
|
+
backoffMultiplier: 2,
|
|
1211
|
+
initialDelaySeconds: 5,
|
|
1212
|
+
maxDelaySeconds: 60
|
|
1213
|
+
};
|
|
1214
|
+
var DEFAULT_FEEDBACK_TEMPLATE = `
|
|
1215
|
+
## Previous Attempt Failed
|
|
1216
|
+
|
|
1217
|
+
**Feedback attempt:** {{attempt}} of {{maxAttempts}}
|
|
1218
|
+
**Error:** {{error}}
|
|
1219
|
+
{{#if validation}}
|
|
1220
|
+
**Validation Failure:** {{validation}}
|
|
1221
|
+
{{/if}}
|
|
1222
|
+
|
|
1223
|
+
Please address the issue and try again. Focus on:
|
|
1224
|
+
1. Understanding why the previous attempt failed
|
|
1225
|
+
2. Fixing the specific issue mentioned above
|
|
1226
|
+
3. Ensuring your output passes all validation checks
|
|
1227
|
+
|
|
1228
|
+
---
|
|
1229
|
+
|
|
1230
|
+
## Original Task
|
|
1231
|
+
|
|
1232
|
+
{{originalPrompt}}
|
|
1233
|
+
`;
|
|
1234
|
+
var FEEDBACK_RETRYABLE_TAXONOMY_OVERRIDES = [
|
|
1235
|
+
"VALIDATION_FAILED",
|
|
1236
|
+
"SEMANTIC_CHECK_FAILED",
|
|
1237
|
+
"ALLOWLIST_VIOLATION",
|
|
1238
|
+
"ARTIFACT_INVALID"
|
|
1239
|
+
];
|
|
1240
|
+
var FEEDBACK_RETRYABLE_ERRORS = new Set(
|
|
1241
|
+
FEEDBACK_RETRYABLE_TAXONOMY_OVERRIDES
|
|
1242
|
+
);
|
|
1243
|
+
var NON_RETRYABLE_ERRORS = /* @__PURE__ */ new Set([
|
|
1244
|
+
"BUDGET_EXCEEDED",
|
|
1245
|
+
"BUDGET_STAGE_EXCEEDED",
|
|
1246
|
+
"CONFIG_INVALID",
|
|
1247
|
+
"STAGE_NOT_FOUND",
|
|
1248
|
+
"REPO_DIRTY",
|
|
1249
|
+
"GIT_CONFLICT",
|
|
1250
|
+
"MODEL_NOT_FOUND"
|
|
1251
|
+
]);
|
|
1252
|
+
function resolveRetryAuthority(config = {}) {
|
|
1253
|
+
const fullConfig = { ...DEFAULT_RETRY_CONFIG, ...config };
|
|
1254
|
+
return {
|
|
1255
|
+
...fullConfig,
|
|
1256
|
+
// maxRetries is additional feedback retries after the first eligible attempt.
|
|
1257
|
+
maxAttempts: fullConfig.maxRetries + 1
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
function getFeedbackRetryEligibility(error) {
|
|
1261
|
+
if (NON_RETRYABLE_ERRORS.has(error.code)) {
|
|
1262
|
+
return {
|
|
1263
|
+
retryable: false,
|
|
1264
|
+
reason: `Error code ${error.code} is blocked from feedback retry`,
|
|
1265
|
+
overridesTaxonomy: false
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
if (error.retryable) {
|
|
1269
|
+
return {
|
|
1270
|
+
retryable: true,
|
|
1271
|
+
reason: `Error code ${error.code} is retryable by taxonomy`,
|
|
1272
|
+
overridesTaxonomy: false
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
if (FEEDBACK_RETRYABLE_ERRORS.has(error.code)) {
|
|
1276
|
+
return {
|
|
1277
|
+
retryable: true,
|
|
1278
|
+
reason: `Error code ${error.code} is eligible for corrective retry under retry-with-feedback despite taxonomy retryable:false`,
|
|
1279
|
+
overridesTaxonomy: true
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
return {
|
|
1283
|
+
retryable: false,
|
|
1284
|
+
reason: `Error code ${error.code} is not retryable by taxonomy or feedback policy`,
|
|
1285
|
+
overridesTaxonomy: false
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
function isResolvedRetryAuthority(value) {
|
|
1289
|
+
return typeof value === "object";
|
|
1290
|
+
}
|
|
1291
|
+
function shouldRetry(authorityOrPolicy, error, attempt, maxAttempts) {
|
|
1292
|
+
const authority = isResolvedRetryAuthority(authorityOrPolicy) ? authorityOrPolicy : {
|
|
1293
|
+
...DEFAULT_RETRY_CONFIG,
|
|
1294
|
+
onFailure: authorityOrPolicy,
|
|
1295
|
+
maxRetries: typeof maxAttempts === "number" ? maxAttempts - 1 : DEFAULT_RETRY_CONFIG.maxRetries,
|
|
1296
|
+
maxAttempts: typeof maxAttempts === "number" ? maxAttempts : DEFAULT_RETRY_CONFIG.maxRetries + 1
|
|
1297
|
+
};
|
|
1298
|
+
if (authority.onFailure !== "retry-with-feedback") {
|
|
1299
|
+
return false;
|
|
1300
|
+
}
|
|
1301
|
+
if (attempt >= authority.maxAttempts) {
|
|
1302
|
+
return false;
|
|
1303
|
+
}
|
|
1304
|
+
return getFeedbackRetryEligibility(error).retryable;
|
|
1305
|
+
}
|
|
1306
|
+
function buildRetryPrompt(context, template) {
|
|
1307
|
+
const feedbackTemplate = template || DEFAULT_FEEDBACK_TEMPLATE;
|
|
1308
|
+
let validationMessage = "";
|
|
1309
|
+
if (context.lastValidationResult) {
|
|
1310
|
+
validationMessage = `[${context.lastValidationResult.type}] ${context.lastValidationResult.message}`;
|
|
1311
|
+
} else if (context.allValidationResults && context.allValidationResults.length > 0) {
|
|
1312
|
+
const failed = context.allValidationResults.filter((r) => !r.passed);
|
|
1313
|
+
if (failed.length > 0) {
|
|
1314
|
+
validationMessage = failed.map((r) => `[${r.type}] ${r.message}`).join("\n");
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
let feedback = feedbackTemplate.replace(/\{\{error\}\}/g, context.lastError.message).replace(/\{\{errorCode\}\}/g, context.lastError.code).replace(/\{\{attempt\}\}/g, String(context.attempt)).replace(/\{\{maxAttempts\}\}/g, String(context.maxAttempts)).replace(/\{\{originalPrompt\}\}/g, context.originalPrompt);
|
|
1318
|
+
if (validationMessage) {
|
|
1319
|
+
feedback = feedback.replace(/\{\{#if validation\}\}([\s\S]*?)\{\{\/if\}\}/g, "$1");
|
|
1320
|
+
feedback = feedback.replace(/\{\{validation\}\}/g, validationMessage);
|
|
1321
|
+
} else {
|
|
1322
|
+
feedback = feedback.replace(/\{\{#if validation\}\}[\s\S]*?\{\{\/if\}\}/g, "");
|
|
1323
|
+
}
|
|
1324
|
+
return feedback.trim();
|
|
1325
|
+
}
|
|
1326
|
+
function calculateRetryDelay(attempt, config) {
|
|
1327
|
+
const initial = config?.initialDelaySeconds ?? DEFAULT_RETRY_CONFIG.initialDelaySeconds;
|
|
1328
|
+
const multiplier = config?.backoffMultiplier ?? DEFAULT_RETRY_CONFIG.backoffMultiplier;
|
|
1329
|
+
const max = config?.maxDelaySeconds ?? DEFAULT_RETRY_CONFIG.maxDelaySeconds;
|
|
1330
|
+
const delay = initial * multiplier ** (attempt - 1);
|
|
1331
|
+
const jitter = delay * 0.2 * (Math.random() * 2 - 1);
|
|
1332
|
+
return Math.min(delay + jitter, max);
|
|
1333
|
+
}
|
|
1334
|
+
function makeRetryDecision(context, config = {}) {
|
|
1335
|
+
const authority = resolveRetryAuthority(config);
|
|
1336
|
+
const authoritativeContext = { ...context, maxAttempts: authority.maxAttempts };
|
|
1337
|
+
const feedbackEligibility = getFeedbackRetryEligibility(context.lastError);
|
|
1338
|
+
const policy = isValidOnFailurePolicy(authority.onFailure) ? authority.onFailure : "abort";
|
|
1339
|
+
const willRetry = policy === "retry-with-feedback" && shouldRetry(authority, context.lastError, context.attempt);
|
|
1340
|
+
if (!willRetry) {
|
|
1341
|
+
let action;
|
|
1342
|
+
let reason;
|
|
1343
|
+
if (policy === "warn") {
|
|
1344
|
+
action = "warn-continue";
|
|
1345
|
+
reason = "Policy is 'warn': record warning and continue workflow (validation remains failed)";
|
|
1346
|
+
} else if (policy === "skip") {
|
|
1347
|
+
action = "skip-continue";
|
|
1348
|
+
reason = "Policy is 'skip': skip remaining stage work and continue workflow";
|
|
1349
|
+
} else if (policy === "retry-with-feedback") {
|
|
1350
|
+
action = "abort";
|
|
1351
|
+
if (context.attempt >= authority.maxAttempts) {
|
|
1352
|
+
reason = `Exceeded maximum retries (${authority.maxRetries})`;
|
|
1353
|
+
} else {
|
|
1354
|
+
reason = feedbackEligibility.reason;
|
|
1355
|
+
}
|
|
1356
|
+
} else if (!isValidOnFailurePolicy(authority.onFailure)) {
|
|
1357
|
+
action = "abort";
|
|
1358
|
+
reason = `Policy '${String(authority.onFailure)}' is not a valid onFailure value; treating as abort`;
|
|
1359
|
+
} else {
|
|
1360
|
+
action = "abort";
|
|
1361
|
+
reason = "Policy is 'abort': stop workflow on failure";
|
|
1362
|
+
}
|
|
1363
|
+
return {
|
|
1364
|
+
shouldRetry: false,
|
|
1365
|
+
action,
|
|
1366
|
+
reason,
|
|
1367
|
+
retryEligibility: feedbackEligibility,
|
|
1368
|
+
delaySeconds: 0
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
const feedbackInjected = buildRetryPrompt(authoritativeContext, authority.feedbackTemplate);
|
|
1372
|
+
const modifiedPrompt = feedbackInjected;
|
|
1373
|
+
const delaySeconds = calculateRetryDelay(context.attempt, authority);
|
|
1374
|
+
return {
|
|
1375
|
+
shouldRetry: true,
|
|
1376
|
+
action: "retry",
|
|
1377
|
+
reason: `Retrying after ${context.lastError.code} (feedback attempt ${context.attempt + 1} of ${authority.maxAttempts}): ${feedbackEligibility.reason}`,
|
|
1378
|
+
retryEligibility: feedbackEligibility,
|
|
1379
|
+
delaySeconds,
|
|
1380
|
+
modifiedPrompt,
|
|
1381
|
+
feedbackInjected
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// src/automation/flywheel-validation.ts
|
|
1386
|
+
function getValidProviderIds() {
|
|
1387
|
+
try {
|
|
1388
|
+
return { ids: ProviderRegistry.getAllProviderIds() };
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
return {
|
|
1391
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
var VALID_OUTPUT_TYPES = ["diff", "files", "text", "testResults", "json"];
|
|
1396
|
+
var VALIDATION_NO_SIGNALS_WARNING_DETAILS = "has validation enabled but no outputs or checks defined";
|
|
1397
|
+
var VALID_CHECK_TYPES = ["file-exists", "file-contains", "file-extension"];
|
|
1398
|
+
var VALID_PROVIDER_EXECUTION_TRANSPORTS = [
|
|
1399
|
+
"auto",
|
|
1400
|
+
"headless",
|
|
1401
|
+
"tmux-exec",
|
|
1402
|
+
"interactive"
|
|
1403
|
+
];
|
|
1404
|
+
function isValidProviderExecutionTransport(value) {
|
|
1405
|
+
return VALID_PROVIDER_EXECUTION_TRANSPORTS.includes(
|
|
1406
|
+
value
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
function appendSeededPlanCompletionContractErrors(parentStageId, plan, errors, pathPrefix = "") {
|
|
1410
|
+
const subStages = plan?.subStages;
|
|
1411
|
+
if (!subStages || subStages.length === 0) {
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
for (const sub of subStages) {
|
|
1415
|
+
const subId = typeof sub.id === "string" && sub.id.length > 0 ? sub.id : "(anonymous)";
|
|
1416
|
+
const nestedPath = pathPrefix ? `${pathPrefix}/${subId}` : subId;
|
|
1417
|
+
if (sub.validation?.completionContract !== void 0) {
|
|
1418
|
+
const contractValidation = validateCompletionContracts(sub.validation.completionContract);
|
|
1419
|
+
for (const contractError of contractValidation.errors) {
|
|
1420
|
+
errors.push(
|
|
1421
|
+
`Stage '${parentStageId}' seeded substage '${nestedPath}' ${contractError.field}: ${contractError.message}`
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
const nestedPlan = sub.seededPlan;
|
|
1426
|
+
appendSeededPlanCompletionContractErrors(parentStageId, nestedPlan, errors, nestedPath);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
function validateVerifyCommandStrings(commands, label, errors, options) {
|
|
1430
|
+
if (!Array.isArray(commands) || options.requireNonEmpty && commands.length === 0) {
|
|
1431
|
+
errors.push(`${label} must be a non-empty string[]`);
|
|
1432
|
+
return false;
|
|
1433
|
+
}
|
|
1434
|
+
for (const [idx, cmd] of commands.entries()) {
|
|
1435
|
+
if (typeof cmd !== "string" || cmd.trim().length === 0) {
|
|
1436
|
+
errors.push(`${label}[${idx}] must be a non-empty string`);
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
const safety = checkVerifyCommandSafety(cmd);
|
|
1440
|
+
if (!safety.safe) {
|
|
1441
|
+
errors.push(`${label}[${idx}] is unsafe: ${safety.error}`);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
return true;
|
|
1445
|
+
}
|
|
1446
|
+
function validateGeneratedSubStageVerifyCommands(subStages) {
|
|
1447
|
+
const errors = [];
|
|
1448
|
+
for (const subStage of subStages) {
|
|
1449
|
+
const label = `Sub-stage '${subStage.id}' verify.commands`;
|
|
1450
|
+
if (subStage.type === "verify") {
|
|
1451
|
+
validateVerifyCommandStrings(subStage.verify?.commands, label, errors, {
|
|
1452
|
+
requireNonEmpty: true
|
|
1453
|
+
});
|
|
1454
|
+
} else if (subStage.verify?.commands !== void 0) {
|
|
1455
|
+
validateVerifyCommandStrings(subStage.verify.commands, label, errors, {
|
|
1456
|
+
requireNonEmpty: false
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
return errors;
|
|
1461
|
+
}
|
|
1462
|
+
function validateExplicitCheck(check, stageId) {
|
|
1463
|
+
const errors = [];
|
|
1464
|
+
if (!VALID_CHECK_TYPES.includes(check.type)) {
|
|
1465
|
+
errors.push(`Stage '${stageId}' has unknown check type: ${check.type}`);
|
|
1466
|
+
}
|
|
1467
|
+
if ("path" in check && check.path) {
|
|
1468
|
+
const pathResult = isValidCheckPath(check.path);
|
|
1469
|
+
if (!pathResult.valid) {
|
|
1470
|
+
errors.push(`Stage '${stageId}' check path invalid: ${pathResult.error}`);
|
|
1471
|
+
}
|
|
1472
|
+
} else if ("path" in check && !check.path) {
|
|
1473
|
+
errors.push(`Stage '${stageId}' check of type '${check.type}' missing required 'path' field`);
|
|
1474
|
+
}
|
|
1475
|
+
if (check.type === "file-contains") {
|
|
1476
|
+
const containsCheck = check;
|
|
1477
|
+
if (!containsCheck.content || containsCheck.content.length === 0) {
|
|
1478
|
+
errors.push(`Stage '${stageId}' file-contains check missing 'content' field`);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
if (errors.length > 0) {
|
|
1482
|
+
return { valid: false, error: errors.join("; ") };
|
|
1483
|
+
}
|
|
1484
|
+
return { valid: true };
|
|
1485
|
+
}
|
|
1486
|
+
function validateStageConfig(stage) {
|
|
1487
|
+
const errors = [];
|
|
1488
|
+
const warnings = [];
|
|
1489
|
+
if (!stage.id || typeof stage.id !== "string") {
|
|
1490
|
+
errors.push("Stage must have a valid id");
|
|
1491
|
+
} else if (!isPathSafeIdentifier(stage.id)) {
|
|
1492
|
+
errors.push(
|
|
1493
|
+
`Stage id '${stage.id}' contains invalid characters. Use only alphanumeric, underscore, and hyphen.`
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1496
|
+
if (stage.type !== void 0 && stage.type !== "standard" && stage.type !== "competition" && stage.type !== "verify" && stage.type !== "refinement" && stage.type !== "autoloop" && stage.type !== "orchestrator") {
|
|
1497
|
+
errors.push(
|
|
1498
|
+
`Stage '${stage.id}' has invalid type '${String(stage.type)}'. Valid types: standard, competition, verify, refinement, autoloop, orchestrator`
|
|
1499
|
+
);
|
|
1500
|
+
}
|
|
1501
|
+
if (stage.type === "verify") {
|
|
1502
|
+
const cmds = stage.verify?.commands;
|
|
1503
|
+
validateVerifyCommandStrings(cmds, `Stage '${stage.id}' verify.commands`, errors, {
|
|
1504
|
+
requireNonEmpty: true
|
|
1505
|
+
});
|
|
1506
|
+
if (stage.provider) {
|
|
1507
|
+
warnings.push(
|
|
1508
|
+
`Stage '${stage.id}' is type 'verify' but specifies provider '${stage.provider}'. Provider will be ignored (verification stages run locally).`
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
if (stage.cacheable === true) {
|
|
1512
|
+
warnings.push(
|
|
1513
|
+
`Stage '${stage.id}' sets deprecated cacheable: true; verify stage caching is disabled and commands will run`
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
} else if (stage.cacheable === true) {
|
|
1517
|
+
errors.push(
|
|
1518
|
+
`Stage '${stage.id}' sets cacheable: true, but deterministic stage caching is only supported for type 'verify' stages`
|
|
1519
|
+
);
|
|
1520
|
+
} else if (stage.verify?.commands !== void 0) {
|
|
1521
|
+
validateVerifyCommandStrings(
|
|
1522
|
+
stage.verify.commands,
|
|
1523
|
+
`Stage '${stage.id}' verify.commands`,
|
|
1524
|
+
errors,
|
|
1525
|
+
{
|
|
1526
|
+
requireNonEmpty: false
|
|
1527
|
+
}
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1530
|
+
if (stage.competition?.candidateGate) {
|
|
1531
|
+
const gate = stage.competition.candidateGate;
|
|
1532
|
+
const stageId = stage.id || "unknown";
|
|
1533
|
+
if (!Array.isArray(gate.commands) || gate.commands.length === 0) {
|
|
1534
|
+
errors.push(
|
|
1535
|
+
`Stage '${stageId}' competition.candidateGate.commands must be a non-empty string[]`
|
|
1536
|
+
);
|
|
1537
|
+
} else {
|
|
1538
|
+
for (const [idx, cmd] of gate.commands.entries()) {
|
|
1539
|
+
if (typeof cmd !== "string" || cmd.trim().length === 0) {
|
|
1540
|
+
errors.push(
|
|
1541
|
+
`Stage '${stageId}' competition.candidateGate.commands[${idx}] must be a non-empty string`
|
|
1542
|
+
);
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
const safety = checkVerifyCommandSafety(cmd);
|
|
1546
|
+
if (!safety.safe) {
|
|
1547
|
+
errors.push(
|
|
1548
|
+
`Stage '${stageId}' competition.candidateGate.commands[${idx}] is unsafe: ${safety.error}`
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
if (gate.coverage) {
|
|
1554
|
+
const coverage = gate.coverage;
|
|
1555
|
+
if (!Array.isArray(coverage.targets) || coverage.targets.length === 0) {
|
|
1556
|
+
errors.push(
|
|
1557
|
+
`Stage '${stageId}' competition.candidateGate.coverage.targets must be string[]`
|
|
1558
|
+
);
|
|
1559
|
+
} else {
|
|
1560
|
+
for (const [idx, target] of coverage.targets.entries()) {
|
|
1561
|
+
if (typeof target !== "string" || target.trim().length === 0) {
|
|
1562
|
+
errors.push(
|
|
1563
|
+
`Stage '${stageId}' competition.candidateGate.coverage.targets[${idx}] must be a non-empty string`
|
|
1564
|
+
);
|
|
1565
|
+
continue;
|
|
1566
|
+
}
|
|
1567
|
+
const pathResult = isValidCheckPath(target);
|
|
1568
|
+
if (!pathResult.valid) {
|
|
1569
|
+
errors.push(
|
|
1570
|
+
`Stage '${stageId}' competition.candidateGate.coverage.targets[${idx}] invalid: ${pathResult.error}`
|
|
1571
|
+
);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
if (coverage.summaryPath !== void 0) {
|
|
1576
|
+
if (typeof coverage.summaryPath !== "string" || coverage.summaryPath.trim().length === 0) {
|
|
1577
|
+
errors.push(
|
|
1578
|
+
`Stage '${stageId}' competition.candidateGate.coverage.summaryPath must be a non-empty string when provided`
|
|
1579
|
+
);
|
|
1580
|
+
} else {
|
|
1581
|
+
const pathResult = isValidCheckPath(coverage.summaryPath);
|
|
1582
|
+
if (!pathResult.valid) {
|
|
1583
|
+
errors.push(
|
|
1584
|
+
`Stage '${stageId}' competition.candidateGate.coverage.summaryPath invalid: ${pathResult.error}`
|
|
1585
|
+
);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
if (coverage.threshold !== void 0) {
|
|
1590
|
+
if (typeof coverage.threshold !== "number" || !Number.isFinite(coverage.threshold)) {
|
|
1591
|
+
errors.push(
|
|
1592
|
+
`Stage '${stageId}' competition.candidateGate.coverage.threshold must be a finite number when provided`
|
|
1593
|
+
);
|
|
1594
|
+
} else if (coverage.threshold < 0 || coverage.threshold > 100) {
|
|
1595
|
+
errors.push(
|
|
1596
|
+
`Stage '${stageId}' competition.candidateGate.coverage.threshold must be between 0 and 100`
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
if (stage.competition?.judgeConfig) {
|
|
1603
|
+
const judgeValidation = validateCompetitionJudgeConfig(
|
|
1604
|
+
stage.id || "unknown",
|
|
1605
|
+
stage.competition.judgeConfig
|
|
1606
|
+
);
|
|
1607
|
+
if (!judgeValidation.valid && judgeValidation.error) {
|
|
1608
|
+
errors.push(judgeValidation.error);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
if (stage.type === "refinement") {
|
|
1612
|
+
if (!stage.refinement) {
|
|
1613
|
+
warnings.push(
|
|
1614
|
+
`Stage '${stage.id}' is type 'refinement' but has no refinement config; it will run as a normal stage without post-competition inputs/routing.`
|
|
1615
|
+
);
|
|
1616
|
+
}
|
|
1617
|
+
if (stage.competition) {
|
|
1618
|
+
warnings.push(
|
|
1619
|
+
`Stage '${stage.id}' is type 'refinement' but also specifies competition config; competition config will be ignored.`
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
if (stage.verify?.commands?.length) {
|
|
1623
|
+
warnings.push(
|
|
1624
|
+
`Stage '${stage.id}' is type 'refinement' but also specifies verify.commands; verify commands will be ignored.`
|
|
1625
|
+
);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
if (stage.type === "orchestrator") {
|
|
1629
|
+
if (!stage.orchestrator) {
|
|
1630
|
+
warnings.push(
|
|
1631
|
+
`Stage '${stage.id}' is type 'orchestrator' but has no orchestrator config; using defaults.`
|
|
1632
|
+
);
|
|
1633
|
+
} else {
|
|
1634
|
+
const cfg = stage.orchestrator;
|
|
1635
|
+
if (cfg.orchestrationProvider !== void 0) {
|
|
1636
|
+
try {
|
|
1637
|
+
ProviderRegistry.getProviderConfig(cfg.orchestrationProvider);
|
|
1638
|
+
} catch (error) {
|
|
1639
|
+
errors.push(
|
|
1640
|
+
`Stage '${stage.id}' orchestrator.orchestrationProvider is invalid: ${error instanceof Error ? error.message : String(error)}`
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
if (cfg.orchestrationProvider !== void 0 && cfg.orchestrationModel !== void 0) {
|
|
1645
|
+
try {
|
|
1646
|
+
ProviderRegistry.resolveModel(cfg.orchestrationProvider, cfg.orchestrationModel);
|
|
1647
|
+
} catch (error) {
|
|
1648
|
+
errors.push(
|
|
1649
|
+
`Stage '${stage.id}' orchestrator.orchestrationModel is invalid for provider '${cfg.orchestrationProvider}': ${error instanceof Error ? error.message : String(error)}`
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
if (cfg.maxSubStages !== void 0) {
|
|
1654
|
+
if (!Number.isInteger(cfg.maxSubStages) || cfg.maxSubStages < 1) {
|
|
1655
|
+
errors.push(
|
|
1656
|
+
`Stage '${stage.id}' orchestrator.maxSubStages must be a positive integer (got ${cfg.maxSubStages})`
|
|
1657
|
+
);
|
|
1658
|
+
} else if (cfg.maxSubStages > 20) {
|
|
1659
|
+
warnings.push(
|
|
1660
|
+
`Stage '${stage.id}' orchestrator.maxSubStages is very high (${cfg.maxSubStages}). Consider limiting to \u226410 for cost control.`
|
|
1661
|
+
);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
if (cfg.budgetPerSubStage !== void 0) {
|
|
1665
|
+
if (!Number.isFinite(cfg.budgetPerSubStage) || cfg.budgetPerSubStage < 0) {
|
|
1666
|
+
errors.push(
|
|
1667
|
+
`Stage '${stage.id}' orchestrator.budgetPerSubStage must be non-negative (got ${cfg.budgetPerSubStage})`
|
|
1668
|
+
);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
if (cfg.totalBudget !== void 0) {
|
|
1672
|
+
if (!Number.isFinite(cfg.totalBudget) || cfg.totalBudget < 0) {
|
|
1673
|
+
errors.push(
|
|
1674
|
+
`Stage '${stage.id}' orchestrator.totalBudget must be non-negative (got ${cfg.totalBudget})`
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
if (cfg.maxReplanningAttempts !== void 0) {
|
|
1679
|
+
if (!Number.isInteger(cfg.maxReplanningAttempts) || cfg.maxReplanningAttempts < 0) {
|
|
1680
|
+
errors.push(
|
|
1681
|
+
`Stage '${stage.id}' orchestrator.maxReplanningAttempts must be non-negative integer (got ${cfg.maxReplanningAttempts})`
|
|
1682
|
+
);
|
|
1683
|
+
} else if (cfg.maxReplanningAttempts > 1) {
|
|
1684
|
+
errors.push(
|
|
1685
|
+
`Stage '${stage.id}' orchestrator.maxReplanningAttempts cannot exceed 1 until multi-pass replanning is supported (got ${cfg.maxReplanningAttempts})`
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
if (cfg.retryBudgetPerSubStage !== void 0) {
|
|
1690
|
+
if (!Number.isInteger(cfg.retryBudgetPerSubStage) || cfg.retryBudgetPerSubStage < 0) {
|
|
1691
|
+
errors.push(
|
|
1692
|
+
`Stage '${stage.id}' orchestrator.retryBudgetPerSubStage must be non-negative integer (got ${cfg.retryBudgetPerSubStage})`
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
if (cfg.maxRecursionDepth !== void 0) {
|
|
1697
|
+
if (!Number.isInteger(cfg.maxRecursionDepth) || cfg.maxRecursionDepth < 0) {
|
|
1698
|
+
errors.push(
|
|
1699
|
+
`Stage '${stage.id}' orchestrator.maxRecursionDepth must be non-negative integer (got ${cfg.maxRecursionDepth})`
|
|
1700
|
+
);
|
|
1701
|
+
} else if (cfg.maxRecursionDepth > 3) {
|
|
1702
|
+
warnings.push(
|
|
1703
|
+
`Stage '${stage.id}' orchestrator.maxRecursionDepth is high (${cfg.maxRecursionDepth}). Deep recursion may be expensive.`
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
if (stage.competition) {
|
|
1709
|
+
warnings.push(
|
|
1710
|
+
`Stage '${stage.id}' is type 'orchestrator' but also specifies competition config; competition config will be ignored.`
|
|
1711
|
+
);
|
|
1712
|
+
}
|
|
1713
|
+
if (stage.autoloop) {
|
|
1714
|
+
warnings.push(
|
|
1715
|
+
`Stage '${stage.id}' is type 'orchestrator' but also specifies autoloop config; autoloop config will be ignored.`
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1718
|
+
if (stage.verify?.commands?.length) {
|
|
1719
|
+
warnings.push(
|
|
1720
|
+
`Stage '${stage.id}' is type 'orchestrator' but also specifies verify.commands; verify commands will be ignored.`
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
if (stage.refinement) {
|
|
1724
|
+
warnings.push(
|
|
1725
|
+
`Stage '${stage.id}' is type 'orchestrator' but also specifies refinement config; refinement config will be ignored.`
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
if (stage.type === "autoloop") {
|
|
1730
|
+
if (!stage.autoloop) {
|
|
1731
|
+
warnings.push(
|
|
1732
|
+
`Stage '${stage.id}' is type 'autoloop' but has no autoloop config; using defaults (maxIterations: 3, timeboxMs: 1800000).`
|
|
1733
|
+
);
|
|
1734
|
+
} else {
|
|
1735
|
+
const cfg = stage.autoloop;
|
|
1736
|
+
if (cfg.maxIterations !== void 0) {
|
|
1737
|
+
if (!Number.isInteger(cfg.maxIterations) || cfg.maxIterations < 1) {
|
|
1738
|
+
errors.push(
|
|
1739
|
+
`Stage '${stage.id}' autoloop.maxIterations must be a positive integer (got ${cfg.maxIterations})`
|
|
1740
|
+
);
|
|
1741
|
+
} else if (cfg.maxIterations > 20) {
|
|
1742
|
+
warnings.push(
|
|
1743
|
+
`Stage '${stage.id}' autoloop.maxIterations is very high (${cfg.maxIterations}). Consider limiting to \u226410 for cost control.`
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
if (cfg.timeboxMs !== void 0) {
|
|
1748
|
+
if (!Number.isFinite(cfg.timeboxMs) || cfg.timeboxMs < 1e3) {
|
|
1749
|
+
errors.push(
|
|
1750
|
+
`Stage '${stage.id}' autoloop.timeboxMs must be at least 1000ms (got ${cfg.timeboxMs})`
|
|
1751
|
+
);
|
|
1752
|
+
} else if (cfg.timeboxMs > 36e5) {
|
|
1753
|
+
warnings.push(
|
|
1754
|
+
`Stage '${stage.id}' autoloop.timeboxMs is very high (${cfg.timeboxMs}ms = ${(cfg.timeboxMs / 6e4).toFixed(1)} minutes). Consider limiting to \u226430 minutes.`
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
if (cfg.minProgress !== void 0) {
|
|
1759
|
+
if (!Number.isFinite(cfg.minProgress) || cfg.minProgress < 0 || cfg.minProgress > 1) {
|
|
1760
|
+
errors.push(
|
|
1761
|
+
`Stage '${stage.id}' autoloop.minProgress must be between 0 and 1 (got ${cfg.minProgress})`
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
if (stage.competition) {
|
|
1767
|
+
warnings.push(
|
|
1768
|
+
`Stage '${stage.id}' is type 'autoloop' but also specifies competition config; competition config will be ignored.`
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
if (stage.verify?.commands?.length) {
|
|
1772
|
+
const gatesEnabled = stage.autoloop?.stopOnQualityGate === true || Array.isArray(stage.autoloop?.qualityGateCommands) && stage.autoloop?.qualityGateCommands.length > 0;
|
|
1773
|
+
if (!gatesEnabled) {
|
|
1774
|
+
warnings.push(
|
|
1775
|
+
`Stage '${stage.id}' is type 'autoloop' but also specifies verify.commands; verify commands will be ignored unless autoloop.stopOnQualityGate=true (or autoloop.qualityGateCommands is set).`
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
if (stage.refinement) {
|
|
1780
|
+
warnings.push(
|
|
1781
|
+
`Stage '${stage.id}' is type 'autoloop' but also specifies refinement config; refinement config will be ignored.`
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
if (stage.outputs) {
|
|
1786
|
+
for (const [outputName, outputType] of Object.entries(stage.outputs)) {
|
|
1787
|
+
if (!isPathSafeIdentifier(outputName)) {
|
|
1788
|
+
errors.push(
|
|
1789
|
+
`Stage '${stage.id}' output name '${outputName}' contains invalid characters. Use only alphanumeric, underscore, and hyphen.`
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
if (!VALID_OUTPUT_TYPES.includes(outputType)) {
|
|
1793
|
+
errors.push(
|
|
1794
|
+
`Stage '${stage.id}' output '${outputName}' has invalid type '${outputType}'. Valid types: ${VALID_OUTPUT_TYPES.join(", ")}`
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
if (stage.provider !== void 0) {
|
|
1800
|
+
const providerIds = getValidProviderIds();
|
|
1801
|
+
if (!providerIds.ids) {
|
|
1802
|
+
errors.push(
|
|
1803
|
+
`Stage '${stage.id}' provider '${stage.provider}' could not be validated because the provider registry is unavailable: ${providerIds.error}`
|
|
1804
|
+
);
|
|
1805
|
+
} else if (!providerIds.ids.includes(stage.provider)) {
|
|
1806
|
+
errors.push(
|
|
1807
|
+
`Stage '${stage.id}' has invalid provider '${stage.provider}'. Valid providers: ${providerIds.ids.join(", ")}`
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
if (stage.model !== void 0 && stage.provider === void 0) {
|
|
1812
|
+
warnings.push(
|
|
1813
|
+
`Stage '${stage.id}' specifies model '${stage.model}' without provider. Model may be ignored during routing. Consider setting provider explicitly.`
|
|
1814
|
+
);
|
|
1815
|
+
}
|
|
1816
|
+
if (stage.providerExecutionTransport !== void 0 && !isValidProviderExecutionTransport(stage.providerExecutionTransport)) {
|
|
1817
|
+
errors.push(
|
|
1818
|
+
`Stage '${stage.id}' has invalid providerExecutionTransport '${String(
|
|
1819
|
+
stage.providerExecutionTransport
|
|
1820
|
+
)}'. Valid values: ${VALID_PROVIDER_EXECUTION_TRANSPORTS.join(", ")}`
|
|
1821
|
+
);
|
|
1822
|
+
}
|
|
1823
|
+
if (stage.competition?.variants) {
|
|
1824
|
+
for (const [variantIdx, variant] of stage.competition.variants.entries()) {
|
|
1825
|
+
if (variant.providerExecutionTransport !== void 0 && !isValidProviderExecutionTransport(variant.providerExecutionTransport)) {
|
|
1826
|
+
errors.push(
|
|
1827
|
+
`Stage '${stage.id}' competition.variants[${variantIdx}] has invalid providerExecutionTransport '${String(
|
|
1828
|
+
variant.providerExecutionTransport
|
|
1829
|
+
)}'. Valid values: ${VALID_PROVIDER_EXECUTION_TRANSPORTS.join(", ")}`
|
|
1830
|
+
);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
if (stage.modelRole !== void 0 && stage.provider === void 0) {
|
|
1835
|
+
warnings.push(
|
|
1836
|
+
`Stage '${stage.id}' specifies modelRole '${stage.modelRole}' without provider. modelRole requires provider to be set for registry role lookup.`
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
if (stage.validation?.enabled && stage.outputs) {
|
|
1840
|
+
const outputCount = Object.keys(stage.outputs).length;
|
|
1841
|
+
if (outputCount > 1 && !stage.validation.target) {
|
|
1842
|
+
errors.push(
|
|
1843
|
+
`Stage '${stage.id}' has ${outputCount} outputs but validation.target not specified. Must specify which output to validate: [${Object.keys(stage.outputs).join(", ")}]`
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
if (stage.validation.target) {
|
|
1847
|
+
if (!Object.hasOwn(stage.outputs, stage.validation.target)) {
|
|
1848
|
+
errors.push(
|
|
1849
|
+
`Stage '${stage.id}' validation target '${stage.validation.target}' not found in outputs: [${Object.keys(stage.outputs).join(", ")}]`
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
if (stage.validation?.threshold !== void 0) {
|
|
1855
|
+
if (stage.validation.threshold < 0 || stage.validation.threshold > 100) {
|
|
1856
|
+
errors.push(
|
|
1857
|
+
`Stage '${stage.id}' validation threshold ${stage.validation.threshold} must be between 0 and 100`
|
|
1858
|
+
);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
if (stage.validation?.maxRetries !== void 0) {
|
|
1862
|
+
if (stage.validation.maxRetries < 0 || stage.validation.maxRetries > 10) {
|
|
1863
|
+
errors.push(
|
|
1864
|
+
`Stage '${stage.id}' validation maxRetries ${stage.validation.maxRetries} must be between 0 and 10`
|
|
1865
|
+
);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
if (stage.validation?.onFailure !== void 0) {
|
|
1869
|
+
if (!isValidOnFailurePolicy(stage.validation.onFailure)) {
|
|
1870
|
+
errors.push(
|
|
1871
|
+
`Stage '${stage.id}' validation.onFailure '${String(stage.validation.onFailure)}' is invalid. Valid values: ${VALID_ON_FAILURE_POLICIES.join(", ")}`
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1874
|
+
if (stage.validation.onFailure === "retry-with-feedback" && stage.type !== void 0 && stage.type !== "standard" && stage.type !== "refinement") {
|
|
1875
|
+
errors.push(
|
|
1876
|
+
`Stage '${stage.id}' validation.onFailure 'retry-with-feedback' is only supported for standard and refinement stages`
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
if (stage.validation?.checks && stage.validation.checks.length > 0) {
|
|
1881
|
+
for (const check of stage.validation.checks) {
|
|
1882
|
+
const checkResult = validateExplicitCheck(check, stage.id);
|
|
1883
|
+
if (!checkResult.valid && checkResult.error) {
|
|
1884
|
+
errors.push(checkResult.error);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
if (stage.validation?.requiredFiles && stage.validation.requiredFiles.length > 0) {
|
|
1889
|
+
for (const requiredFile of stage.validation.requiredFiles) {
|
|
1890
|
+
const pathResult = isValidCheckPath(requiredFile);
|
|
1891
|
+
if (!pathResult.valid) {
|
|
1892
|
+
errors.push(`Stage '${stage.id}' requiredFiles path invalid: ${pathResult.error}`);
|
|
1893
|
+
continue;
|
|
1894
|
+
}
|
|
1895
|
+
if (requiredFile.endsWith("/")) {
|
|
1896
|
+
errors.push(
|
|
1897
|
+
`Stage '${stage.id}' requiredFiles must use exact file paths, not directories: ${requiredFile}`
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
if (stage.validation?.completionContract !== void 0) {
|
|
1903
|
+
const contractValidation = validateCompletionContracts(stage.validation.completionContract);
|
|
1904
|
+
for (const contractError of contractValidation.errors) {
|
|
1905
|
+
errors.push(`Stage '${stage.id}' ${contractError.field}: ${contractError.message}`);
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
appendSeededPlanCompletionContractErrors(stage.id, stage.seededPlan, errors);
|
|
1909
|
+
if (stage.allowlist && stage.allowlist.length > 0) {
|
|
1910
|
+
for (const allowlistPath of stage.allowlist) {
|
|
1911
|
+
const pathResult = isValidCheckPath(allowlistPath);
|
|
1912
|
+
if (!pathResult.valid) {
|
|
1913
|
+
errors.push(`Stage '${stage.id}' allowlist path invalid: ${pathResult.error}`);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
if (stage.type !== "verify" && stage.capabilities?.executionIntent === "analysis" && (!stage.outputs || Object.keys(stage.outputs).length === 0)) {
|
|
1918
|
+
errors.push(
|
|
1919
|
+
`Stage '${stage.id}' is analysis-intent but has no outputs configured. Add outputs (recommended: outputs: { report: 'text' }).`
|
|
1920
|
+
);
|
|
1921
|
+
}
|
|
1922
|
+
if (stage.allowlist && stage.allowlist.length > 0 && stage.outputs) {
|
|
1923
|
+
const outputTypes = Object.values(stage.outputs);
|
|
1924
|
+
const hasCodeOutput = outputTypes.some((t) => t === "diff" || t === "files");
|
|
1925
|
+
const hasTextOutput = outputTypes.some(
|
|
1926
|
+
(t) => t === "text" || t === "json" || t === "testResults"
|
|
1927
|
+
);
|
|
1928
|
+
if (hasTextOutput && !hasCodeOutput) {
|
|
1929
|
+
if (stage.capabilities?.executionIntent === "analysis") {
|
|
1930
|
+
warnings.push(
|
|
1931
|
+
`Stage '${stage.id}' sets executionIntent: analysis with a non-empty allowlist. Providers run read-only; allowlisted edits will be denied unless intent or outputs change.`
|
|
1932
|
+
);
|
|
1933
|
+
} else if (stage.capabilities?.executionIntent !== "edit") {
|
|
1934
|
+
warnings.push(
|
|
1935
|
+
`Stage '${stage.id}' declares an allowlist but only text-type outputs. Execution runs with edit intent because the allowlist signals file modifications. Add a code-type output (e.g. outputs: { patch: diff }) or set capabilities.executionIntent: edit.`
|
|
1936
|
+
);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
if (stage.allowlistMode === "strict") {
|
|
1941
|
+
if (!stage.allowlist || stage.allowlist.length === 0) {
|
|
1942
|
+
warnings.push(
|
|
1943
|
+
`Stage '${stage.id}' uses allowlistMode: strict with no allowlist entries. Any file changes will fail the stage.`
|
|
1944
|
+
);
|
|
1945
|
+
} else if (!stage.allowlist.some((p) => p.endsWith("/"))) {
|
|
1946
|
+
warnings.push(
|
|
1947
|
+
`Stage '${stage.id}' uses allowlistMode: strict with file-level allowlist entries only. This commonly fails on ripple edits (e.g. async propagation). Consider directory prefixes or allowlistMode: warn.`
|
|
1948
|
+
);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
if (stage.validation?.enabled && stage.type !== "verify" && !stage.outputs && !stage.validation?.checks?.length && !stage.validation?.semanticChecks?.length && !stage.verify?.commands?.length) {
|
|
1952
|
+
warnings.push(
|
|
1953
|
+
`Stage '${stage.id}' ${VALIDATION_NO_SIGNALS_WARNING_DETAILS}. This is informational only and does not cause stage failure. Consider adding validation.checks or outputs for meaningful validation signals.`
|
|
1954
|
+
);
|
|
1955
|
+
}
|
|
1956
|
+
if (stage.gates && stage.gates.length > 0) {
|
|
1957
|
+
for (const [idx, gate] of stage.gates.entries()) {
|
|
1958
|
+
const validGateTypes = ["biome", "test", "build", "custom"];
|
|
1959
|
+
if (!validGateTypes.includes(gate.type)) {
|
|
1960
|
+
errors.push(
|
|
1961
|
+
`Stage '${stage.id}' gates[${idx}].type is invalid: '${gate.type}'. Valid types: ${validGateTypes.join(", ")}`
|
|
1962
|
+
);
|
|
1963
|
+
}
|
|
1964
|
+
const validGateModes = ["block", "warn"];
|
|
1965
|
+
if (!validGateModes.includes(gate.mode)) {
|
|
1966
|
+
errors.push(
|
|
1967
|
+
`Stage '${stage.id}' gates[${idx}].mode is invalid: '${gate.mode}'. Valid modes: ${validGateModes.join(", ")}`
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
if (gate.files && gate.files.length > 0) {
|
|
1971
|
+
for (const [fileIdx, filePath] of gate.files.entries()) {
|
|
1972
|
+
const pathResult = isValidCheckPath(filePath);
|
|
1973
|
+
if (!pathResult.valid) {
|
|
1974
|
+
errors.push(
|
|
1975
|
+
`Stage '${stage.id}' gates[${idx}].files[${fileIdx}] invalid: ${pathResult.error}`
|
|
1976
|
+
);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
if (gate.type === "custom" && (!gate.command || gate.command.trim().length === 0)) {
|
|
1981
|
+
errors.push(
|
|
1982
|
+
`Stage '${stage.id}' gates[${idx}] type 'custom' requires non-empty 'command' field`
|
|
1983
|
+
);
|
|
1984
|
+
}
|
|
1985
|
+
if (gate.maxDiagnostics !== void 0) {
|
|
1986
|
+
if (gate.type !== "biome") {
|
|
1987
|
+
warnings.push(
|
|
1988
|
+
`Stage '${stage.id}' gates[${idx}].maxDiagnostics is set but gate type is '${gate.type}' (only applies to 'biome')`
|
|
1989
|
+
);
|
|
1990
|
+
} else if (!Number.isInteger(gate.maxDiagnostics) || gate.maxDiagnostics < 1 || gate.maxDiagnostics > 1e3) {
|
|
1991
|
+
errors.push(
|
|
1992
|
+
`Stage '${stage.id}' gates[${idx}].maxDiagnostics must be an integer between 1 and 1000 (got ${gate.maxDiagnostics})`
|
|
1993
|
+
);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
if (errors.length > 0) {
|
|
1999
|
+
return {
|
|
2000
|
+
valid: false,
|
|
2001
|
+
error: errors.join("; "),
|
|
2002
|
+
warnings: warnings.length > 0 ? warnings : void 0
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
return {
|
|
2006
|
+
valid: true,
|
|
2007
|
+
warnings: warnings.length > 0 ? warnings : void 0
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
function isValidationNoSignalsWarning(warning, stageId) {
|
|
2011
|
+
return warning.startsWith(`Stage '${stageId}' ${VALIDATION_NO_SIGNALS_WARNING_DETAILS}.`);
|
|
2012
|
+
}
|
|
2013
|
+
function validateWorkflowConfig(workflow) {
|
|
2014
|
+
const errors = [];
|
|
2015
|
+
const warnings = [];
|
|
2016
|
+
const validationNoSignalsStageIds = [];
|
|
2017
|
+
if (!workflow.stages || workflow.stages.length === 0) {
|
|
2018
|
+
return {
|
|
2019
|
+
valid: false,
|
|
2020
|
+
error: "Workflow must have at least one stage"
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
const stageIds = new Set(workflow.stages.map((s) => s.id));
|
|
2024
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
2025
|
+
for (const stage of workflow.stages) {
|
|
2026
|
+
if (seenIds.has(stage.id)) {
|
|
2027
|
+
errors.push(`Duplicate stage id: '${stage.id}'`);
|
|
2028
|
+
}
|
|
2029
|
+
seenIds.add(stage.id);
|
|
2030
|
+
}
|
|
2031
|
+
for (const stage of workflow.stages) {
|
|
2032
|
+
const stageResult = validateStageConfig(stage);
|
|
2033
|
+
if (!stageResult.valid && stageResult.error) {
|
|
2034
|
+
errors.push(stageResult.error);
|
|
2035
|
+
}
|
|
2036
|
+
if (stageResult.warnings) {
|
|
2037
|
+
for (const warning of stageResult.warnings) {
|
|
2038
|
+
if (isValidationNoSignalsWarning(warning, stage.id)) {
|
|
2039
|
+
validationNoSignalsStageIds.push(stage.id);
|
|
2040
|
+
} else {
|
|
2041
|
+
warnings.push(warning);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
if (stage.deps) {
|
|
2046
|
+
for (const dep of stage.deps) {
|
|
2047
|
+
if (!stageIds.has(dep)) {
|
|
2048
|
+
errors.push(
|
|
2049
|
+
`Stage '${stage.id}' has dependency '${dep}' which does not exist. Available stages: [${Array.from(stageIds).join(", ")}]`
|
|
2050
|
+
);
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
const cycleError = detectCircularDependencies(workflow.stages);
|
|
2056
|
+
if (cycleError) {
|
|
2057
|
+
errors.push(cycleError);
|
|
2058
|
+
}
|
|
2059
|
+
errors.push(...validateArtifactReferences(workflow));
|
|
2060
|
+
if (workflow.strict?.denylist && workflow.strict.denylist.length > 0) {
|
|
2061
|
+
for (const denylistPath of workflow.strict.denylist) {
|
|
2062
|
+
const pathResult = isValidCheckPath(denylistPath);
|
|
2063
|
+
if (!pathResult.valid) {
|
|
2064
|
+
errors.push(`Workflow strict.denylist path invalid: ${pathResult.error}`);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
if (workflow.strict?.denylistMode) {
|
|
2069
|
+
const mode = workflow.strict.denylistMode;
|
|
2070
|
+
if (mode !== "off" && mode !== "warn" && mode !== "strict") {
|
|
2071
|
+
errors.push(
|
|
2072
|
+
`Workflow strict.denylistMode invalid: '${String(mode)}' (expected off|warn|strict)`
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
if (validationNoSignalsStageIds.length > 0) {
|
|
2077
|
+
warnings.push(
|
|
2078
|
+
`Workflow has validation enabled but no outputs or checks defined for stages: ${validationNoSignalsStageIds.join(
|
|
2079
|
+
", "
|
|
2080
|
+
)}. This is informational only; add validation.checks or outputs for meaningful validation signals.`
|
|
2081
|
+
);
|
|
2082
|
+
}
|
|
2083
|
+
if (errors.length > 0) {
|
|
2084
|
+
return {
|
|
2085
|
+
valid: false,
|
|
2086
|
+
error: errors.join("; "),
|
|
2087
|
+
warnings: warnings.length > 0 ? warnings : void 0
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
return {
|
|
2091
|
+
valid: true,
|
|
2092
|
+
warnings: warnings.length > 0 ? warnings : void 0
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
function validateArtifactReferences(workflow) {
|
|
2096
|
+
const errors = [];
|
|
2097
|
+
const stageOutputs = /* @__PURE__ */ new Map();
|
|
2098
|
+
const stageMap = /* @__PURE__ */ new Map();
|
|
2099
|
+
for (const stage of workflow.stages ?? []) {
|
|
2100
|
+
stageMap.set(stage.id, stage);
|
|
2101
|
+
stageOutputs.set(stage.id, new Set(Object.keys(stage.outputs ?? {})));
|
|
2102
|
+
}
|
|
2103
|
+
const dependencyClosureCache = /* @__PURE__ */ new Map();
|
|
2104
|
+
const getDependencyClosure = (stageId) => {
|
|
2105
|
+
const cached = dependencyClosureCache.get(stageId);
|
|
2106
|
+
if (cached) return cached;
|
|
2107
|
+
const closure = /* @__PURE__ */ new Set();
|
|
2108
|
+
const visit = (depId) => {
|
|
2109
|
+
if (closure.has(depId)) return;
|
|
2110
|
+
closure.add(depId);
|
|
2111
|
+
const depStage = stageMap.get(depId);
|
|
2112
|
+
for (const transitiveDep of depStage?.deps ?? []) {
|
|
2113
|
+
visit(transitiveDep);
|
|
2114
|
+
}
|
|
2115
|
+
};
|
|
2116
|
+
const stage = stageMap.get(stageId);
|
|
2117
|
+
for (const dep of stage?.deps ?? []) {
|
|
2118
|
+
visit(dep);
|
|
2119
|
+
}
|
|
2120
|
+
dependencyClosureCache.set(stageId, closure);
|
|
2121
|
+
return closure;
|
|
2122
|
+
};
|
|
2123
|
+
for (const stage of workflow.stages ?? []) {
|
|
2124
|
+
for (const ref of stage.inputs ?? []) {
|
|
2125
|
+
try {
|
|
2126
|
+
const { stageId, outputName } = parseArtifactRef(ref);
|
|
2127
|
+
const outputs = stageOutputs.get(stageId);
|
|
2128
|
+
if (!outputs) {
|
|
2129
|
+
errors.push(
|
|
2130
|
+
`Stage '${stage.id}' references artifact '${ref}' from non-existent stage '${stageId}'`
|
|
2131
|
+
);
|
|
2132
|
+
continue;
|
|
2133
|
+
}
|
|
2134
|
+
if (!outputs.has(outputName)) {
|
|
2135
|
+
const available = outputs.size > 0 ? Array.from(outputs).join(", ") : "none";
|
|
2136
|
+
errors.push(
|
|
2137
|
+
`Stage '${stage.id}' references output '${outputName}' which doesn't exist in stage '${stageId}'. Available: [${available}]`
|
|
2138
|
+
);
|
|
2139
|
+
continue;
|
|
2140
|
+
}
|
|
2141
|
+
if (!getDependencyClosure(stage.id).has(stageId)) {
|
|
2142
|
+
errors.push(
|
|
2143
|
+
`Stage '${stage.id}' references artifact '${ref}' but does not depend on producer stage '${stageId}'`
|
|
2144
|
+
);
|
|
2145
|
+
}
|
|
2146
|
+
} catch (err) {
|
|
2147
|
+
errors.push(
|
|
2148
|
+
`Stage '${stage.id}' has invalid artifact reference '${ref}': ${err instanceof Error ? err.message : String(err)}`
|
|
2149
|
+
);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
return errors;
|
|
2154
|
+
}
|
|
2155
|
+
function detectCircularDependencies(stages) {
|
|
2156
|
+
const stageMap = new Map(stages.map((s) => [s.id, s]));
|
|
2157
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
2158
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2159
|
+
function dfs(stageId, path4) {
|
|
2160
|
+
if (visited.has(stageId)) {
|
|
2161
|
+
return null;
|
|
2162
|
+
}
|
|
2163
|
+
if (visiting.has(stageId)) {
|
|
2164
|
+
const cycleStart = path4.indexOf(stageId);
|
|
2165
|
+
const cycle = [...path4.slice(cycleStart), stageId].join(" \u2192 ");
|
|
2166
|
+
return `Circular dependency detected: ${cycle}`;
|
|
2167
|
+
}
|
|
2168
|
+
visiting.add(stageId);
|
|
2169
|
+
path4.push(stageId);
|
|
2170
|
+
const stage = stageMap.get(stageId);
|
|
2171
|
+
if (stage?.deps) {
|
|
2172
|
+
for (const dep of stage.deps) {
|
|
2173
|
+
const result = dfs(dep, [...path4]);
|
|
2174
|
+
if (result) {
|
|
2175
|
+
return result;
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
visiting.delete(stageId);
|
|
2180
|
+
visited.add(stageId);
|
|
2181
|
+
return null;
|
|
2182
|
+
}
|
|
2183
|
+
for (const stage of stages) {
|
|
2184
|
+
const result = dfs(stage.id, []);
|
|
2185
|
+
if (result) {
|
|
2186
|
+
return result;
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
return null;
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
// src/automation/flywheel-preflight.ts
|
|
2193
|
+
var PACKAGE_ROOT_ENV = "POETIC_PACKAGE_ROOT";
|
|
2194
|
+
function isCodexProviderInsideCodexSandbox(providerId) {
|
|
2195
|
+
return providerId === "codex" && process.env.CODEX_SANDBOX !== void 0;
|
|
2196
|
+
}
|
|
2197
|
+
async function checkRepoClean(cwd, mode, suppressExpectedSetupWarning = false) {
|
|
2198
|
+
const start = Date.now();
|
|
2199
|
+
const check = "repo-clean";
|
|
2200
|
+
const description = "Repository working directory is clean";
|
|
2201
|
+
try {
|
|
2202
|
+
const { stdout } = await execa2("git", ["status", "--porcelain"], {
|
|
2203
|
+
cwd,
|
|
2204
|
+
stdio: "pipe",
|
|
2205
|
+
timeout: 1e4
|
|
2206
|
+
});
|
|
2207
|
+
const lines = filterWorktreeRunSentinelStatusOutput(stdout).trim().split("\n").filter((line) => line.length > 0);
|
|
2208
|
+
if (lines.length === 0) {
|
|
2209
|
+
return {
|
|
2210
|
+
check,
|
|
2211
|
+
description,
|
|
2212
|
+
passed: true,
|
|
2213
|
+
durationMs: Date.now() - start
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
const extractStatusPaths = (line) => {
|
|
2217
|
+
const payload = line.slice(2).trimStart();
|
|
2218
|
+
if (payload.length === 0) return [];
|
|
2219
|
+
const stripQuotes = (value) => value.replace(/^"(.*)"$/, "$1").trim();
|
|
2220
|
+
if (payload.includes(" -> ")) {
|
|
2221
|
+
return payload.split(" -> ").map((part) => stripQuotes(part)).filter((part) => part.length > 0);
|
|
2222
|
+
}
|
|
2223
|
+
return [stripQuotes(payload)];
|
|
2224
|
+
};
|
|
2225
|
+
const changedPaths = lines.flatMap(extractStatusPaths);
|
|
2226
|
+
const pathSummary = summarizeRepoPaths(changedPaths);
|
|
2227
|
+
const conflictStatuses = /* @__PURE__ */ new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
|
|
2228
|
+
const hasConflictStatuses = lines.some((line) => conflictStatuses.has(line.slice(0, 2)));
|
|
2229
|
+
const runtimeArtifactOnly = pathSummary.categories.length > 0 && pathSummary.categories.every(
|
|
2230
|
+
(category) => category === "runtime-artifact" || category === "generated-artifact"
|
|
2231
|
+
);
|
|
2232
|
+
const expectedSetupDirt = pathSummary.systemWriteOnly;
|
|
2233
|
+
const unstaged = lines.filter((l) => l.startsWith(" M") || l.startsWith("??"));
|
|
2234
|
+
const staged = lines.filter((l) => l.startsWith("M ") || l.startsWith("A "));
|
|
2235
|
+
let message = `Repository has ${lines.length} uncommitted change(s):
|
|
2236
|
+
`;
|
|
2237
|
+
if (staged.length > 0) {
|
|
2238
|
+
message += ` Staged: ${staged.length} file(s)
|
|
2239
|
+
`;
|
|
2240
|
+
}
|
|
2241
|
+
if (unstaged.length > 0) {
|
|
2242
|
+
message += ` Unstaged/Untracked: ${unstaged.length} file(s)
|
|
2243
|
+
`;
|
|
2244
|
+
}
|
|
2245
|
+
if (hasConflictStatuses) {
|
|
2246
|
+
message += " Scope: merge/conflict state detected\n";
|
|
2247
|
+
} else if (pathSummary.systemWriteOnly) {
|
|
2248
|
+
message += " Scope: managed/runtime metadata only (task/, sprint/, backlog/, .poetic/codex-home/, .poetic/plans/, .poetic/planning/)\n";
|
|
2249
|
+
} else if (pathSummary.harmlessOnly) {
|
|
2250
|
+
message += " Scope: generated/runtime artifacts only\n";
|
|
2251
|
+
} else {
|
|
2252
|
+
message += " Scope: includes repository content changes\n";
|
|
2253
|
+
}
|
|
2254
|
+
message += `Run 'git status' to see details.`;
|
|
2255
|
+
const managedSetupOnly = pathSummary.systemWriteOnly || pathSummary.harmlessOnly;
|
|
2256
|
+
const metadata = {
|
|
2257
|
+
changedPaths,
|
|
2258
|
+
systemWriteOnly: pathSummary.systemWriteOnly,
|
|
2259
|
+
harmlessOnly: pathSummary.harmlessOnly,
|
|
2260
|
+
categories: pathSummary.categories,
|
|
2261
|
+
hasConflictStatuses,
|
|
2262
|
+
runtimeArtifactOnly,
|
|
2263
|
+
expectedSetupDirt
|
|
2264
|
+
};
|
|
2265
|
+
if (hasConflictStatuses || mode === "fail" && !runtimeArtifactOnly && !managedSetupOnly) {
|
|
2266
|
+
message += hasConflictStatuses ? " Resolve conflicts before running Flywheel." : " Commit or stash changes before running Flywheel (or run without strict cleanliness).";
|
|
2267
|
+
return {
|
|
2268
|
+
check,
|
|
2269
|
+
description,
|
|
2270
|
+
passed: false,
|
|
2271
|
+
error: message,
|
|
2272
|
+
metadata,
|
|
2273
|
+
durationMs: Date.now() - start
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
if (suppressExpectedSetupWarning && managedSetupOnly) {
|
|
2277
|
+
return {
|
|
2278
|
+
check,
|
|
2279
|
+
description,
|
|
2280
|
+
passed: true,
|
|
2281
|
+
metadata,
|
|
2282
|
+
durationMs: Date.now() - start
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
return {
|
|
2286
|
+
check,
|
|
2287
|
+
description,
|
|
2288
|
+
passed: true,
|
|
2289
|
+
warning: message,
|
|
2290
|
+
metadata,
|
|
2291
|
+
durationMs: Date.now() - start
|
|
2292
|
+
};
|
|
2293
|
+
} catch (err) {
|
|
2294
|
+
return {
|
|
2295
|
+
check,
|
|
2296
|
+
description,
|
|
2297
|
+
passed: false,
|
|
2298
|
+
error: `Failed to check git status: ${err instanceof Error ? err.message : String(err)}`,
|
|
2299
|
+
durationMs: Date.now() - start
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
async function checkWorktreeSanity(baseDir) {
|
|
2304
|
+
const start = Date.now();
|
|
2305
|
+
const check = "worktree-sanity";
|
|
2306
|
+
const description = "Worktree base directory is accessible and writable";
|
|
2307
|
+
try {
|
|
2308
|
+
await fs2.mkdir(baseDir, { recursive: true });
|
|
2309
|
+
const testFile = path3.join(baseDir, ".preflight-test");
|
|
2310
|
+
await fs2.writeFile(testFile, "test");
|
|
2311
|
+
await fs2.unlink(testFile);
|
|
2312
|
+
return {
|
|
2313
|
+
check,
|
|
2314
|
+
description,
|
|
2315
|
+
passed: true,
|
|
2316
|
+
durationMs: Date.now() - start
|
|
2317
|
+
};
|
|
2318
|
+
} catch (err) {
|
|
2319
|
+
return {
|
|
2320
|
+
check,
|
|
2321
|
+
description,
|
|
2322
|
+
passed: false,
|
|
2323
|
+
error: `Worktree base directory ${baseDir} is not writable: ${err instanceof Error ? err.message : String(err)}`,
|
|
2324
|
+
durationMs: Date.now() - start
|
|
2325
|
+
};
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
async function registeredWorktreeCount(repoRoot) {
|
|
2329
|
+
try {
|
|
2330
|
+
const result = await execGit(["worktree", "list", "--porcelain"], { cwd: repoRoot });
|
|
2331
|
+
if (result.exitCode !== 0) return void 0;
|
|
2332
|
+
return result.stdout.split("\n").filter((line) => line.startsWith("worktree ")).length;
|
|
2333
|
+
} catch {
|
|
2334
|
+
return void 0;
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
async function checkWorktreeHygiene(baseDir, repoRoot, hygieneConfig) {
|
|
2338
|
+
const start = Date.now();
|
|
2339
|
+
const check = "worktree-hygiene";
|
|
2340
|
+
const description = "Worktree count and age are within acceptable limits";
|
|
2341
|
+
const warnThreshold = hygieneConfig?.warnThreshold ?? 50;
|
|
2342
|
+
const blockThreshold = hygieneConfig?.blockThreshold ?? 100;
|
|
2343
|
+
const registeredWarnThreshold = hygieneConfig?.registeredWarnThreshold ?? warnThreshold;
|
|
2344
|
+
const staleAgeDays = hygieneConfig?.staleAgeDays ?? 7;
|
|
2345
|
+
const autoCleanup = hygieneConfig?.autoCleanup ?? false;
|
|
2346
|
+
const autoCleanupDryRun = hygieneConfig?.autoCleanupDryRun ?? false;
|
|
2347
|
+
try {
|
|
2348
|
+
const entries = await fs2.readdir(baseDir, { withFileTypes: true });
|
|
2349
|
+
const worktrees = entries.filter((e) => e.isDirectory());
|
|
2350
|
+
const totalCount = worktrees.length;
|
|
2351
|
+
const registeredCount = await registeredWorktreeCount(repoRoot);
|
|
2352
|
+
const staleAgeMs = staleAgeDays * 24 * 60 * 60 * 1e3;
|
|
2353
|
+
const now = Date.now();
|
|
2354
|
+
let staleCount = 0;
|
|
2355
|
+
for (const entry of worktrees) {
|
|
2356
|
+
try {
|
|
2357
|
+
const wtPath = path3.join(baseDir, entry.name);
|
|
2358
|
+
const stats = await fs2.stat(wtPath);
|
|
2359
|
+
const age = now - stats.mtimeMs;
|
|
2360
|
+
if (age > staleAgeMs) {
|
|
2361
|
+
staleCount++;
|
|
2362
|
+
}
|
|
2363
|
+
} catch {
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
let currentCount = totalCount;
|
|
2367
|
+
if (autoCleanup && staleCount > 0) {
|
|
2368
|
+
try {
|
|
2369
|
+
const cleanupManager = new WorktreeCleanupManager(baseDir);
|
|
2370
|
+
const report = await cleanupManager.cleanup({
|
|
2371
|
+
maxAgeMs: staleAgeMs,
|
|
2372
|
+
dryRun: autoCleanupDryRun
|
|
2373
|
+
});
|
|
2374
|
+
if (!autoCleanupDryRun && report.deleted > 0) {
|
|
2375
|
+
console.log(`Cleaned ${report.deleted} stale worktree(s) (older than ${staleAgeDays}d)`);
|
|
2376
|
+
const updatedEntries = await fs2.readdir(baseDir, { withFileTypes: true });
|
|
2377
|
+
const updatedWorktrees = updatedEntries.filter((e) => e.isDirectory());
|
|
2378
|
+
currentCount = updatedWorktrees.length;
|
|
2379
|
+
staleCount = 0;
|
|
2380
|
+
} else if (autoCleanupDryRun && report.deleted > 0) {
|
|
2381
|
+
console.log(
|
|
2382
|
+
`Would clean ${report.deleted} stale worktree(s) (older than ${staleAgeDays}d)`
|
|
2383
|
+
);
|
|
2384
|
+
}
|
|
2385
|
+
} catch (cleanupErr) {
|
|
2386
|
+
console.warn(
|
|
2387
|
+
`Auto-cleanup failed: ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}`
|
|
2388
|
+
);
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
const metadata = {
|
|
2392
|
+
baseDirectoryCount: currentCount,
|
|
2393
|
+
...registeredCount === void 0 ? {} : { registeredWorktreeCount: registeredCount }
|
|
2394
|
+
};
|
|
2395
|
+
if (currentCount >= blockThreshold) {
|
|
2396
|
+
return {
|
|
2397
|
+
check,
|
|
2398
|
+
description,
|
|
2399
|
+
passed: false,
|
|
2400
|
+
error: `Worktree count (${currentCount}) exceeds block threshold (${blockThreshold}). Run poetic cleanup doctor or poetic cleanup worktrees --show-preserved to review preserved categories (dirty, uninspectable, active-run, lab/manual-only, nested-child, external, missing path, too-recent, branch-attached/protected/current) before any cleanup.`,
|
|
2401
|
+
metadata,
|
|
2402
|
+
durationMs: Date.now() - start
|
|
2403
|
+
};
|
|
2404
|
+
}
|
|
2405
|
+
const warnings = [];
|
|
2406
|
+
if (currentCount >= warnThreshold) {
|
|
2407
|
+
warnings.push(
|
|
2408
|
+
`Worktree count (${currentCount}) exceeds warning threshold (${warnThreshold}). Consider cleaning up old worktrees.`
|
|
2409
|
+
);
|
|
2410
|
+
}
|
|
2411
|
+
if (registeredCount !== void 0 && registeredCount >= registeredWarnThreshold) {
|
|
2412
|
+
warnings.push(
|
|
2413
|
+
`Worktrees registered to this repository (${registeredCount}, across all linked roots) exceed advisory threshold (${registeredWarnThreshold}). Review with poetic cleanup worktrees --show-preserved --verbose.`
|
|
2414
|
+
);
|
|
2415
|
+
}
|
|
2416
|
+
if (!autoCleanup && staleCount > 0) {
|
|
2417
|
+
warnings.push(
|
|
2418
|
+
`Found ${staleCount} stale worktree(s) (older than ${staleAgeDays}d). Consider enabling --auto-cleanup.`
|
|
2419
|
+
);
|
|
2420
|
+
}
|
|
2421
|
+
if (warnings.length > 0) {
|
|
2422
|
+
return {
|
|
2423
|
+
check,
|
|
2424
|
+
description,
|
|
2425
|
+
passed: true,
|
|
2426
|
+
warning: warnings.join(" "),
|
|
2427
|
+
metadata,
|
|
2428
|
+
durationMs: Date.now() - start
|
|
2429
|
+
};
|
|
2430
|
+
}
|
|
2431
|
+
return {
|
|
2432
|
+
check,
|
|
2433
|
+
description,
|
|
2434
|
+
passed: true,
|
|
2435
|
+
metadata,
|
|
2436
|
+
durationMs: Date.now() - start
|
|
2437
|
+
};
|
|
2438
|
+
} catch (err) {
|
|
2439
|
+
return {
|
|
2440
|
+
check,
|
|
2441
|
+
description,
|
|
2442
|
+
passed: false,
|
|
2443
|
+
error: `Worktree hygiene check failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2444
|
+
durationMs: Date.now() - start
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
async function checkWorktreeRecovery(hygieneConfig) {
|
|
2449
|
+
const start = Date.now();
|
|
2450
|
+
const check = "worktree-recovery";
|
|
2451
|
+
const description = "Recover orphaned worktrees from prior runs";
|
|
2452
|
+
try {
|
|
2453
|
+
const maxAgeMs = (hygieneConfig?.staleAgeDays ?? 7) * 24 * 60 * 60 * 1e3;
|
|
2454
|
+
const { recoverPendingWorktrees: recovery, enforceRetentionBounds: retention } = await runFlywheelWorktreeHygiene({
|
|
2455
|
+
dryRun: hygieneConfig?.recoveryDryRun ?? true,
|
|
2456
|
+
verbose: false,
|
|
2457
|
+
maxAgeMs,
|
|
2458
|
+
maxCount: 20
|
|
2459
|
+
});
|
|
2460
|
+
const totalRecovered = recovery.recovered.length + recovery.alreadyGone.length;
|
|
2461
|
+
const totalPruned = retention.pruned.length;
|
|
2462
|
+
const totalErrors = recovery.errors.length + retention.errors.length;
|
|
2463
|
+
const metadata = {
|
|
2464
|
+
dryRun: hygieneConfig?.recoveryDryRun ?? true,
|
|
2465
|
+
recovered: recovery.recovered,
|
|
2466
|
+
recoveredCount: recovery.recovered.length,
|
|
2467
|
+
alreadyGone: recovery.alreadyGone,
|
|
2468
|
+
alreadyGoneCount: recovery.alreadyGone.length,
|
|
2469
|
+
skippedActive: recovery.skippedActive,
|
|
2470
|
+
skippedActiveCount: recovery.skippedActive.length,
|
|
2471
|
+
skippedDirty: recovery.skippedDirty,
|
|
2472
|
+
skippedDirtyCount: recovery.skippedDirty.length,
|
|
2473
|
+
pruned: retention.pruned,
|
|
2474
|
+
prunedCount: retention.pruned.length,
|
|
2475
|
+
errors: [...recovery.errors, ...retention.errors],
|
|
2476
|
+
errorCount: totalErrors,
|
|
2477
|
+
totalRecovered,
|
|
2478
|
+
totalPruned
|
|
2479
|
+
};
|
|
2480
|
+
let warning;
|
|
2481
|
+
if (totalRecovered > 0 || totalPruned > 0 || totalErrors > 0 || recovery.skippedActive.length > 0 || recovery.skippedDirty.length > 0) {
|
|
2482
|
+
const parts = [];
|
|
2483
|
+
if (recovery.recovered.length > 0) {
|
|
2484
|
+
parts.push(`recovered ${recovery.recovered.length} orphaned worktree(s)`);
|
|
2485
|
+
}
|
|
2486
|
+
if (recovery.alreadyGone.length > 0) {
|
|
2487
|
+
parts.push(`reconciled ${recovery.alreadyGone.length} already-removed record(s)`);
|
|
2488
|
+
}
|
|
2489
|
+
if (totalPruned > 0) {
|
|
2490
|
+
parts.push(`pruned ${totalPruned} over-retained worktree(s)`);
|
|
2491
|
+
}
|
|
2492
|
+
if (recovery.skippedActive.length > 0) {
|
|
2493
|
+
parts.push(`preserved ${recovery.skippedActive.length} active worktree(s)`);
|
|
2494
|
+
}
|
|
2495
|
+
if (recovery.skippedDirty.length > 0) {
|
|
2496
|
+
parts.push(`skipped ${recovery.skippedDirty.length} dirty worktree(s)`);
|
|
2497
|
+
}
|
|
2498
|
+
if (totalErrors > 0) {
|
|
2499
|
+
const sampleError = recovery.errors[0]?.error ?? retention.errors[0]?.error ?? "see logs for details";
|
|
2500
|
+
const trimmedSample = sampleError.length > 200 ? `${sampleError.slice(0, 197)}...` : sampleError;
|
|
2501
|
+
parts.push(`encountered ${totalErrors} error(s) during cleanup (first: ${trimmedSample})`);
|
|
2502
|
+
}
|
|
2503
|
+
warning = `Worktree recovery: ${parts.join("; ")}`;
|
|
2504
|
+
}
|
|
2505
|
+
return {
|
|
2506
|
+
check,
|
|
2507
|
+
description,
|
|
2508
|
+
passed: true,
|
|
2509
|
+
warning,
|
|
2510
|
+
metadata,
|
|
2511
|
+
durationMs: Date.now() - start
|
|
2512
|
+
};
|
|
2513
|
+
} catch (err) {
|
|
2514
|
+
return {
|
|
2515
|
+
check,
|
|
2516
|
+
description,
|
|
2517
|
+
passed: true,
|
|
2518
|
+
// Don't block execution on recovery failure
|
|
2519
|
+
warning: `Worktree recovery failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2520
|
+
durationMs: Date.now() - start
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
async function checkProviderAuth(workflow, cwd, authProbeEnv) {
|
|
2525
|
+
const start = Date.now();
|
|
2526
|
+
const check = "provider-auth";
|
|
2527
|
+
const description = "All providers are authenticated and available";
|
|
2528
|
+
try {
|
|
2529
|
+
const providers = /* @__PURE__ */ new Set();
|
|
2530
|
+
const implicitDefaultProviderStages = [];
|
|
2531
|
+
const defaultProvider = resolveDefaultProvider(void 0, cwd).value;
|
|
2532
|
+
for (const stage of workflow.stages) {
|
|
2533
|
+
const hasCompetitionVariants = Array.isArray(stage.competition?.variants) && stage.competition.variants.length > 0;
|
|
2534
|
+
if (stage.provider) {
|
|
2535
|
+
providers.add(stage.provider);
|
|
2536
|
+
} else if (stage.type !== "verify" && !hasCompetitionVariants) {
|
|
2537
|
+
providers.add(defaultProvider);
|
|
2538
|
+
implicitDefaultProviderStages.push(stage.id);
|
|
2539
|
+
}
|
|
2540
|
+
if (stage.orchestrator?.orchestrationProvider) {
|
|
2541
|
+
providers.add(stage.orchestrator.orchestrationProvider);
|
|
2542
|
+
}
|
|
2543
|
+
if (stage.competition?.variants) {
|
|
2544
|
+
for (const variant of stage.competition.variants) {
|
|
2545
|
+
providers.add(variant.provider);
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
if (providers.size === 0) {
|
|
2550
|
+
return {
|
|
2551
|
+
check,
|
|
2552
|
+
description,
|
|
2553
|
+
passed: true,
|
|
2554
|
+
metadata: {
|
|
2555
|
+
info: "No provider-backed stages found"
|
|
2556
|
+
},
|
|
2557
|
+
durationMs: Date.now() - start
|
|
2558
|
+
};
|
|
2559
|
+
}
|
|
2560
|
+
const errors = [];
|
|
2561
|
+
const warnings = [];
|
|
2562
|
+
for (const providerId of providers) {
|
|
2563
|
+
try {
|
|
2564
|
+
const isEnabled = ProviderRegistry.isProviderEnabled(providerId);
|
|
2565
|
+
if (!isEnabled) {
|
|
2566
|
+
errors.push(`Provider '${providerId}' is not enabled`);
|
|
2567
|
+
continue;
|
|
2568
|
+
}
|
|
2569
|
+
if (isCodexProviderInsideCodexSandbox(providerId)) {
|
|
2570
|
+
errors.push(
|
|
2571
|
+
"Provider 'codex' cannot run from inside Codex's outer sandbox; run Codex-provider stages from a normal terminal or choose a different provider."
|
|
2572
|
+
);
|
|
2573
|
+
continue;
|
|
2574
|
+
}
|
|
2575
|
+
const providerConfig = ProviderRegistry.getProviderConfig(providerId);
|
|
2576
|
+
if (providerConfig?.experimental) {
|
|
2577
|
+
warnings.push(
|
|
2578
|
+
`Provider '${providerId}' is experimental; startup behavior may be less stable.`
|
|
2579
|
+
);
|
|
2580
|
+
}
|
|
2581
|
+
const health = getProviderHealth(providerId);
|
|
2582
|
+
if (health.lastFailurePhase && health.consecutiveFailures > 0) {
|
|
2583
|
+
warnings.push(
|
|
2584
|
+
`Provider '${providerId}' has recorded ${health.lastFailurePhase} failure history (health ${health.score.toFixed(2)}); watch for recurrence.`
|
|
2585
|
+
);
|
|
2586
|
+
}
|
|
2587
|
+
for (const stage of workflow.stages) {
|
|
2588
|
+
const pinnedModels = [];
|
|
2589
|
+
if (stage.provider === providerId) {
|
|
2590
|
+
pinnedModels.push({ provider: providerId, model: stage.model, stageId: stage.id });
|
|
2591
|
+
}
|
|
2592
|
+
if (stage.orchestrator?.orchestrationProvider === providerId) {
|
|
2593
|
+
pinnedModels.push({
|
|
2594
|
+
provider: providerId,
|
|
2595
|
+
model: stage.orchestrator.orchestrationModel,
|
|
2596
|
+
stageId: stage.id
|
|
2597
|
+
});
|
|
2598
|
+
}
|
|
2599
|
+
if (stage.competition?.variants) {
|
|
2600
|
+
for (const variant of stage.competition.variants) {
|
|
2601
|
+
if (variant.provider === providerId) {
|
|
2602
|
+
pinnedModels.push({
|
|
2603
|
+
provider: providerId,
|
|
2604
|
+
model: variant.model,
|
|
2605
|
+
stageId: stage.id
|
|
2606
|
+
});
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
for (const pinned of pinnedModels) {
|
|
2611
|
+
try {
|
|
2612
|
+
ProviderRegistry.resolveModel(pinned.provider, pinned.model);
|
|
2613
|
+
} catch (err) {
|
|
2614
|
+
errors.push(
|
|
2615
|
+
`Stage '${pinned.stageId}' pins invalid model for provider '${pinned.provider}': ` + (err instanceof Error ? err.message : String(err))
|
|
2616
|
+
);
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
const readiness = await getProviderReadiness(providerId, {
|
|
2621
|
+
mode: "execute",
|
|
2622
|
+
authProbeMode: "passive",
|
|
2623
|
+
...authProbeEnv ? { authProbeEnv } : {}
|
|
2624
|
+
});
|
|
2625
|
+
if (readiness.known === false || readiness.configured === false) {
|
|
2626
|
+
errors.push(
|
|
2627
|
+
`Provider '${providerId}' is unavailable: ${readiness.reason ?? "provider is not configured for CLI execution"}`
|
|
2628
|
+
);
|
|
2629
|
+
} else if (!readiness.installed) {
|
|
2630
|
+
errors.push(
|
|
2631
|
+
`Provider '${providerId}' is unavailable: ${readiness.reason ?? "CLI is not installed"}`
|
|
2632
|
+
);
|
|
2633
|
+
} else if (!readiness.cliHealthy) {
|
|
2634
|
+
errors.push(
|
|
2635
|
+
`Provider '${providerId}' is unavailable: ${readiness.reason ?? "CLI health check failed"}`
|
|
2636
|
+
);
|
|
2637
|
+
} else if (readiness.sandboxCapable === false) {
|
|
2638
|
+
errors.push(
|
|
2639
|
+
`Provider '${providerId}' is unavailable: ${readiness.sandboxReason ?? readiness.reason ?? "sandbox capability check failed"}`
|
|
2640
|
+
);
|
|
2641
|
+
} else if (readiness.authenticated === "no") {
|
|
2642
|
+
errors.push(
|
|
2643
|
+
`Provider '${providerId}' has its CLI but authentication is missing.` + (readiness.authCommand ? ` ${readiness.authCommand}` : "")
|
|
2644
|
+
);
|
|
2645
|
+
} else if (readiness.authenticated === "unknown") {
|
|
2646
|
+
warnings.push(
|
|
2647
|
+
`Provider '${providerId}' auth status is unreported by passive preflight; runtime will defer to the provider CLI.`
|
|
2648
|
+
);
|
|
2649
|
+
}
|
|
2650
|
+
} catch (err) {
|
|
2651
|
+
warnings.push(
|
|
2652
|
+
`Could not verify provider '${providerId}': ${err instanceof Error ? err.message : String(err)}`
|
|
2653
|
+
);
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
if (errors.length > 0) {
|
|
2657
|
+
return {
|
|
2658
|
+
check,
|
|
2659
|
+
description,
|
|
2660
|
+
passed: false,
|
|
2661
|
+
error: errors.join("\n"),
|
|
2662
|
+
durationMs: Date.now() - start
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2665
|
+
return {
|
|
2666
|
+
check,
|
|
2667
|
+
description,
|
|
2668
|
+
passed: true,
|
|
2669
|
+
warning: warnings.length > 0 ? warnings.join("\n") : void 0,
|
|
2670
|
+
metadata: implicitDefaultProviderStages.length > 0 ? { implicitDefaultProvider: defaultProvider, implicitDefaultProviderStages } : void 0,
|
|
2671
|
+
durationMs: Date.now() - start
|
|
2672
|
+
};
|
|
2673
|
+
} catch (err) {
|
|
2674
|
+
return {
|
|
2675
|
+
check,
|
|
2676
|
+
description,
|
|
2677
|
+
passed: false,
|
|
2678
|
+
error: `Provider auth check failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2679
|
+
durationMs: Date.now() - start
|
|
2680
|
+
};
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
async function checkArtifactRefs(workflow) {
|
|
2684
|
+
const start = Date.now();
|
|
2685
|
+
const check = "artifact-refs";
|
|
2686
|
+
const description = "All artifact references are valid (DAG validation)";
|
|
2687
|
+
try {
|
|
2688
|
+
const errors = [];
|
|
2689
|
+
const stageOutputs = /* @__PURE__ */ new Map();
|
|
2690
|
+
for (const stage of workflow.stages) {
|
|
2691
|
+
const outputs = /* @__PURE__ */ new Set();
|
|
2692
|
+
if (stage.outputs) {
|
|
2693
|
+
for (const outputName of Object.keys(stage.outputs)) {
|
|
2694
|
+
outputs.add(outputName);
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
stageOutputs.set(stage.id, outputs);
|
|
2698
|
+
}
|
|
2699
|
+
const stageOrder = /* @__PURE__ */ new Map();
|
|
2700
|
+
for (let i = 0; i < workflow.stages.length; i++) {
|
|
2701
|
+
const stage = workflow.stages[i];
|
|
2702
|
+
if (stage) {
|
|
2703
|
+
stageOrder.set(stage.id, i);
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
const stageMap = new Map(workflow.stages.map((stage) => [stage.id, stage]));
|
|
2707
|
+
const closureCache = /* @__PURE__ */ new Map();
|
|
2708
|
+
const getDependencyClosure = (stageId) => {
|
|
2709
|
+
const cached = closureCache.get(stageId);
|
|
2710
|
+
if (cached) return cached;
|
|
2711
|
+
const closure = /* @__PURE__ */ new Set();
|
|
2712
|
+
const visit = (depId) => {
|
|
2713
|
+
if (closure.has(depId)) return;
|
|
2714
|
+
closure.add(depId);
|
|
2715
|
+
for (const transitiveDep of stageMap.get(depId)?.deps ?? []) {
|
|
2716
|
+
visit(transitiveDep);
|
|
2717
|
+
}
|
|
2718
|
+
};
|
|
2719
|
+
for (const dep of stageMap.get(stageId)?.deps ?? []) {
|
|
2720
|
+
visit(dep);
|
|
2721
|
+
}
|
|
2722
|
+
closureCache.set(stageId, closure);
|
|
2723
|
+
return closure;
|
|
2724
|
+
};
|
|
2725
|
+
for (const stage of workflow.stages) {
|
|
2726
|
+
if (!stage.inputs || stage.inputs.length === 0) continue;
|
|
2727
|
+
const stageIndex = stageOrder.get(stage.id) ?? 0;
|
|
2728
|
+
for (const ref of stage.inputs) {
|
|
2729
|
+
try {
|
|
2730
|
+
const { stageId, outputName } = parseArtifactRef(ref);
|
|
2731
|
+
if (!stageOutputs.has(stageId)) {
|
|
2732
|
+
errors.push(
|
|
2733
|
+
`Stage '${stage.id}' references artifact '${ref}' from non-existent stage '${stageId}'`
|
|
2734
|
+
);
|
|
2735
|
+
continue;
|
|
2736
|
+
}
|
|
2737
|
+
const outputs = stageOutputs.get(stageId);
|
|
2738
|
+
if (!outputs) {
|
|
2739
|
+
errors.push(
|
|
2740
|
+
`Stage '${stage.id}' references artifact '${ref}' from stage '${stageId}', but that stage has no recorded outputs`
|
|
2741
|
+
);
|
|
2742
|
+
continue;
|
|
2743
|
+
}
|
|
2744
|
+
if (!outputs.has(outputName)) {
|
|
2745
|
+
const available = outputs.size > 0 ? Array.from(outputs).join(", ") : "none";
|
|
2746
|
+
errors.push(
|
|
2747
|
+
`Stage '${stage.id}' references output '${outputName}' which doesn't exist in stage '${stageId}'. Available: [${available}]`
|
|
2748
|
+
);
|
|
2749
|
+
continue;
|
|
2750
|
+
}
|
|
2751
|
+
const sourceIndex = stageOrder.get(stageId) ?? 0;
|
|
2752
|
+
if (sourceIndex >= stageIndex) {
|
|
2753
|
+
errors.push(
|
|
2754
|
+
`Stage '${stage.id}' has forward reference to '${ref}' (source stage '${stageId}' must come before this stage)`
|
|
2755
|
+
);
|
|
2756
|
+
continue;
|
|
2757
|
+
}
|
|
2758
|
+
if (!getDependencyClosure(stage.id).has(stageId)) {
|
|
2759
|
+
errors.push(
|
|
2760
|
+
`Stage '${stage.id}' references artifact '${ref}' but does not depend on producer stage '${stageId}'`
|
|
2761
|
+
);
|
|
2762
|
+
}
|
|
2763
|
+
} catch (err) {
|
|
2764
|
+
errors.push(
|
|
2765
|
+
`Stage '${stage.id}' has invalid artifact reference '${ref}': ${err instanceof Error ? err.message : String(err)}`
|
|
2766
|
+
);
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
if (errors.length > 0) {
|
|
2771
|
+
return {
|
|
2772
|
+
check,
|
|
2773
|
+
description,
|
|
2774
|
+
passed: false,
|
|
2775
|
+
error: errors.join("\n"),
|
|
2776
|
+
durationMs: Date.now() - start
|
|
2777
|
+
};
|
|
2778
|
+
}
|
|
2779
|
+
return {
|
|
2780
|
+
check,
|
|
2781
|
+
description,
|
|
2782
|
+
passed: true,
|
|
2783
|
+
durationMs: Date.now() - start
|
|
2784
|
+
};
|
|
2785
|
+
} catch (err) {
|
|
2786
|
+
return {
|
|
2787
|
+
check,
|
|
2788
|
+
description,
|
|
2789
|
+
passed: false,
|
|
2790
|
+
error: `Artifact reference validation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2791
|
+
durationMs: Date.now() - start
|
|
2792
|
+
};
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
async function checkConfig(workflow) {
|
|
2796
|
+
const start = Date.now();
|
|
2797
|
+
const check = "config-validation";
|
|
2798
|
+
const description = "Workflow configuration is valid";
|
|
2799
|
+
try {
|
|
2800
|
+
const result = validateWorkflowConfig(workflow);
|
|
2801
|
+
if (!result.valid) {
|
|
2802
|
+
return {
|
|
2803
|
+
check,
|
|
2804
|
+
description,
|
|
2805
|
+
passed: false,
|
|
2806
|
+
error: result.error || "Configuration validation failed",
|
|
2807
|
+
durationMs: Date.now() - start
|
|
2808
|
+
};
|
|
2809
|
+
}
|
|
2810
|
+
return {
|
|
2811
|
+
check,
|
|
2812
|
+
description,
|
|
2813
|
+
passed: true,
|
|
2814
|
+
warning: result.warnings?.join("\n"),
|
|
2815
|
+
durationMs: Date.now() - start
|
|
2816
|
+
};
|
|
2817
|
+
} catch (err) {
|
|
2818
|
+
return {
|
|
2819
|
+
check,
|
|
2820
|
+
description,
|
|
2821
|
+
passed: false,
|
|
2822
|
+
error: `Configuration validation error: ${err instanceof Error ? err.message : String(err)}`,
|
|
2823
|
+
durationMs: Date.now() - start
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
async function checkContextUtilization(workflow, cwd) {
|
|
2828
|
+
const start = Date.now();
|
|
2829
|
+
const check = "context-utilization";
|
|
2830
|
+
const description = "Prompt context fits within context window limits";
|
|
2831
|
+
try {
|
|
2832
|
+
const estimator = new TokenEstimator();
|
|
2833
|
+
const warnings = [];
|
|
2834
|
+
const WARN_THRESHOLD = 0.65;
|
|
2835
|
+
const defaultProvider = resolveDefaultProvider(void 0, cwd).value;
|
|
2836
|
+
for (const stage of workflow.stages) {
|
|
2837
|
+
if (!stage.prompt) continue;
|
|
2838
|
+
const provider = stage.provider || defaultProvider;
|
|
2839
|
+
const model = stage.model || applyFlywheelModelDefaults(stage, provider);
|
|
2840
|
+
if (!model) continue;
|
|
2841
|
+
const decision = await estimator.analyzeTask(stage.prompt, provider, model);
|
|
2842
|
+
if (decision.utilizationPercent / 100 >= WARN_THRESHOLD) {
|
|
2843
|
+
const utilizationPct = decision.utilizationPercent.toFixed(1);
|
|
2844
|
+
const adjustedPct = decision.adjustedUtilizationPercent.toFixed(1);
|
|
2845
|
+
warnings.push(
|
|
2846
|
+
`Stage '${stage.id}': Prompt context utilization is ${utilizationPct}% (${adjustedPct}% with overhead) of ${decision.contextWindow.toLocaleString()} token context window. Estimated: ${decision.estimatedTokens.toLocaleString()} tokens. Consider splitting task or reducing context to improve reliability.`
|
|
2847
|
+
);
|
|
2848
|
+
}
|
|
2849
|
+
if (stage.competition?.variants && stage.prompt) {
|
|
2850
|
+
for (const variant of stage.competition.variants) {
|
|
2851
|
+
const variantProvider = variant.provider || provider;
|
|
2852
|
+
const variantModel = variant.model || applyFlywheelModelDefaults(stage, variantProvider);
|
|
2853
|
+
if (!variantModel) continue;
|
|
2854
|
+
if (variantProvider === provider && variantModel === model) continue;
|
|
2855
|
+
const variantDecision = await estimator.analyzeTask(
|
|
2856
|
+
stage.prompt,
|
|
2857
|
+
variantProvider,
|
|
2858
|
+
variantModel
|
|
2859
|
+
);
|
|
2860
|
+
if (variantDecision.utilizationPercent / 100 >= WARN_THRESHOLD) {
|
|
2861
|
+
const utilizationPct = variantDecision.utilizationPercent.toFixed(1);
|
|
2862
|
+
const adjustedPct = variantDecision.adjustedUtilizationPercent.toFixed(1);
|
|
2863
|
+
warnings.push(
|
|
2864
|
+
`Stage '${stage.id}' variant '${variant.provider}:${variant.strategy || "conservative"}': Prompt context utilization is ${utilizationPct}% (${adjustedPct}% with overhead) of ${variantDecision.contextWindow.toLocaleString()} token context window. Estimated: ${variantDecision.estimatedTokens.toLocaleString()} tokens. Consider splitting task or reducing context to improve reliability.`
|
|
2865
|
+
);
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
estimator.cleanup();
|
|
2871
|
+
return {
|
|
2872
|
+
check,
|
|
2873
|
+
description,
|
|
2874
|
+
passed: true,
|
|
2875
|
+
warning: warnings.length > 0 ? warnings.join("\n") : void 0,
|
|
2876
|
+
durationMs: Date.now() - start
|
|
2877
|
+
};
|
|
2878
|
+
} catch (err) {
|
|
2879
|
+
return {
|
|
2880
|
+
check,
|
|
2881
|
+
description,
|
|
2882
|
+
passed: true,
|
|
2883
|
+
warning: `Context utilization check failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2884
|
+
durationMs: Date.now() - start
|
|
2885
|
+
};
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
function getStringProp(obj, key) {
|
|
2889
|
+
if (typeof obj === "object" && obj !== null) {
|
|
2890
|
+
const val = obj[key];
|
|
2891
|
+
return typeof val === "string" ? val : void 0;
|
|
2892
|
+
}
|
|
2893
|
+
return void 0;
|
|
2894
|
+
}
|
|
2895
|
+
function extractFileRefs(prompt) {
|
|
2896
|
+
const refs = [];
|
|
2897
|
+
const pattern = /^\s*FILE:\s+(\S+)/gm;
|
|
2898
|
+
let match;
|
|
2899
|
+
while ((match = pattern.exec(prompt)) !== null) {
|
|
2900
|
+
const filePath = match[1]?.trim();
|
|
2901
|
+
if (filePath) {
|
|
2902
|
+
refs.push(filePath);
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
return refs;
|
|
2906
|
+
}
|
|
2907
|
+
function extractCreateAnnotatedFileRefs(prompt) {
|
|
2908
|
+
const createRefs = /* @__PURE__ */ new Set();
|
|
2909
|
+
const pattern = /^\s*FILE:\s+(\S+)([^\n]*)/gm;
|
|
2910
|
+
let match;
|
|
2911
|
+
while ((match = pattern.exec(prompt)) !== null) {
|
|
2912
|
+
const filePath = match[1]?.trim();
|
|
2913
|
+
const rest = match[2] ?? "";
|
|
2914
|
+
if (filePath && /^\s*\(\s*(?:new|create)\s*\)(?:\s|$)/i.test(rest)) {
|
|
2915
|
+
createRefs.add(filePath);
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
return createRefs;
|
|
2919
|
+
}
|
|
2920
|
+
async function checkTaskSpecFreshness(workflow, cwd) {
|
|
2921
|
+
const start = Date.now();
|
|
2922
|
+
const check = "task-spec-freshness";
|
|
2923
|
+
const description = "Task spec files exist and git refs are valid";
|
|
2924
|
+
const errors = [];
|
|
2925
|
+
const warnings = [];
|
|
2926
|
+
try {
|
|
2927
|
+
const displayPath = (filePath) => path3.relative(cwd, filePath).replace(/\\/g, "/");
|
|
2928
|
+
for (const stage of workflow.stages) {
|
|
2929
|
+
const stageUnknown = stage;
|
|
2930
|
+
const taskId = getStringProp(stageUnknown, "taskId");
|
|
2931
|
+
if (taskId) {
|
|
2932
|
+
const taskBucketPath = getBucketPathByKind("task", cwd);
|
|
2933
|
+
const taskFilePath = path3.join(taskBucketPath, `${taskId}.md`);
|
|
2934
|
+
try {
|
|
2935
|
+
await fs2.access(taskFilePath);
|
|
2936
|
+
} catch {
|
|
2937
|
+
errors.push(`Stage '${stage.id}': task file not found: ${displayPath(taskFilePath)}`);
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
const backlogItemId = getStringProp(stageUnknown, "backlogItemId");
|
|
2941
|
+
if (backlogItemId) {
|
|
2942
|
+
const backlogBucketPath = getBucketPathByKind("backlog", cwd);
|
|
2943
|
+
const backlogFilePath = path3.join(backlogBucketPath, `${backlogItemId}.md`);
|
|
2944
|
+
try {
|
|
2945
|
+
await fs2.access(backlogFilePath);
|
|
2946
|
+
} catch {
|
|
2947
|
+
errors.push(
|
|
2948
|
+
`Stage '${stage.id}': backlog file not found: ${displayPath(backlogFilePath)}`
|
|
2949
|
+
);
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
if (stage.prompt) {
|
|
2953
|
+
const fileRefs = extractFileRefs(stage.prompt);
|
|
2954
|
+
const createAnnotatedRefs = extractCreateAnnotatedFileRefs(stage.prompt);
|
|
2955
|
+
for (const fileRef of fileRefs) {
|
|
2956
|
+
const filePath = path3.isAbsolute(fileRef) ? fileRef : path3.join(cwd, fileRef);
|
|
2957
|
+
try {
|
|
2958
|
+
await fs2.access(filePath);
|
|
2959
|
+
} catch {
|
|
2960
|
+
if (!createAnnotatedRefs.has(fileRef)) {
|
|
2961
|
+
errors.push(`Stage '${stage.id}': FILE reference not found on disk: ${fileRef}`);
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
const baseRef = getStringProp(stageUnknown, "baseRef");
|
|
2967
|
+
if (baseRef) {
|
|
2968
|
+
try {
|
|
2969
|
+
await execa2("git", ["rev-parse", "--verify", baseRef], {
|
|
2970
|
+
cwd,
|
|
2971
|
+
stdio: "pipe",
|
|
2972
|
+
timeout: 1e4
|
|
2973
|
+
});
|
|
2974
|
+
} catch {
|
|
2975
|
+
errors.push(`Stage '${stage.id}': baseRef '${baseRef}' does not exist in git repository`);
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
const baseSha = getStringProp(stageUnknown, "baseSha");
|
|
2979
|
+
if (baseSha) {
|
|
2980
|
+
try {
|
|
2981
|
+
await execa2("git", ["rev-parse", "--verify", baseSha], {
|
|
2982
|
+
cwd,
|
|
2983
|
+
stdio: "pipe",
|
|
2984
|
+
timeout: 1e4
|
|
2985
|
+
});
|
|
2986
|
+
try {
|
|
2987
|
+
await execa2("git", ["merge-base", "--is-ancestor", baseSha, "HEAD"], {
|
|
2988
|
+
cwd,
|
|
2989
|
+
stdio: "pipe",
|
|
2990
|
+
timeout: 1e4
|
|
2991
|
+
});
|
|
2992
|
+
} catch {
|
|
2993
|
+
warnings.push(
|
|
2994
|
+
`Stage '${stage.id}': baseSha '${baseSha}' exists but is not an ancestor of HEAD \u2014 may reference a rebased or force-pushed commit`
|
|
2995
|
+
);
|
|
2996
|
+
}
|
|
2997
|
+
} catch {
|
|
2998
|
+
errors.push(
|
|
2999
|
+
`Stage '${stage.id}': baseSha '${baseSha}' does not exist in git repository (SHA unreachable \u2014 commit may have been pruned or the ref was never pushed)`
|
|
3000
|
+
);
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
if (errors.length > 0) {
|
|
3005
|
+
return {
|
|
3006
|
+
check,
|
|
3007
|
+
description,
|
|
3008
|
+
passed: false,
|
|
3009
|
+
error: errors.join("\n"),
|
|
3010
|
+
warning: warnings.length > 0 ? warnings.join("\n") : void 0,
|
|
3011
|
+
durationMs: Date.now() - start
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
return {
|
|
3015
|
+
check,
|
|
3016
|
+
description,
|
|
3017
|
+
passed: true,
|
|
3018
|
+
warning: warnings.length > 0 ? warnings.join("\n") : void 0,
|
|
3019
|
+
durationMs: Date.now() - start
|
|
3020
|
+
};
|
|
3021
|
+
} catch (err) {
|
|
3022
|
+
return {
|
|
3023
|
+
check,
|
|
3024
|
+
description,
|
|
3025
|
+
passed: false,
|
|
3026
|
+
error: `Task spec freshness check failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
3027
|
+
durationMs: Date.now() - start
|
|
3028
|
+
};
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
async function collectBuildInputFiles(dir) {
|
|
3032
|
+
let entries;
|
|
3033
|
+
try {
|
|
3034
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
3035
|
+
} catch (error) {
|
|
3036
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
3037
|
+
return [];
|
|
3038
|
+
}
|
|
3039
|
+
throw error;
|
|
3040
|
+
}
|
|
3041
|
+
const files = [];
|
|
3042
|
+
for (const entry of entries) {
|
|
3043
|
+
const fullPath = path3.join(dir, entry.name);
|
|
3044
|
+
if (entry.isDirectory()) {
|
|
3045
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git") {
|
|
3046
|
+
continue;
|
|
3047
|
+
}
|
|
3048
|
+
files.push(...await collectBuildInputFiles(fullPath));
|
|
3049
|
+
continue;
|
|
3050
|
+
}
|
|
3051
|
+
if (!entry.isFile()) continue;
|
|
3052
|
+
if (!entry.name.endsWith(".ts") && !entry.name.endsWith(".tsx")) continue;
|
|
3053
|
+
if (entry.name.endsWith(".test.ts") || entry.name.endsWith(".spec.ts")) continue;
|
|
3054
|
+
files.push(fullPath);
|
|
3055
|
+
}
|
|
3056
|
+
return files;
|
|
3057
|
+
}
|
|
3058
|
+
async function collectGeneratedProviderFiles(dir) {
|
|
3059
|
+
let entries;
|
|
3060
|
+
try {
|
|
3061
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
3062
|
+
} catch (error) {
|
|
3063
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
3064
|
+
return [];
|
|
3065
|
+
}
|
|
3066
|
+
throw error;
|
|
3067
|
+
}
|
|
3068
|
+
const files = [];
|
|
3069
|
+
for (const entry of entries) {
|
|
3070
|
+
const fullPath = path3.join(dir, entry.name);
|
|
3071
|
+
if (entry.isDirectory()) {
|
|
3072
|
+
files.push(...await collectGeneratedProviderFiles(fullPath));
|
|
3073
|
+
continue;
|
|
3074
|
+
}
|
|
3075
|
+
if (!entry.isFile()) continue;
|
|
3076
|
+
if (entry.name.endsWith(".lock")) continue;
|
|
3077
|
+
if (!entry.name.endsWith(".json")) continue;
|
|
3078
|
+
files.push(fullPath);
|
|
3079
|
+
}
|
|
3080
|
+
return files;
|
|
3081
|
+
}
|
|
3082
|
+
var BUILD_INPUT_PATHS = [
|
|
3083
|
+
"src",
|
|
3084
|
+
"package.json",
|
|
3085
|
+
"tsconfig.json",
|
|
3086
|
+
"tsup.config.ts",
|
|
3087
|
+
".poetic/config/providers.yaml",
|
|
3088
|
+
"providers"
|
|
3089
|
+
];
|
|
3090
|
+
function parseLineOutput(stdout) {
|
|
3091
|
+
return stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
3092
|
+
}
|
|
3093
|
+
async function getDirtyBuildInputPaths(projectRoot) {
|
|
3094
|
+
try {
|
|
3095
|
+
const tracked = await execa2(
|
|
3096
|
+
"git",
|
|
3097
|
+
["diff", "--name-only", "HEAD", "--", ...BUILD_INPUT_PATHS],
|
|
3098
|
+
{
|
|
3099
|
+
cwd: projectRoot,
|
|
3100
|
+
stdio: "pipe",
|
|
3101
|
+
reject: false
|
|
3102
|
+
}
|
|
3103
|
+
);
|
|
3104
|
+
const untracked = await execa2(
|
|
3105
|
+
"git",
|
|
3106
|
+
["ls-files", "--others", "--exclude-standard", "--", ...BUILD_INPUT_PATHS],
|
|
3107
|
+
{
|
|
3108
|
+
cwd: projectRoot,
|
|
3109
|
+
stdio: "pipe",
|
|
3110
|
+
reject: false
|
|
3111
|
+
}
|
|
3112
|
+
);
|
|
3113
|
+
if (tracked.exitCode !== 0 || untracked.exitCode !== 0) {
|
|
3114
|
+
return null;
|
|
3115
|
+
}
|
|
3116
|
+
return new Set(
|
|
3117
|
+
[...parseLineOutput(tracked.stdout), ...parseLineOutput(untracked.stdout)].map(
|
|
3118
|
+
(relativePath) => path3.join(projectRoot, relativePath)
|
|
3119
|
+
)
|
|
3120
|
+
);
|
|
3121
|
+
} catch {
|
|
3122
|
+
return null;
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
async function hasSourceBuildInputs(projectRoot) {
|
|
3126
|
+
try {
|
|
3127
|
+
await fs2.stat(path3.join(projectRoot, "src"));
|
|
3128
|
+
await fs2.stat(path3.join(projectRoot, "tsup.config.ts"));
|
|
3129
|
+
return true;
|
|
3130
|
+
} catch (error) {
|
|
3131
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
3132
|
+
return false;
|
|
3133
|
+
}
|
|
3134
|
+
throw error;
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
async function getLatestCommittedBuildInput(projectRoot) {
|
|
3138
|
+
try {
|
|
3139
|
+
const latestCommit = await execa2(
|
|
3140
|
+
"git",
|
|
3141
|
+
["log", "-1", "--format=%ct%x00%H", "--", ...BUILD_INPUT_PATHS],
|
|
3142
|
+
{
|
|
3143
|
+
cwd: projectRoot,
|
|
3144
|
+
stdio: "pipe",
|
|
3145
|
+
reject: false
|
|
3146
|
+
}
|
|
3147
|
+
);
|
|
3148
|
+
if (latestCommit.exitCode !== 0 || !latestCommit.stdout) {
|
|
3149
|
+
return null;
|
|
3150
|
+
}
|
|
3151
|
+
const [timestampRaw, commitSha] = latestCommit.stdout.split("\0");
|
|
3152
|
+
const committedAtMs = Number.parseInt(timestampRaw ?? "", 10) * 1e3;
|
|
3153
|
+
if (!Number.isFinite(committedAtMs) || !commitSha) {
|
|
3154
|
+
return null;
|
|
3155
|
+
}
|
|
3156
|
+
const latestPaths = await execa2(
|
|
3157
|
+
"git",
|
|
3158
|
+
["diff-tree", "--no-commit-id", "--name-only", "-r", commitSha, "--", ...BUILD_INPUT_PATHS],
|
|
3159
|
+
{
|
|
3160
|
+
cwd: projectRoot,
|
|
3161
|
+
stdio: "pipe",
|
|
3162
|
+
reject: false
|
|
3163
|
+
}
|
|
3164
|
+
);
|
|
3165
|
+
const latestPath = latestPaths.exitCode === 0 ? parseLineOutput(latestPaths.stdout)[0] : void 0;
|
|
3166
|
+
return {
|
|
3167
|
+
path: latestPath,
|
|
3168
|
+
committedAtMs
|
|
3169
|
+
};
|
|
3170
|
+
} catch {
|
|
3171
|
+
return null;
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
function resolvePoeticPackageRoot() {
|
|
3175
|
+
const configuredRoot = process.env[PACKAGE_ROOT_ENV]?.trim();
|
|
3176
|
+
if (configuredRoot) return path3.resolve(configuredRoot);
|
|
3177
|
+
let current = path3.dirname(fileURLToPath(import.meta.url));
|
|
3178
|
+
let distOnlyPackageRoot;
|
|
3179
|
+
while (true) {
|
|
3180
|
+
if (path3.basename(current) === "dist" && existsSync2(path3.join(current, "entry.js"))) {
|
|
3181
|
+
distOnlyPackageRoot ??= path3.dirname(current);
|
|
3182
|
+
}
|
|
3183
|
+
const packageJsonPath = path3.join(current, "package.json");
|
|
3184
|
+
if (existsSync2(packageJsonPath)) {
|
|
3185
|
+
try {
|
|
3186
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
3187
|
+
if (packageJson.name === "@poetic-ai/poetic") {
|
|
3188
|
+
return current;
|
|
3189
|
+
}
|
|
3190
|
+
} catch {
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
const parent = path3.dirname(current);
|
|
3194
|
+
if (parent === current) {
|
|
3195
|
+
return distOnlyPackageRoot ?? getProjectRoot();
|
|
3196
|
+
}
|
|
3197
|
+
current = parent;
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
async function checkBuildFreshness(projectRoot = resolvePoeticPackageRoot()) {
|
|
3201
|
+
const start = Date.now();
|
|
3202
|
+
const check = "build-freshness";
|
|
3203
|
+
const description = "Built CLI artifacts are up-to-date";
|
|
3204
|
+
const distEntry = path3.join(projectRoot, "dist", "entry.js");
|
|
3205
|
+
try {
|
|
3206
|
+
let distStat;
|
|
3207
|
+
try {
|
|
3208
|
+
distStat = await fs2.stat(distEntry);
|
|
3209
|
+
} catch {
|
|
3210
|
+
return {
|
|
3211
|
+
check,
|
|
3212
|
+
description,
|
|
3213
|
+
passed: false,
|
|
3214
|
+
error: "dist/entry.js not found. Run `npm run build` before executing the pipeline.",
|
|
3215
|
+
durationMs: Date.now() - start
|
|
3216
|
+
};
|
|
3217
|
+
}
|
|
3218
|
+
const distMtimeMs = distStat.mtimeMs;
|
|
3219
|
+
const distEntrySource = await fs2.readFile(distEntry, "utf8");
|
|
3220
|
+
for (const relativeChunk of collectReferencedDistChunks(distEntrySource)) {
|
|
3221
|
+
const chunkPath = path3.join(projectRoot, "dist", relativeChunk);
|
|
3222
|
+
try {
|
|
3223
|
+
await fs2.stat(chunkPath);
|
|
3224
|
+
} catch {
|
|
3225
|
+
return {
|
|
3226
|
+
check,
|
|
3227
|
+
description,
|
|
3228
|
+
passed: false,
|
|
3229
|
+
error: `Build is inconsistent: dist/entry.js references missing dist/${relativeChunk}. Run \`npm run build\` before executing.`,
|
|
3230
|
+
durationMs: Date.now() - start
|
|
3231
|
+
};
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
const sourceBuildInputsPresent = await hasSourceBuildInputs(projectRoot);
|
|
3235
|
+
if (!sourceBuildInputsPresent) {
|
|
3236
|
+
return {
|
|
3237
|
+
check,
|
|
3238
|
+
description,
|
|
3239
|
+
passed: true,
|
|
3240
|
+
metadata: { packageMode: true },
|
|
3241
|
+
durationMs: Date.now() - start
|
|
3242
|
+
};
|
|
3243
|
+
}
|
|
3244
|
+
const buildInputs = [
|
|
3245
|
+
...await collectBuildInputFiles(path3.join(projectRoot, "src")),
|
|
3246
|
+
...await collectGeneratedProviderFiles(path3.join(projectRoot, "providers")),
|
|
3247
|
+
...BUILD_INPUT_PATHS.slice(1).filter((relativePath) => relativePath !== "providers").map((relativePath) => path3.join(projectRoot, relativePath))
|
|
3248
|
+
];
|
|
3249
|
+
const dirtyBuildInputPaths = await getDirtyBuildInputPaths(projectRoot);
|
|
3250
|
+
const latestCommittedBuildInput = dirtyBuildInputPaths ? await getLatestCommittedBuildInput(projectRoot) : null;
|
|
3251
|
+
let newestInputPath;
|
|
3252
|
+
let newestInputMtimeMs = Number.NEGATIVE_INFINITY;
|
|
3253
|
+
for (const buildInput of buildInputs) {
|
|
3254
|
+
try {
|
|
3255
|
+
const inputStat = await fs2.stat(buildInput);
|
|
3256
|
+
const isDirty = dirtyBuildInputPaths?.has(buildInput) ?? true;
|
|
3257
|
+
if (isDirty && inputStat.mtimeMs > distMtimeMs) {
|
|
3258
|
+
return {
|
|
3259
|
+
check,
|
|
3260
|
+
description,
|
|
3261
|
+
passed: false,
|
|
3262
|
+
error: `Build is stale: ${path3.relative(projectRoot, buildInput)} is newer than dist/entry.js. Run \`npm run build\` before executing.`,
|
|
3263
|
+
durationMs: Date.now() - start
|
|
3264
|
+
};
|
|
3265
|
+
}
|
|
3266
|
+
if (inputStat.mtimeMs > newestInputMtimeMs) {
|
|
3267
|
+
newestInputMtimeMs = inputStat.mtimeMs;
|
|
3268
|
+
newestInputPath = buildInput;
|
|
3269
|
+
}
|
|
3270
|
+
} catch (error) {
|
|
3271
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
3272
|
+
continue;
|
|
3273
|
+
}
|
|
3274
|
+
throw error;
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
if (latestCommittedBuildInput && latestCommittedBuildInput.committedAtMs > distMtimeMs) {
|
|
3278
|
+
const rel = latestCommittedBuildInput.path ? latestCommittedBuildInput.path : BUILD_INPUT_PATHS.join(", ");
|
|
3279
|
+
return {
|
|
3280
|
+
check,
|
|
3281
|
+
description,
|
|
3282
|
+
passed: false,
|
|
3283
|
+
error: `Build is stale: ${rel} is newer than dist/entry.js. Run \`npm run build\` before executing.`,
|
|
3284
|
+
durationMs: Date.now() - start
|
|
3285
|
+
};
|
|
3286
|
+
}
|
|
3287
|
+
if (!dirtyBuildInputPaths && newestInputPath && newestInputMtimeMs > distMtimeMs) {
|
|
3288
|
+
const rel = path3.relative(projectRoot, newestInputPath);
|
|
3289
|
+
return {
|
|
3290
|
+
check,
|
|
3291
|
+
description,
|
|
3292
|
+
passed: false,
|
|
3293
|
+
error: `Build is stale: ${rel} is newer than dist/entry.js. Run \`npm run build\` before executing.`,
|
|
3294
|
+
durationMs: Date.now() - start
|
|
3295
|
+
};
|
|
3296
|
+
}
|
|
3297
|
+
return {
|
|
3298
|
+
check,
|
|
3299
|
+
description,
|
|
3300
|
+
passed: true,
|
|
3301
|
+
durationMs: Date.now() - start
|
|
3302
|
+
};
|
|
3303
|
+
} catch (err) {
|
|
3304
|
+
return {
|
|
3305
|
+
check,
|
|
3306
|
+
description,
|
|
3307
|
+
passed: false,
|
|
3308
|
+
error: `Build freshness check failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
3309
|
+
durationMs: Date.now() - start
|
|
3310
|
+
};
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
async function ensureFreshBuild(options) {
|
|
3314
|
+
if (process.env.POETIC_CLI_REEXEC_AFTER_BUILD === "1") {
|
|
3315
|
+
return { fresh: true, rebuilt: false };
|
|
3316
|
+
}
|
|
3317
|
+
const initialResult = await checkBuildFreshness();
|
|
3318
|
+
if (initialResult.passed) {
|
|
3319
|
+
return { fresh: true, rebuilt: false };
|
|
3320
|
+
}
|
|
3321
|
+
const originalError = initialResult.error ?? "Build is stale or missing.";
|
|
3322
|
+
const quiet = options?.quiet ?? isQuietOutputRequested();
|
|
3323
|
+
if (!quiet) {
|
|
3324
|
+
console.log(chalk.yellow("Build is stale. Auto-rebuilding..."));
|
|
3325
|
+
}
|
|
3326
|
+
const projectRoot = resolvePoeticPackageRoot();
|
|
3327
|
+
const buildResult = await execa2("npm", ["run", "build"], {
|
|
3328
|
+
cwd: projectRoot,
|
|
3329
|
+
stdio: quiet ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
3330
|
+
reject: false
|
|
3331
|
+
});
|
|
3332
|
+
if (buildResult.exitCode !== 0) {
|
|
3333
|
+
const output = [buildResult.stderr, buildResult.stdout].map((value) => value?.trim()).filter(Boolean).join("\n").slice(0, 4e3);
|
|
3334
|
+
throw new Error(
|
|
3335
|
+
`npm run build failed with exit code ${buildResult.exitCode}${output ? `:
|
|
3336
|
+
${output}` : ""}`
|
|
3337
|
+
);
|
|
3338
|
+
}
|
|
3339
|
+
try {
|
|
3340
|
+
const headResult = await execa2("git", ["rev-parse", "HEAD"], {
|
|
3341
|
+
cwd: projectRoot,
|
|
3342
|
+
stdio: "pipe"
|
|
3343
|
+
});
|
|
3344
|
+
const gitHead = headResult.stdout.trim();
|
|
3345
|
+
if (gitHead) {
|
|
3346
|
+
const metaPath = path3.join(projectRoot, "dist", ".poetic-build-meta.json");
|
|
3347
|
+
await fs2.writeFile(
|
|
3348
|
+
metaPath,
|
|
3349
|
+
`${JSON.stringify({ gitHead, builtAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
|
|
3350
|
+
`,
|
|
3351
|
+
"utf8"
|
|
3352
|
+
);
|
|
3353
|
+
}
|
|
3354
|
+
} catch {
|
|
3355
|
+
}
|
|
3356
|
+
const verifyResult = await checkBuildFreshness(projectRoot);
|
|
3357
|
+
if (verifyResult.passed) {
|
|
3358
|
+
return { fresh: true, rebuilt: true };
|
|
3359
|
+
}
|
|
3360
|
+
throw new Error(`${originalError} Auto-rebuild did not resolve the issue.`);
|
|
3361
|
+
}
|
|
3362
|
+
function collectReferencedDistChunks(entrySource) {
|
|
3363
|
+
const matches = entrySource.matchAll(
|
|
3364
|
+
/(?:from\s+|import\()\s*['"]\.\/([^'"]+\.js(?:\.map)?)['"]/g
|
|
3365
|
+
);
|
|
3366
|
+
const chunks = /* @__PURE__ */ new Set();
|
|
3367
|
+
for (const match of matches) {
|
|
3368
|
+
const chunk = match[1];
|
|
3369
|
+
if (!chunk || chunk === "entry.js") continue;
|
|
3370
|
+
chunks.add(chunk);
|
|
3371
|
+
}
|
|
3372
|
+
return [...chunks];
|
|
3373
|
+
}
|
|
3374
|
+
var CLI_REEXEC_ENV = "POETIC_CLI_REEXEC_AFTER_BUILD";
|
|
3375
|
+
var JSONL_CAPTURE_FILE_ENV = "POETIC_JSONL_CAPTURE_FILE";
|
|
3376
|
+
var REEXEC_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
3377
|
+
function signalToExitCode(signal) {
|
|
3378
|
+
switch (signal) {
|
|
3379
|
+
case "SIGINT":
|
|
3380
|
+
return 130;
|
|
3381
|
+
case "SIGTERM":
|
|
3382
|
+
return 143;
|
|
3383
|
+
case "SIGHUP":
|
|
3384
|
+
return 129;
|
|
3385
|
+
default:
|
|
3386
|
+
return signal ? 1 : null;
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
function resolveCliReexecEntry() {
|
|
3390
|
+
const packageRoot = resolvePoeticPackageRoot();
|
|
3391
|
+
const fallbackEntry = path3.join(packageRoot, "dist", "entry.js");
|
|
3392
|
+
const invokedEntry = process.argv[1] ? path3.resolve(process.argv[1]) : fallbackEntry;
|
|
3393
|
+
return invokedEntry.endsWith(".js") || invokedEntry.endsWith(".mjs") ? invokedEntry : fallbackEntry;
|
|
3394
|
+
}
|
|
3395
|
+
function writeAllSyncToFd(fd, text) {
|
|
3396
|
+
const buffer = Buffer.from(text, "utf8");
|
|
3397
|
+
let offset = 0;
|
|
3398
|
+
let noProgressAttempts = 0;
|
|
3399
|
+
while (offset < buffer.length) {
|
|
3400
|
+
try {
|
|
3401
|
+
const written = writeSync(fd, buffer, offset, buffer.length - offset);
|
|
3402
|
+
offset += written;
|
|
3403
|
+
if (written === 0) {
|
|
3404
|
+
noProgressAttempts = recordSyncWriteNoProgress("writeAllSyncToFd", noProgressAttempts);
|
|
3405
|
+
} else {
|
|
3406
|
+
noProgressAttempts = 0;
|
|
3407
|
+
}
|
|
3408
|
+
} catch (error) {
|
|
3409
|
+
if (error.code === "EAGAIN") {
|
|
3410
|
+
noProgressAttempts = recordSyncWriteNoProgress("writeAllSyncToFd", noProgressAttempts);
|
|
3411
|
+
continue;
|
|
3412
|
+
}
|
|
3413
|
+
throw error;
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
var MAX_CONSECUTIVE_ZERO_BYTE_WRITES = 1e3;
|
|
3418
|
+
var TRUSTED_LSOF_PATHS = ["/usr/sbin/lsof", "/usr/bin/lsof"];
|
|
3419
|
+
function recordSyncWriteNoProgress(functionName, attempts) {
|
|
3420
|
+
const nextAttempts = attempts + 1;
|
|
3421
|
+
if (nextAttempts > MAX_CONSECUTIVE_ZERO_BYTE_WRITES) {
|
|
3422
|
+
throw new Error(`${functionName} made no progress (repeated 0-byte writes or EAGAIN)`);
|
|
3423
|
+
}
|
|
3424
|
+
return nextAttempts;
|
|
3425
|
+
}
|
|
3426
|
+
function resolveTrustedLsofPath() {
|
|
3427
|
+
for (const candidate of TRUSTED_LSOF_PATHS) {
|
|
3428
|
+
try {
|
|
3429
|
+
if (!statSync(candidate).isFile()) continue;
|
|
3430
|
+
accessSync(candidate, fsConstants.X_OK);
|
|
3431
|
+
return candidate;
|
|
3432
|
+
} catch {
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
return void 0;
|
|
3436
|
+
}
|
|
3437
|
+
function resolveStdoutRegularFilePath() {
|
|
3438
|
+
try {
|
|
3439
|
+
const stat3 = fstatSync(1);
|
|
3440
|
+
if (!stat3.isFile()) return void 0;
|
|
3441
|
+
} catch {
|
|
3442
|
+
return void 0;
|
|
3443
|
+
}
|
|
3444
|
+
const lsofPath = resolveTrustedLsofPath();
|
|
3445
|
+
if (!lsofPath) return void 0;
|
|
3446
|
+
try {
|
|
3447
|
+
const output = execFileSync(lsofPath, ["-p", String(process.pid), "-a", "-d", "1", "-Fn"], {
|
|
3448
|
+
encoding: "utf8",
|
|
3449
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
3450
|
+
timeout: 1e3
|
|
3451
|
+
});
|
|
3452
|
+
const rawPath = output.split("\n").find((line) => line.startsWith("n/"))?.slice(1).replace(/ \(deleted\)$/, "");
|
|
3453
|
+
if (!rawPath) return void 0;
|
|
3454
|
+
const pathStat = statSync(rawPath);
|
|
3455
|
+
return pathStat.isFile() ? rawPath : void 0;
|
|
3456
|
+
} catch {
|
|
3457
|
+
return void 0;
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
function writeJsonStdoutText(text, stdoutFilePath) {
|
|
3461
|
+
if (!text) return;
|
|
3462
|
+
if (stdoutFilePath) {
|
|
3463
|
+
writeFileSync(stdoutFilePath, text, "utf8");
|
|
3464
|
+
return;
|
|
3465
|
+
}
|
|
3466
|
+
writeAllSyncToFd(1, text);
|
|
3467
|
+
}
|
|
3468
|
+
function argvRequestsJsonOutput(argv) {
|
|
3469
|
+
return argv.includes("--json") || argv.includes("-j") || argv.includes("--output=json");
|
|
3470
|
+
}
|
|
3471
|
+
async function reexecCurrentCliCommand(cwd) {
|
|
3472
|
+
const entryPath = resolveCliReexecEntry();
|
|
3473
|
+
const captureJsonStdout = isJsonOutputRequested() || argvRequestsJsonOutput(process.argv);
|
|
3474
|
+
const jsonCaptureDir = captureJsonStdout ? await fs2.mkdtemp(path3.join(os2.tmpdir(), "poetic-jsonl-reexec-")) : void 0;
|
|
3475
|
+
const jsonCaptureFile = jsonCaptureDir ? path3.join(jsonCaptureDir, "stdout.jsonl") : void 0;
|
|
3476
|
+
const stdoutFilePath = captureJsonStdout ? resolveStdoutRegularFilePath() : void 0;
|
|
3477
|
+
signalCoordinator.suspendProcessSignalHandlers();
|
|
3478
|
+
let child = null;
|
|
3479
|
+
let pendingSignal = null;
|
|
3480
|
+
const forwardSignal = (signal) => {
|
|
3481
|
+
if (!child) {
|
|
3482
|
+
pendingSignal = signal;
|
|
3483
|
+
return;
|
|
3484
|
+
}
|
|
3485
|
+
if (process.platform !== "win32" && typeof child.pid === "number") {
|
|
3486
|
+
try {
|
|
3487
|
+
process.kill(child.pid, signal);
|
|
3488
|
+
return;
|
|
3489
|
+
} catch {
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
child.kill?.(signal);
|
|
3493
|
+
};
|
|
3494
|
+
const forwardSigint = () => forwardSignal("SIGINT");
|
|
3495
|
+
const forwardSigterm = () => forwardSignal("SIGTERM");
|
|
3496
|
+
const forwardSighup = () => forwardSignal("SIGHUP");
|
|
3497
|
+
const forwarders = {
|
|
3498
|
+
SIGINT: forwardSigint,
|
|
3499
|
+
SIGTERM: forwardSigterm,
|
|
3500
|
+
SIGHUP: forwardSighup
|
|
3501
|
+
};
|
|
3502
|
+
for (const signal of REEXEC_SIGNALS) {
|
|
3503
|
+
process.on(signal, forwarders[signal]);
|
|
3504
|
+
}
|
|
3505
|
+
const exitCode = await (async () => {
|
|
3506
|
+
try {
|
|
3507
|
+
child = execa2(process.execPath, [entryPath, ...process.argv.slice(2)], {
|
|
3508
|
+
cwd: cwd ?? getProjectRoot(),
|
|
3509
|
+
stdio: captureJsonStdout ? ["inherit", "pipe", "inherit"] : "inherit",
|
|
3510
|
+
reject: false,
|
|
3511
|
+
env: {
|
|
3512
|
+
...process.env,
|
|
3513
|
+
[CLI_REEXEC_ENV]: "1",
|
|
3514
|
+
...jsonCaptureFile ? { [JSONL_CAPTURE_FILE_ENV]: jsonCaptureFile } : {}
|
|
3515
|
+
}
|
|
3516
|
+
});
|
|
3517
|
+
if (pendingSignal) {
|
|
3518
|
+
forwardSignal(pendingSignal);
|
|
3519
|
+
}
|
|
3520
|
+
const result = await child;
|
|
3521
|
+
if (jsonCaptureFile) {
|
|
3522
|
+
const captured = await fs2.readFile(jsonCaptureFile, "utf8").catch(() => "");
|
|
3523
|
+
if (captured) {
|
|
3524
|
+
writeJsonStdoutText(captured, stdoutFilePath);
|
|
3525
|
+
} else if (typeof result.stdout === "string" && result.stdout) {
|
|
3526
|
+
writeJsonStdoutText(result.stdout, stdoutFilePath);
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
return result.exitCode ?? signalToExitCode(result.signal) ?? 0;
|
|
3530
|
+
} finally {
|
|
3531
|
+
if (jsonCaptureDir) {
|
|
3532
|
+
await fs2.rm(jsonCaptureDir, { recursive: true, force: true }).catch(() => void 0);
|
|
3533
|
+
}
|
|
3534
|
+
for (const signal of REEXEC_SIGNALS) {
|
|
3535
|
+
process.removeListener(signal, forwarders[signal]);
|
|
3536
|
+
}
|
|
3537
|
+
signalCoordinator.resumeProcessSignalHandlers();
|
|
3538
|
+
}
|
|
3539
|
+
})();
|
|
3540
|
+
process.exit(exitCode);
|
|
3541
|
+
}
|
|
3542
|
+
async function runWorkflowPreflight(workflow, options = {}) {
|
|
3543
|
+
const startTime = Date.now();
|
|
3544
|
+
const cwd = options.cwd || process.cwd();
|
|
3545
|
+
const worktreeRoot = options.worktreeHygiene?.baseDir ?? getWorktreeBaseDir();
|
|
3546
|
+
const skip = options.skip || {};
|
|
3547
|
+
const checks = [];
|
|
3548
|
+
const errors = [];
|
|
3549
|
+
const warnings = [];
|
|
3550
|
+
const repoCleanMode = options.repoCleanMode ?? "warn";
|
|
3551
|
+
const suppressExpectedSetupRepoDirtyWarning = options.suppressExpectedSetupRepoDirtyWarning ?? false;
|
|
3552
|
+
const runCheck = async (name, skipFlag, fn) => {
|
|
3553
|
+
if (skipFlag) {
|
|
3554
|
+
if (options.verbose) {
|
|
3555
|
+
console.log(` [SKIP] ${name}`);
|
|
3556
|
+
}
|
|
3557
|
+
return;
|
|
3558
|
+
}
|
|
3559
|
+
if (options.verbose) {
|
|
3560
|
+
process.stdout.write(` [....] ${name}`);
|
|
3561
|
+
}
|
|
3562
|
+
const result = await fn();
|
|
3563
|
+
checks.push(result);
|
|
3564
|
+
if (!result.passed && result.error) {
|
|
3565
|
+
errors.push(result.error);
|
|
3566
|
+
}
|
|
3567
|
+
if (result.warning) {
|
|
3568
|
+
warnings.push(result.warning);
|
|
3569
|
+
}
|
|
3570
|
+
if (options.verbose) {
|
|
3571
|
+
const status = result.passed ? result.warning ? "WARN" : "PASS" : "FAIL";
|
|
3572
|
+
process.stdout.write(`\r [${status}] ${name} (${result.durationMs}ms)
|
|
3573
|
+
`);
|
|
3574
|
+
}
|
|
3575
|
+
};
|
|
3576
|
+
if (options.verbose) {
|
|
3577
|
+
console.log("\nRunning preflight checks...\n");
|
|
3578
|
+
}
|
|
3579
|
+
await runCheck(
|
|
3580
|
+
"Build freshness",
|
|
3581
|
+
skip.buildFreshness || process.env[CLI_REEXEC_ENV] === "1",
|
|
3582
|
+
() => checkBuildFreshness()
|
|
3583
|
+
);
|
|
3584
|
+
await runCheck("Configuration validation", skip.configValidation, () => checkConfig(workflow));
|
|
3585
|
+
await runCheck(
|
|
3586
|
+
"Task spec freshness",
|
|
3587
|
+
skip.taskSpecFreshness,
|
|
3588
|
+
() => checkTaskSpecFreshness(workflow, cwd)
|
|
3589
|
+
);
|
|
3590
|
+
await runCheck(
|
|
3591
|
+
"Repository cleanliness",
|
|
3592
|
+
skip.repoClean,
|
|
3593
|
+
() => checkRepoClean(cwd, repoCleanMode, suppressExpectedSetupRepoDirtyWarning)
|
|
3594
|
+
);
|
|
3595
|
+
await runCheck("Worktree sanity", skip.worktreeSanity, () => checkWorktreeSanity(worktreeRoot));
|
|
3596
|
+
await runCheck(
|
|
3597
|
+
"Worktree hygiene",
|
|
3598
|
+
skip.worktreeHygiene,
|
|
3599
|
+
() => checkWorktreeHygiene(worktreeRoot, cwd, { ...options.worktreeHygiene, baseDir: worktreeRoot })
|
|
3600
|
+
);
|
|
3601
|
+
await runCheck(
|
|
3602
|
+
"Worktree recovery",
|
|
3603
|
+
skip.worktreeRecovery,
|
|
3604
|
+
() => checkWorktreeRecovery({ ...options.worktreeHygiene, baseDir: worktreeRoot })
|
|
3605
|
+
);
|
|
3606
|
+
await runCheck(
|
|
3607
|
+
"Provider authentication",
|
|
3608
|
+
skip.providerAuth,
|
|
3609
|
+
() => checkProviderAuth(workflow, cwd, options.authProbeEnv)
|
|
3610
|
+
);
|
|
3611
|
+
await runCheck("Artifact references", skip.artifactRefs, () => checkArtifactRefs(workflow));
|
|
3612
|
+
await runCheck(
|
|
3613
|
+
"Context window utilization",
|
|
3614
|
+
skip.contextUtilization,
|
|
3615
|
+
() => checkContextUtilization(workflow, cwd)
|
|
3616
|
+
);
|
|
3617
|
+
const totalDurationMs = Date.now() - startTime;
|
|
3618
|
+
const passed = errors.length === 0;
|
|
3619
|
+
const passedCount = checks.filter((c) => c.passed).length;
|
|
3620
|
+
const failedCount = checks.filter((c) => !c.passed).length;
|
|
3621
|
+
const warnCount = checks.filter((c) => c.warning).length;
|
|
3622
|
+
let summary = `Preflight: ${passedCount}/${checks.length} checks passed`;
|
|
3623
|
+
if (failedCount > 0) {
|
|
3624
|
+
summary += `, ${failedCount} failed`;
|
|
3625
|
+
}
|
|
3626
|
+
if (warnCount > 0) {
|
|
3627
|
+
summary += `, ${warnCount} warnings`;
|
|
3628
|
+
}
|
|
3629
|
+
summary += ` (${totalDurationMs}ms)`;
|
|
3630
|
+
if (options.verbose) {
|
|
3631
|
+
console.log(`
|
|
3632
|
+
${passed ? "Preflight PASSED" : "Preflight FAILED"}: ${summary}
|
|
3633
|
+
`);
|
|
3634
|
+
if (errors.length > 0) {
|
|
3635
|
+
console.log("Errors:");
|
|
3636
|
+
for (const error of errors) {
|
|
3637
|
+
console.log(` - ${error}`);
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
if (warnings.length > 0) {
|
|
3641
|
+
console.log("Warnings:");
|
|
3642
|
+
for (const warning of warnings) {
|
|
3643
|
+
console.log(` - ${warning}`);
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
3647
|
+
return {
|
|
3648
|
+
passed,
|
|
3649
|
+
checks,
|
|
3650
|
+
errors,
|
|
3651
|
+
warnings,
|
|
3652
|
+
totalDurationMs,
|
|
3653
|
+
summary
|
|
3654
|
+
};
|
|
3655
|
+
}
|
|
3656
|
+
async function runQuickPreflight(cwd) {
|
|
3657
|
+
const issues = [];
|
|
3658
|
+
const workingDir = cwd || process.cwd();
|
|
3659
|
+
try {
|
|
3660
|
+
const { stdout } = await execa2("git", ["status", "--porcelain"], {
|
|
3661
|
+
cwd: workingDir,
|
|
3662
|
+
stdio: "pipe",
|
|
3663
|
+
timeout: 5e3
|
|
3664
|
+
});
|
|
3665
|
+
if (filterWorktreeRunSentinelStatusOutput(stdout).trim().length > 0) {
|
|
3666
|
+
issues.push("Repository has uncommitted changes");
|
|
3667
|
+
}
|
|
3668
|
+
} catch {
|
|
3669
|
+
issues.push("Not in a git repository");
|
|
3670
|
+
}
|
|
3671
|
+
try {
|
|
3672
|
+
await fs2.access(getWorktreeBaseDir(), fs2.constants.W_OK);
|
|
3673
|
+
} catch {
|
|
3674
|
+
try {
|
|
3675
|
+
await fs2.mkdir(getWorktreeBaseDir(), { recursive: true });
|
|
3676
|
+
} catch {
|
|
3677
|
+
issues.push(`Worktree directory ${getWorktreeBaseDir()} is not writable`);
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
return {
|
|
3681
|
+
passed: issues.length === 0,
|
|
3682
|
+
issues
|
|
3683
|
+
};
|
|
3684
|
+
}
|
|
3685
|
+
async function runPlanPreflight(cwd, options) {
|
|
3686
|
+
let buildResult;
|
|
3687
|
+
try {
|
|
3688
|
+
buildResult = await ensureFreshBuild();
|
|
3689
|
+
} catch (error) {
|
|
3690
|
+
return { passed: false, error: error instanceof Error ? error.message : String(error) };
|
|
3691
|
+
}
|
|
3692
|
+
if (options?.reexecOnRebuild === true && buildResult.rebuilt && process.env[CLI_REEXEC_ENV] !== "1") {
|
|
3693
|
+
await reexecCurrentCliCommand(cwd);
|
|
3694
|
+
}
|
|
3695
|
+
const workingDir = cwd ?? process.cwd();
|
|
3696
|
+
const repoResult = await checkRepoClean(workingDir, "warn");
|
|
3697
|
+
if (!repoResult.passed) {
|
|
3698
|
+
const meta = repoResult.metadata;
|
|
3699
|
+
return {
|
|
3700
|
+
passed: false,
|
|
3701
|
+
error: meta?.hasConflictStatuses ? `${repoResult.error?.split("\n")[0] ?? "Repository has uncommitted changes"}
|
|
3702
|
+
Resolve conflicts before running planned build.` : repoResult.error
|
|
3703
|
+
};
|
|
3704
|
+
}
|
|
3705
|
+
if (repoResult.warning) {
|
|
3706
|
+
const meta = repoResult.metadata;
|
|
3707
|
+
if (meta?.hasConflictStatuses) {
|
|
3708
|
+
return {
|
|
3709
|
+
passed: false,
|
|
3710
|
+
error: `${repoResult.warning.split("\n")[0]}
|
|
3711
|
+
Resolve conflicts before running planned build.`
|
|
3712
|
+
};
|
|
3713
|
+
}
|
|
3714
|
+
return {
|
|
3715
|
+
passed: true,
|
|
3716
|
+
warning: repoResult.warning
|
|
3717
|
+
};
|
|
3718
|
+
}
|
|
3719
|
+
return { passed: true };
|
|
3720
|
+
}
|
|
3721
|
+
|
|
3722
|
+
export {
|
|
3723
|
+
getExtensionsRootDir,
|
|
3724
|
+
getExtensionDirs,
|
|
3725
|
+
runAsyncWithOptionalTimeout,
|
|
3726
|
+
cleanupWorkflowRelatedBranchesAndWorktrees,
|
|
3727
|
+
cleanupStaleWorktrees,
|
|
3728
|
+
recoverPendingWorktrees,
|
|
3729
|
+
enforceRetentionBounds,
|
|
3730
|
+
getFlywheelCleanupDryRunWorktreeSummary,
|
|
3731
|
+
PATH_SAFE_IDENTIFIER_PATTERN,
|
|
3732
|
+
isPathSafeIdentifier,
|
|
3733
|
+
makeRetryDecision,
|
|
3734
|
+
validateGeneratedSubStageVerifyCommands,
|
|
3735
|
+
validateWorkflowConfig,
|
|
3736
|
+
BUILD_INPUT_PATHS,
|
|
3737
|
+
checkBuildFreshness,
|
|
3738
|
+
ensureFreshBuild,
|
|
3739
|
+
writeAllSyncToFd,
|
|
3740
|
+
resolveStdoutRegularFilePath,
|
|
3741
|
+
reexecCurrentCliCommand,
|
|
3742
|
+
runWorkflowPreflight,
|
|
3743
|
+
runQuickPreflight,
|
|
3744
|
+
runPlanPreflight
|
|
3745
|
+
};
|
|
3746
|
+
//# sourceMappingURL=chunk-6VWVO2TU.js.map
|