@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,2626 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PUBLIC_ROOT_SUMMARIES,
|
|
3
|
+
TIER_LABELS,
|
|
4
|
+
getCommandTier,
|
|
5
|
+
listPublicRootsByDomain,
|
|
6
|
+
listRootsByTier
|
|
7
|
+
} from "./chunk-YESZJFK6.js";
|
|
8
|
+
import {
|
|
9
|
+
parseProjectDependencySetupCommand,
|
|
10
|
+
runVerificationArgv,
|
|
11
|
+
runVerificationCommand
|
|
12
|
+
} from "./chunk-D5RYINO7.js";
|
|
13
|
+
import {
|
|
14
|
+
createCleanChildEnv
|
|
15
|
+
} from "./chunk-6TZJRKNW.js";
|
|
16
|
+
import {
|
|
17
|
+
abandonHostPublicationGenerationJournal,
|
|
18
|
+
attachOutstandingStatusPublicationMac,
|
|
19
|
+
beginHostPublicationGenerationJournal,
|
|
20
|
+
recoverHostPublicationGeneration,
|
|
21
|
+
syncHostPublicationGeneration
|
|
22
|
+
} from "./chunk-PCAGC5MR.js";
|
|
23
|
+
import {
|
|
24
|
+
QUICK_START_GUIDE_URL
|
|
25
|
+
} from "./chunk-5EERLVZB.js";
|
|
26
|
+
import {
|
|
27
|
+
execGit
|
|
28
|
+
} from "./chunk-KNDYMVWN.js";
|
|
29
|
+
import {
|
|
30
|
+
findNearestPoeticDir
|
|
31
|
+
} from "./chunk-FD4ERYEG.js";
|
|
32
|
+
import {
|
|
33
|
+
getProjectDataDir,
|
|
34
|
+
getStateDir
|
|
35
|
+
} from "./chunk-UUH2RVKS.js";
|
|
36
|
+
import {
|
|
37
|
+
POETIC_LOCAL_CI_APPROVE_ENTRYPOINT_SHA,
|
|
38
|
+
readEnv
|
|
39
|
+
} from "./chunk-5DLKYTQX.js";
|
|
40
|
+
|
|
41
|
+
// src/cli/utils/command-surface-help.ts
|
|
42
|
+
function padCommand(name, width) {
|
|
43
|
+
return name.length >= width ? name : `${name}${" ".repeat(width - name.length)}`;
|
|
44
|
+
}
|
|
45
|
+
function renderOrdinaryHelp(version) {
|
|
46
|
+
const groups = listPublicRootsByDomain();
|
|
47
|
+
const lines = [
|
|
48
|
+
"Poetic - AI orchestration CLI for intelligent software development",
|
|
49
|
+
"",
|
|
50
|
+
"Usage: poetic [command] [options]",
|
|
51
|
+
"",
|
|
52
|
+
"Public commands:"
|
|
53
|
+
];
|
|
54
|
+
for (const group of groups) {
|
|
55
|
+
lines.push("");
|
|
56
|
+
lines.push(`${group.domain}:`);
|
|
57
|
+
for (const root of group.roots) {
|
|
58
|
+
lines.push(` ${padCommand(root, 12)} ${PUBLIC_ROOT_SUMMARIES[root]}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
lines.push("");
|
|
62
|
+
lines.push("First-run journey:");
|
|
63
|
+
lines.push(" poetic init");
|
|
64
|
+
lines.push(" poetic init quickstart");
|
|
65
|
+
lines.push(" poetic provider setup --first-run");
|
|
66
|
+
lines.push(" poetic doctor");
|
|
67
|
+
lines.push(` Guide: ${QUICK_START_GUIDE_URL}`);
|
|
68
|
+
lines.push("");
|
|
69
|
+
lines.push("Examples:");
|
|
70
|
+
lines.push(" poetic plan next");
|
|
71
|
+
lines.push(' poetic plan backlog add "Add JWT auth to the API"');
|
|
72
|
+
lines.push(" poetic run task --backlog-item <id>");
|
|
73
|
+
lines.push(" poetic run competition --backlog-item <id>");
|
|
74
|
+
lines.push(' poetic build "Ship a health endpoint"');
|
|
75
|
+
lines.push("");
|
|
76
|
+
lines.push("Run 'poetic <command> --help' for command-specific help.");
|
|
77
|
+
lines.push("Run 'poetic --help-full' for complete command reference (loads full CLI).");
|
|
78
|
+
lines.push("");
|
|
79
|
+
lines.push(`Version: ${version}`);
|
|
80
|
+
lines.push("");
|
|
81
|
+
return lines.join("\n");
|
|
82
|
+
}
|
|
83
|
+
function describeCommand(program, name) {
|
|
84
|
+
if (!program) {
|
|
85
|
+
return PUBLIC_ROOT_SUMMARIES[name] ?? "";
|
|
86
|
+
}
|
|
87
|
+
const cmd = program.commands.find((c) => c.name() === name);
|
|
88
|
+
if (!cmd) {
|
|
89
|
+
return PUBLIC_ROOT_SUMMARIES[name] ?? "";
|
|
90
|
+
}
|
|
91
|
+
const raw = cmd.description() || "";
|
|
92
|
+
const firstLine = raw.split("\n")[0]?.trim() ?? "";
|
|
93
|
+
return firstLine;
|
|
94
|
+
}
|
|
95
|
+
function renderFullTieredHelp(program, version) {
|
|
96
|
+
const lines = [
|
|
97
|
+
"Poetic - AI orchestration CLI for intelligent software development",
|
|
98
|
+
"",
|
|
99
|
+
"Usage: poetic [command] [options]",
|
|
100
|
+
"",
|
|
101
|
+
"Complete command reference (Public / Advanced / Internal).",
|
|
102
|
+
"Hidden compatibility aliases remain callable but are omitted here.",
|
|
103
|
+
""
|
|
104
|
+
];
|
|
105
|
+
const sections = [
|
|
106
|
+
{ tier: "default", heading: `${TIER_LABELS.default} commands` },
|
|
107
|
+
{ tier: "operator", heading: `${TIER_LABELS.operator} commands` },
|
|
108
|
+
{ tier: "power", heading: `${TIER_LABELS.power} commands` }
|
|
109
|
+
];
|
|
110
|
+
for (const section of sections) {
|
|
111
|
+
const roots = listRootsByTier(section.tier);
|
|
112
|
+
if (roots.length === 0) continue;
|
|
113
|
+
lines.push(`${section.heading}:`);
|
|
114
|
+
if (section.tier === "default") {
|
|
115
|
+
for (const group of listPublicRootsByDomain()) {
|
|
116
|
+
lines.push(` ${group.domain}`);
|
|
117
|
+
for (const root of group.roots) {
|
|
118
|
+
lines.push(` ${padCommand(root, 12)} ${describeCommand(program, root)}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
for (const root of roots) {
|
|
123
|
+
lines.push(` ${padCommand(root, 18)} ${describeCommand(program, root)}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
lines.push("");
|
|
127
|
+
}
|
|
128
|
+
lines.push("Global options:");
|
|
129
|
+
lines.push(" -q, --quiet Suppress non-essential CLI diagnostics");
|
|
130
|
+
lines.push(" --no-color Disable ANSI color output");
|
|
131
|
+
lines.push(" --no-auto-init Skip automatic initialization check");
|
|
132
|
+
lines.push(" -h, --help Display help");
|
|
133
|
+
lines.push(" -V, --version Display version");
|
|
134
|
+
lines.push("");
|
|
135
|
+
lines.push("Run 'poetic <command> --help' for command-specific help.");
|
|
136
|
+
lines.push(`Version: ${version}`);
|
|
137
|
+
lines.push("");
|
|
138
|
+
for (const name of program.commands.map((c) => c.name())) {
|
|
139
|
+
if (name === "help") continue;
|
|
140
|
+
const hidden = program.commands.find((c) => c.name() === name)?._hidden;
|
|
141
|
+
if (hidden) continue;
|
|
142
|
+
if (getCommandTier(name) === void 0) {
|
|
143
|
+
lines.push(`Warning: unclassified visible command '${name}'`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return lines.join("\n");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/cli/utils/global-command-routing.ts
|
|
150
|
+
var GLOBAL_FLAGS_WITHOUT_VALUES = /* @__PURE__ */ new Set([
|
|
151
|
+
"-q",
|
|
152
|
+
"--quiet",
|
|
153
|
+
"--no-color",
|
|
154
|
+
"-v",
|
|
155
|
+
"--verbose",
|
|
156
|
+
"--debug"
|
|
157
|
+
]);
|
|
158
|
+
var GLOBAL_FLAGS_WITH_VALUES = /* @__PURE__ */ new Set([]);
|
|
159
|
+
var FORWARDED_GLOBAL_FLAGS_WITHOUT_VALUES = /* @__PURE__ */ new Set(["-j", "--json"]);
|
|
160
|
+
var FORWARDED_GLOBAL_FLAGS_WITH_VALUES = /* @__PURE__ */ new Set(["--output"]);
|
|
161
|
+
var OPTIONAL_BOOLEAN_VALUE_FLAGS = /* @__PURE__ */ new Set(["--color"]);
|
|
162
|
+
var BOOLEAN_FLAG_VALUES = /* @__PURE__ */ new Set(["true", "false"]);
|
|
163
|
+
var CLEANUP_DRY_RUN_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
164
|
+
"branches",
|
|
165
|
+
"external-temp",
|
|
166
|
+
"provider-temp",
|
|
167
|
+
"prune",
|
|
168
|
+
"prune-archives",
|
|
169
|
+
"remote",
|
|
170
|
+
"stale",
|
|
171
|
+
"state",
|
|
172
|
+
"variants",
|
|
173
|
+
"worktrees"
|
|
174
|
+
]);
|
|
175
|
+
var CLEANUP_READ_ONLY_SUBCOMMANDS = /* @__PURE__ */ new Set(["doctor", "preview"]);
|
|
176
|
+
function isInlineValueFlag(arg, flags) {
|
|
177
|
+
return Array.from(flags).some((flag) => arg.startsWith(`${flag}=`));
|
|
178
|
+
}
|
|
179
|
+
function resolveCliCommand(args) {
|
|
180
|
+
const forwardedGlobalArgs = [];
|
|
181
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
182
|
+
const arg = args[i];
|
|
183
|
+
if (!arg) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (arg === "--") {
|
|
187
|
+
return {
|
|
188
|
+
command: void 0,
|
|
189
|
+
commandIndex: -1,
|
|
190
|
+
commandArgs: [],
|
|
191
|
+
forwardedGlobalArgs
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (GLOBAL_FLAGS_WITHOUT_VALUES.has(arg)) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (OPTIONAL_BOOLEAN_VALUE_FLAGS.has(arg)) {
|
|
198
|
+
const next = args[i + 1];
|
|
199
|
+
if (next !== void 0 && BOOLEAN_FLAG_VALUES.has(next.toLowerCase())) {
|
|
200
|
+
i += 1;
|
|
201
|
+
}
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (FORWARDED_GLOBAL_FLAGS_WITHOUT_VALUES.has(arg)) {
|
|
205
|
+
forwardedGlobalArgs.push(arg);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (GLOBAL_FLAGS_WITH_VALUES.has(arg)) {
|
|
209
|
+
i += 1;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (FORWARDED_GLOBAL_FLAGS_WITH_VALUES.has(arg)) {
|
|
213
|
+
const next = args[i + 1];
|
|
214
|
+
if (next !== void 0) {
|
|
215
|
+
forwardedGlobalArgs.push(arg, next);
|
|
216
|
+
i += 1;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
command: arg,
|
|
221
|
+
commandIndex: i,
|
|
222
|
+
commandArgs: args.slice(i),
|
|
223
|
+
forwardedGlobalArgs
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (isInlineValueFlag(arg, GLOBAL_FLAGS_WITH_VALUES)) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (isInlineValueFlag(arg, OPTIONAL_BOOLEAN_VALUE_FLAGS)) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (isInlineValueFlag(arg, FORWARDED_GLOBAL_FLAGS_WITH_VALUES)) {
|
|
233
|
+
forwardedGlobalArgs.push(arg);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
command: arg,
|
|
238
|
+
commandIndex: i,
|
|
239
|
+
commandArgs: args.slice(i),
|
|
240
|
+
forwardedGlobalArgs
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
command: void 0,
|
|
245
|
+
commandIndex: -1,
|
|
246
|
+
commandArgs: [],
|
|
247
|
+
forwardedGlobalArgs
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function buildRoutedArgv(argv, route) {
|
|
251
|
+
if (route.commandIndex < 0) {
|
|
252
|
+
return [...argv];
|
|
253
|
+
}
|
|
254
|
+
return [
|
|
255
|
+
argv[0] ?? "node",
|
|
256
|
+
argv[1] ?? "poetic",
|
|
257
|
+
...route.commandArgs,
|
|
258
|
+
...route.forwardedGlobalArgs
|
|
259
|
+
];
|
|
260
|
+
}
|
|
261
|
+
function isHelpOrVersionArg(arg) {
|
|
262
|
+
return arg === "--help" || arg === "-h" || arg === "--help-advanced" || arg.startsWith("--help-") || arg === "--version" || arg === "-V";
|
|
263
|
+
}
|
|
264
|
+
function isCleanupPreviewInvocation(args) {
|
|
265
|
+
const route = resolveCliCommand(args);
|
|
266
|
+
if (route.command !== "cleanup") {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
const subcommand = route.commandArgs[1] && !route.commandArgs[1].startsWith("-") ? route.commandArgs[1] : void 0;
|
|
270
|
+
if (!subcommand) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
if (CLEANUP_READ_ONLY_SUBCOMMANDS.has(subcommand)) {
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
return CLEANUP_DRY_RUN_SUBCOMMANDS.has(subcommand) && !route.commandArgs.includes("--execute");
|
|
277
|
+
}
|
|
278
|
+
var READ_ONLY_PLAN_SUBCOMMANDS = /* @__PURE__ */ new Set(["inspect", "status", "next"]);
|
|
279
|
+
function planNestedPositionals(commandArgs) {
|
|
280
|
+
const tokens = [];
|
|
281
|
+
for (let i = 1; i < commandArgs.length; i += 1) {
|
|
282
|
+
const arg = commandArgs[i];
|
|
283
|
+
if (!arg || arg === "--") {
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
if (arg.startsWith("-")) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
tokens.push(arg);
|
|
290
|
+
}
|
|
291
|
+
return tokens;
|
|
292
|
+
}
|
|
293
|
+
function matchPlanNestedCommand(command, commandArgs, noun, verb) {
|
|
294
|
+
if (command !== "plan" && command !== "pl") {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
const [nestedNoun, nestedVerb] = planNestedPositionals(commandArgs);
|
|
298
|
+
return nestedNoun === noun && nestedVerb === verb;
|
|
299
|
+
}
|
|
300
|
+
function isReadOnlyPlanInvocation(args) {
|
|
301
|
+
const route = resolveCliCommand(args);
|
|
302
|
+
if (route.command !== "plan" && route.command !== "pl") {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
const [subcommand, nestedVerb] = planNestedPositionals(route.commandArgs);
|
|
306
|
+
if (subcommand !== void 0 && READ_ONLY_PLAN_SUBCOMMANDS.has(subcommand)) {
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
return subcommand === "task" && (nestedVerb === "list" || nestedVerb === "status") || subcommand === "sprint" && (nestedVerb === "status" || nestedVerb === "list") || subcommand === "backlog" && (nestedVerb === "list" || nestedVerb === "show");
|
|
310
|
+
}
|
|
311
|
+
var PROCESSES_VALUE_FLAGS = /* @__PURE__ */ new Set(["--kill", "--category", "--type"]);
|
|
312
|
+
function processesPositionalAction(commandArgs) {
|
|
313
|
+
for (let i = 1; i < commandArgs.length; i += 1) {
|
|
314
|
+
const arg = commandArgs[i];
|
|
315
|
+
if (!arg || arg === "--") {
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
if (arg.startsWith("--") && arg.includes("=")) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (arg.startsWith("-")) {
|
|
322
|
+
if (PROCESSES_VALUE_FLAGS.has(arg)) {
|
|
323
|
+
i += 1;
|
|
324
|
+
}
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
return arg;
|
|
328
|
+
}
|
|
329
|
+
return void 0;
|
|
330
|
+
}
|
|
331
|
+
function hasProcessesMutatingFlag(commandArgs) {
|
|
332
|
+
const action = processesPositionalAction(commandArgs);
|
|
333
|
+
if (action === "cleanup" || action === "kill") {
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
return commandArgs.some(
|
|
337
|
+
(arg) => arg === "--cleanup" || arg === "-c" || arg === "--kill" || arg.startsWith("--kill=")
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
function processesInvocationArgs(args) {
|
|
341
|
+
const route = resolveCliCommand(args);
|
|
342
|
+
if (route.command !== "processes") {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
return [...route.commandArgs, ...route.forwardedGlobalArgs];
|
|
346
|
+
}
|
|
347
|
+
function isObservationalProcessesHealthInvocation(args) {
|
|
348
|
+
const commandArgs = processesInvocationArgs(args);
|
|
349
|
+
if (!commandArgs || !commandArgs.includes("--health")) {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
return !hasProcessesMutatingFlag(commandArgs);
|
|
353
|
+
}
|
|
354
|
+
function isObservationalProcessesJsonInvocation(args) {
|
|
355
|
+
const commandArgs = processesInvocationArgs(args);
|
|
356
|
+
if (!commandArgs) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
const hasJson = commandArgs.includes("--json") || commandArgs.includes("-j");
|
|
360
|
+
if (!hasJson) {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
return !hasProcessesMutatingFlag(commandArgs);
|
|
364
|
+
}
|
|
365
|
+
function isDocumentedReadOnlyInvocation(args) {
|
|
366
|
+
if (isReadOnlyPlanInvocation(args)) {
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
if (isObservationalProcessesJsonInvocation(args)) {
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
const route = resolveCliCommand(args);
|
|
373
|
+
return route.command === "task" && route.commandArgs[1] === "list" || route.command === "config" && route.commandArgs[1] === "resolved";
|
|
374
|
+
}
|
|
375
|
+
function shouldOwnStartupCleanup(args, env = process.env) {
|
|
376
|
+
const routeArgs = args.filter((arg) => arg !== "--no-auto-init");
|
|
377
|
+
const routeEnv = { ...env };
|
|
378
|
+
delete routeEnv.POETIC_NO_AUTO_INIT;
|
|
379
|
+
return !isObservationalInvocation(routeArgs, routeEnv);
|
|
380
|
+
}
|
|
381
|
+
function isObservationalInvocation(args, env = process.env) {
|
|
382
|
+
if (env.POETIC_NO_AUTO_INIT === "1" || args.includes("--no-auto-init")) {
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
if (args.some(isHelpOrVersionArg)) {
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
const route = resolveCliCommand(args);
|
|
389
|
+
const command = route.command;
|
|
390
|
+
if (!command) {
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
if (isCleanupPreviewInvocation(args)) {
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
if (command === "context") {
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
if (isDocumentedReadOnlyInvocation(args)) {
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
return (command === "doctor" || command === "d") && !route.commandArgs.includes("--fix");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// src/cli/utils/startup-poetic-dir.ts
|
|
406
|
+
import path from "path";
|
|
407
|
+
function resolveStartupPoeticDir(options) {
|
|
408
|
+
const repoRoot = path.resolve(options.repoRoot);
|
|
409
|
+
const repoLocalPoeticDir = path.join(repoRoot, ".poetic");
|
|
410
|
+
if (!isObservationalInvocation(options.args, options.env)) {
|
|
411
|
+
return repoLocalPoeticDir;
|
|
412
|
+
}
|
|
413
|
+
return findNearestPoeticDir(repoRoot) ?? getProjectDataDir(repoRoot);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/core/ci/local-ci-paths.ts
|
|
417
|
+
import { createHash } from "crypto";
|
|
418
|
+
import path2 from "path";
|
|
419
|
+
function getLocalCiStateRoot() {
|
|
420
|
+
return path2.join(getStateDir(), "local-ci");
|
|
421
|
+
}
|
|
422
|
+
function repoRootStateKey(repoRoot) {
|
|
423
|
+
return createHash("sha256").update(path2.resolve(repoRoot)).digest("hex").slice(0, 24);
|
|
424
|
+
}
|
|
425
|
+
function localCiRootMappingPath(repoRoot) {
|
|
426
|
+
return path2.join(getLocalCiStateRoot(), "roots", `${repoRootStateKey(repoRoot)}.json`);
|
|
427
|
+
}
|
|
428
|
+
function localCiSchedulingEnvelopePath(stateRoot, repositoryKey) {
|
|
429
|
+
return path2.join(stateRoot, repositoryKey, "scheduling-envelope.json");
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/core/ci/local-ci-state.ts
|
|
433
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
434
|
+
import { access, link, mkdir, readFile, rename, unlink, writeFile } from "fs/promises";
|
|
435
|
+
import path3 from "path";
|
|
436
|
+
import lockfile from "proper-lockfile";
|
|
437
|
+
import { z } from "zod";
|
|
438
|
+
|
|
439
|
+
// src/core/ci/local-ci-process.ts
|
|
440
|
+
function processIsAlive(pid) {
|
|
441
|
+
try {
|
|
442
|
+
process.kill(pid, 0);
|
|
443
|
+
return true;
|
|
444
|
+
} catch {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/core/ci/local-ci-state.ts
|
|
450
|
+
var DEFAULT_LOCAL_CI_STATUS_CONTEXT = "Poetic Local CI";
|
|
451
|
+
var DEFAULT_LOCAL_CI_MAX_CONCURRENT = 2;
|
|
452
|
+
var LocalCiRunStatusSchema = z.enum([
|
|
453
|
+
"queued",
|
|
454
|
+
"running",
|
|
455
|
+
/**
|
|
456
|
+
* Validation passed; remote commit-status publication is not yet confirmed.
|
|
457
|
+
* Distinct from `succeeded_pending` (awaiting attestation preconditions).
|
|
458
|
+
*/
|
|
459
|
+
"publication_pending",
|
|
460
|
+
"succeeded_pending",
|
|
461
|
+
"succeeded",
|
|
462
|
+
"skipped",
|
|
463
|
+
"failed",
|
|
464
|
+
"superseded"
|
|
465
|
+
]);
|
|
466
|
+
var LocalCiLegacyRunStatusSchema = z.enum(["succeeded_manual", "merged", "merge_blocked"]);
|
|
467
|
+
var LocalCiPersistedRunStatusSchema = z.union([
|
|
468
|
+
LocalCiRunStatusSchema,
|
|
469
|
+
LocalCiLegacyRunStatusSchema
|
|
470
|
+
]);
|
|
471
|
+
function normalizeLocalCiRunStatus(status) {
|
|
472
|
+
switch (status) {
|
|
473
|
+
case "succeeded_manual":
|
|
474
|
+
case "merged":
|
|
475
|
+
case "merge_blocked":
|
|
476
|
+
return "succeeded";
|
|
477
|
+
default:
|
|
478
|
+
return status;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
var LocalCiPublicationTargetStatusSchema = z.enum(["succeeded", "succeeded_pending"]);
|
|
482
|
+
var LocalCiPersistedPublicationTargetStatusSchema = z.enum(["succeeded", "succeeded_pending", "succeeded_manual"]).transform(
|
|
483
|
+
(status) => status === "succeeded_manual" ? "succeeded" : status
|
|
484
|
+
);
|
|
485
|
+
var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["queued", "running"]);
|
|
486
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
487
|
+
"publication_pending",
|
|
488
|
+
"succeeded_pending",
|
|
489
|
+
"succeeded",
|
|
490
|
+
"skipped",
|
|
491
|
+
"failed",
|
|
492
|
+
"superseded"
|
|
493
|
+
]);
|
|
494
|
+
function isLocalCiActiveStatus(status) {
|
|
495
|
+
return ACTIVE_STATUSES.has(status);
|
|
496
|
+
}
|
|
497
|
+
function isLocalCiTerminalStatus(status) {
|
|
498
|
+
return TERMINAL_STATUSES.has(status);
|
|
499
|
+
}
|
|
500
|
+
var LocalCiCommandResultSchema = z.object({
|
|
501
|
+
command: z.string().min(1),
|
|
502
|
+
exitCode: z.number().int(),
|
|
503
|
+
output: z.string(),
|
|
504
|
+
id: z.string().min(1).optional(),
|
|
505
|
+
phase: z.string().min(1).optional(),
|
|
506
|
+
durationMs: z.number().int().nonnegative().optional()
|
|
507
|
+
});
|
|
508
|
+
var LocalCiFailingTestSchema = z.object({
|
|
509
|
+
/** Test file path when the identifier carried one (vitest/pytest/go path forms). */
|
|
510
|
+
file: z.string().min(1).optional(),
|
|
511
|
+
/** Failing test identifier (suite/test path, or raw name when no file parsed). */
|
|
512
|
+
test: z.string().min(1),
|
|
513
|
+
/** Verification phase of the failing step (e.g. 'verify'). */
|
|
514
|
+
phase: z.string().min(1)
|
|
515
|
+
});
|
|
516
|
+
var WorkerClaimPayloadSchema = z.object({
|
|
517
|
+
pid: z.number().int().positive(),
|
|
518
|
+
token: z.string().uuid(),
|
|
519
|
+
claimedAt: z.iso.datetime(),
|
|
520
|
+
/** Deterministic fingerprint of the authorized record generation; optional for legacy. */
|
|
521
|
+
generationKey: z.string().min(1).optional()
|
|
522
|
+
});
|
|
523
|
+
var CROSS_PROCESS_FS_LOCK_STALE_MS = 18e4;
|
|
524
|
+
var CROSS_PROCESS_FS_LOCK_UPDATE_MS = 1e4;
|
|
525
|
+
var PROPER_LOCKFILE_STALE_MIN_MS = 2e3;
|
|
526
|
+
var PROPER_LOCKFILE_UPDATE_MIN_MS = 1e3;
|
|
527
|
+
function resolveCrossProcessFsLockPolicy() {
|
|
528
|
+
const stale = Math.max(CROSS_PROCESS_FS_LOCK_STALE_MS, PROPER_LOCKFILE_STALE_MIN_MS);
|
|
529
|
+
const configuredUpdate = CROSS_PROCESS_FS_LOCK_UPDATE_MS;
|
|
530
|
+
const update = Math.max(Math.min(configuredUpdate, stale / 2), PROPER_LOCKFILE_UPDATE_MIN_MS);
|
|
531
|
+
return { stale, update };
|
|
532
|
+
}
|
|
533
|
+
var CROSS_PROCESS_FS_LOCK_OPTIONS = {
|
|
534
|
+
...resolveCrossProcessFsLockPolicy(),
|
|
535
|
+
realpath: false,
|
|
536
|
+
retries: {
|
|
537
|
+
retries: 400,
|
|
538
|
+
minTimeout: 15,
|
|
539
|
+
maxTimeout: 100,
|
|
540
|
+
factor: 1.2
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
async function acquireFencedFsLock(lockTarget, options = CROSS_PROCESS_FS_LOCK_OPTIONS) {
|
|
544
|
+
let compromised = false;
|
|
545
|
+
const releaseUnderlying = await lockfile.lock(lockTarget, {
|
|
546
|
+
...options,
|
|
547
|
+
// Mark ownership lost without asynchronously throwing into unrelated code.
|
|
548
|
+
onCompromised: () => {
|
|
549
|
+
compromised = true;
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
const lockfilePath = `${path3.resolve(lockTarget)}.lock`;
|
|
553
|
+
const ownerToken = randomUUID();
|
|
554
|
+
const ownerTokenPath = path3.join(lockfilePath, "owner.token");
|
|
555
|
+
try {
|
|
556
|
+
await writeFile(ownerTokenPath, `${ownerToken}
|
|
557
|
+
`, { encoding: "utf8", mode: 384 });
|
|
558
|
+
} catch (error) {
|
|
559
|
+
compromised = true;
|
|
560
|
+
try {
|
|
561
|
+
await releaseUnderlying();
|
|
562
|
+
} catch {
|
|
563
|
+
}
|
|
564
|
+
throw error;
|
|
565
|
+
}
|
|
566
|
+
const assertOwned = async () => {
|
|
567
|
+
if (compromised) {
|
|
568
|
+
throw new Error("Local CI filesystem lock ownership was compromised.");
|
|
569
|
+
}
|
|
570
|
+
let raw;
|
|
571
|
+
try {
|
|
572
|
+
raw = await readFile(ownerTokenPath, "utf8");
|
|
573
|
+
} catch {
|
|
574
|
+
compromised = true;
|
|
575
|
+
throw new Error("Local CI filesystem lock ownership was lost.");
|
|
576
|
+
}
|
|
577
|
+
if (raw.trim() !== ownerToken) {
|
|
578
|
+
compromised = true;
|
|
579
|
+
throw new Error("Local CI filesystem lock ownership was superseded.");
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
const release = async () => {
|
|
583
|
+
if (compromised) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
try {
|
|
587
|
+
const raw = await readFile(ownerTokenPath, "utf8");
|
|
588
|
+
if (raw.trim() !== ownerToken) {
|
|
589
|
+
compromised = true;
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
} catch {
|
|
593
|
+
compromised = true;
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
await unlink(ownerTokenPath).catch(() => void 0);
|
|
597
|
+
try {
|
|
598
|
+
await releaseUnderlying();
|
|
599
|
+
} catch {
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
return {
|
|
603
|
+
fence: {
|
|
604
|
+
assertOwned,
|
|
605
|
+
isOwned: () => !compromised
|
|
606
|
+
},
|
|
607
|
+
release
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
var LOCAL_CI_REPOSITORY_IDENTITY_MAX = 256;
|
|
611
|
+
function parseLocalCiRepositoryIdentity(value) {
|
|
612
|
+
const trimmed = value.trim();
|
|
613
|
+
if (!trimmed || trimmed.length > LOCAL_CI_REPOSITORY_IDENTITY_MAX) return null;
|
|
614
|
+
if (/\s/.test(trimmed) || trimmed.includes("\\") || trimmed.includes("\0")) return null;
|
|
615
|
+
if (trimmed.startsWith("/") || trimmed.endsWith("/")) return null;
|
|
616
|
+
if (trimmed.includes("//")) return null;
|
|
617
|
+
const segments = trimmed.split("/");
|
|
618
|
+
if (segments.length < 2 || segments.length > 24) return null;
|
|
619
|
+
for (const segment of segments) {
|
|
620
|
+
if (!segment || segment === "." || segment === "..") return null;
|
|
621
|
+
if (!/^[A-Za-z0-9._~+-]+$/.test(segment)) return null;
|
|
622
|
+
}
|
|
623
|
+
return trimmed;
|
|
624
|
+
}
|
|
625
|
+
var LocalCiRepositoryIdentitySchema = z.string().refine((value) => parseLocalCiRepositoryIdentity(value) !== null, {
|
|
626
|
+
message: "Invalid local CI repository identity"
|
|
627
|
+
});
|
|
628
|
+
var OutstandingRemoteStatusPublicationSchema = z.object({
|
|
629
|
+
repository: LocalCiRepositoryIdentitySchema,
|
|
630
|
+
sha: z.string().regex(/^[a-f0-9]{40}$/i),
|
|
631
|
+
context: z.string().min(1).max(100),
|
|
632
|
+
state: z.enum(["failure", "success", "pending"]),
|
|
633
|
+
description: z.string().min(1).max(140),
|
|
634
|
+
/**
|
|
635
|
+
* Local status to apply once remote publication is confirmed. Required for
|
|
636
|
+
* success/pending remote states; omitted for failure (record is already failed).
|
|
637
|
+
* Legacy `succeeded_manual` normalizes to `succeeded` at read time.
|
|
638
|
+
*/
|
|
639
|
+
targetStatus: LocalCiPersistedPublicationTargetStatusSchema.optional(),
|
|
640
|
+
/**
|
|
641
|
+
* Host-issued generation bound outside candidate-writable state.
|
|
642
|
+
* Optional on disk so legacy records still parse; the publisher fails closed
|
|
643
|
+
* when this is missing or does not match the host generation file.
|
|
644
|
+
*/
|
|
645
|
+
generation: z.string().uuid().optional(),
|
|
646
|
+
/**
|
|
647
|
+
* Host-keyed HMAC over repository/sha/context/state/description/targetStatus
|
|
648
|
+
* and generation. Optional on disk so legacy records still parse; the
|
|
649
|
+
* publisher fails closed when this is missing or invalid and never publishes
|
|
650
|
+
* success.
|
|
651
|
+
*/
|
|
652
|
+
mac: z.string().regex(/^[a-f0-9]{64}$/i).optional()
|
|
653
|
+
}).superRefine((value, ctx) => {
|
|
654
|
+
if (value.state === "failure" && value.targetStatus !== void 0) {
|
|
655
|
+
ctx.addIssue({
|
|
656
|
+
code: "custom",
|
|
657
|
+
message: "Failure status publications must not carry a success targetStatus.",
|
|
658
|
+
path: ["targetStatus"]
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
if (value.state !== "failure" && value.targetStatus === void 0) {
|
|
662
|
+
ctx.addIssue({
|
|
663
|
+
code: "custom",
|
|
664
|
+
message: "Success-path status publications require targetStatus.",
|
|
665
|
+
path: ["targetStatus"]
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
var FailurePublicationLeaseSchema = z.object({
|
|
670
|
+
/** Unique lease token; required for post-publish clear/readback authorization. */
|
|
671
|
+
token: z.string().uuid(),
|
|
672
|
+
/** Process that holds the in-flight remote publication. */
|
|
673
|
+
ownerPid: z.number().int().positive(),
|
|
674
|
+
/**
|
|
675
|
+
* Fingerprint of the record generation authorized for this remote write
|
|
676
|
+
* (same identity fields as record CAS, frozen at lease acquire).
|
|
677
|
+
*/
|
|
678
|
+
generationKey: z.string().min(1),
|
|
679
|
+
claimedAt: z.iso.datetime()
|
|
680
|
+
});
|
|
681
|
+
var LocalCiRunRecordSchema = z.object({
|
|
682
|
+
schemaVersion: z.literal(1),
|
|
683
|
+
repoRoot: z.string().min(1),
|
|
684
|
+
repository: LocalCiRepositoryIdentitySchema,
|
|
685
|
+
sha: z.string().regex(/^[a-f0-9]{40}$/i),
|
|
686
|
+
branch: z.string().min(1),
|
|
687
|
+
remote: z.string().min(1),
|
|
688
|
+
statusContext: z.string().min(1).max(100),
|
|
689
|
+
maxConcurrent: z.number().int().min(1).max(8),
|
|
690
|
+
status: LocalCiPersistedRunStatusSchema,
|
|
691
|
+
commands: z.array(LocalCiCommandResultSchema),
|
|
692
|
+
/** Immutable branch-submission order; legacy records fall back to createdAt. */
|
|
693
|
+
submittedAt: z.iso.datetime().optional(),
|
|
694
|
+
createdAt: z.iso.datetime(),
|
|
695
|
+
updatedAt: z.iso.datetime(),
|
|
696
|
+
startedAt: z.iso.datetime().optional(),
|
|
697
|
+
queueWaitMs: z.number().int().nonnegative().optional(),
|
|
698
|
+
pid: z.number().int().positive().optional(),
|
|
699
|
+
worktreePath: z.string().min(1).optional(),
|
|
700
|
+
/** Count of automatic unresolved-trusted-base recovery requeues. */
|
|
701
|
+
recoveryAttempts: z.number().int().nonnegative().optional(),
|
|
702
|
+
error: z.string().min(1).optional(),
|
|
703
|
+
trustedBaseRef: z.string().min(1).optional(),
|
|
704
|
+
trustedBaseSha: z.string().regex(/^[a-f0-9]{40}$/i).optional(),
|
|
705
|
+
/** Open PR identity observed when the exact head/base pair was admitted. */
|
|
706
|
+
pullRequestNumber: z.number().int().positive().optional(),
|
|
707
|
+
/** Pre-validation hold: an intentional entrypoint change still needs exact-SHA approval. */
|
|
708
|
+
entrypointApprovalPending: z.boolean().optional(),
|
|
709
|
+
/**
|
|
710
|
+
* Authority-selected validation profile id (`full` or trusted-base profile).
|
|
711
|
+
* Optional for legacy records; missing means pre-profile runs (treat as full).
|
|
712
|
+
* Telemetry / runtime propagation only — never changes pass/fail by itself.
|
|
713
|
+
*/
|
|
714
|
+
validationProfile: z.string().min(1).max(64).optional(),
|
|
715
|
+
/**
|
|
716
|
+
* Human-readable reason for {@link validationProfile}. Optional for legacy.
|
|
717
|
+
*/
|
|
718
|
+
validationProfileReason: z.string().min(1).max(500).optional(),
|
|
719
|
+
/**
|
|
720
|
+
* Immediate failed predecessor whose successful release-gate slices may be
|
|
721
|
+
* reused once. Submission assigns this only on the first same-branch
|
|
722
|
+
* successor; the worker revalidates every retry invariant before use.
|
|
723
|
+
*/
|
|
724
|
+
retryOfSha: z.string().regex(/^[a-f0-9]{40}$/i).optional(),
|
|
725
|
+
/** Successor SHA that atomically claimed this failed run's one retry slot. */
|
|
726
|
+
retryClaimedBySha: z.string().regex(/^[a-f0-9]{40}$/i).optional(),
|
|
727
|
+
/** Durable provenance for an accepted or attempted release-gate slice retry. */
|
|
728
|
+
gateRetry: z.object({
|
|
729
|
+
sourceSha: z.string().regex(/^[a-f0-9]{40}$/i),
|
|
730
|
+
manifestDigest: z.string().regex(/^[a-f0-9]{64}$/),
|
|
731
|
+
rerunLabels: z.array(z.string().min(1).max(120)).min(1).max(200),
|
|
732
|
+
reusedLabels: z.array(z.string().min(1).max(120)).min(1).max(200),
|
|
733
|
+
applied: z.boolean().optional()
|
|
734
|
+
}).optional(),
|
|
735
|
+
/**
|
|
736
|
+
* Structured failing tests parsed from a failed test-phase command's captured
|
|
737
|
+
* output. Telemetry only — never consulted by pass/fail decisions.
|
|
738
|
+
*/
|
|
739
|
+
failingTests: z.array(LocalCiFailingTestSchema).optional(),
|
|
740
|
+
/** True when failingTests hit the retention cap and dropped further entries. */
|
|
741
|
+
failingTestsTruncated: z.boolean().optional(),
|
|
742
|
+
/**
|
|
743
|
+
* Cross-machine verdict reuse provenance. Present only when this run skipped
|
|
744
|
+
* local compute because an identical executed tree had already passed the
|
|
745
|
+
* same suite under an equivalent authenticated toolchain.
|
|
746
|
+
*
|
|
747
|
+
* Recorded so a reused run is never mistaken for one validated on this
|
|
748
|
+
* machine: `poetic ci status` must be able to say the compute was reused and
|
|
749
|
+
* name the producing machine and verdict ref. Telemetry/provenance only —
|
|
750
|
+
* never consulted by pass/fail decisions.
|
|
751
|
+
*/
|
|
752
|
+
verdictReuse: z.object({
|
|
753
|
+
/** Verdict ref that authorized the skip. */
|
|
754
|
+
ref: z.string().min(1).max(300),
|
|
755
|
+
/** Executed tree the verdict attests to (field name retained). */
|
|
756
|
+
mergeTree: z.string().min(1).max(64),
|
|
757
|
+
/** Machine that originally produced the verdict (provenance only). */
|
|
758
|
+
producer: z.string().min(1).max(32),
|
|
759
|
+
/** Completion time of the ORIGINAL validating run. */
|
|
760
|
+
originallyCompletedAt: z.string().min(1).max(64)
|
|
761
|
+
}).optional(),
|
|
762
|
+
/**
|
|
763
|
+
* Outstanding remote status publication (failure or success-path). Present
|
|
764
|
+
* until exact-context readback confirms the remote state; blocks terminal
|
|
765
|
+
* retention deletion. Success-path records use status `publication_pending`
|
|
766
|
+
* until publication is confirmed.
|
|
767
|
+
*/
|
|
768
|
+
outstandingRemoteStatusPublication: OutstandingRemoteStatusPublicationSchema.optional(),
|
|
769
|
+
/**
|
|
770
|
+
* In-flight remote status publication lease (historical field name). Blocks
|
|
771
|
+
* fresh-generation establishment while ownerPid is alive; recoverable when
|
|
772
|
+
* the owner is dead.
|
|
773
|
+
*/
|
|
774
|
+
failurePublicationLease: FailurePublicationLeaseSchema.optional()
|
|
775
|
+
}).transform((record) => ({
|
|
776
|
+
...record,
|
|
777
|
+
status: normalizeLocalCiRunStatus(record.status)
|
|
778
|
+
}));
|
|
779
|
+
function hasOutstandingRemoteStatusPublication(record) {
|
|
780
|
+
return record.outstandingRemoteStatusPublication !== void 0;
|
|
781
|
+
}
|
|
782
|
+
function isPublicationPending(record) {
|
|
783
|
+
return record.status === "publication_pending";
|
|
784
|
+
}
|
|
785
|
+
function outstandingStatusPublicationMatchesRecord(record, obligation) {
|
|
786
|
+
return obligation.repository === record.repository && obligation.sha.toLowerCase() === record.sha.toLowerCase();
|
|
787
|
+
}
|
|
788
|
+
function hasLiveFailurePublicationLease(record) {
|
|
789
|
+
const lease = record.failurePublicationLease;
|
|
790
|
+
if (!lease) return false;
|
|
791
|
+
return processIsAlive(lease.ownerPid);
|
|
792
|
+
}
|
|
793
|
+
function resolveFailurePublicationClaimedAt(currentUpdatedAt, candidateClaimedAt) {
|
|
794
|
+
const currentMs = Date.parse(currentUpdatedAt);
|
|
795
|
+
const candidateMs = Date.parse(candidateClaimedAt);
|
|
796
|
+
if (!Number.isFinite(currentMs)) {
|
|
797
|
+
return Number.isFinite(candidateMs) ? new Date(candidateMs).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
|
|
798
|
+
}
|
|
799
|
+
const floorMs = currentMs + 1;
|
|
800
|
+
if (!Number.isFinite(candidateMs)) {
|
|
801
|
+
return new Date(floorMs).toISOString();
|
|
802
|
+
}
|
|
803
|
+
return new Date(Math.max(candidateMs, floorMs)).toISOString();
|
|
804
|
+
}
|
|
805
|
+
function createFailurePublicationLease(current, claimedAt) {
|
|
806
|
+
const resolvedClaimedAt = resolveFailurePublicationClaimedAt(
|
|
807
|
+
current.updatedAt,
|
|
808
|
+
claimedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
809
|
+
);
|
|
810
|
+
const postAcquisitionGeneration = {
|
|
811
|
+
...current,
|
|
812
|
+
updatedAt: resolvedClaimedAt,
|
|
813
|
+
// Lease attached below; field is not part of the generation fingerprint.
|
|
814
|
+
failurePublicationLease: void 0
|
|
815
|
+
};
|
|
816
|
+
const lease = FailurePublicationLeaseSchema.parse({
|
|
817
|
+
token: randomUUID(),
|
|
818
|
+
ownerPid: process.pid,
|
|
819
|
+
generationKey: localCiRecordGenerationKey(postAcquisitionGeneration),
|
|
820
|
+
claimedAt: resolvedClaimedAt
|
|
821
|
+
});
|
|
822
|
+
const leased = {
|
|
823
|
+
...postAcquisitionGeneration,
|
|
824
|
+
failurePublicationLease: lease
|
|
825
|
+
};
|
|
826
|
+
return { lease, leased };
|
|
827
|
+
}
|
|
828
|
+
function matchesFailurePublicationLease(record, lease) {
|
|
829
|
+
const current = record.failurePublicationLease;
|
|
830
|
+
if (!current) return false;
|
|
831
|
+
if (current.token !== lease.token || current.generationKey !== lease.generationKey || current.ownerPid !== lease.ownerPid || current.claimedAt !== lease.claimedAt) {
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
return localCiRecordGenerationKey(record) === lease.generationKey;
|
|
835
|
+
}
|
|
836
|
+
function isSameLocalCiRecordGeneration(current, expected) {
|
|
837
|
+
return localCiRecordGenerationKey(current) === localCiRecordGenerationKey(expected);
|
|
838
|
+
}
|
|
839
|
+
function localCiRecordGenerationKey(record) {
|
|
840
|
+
const obligation = record.outstandingRemoteStatusPublication;
|
|
841
|
+
const material = [
|
|
842
|
+
record.repository,
|
|
843
|
+
record.sha.toLowerCase(),
|
|
844
|
+
record.status,
|
|
845
|
+
record.submittedAt ?? record.createdAt,
|
|
846
|
+
record.createdAt,
|
|
847
|
+
record.updatedAt,
|
|
848
|
+
obligation ? [
|
|
849
|
+
obligation.repository,
|
|
850
|
+
obligation.sha.toLowerCase(),
|
|
851
|
+
obligation.context,
|
|
852
|
+
obligation.state,
|
|
853
|
+
obligation.description,
|
|
854
|
+
obligation.targetStatus ?? "",
|
|
855
|
+
obligation.mac ?? ""
|
|
856
|
+
].join("\0") : ""
|
|
857
|
+
].join("\0");
|
|
858
|
+
return createHash2("sha256").update(material).digest("hex");
|
|
859
|
+
}
|
|
860
|
+
function createOutstandingStatusPublication(record, input) {
|
|
861
|
+
if (input.state === "failure" && input.targetStatus !== void 0) {
|
|
862
|
+
throw new Error("Failure status publications must not carry a success targetStatus.");
|
|
863
|
+
}
|
|
864
|
+
if (input.state !== "failure" && input.targetStatus === void 0) {
|
|
865
|
+
throw new Error("Success-path status publications require targetStatus.");
|
|
866
|
+
}
|
|
867
|
+
const parsed = OutstandingRemoteStatusPublicationSchema.parse({
|
|
868
|
+
repository: record.repository,
|
|
869
|
+
sha: record.sha.toLowerCase(),
|
|
870
|
+
context: record.statusContext,
|
|
871
|
+
state: input.state,
|
|
872
|
+
description: input.description.slice(0, 140),
|
|
873
|
+
...input.targetStatus !== void 0 ? { targetStatus: input.targetStatus } : {}
|
|
874
|
+
});
|
|
875
|
+
return OutstandingRemoteStatusPublicationSchema.parse(
|
|
876
|
+
attachOutstandingStatusPublicationMac(parsed)
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
function createOutstandingFailureStatusPublication(record, description) {
|
|
880
|
+
return createOutstandingStatusPublication(record, {
|
|
881
|
+
state: "failure",
|
|
882
|
+
description
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
var DEFAULT_LOCAL_CI_MAX_TERMINAL_RECORDS = 50;
|
|
886
|
+
function resolveMaxTerminalRecords(value) {
|
|
887
|
+
if (value !== void 0 && Number.isInteger(value) && value >= 0) {
|
|
888
|
+
return value;
|
|
889
|
+
}
|
|
890
|
+
return DEFAULT_LOCAL_CI_MAX_TERMINAL_RECORDS;
|
|
891
|
+
}
|
|
892
|
+
function compareRecordsNewestFirst(a, b) {
|
|
893
|
+
const byCreated = b.createdAt.localeCompare(a.createdAt);
|
|
894
|
+
if (byCreated !== 0) return byCreated;
|
|
895
|
+
return b.sha.localeCompare(a.sha);
|
|
896
|
+
}
|
|
897
|
+
function submissionTime(record) {
|
|
898
|
+
return record.submittedAt ?? record.createdAt;
|
|
899
|
+
}
|
|
900
|
+
function compareSubmissionsNewestFirst(a, b) {
|
|
901
|
+
const bySubmitted = submissionTime(b).localeCompare(submissionTime(a));
|
|
902
|
+
if (bySubmitted !== 0) return bySubmitted;
|
|
903
|
+
return b.sha.localeCompare(a.sha);
|
|
904
|
+
}
|
|
905
|
+
function createQueuedRun(input) {
|
|
906
|
+
const timestamp = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
907
|
+
const repository = parseLocalCiRepositoryIdentity(input.repository);
|
|
908
|
+
if (!repository) {
|
|
909
|
+
throw new Error(`Invalid local CI repository identity: ${input.repository}`);
|
|
910
|
+
}
|
|
911
|
+
return LocalCiRunRecordSchema.parse({
|
|
912
|
+
schemaVersion: 1,
|
|
913
|
+
repoRoot: path3.resolve(input.repoRoot),
|
|
914
|
+
repository,
|
|
915
|
+
sha: input.sha.toLowerCase(),
|
|
916
|
+
branch: input.branch,
|
|
917
|
+
remote: input.remote,
|
|
918
|
+
statusContext: input.statusContext ?? DEFAULT_LOCAL_CI_STATUS_CONTEXT,
|
|
919
|
+
maxConcurrent: input.maxConcurrent ?? DEFAULT_LOCAL_CI_MAX_CONCURRENT,
|
|
920
|
+
retryOfSha: input.retryOfSha?.toLowerCase(),
|
|
921
|
+
status: "queued",
|
|
922
|
+
commands: [],
|
|
923
|
+
submittedAt: timestamp,
|
|
924
|
+
createdAt: timestamp,
|
|
925
|
+
updatedAt: timestamp
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
function repoStateKey(repository) {
|
|
929
|
+
const identity = parseLocalCiRepositoryIdentity(repository) ?? repository.trim();
|
|
930
|
+
const readable = identity.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
931
|
+
const digest = createHash2("sha256").update(identity).digest("hex").slice(0, 12);
|
|
932
|
+
return `${readable || "repository"}-${digest}`;
|
|
933
|
+
}
|
|
934
|
+
function stateReadError(filePath, cause) {
|
|
935
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
936
|
+
return new Error(`Invalid local CI state at ${filePath}: ${detail}`, { cause });
|
|
937
|
+
}
|
|
938
|
+
function isNodeErrorCode(error, code) {
|
|
939
|
+
return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
|
|
940
|
+
}
|
|
941
|
+
function isPathUnderRoot(root, candidatePath) {
|
|
942
|
+
const resolvedRoot = path3.resolve(root);
|
|
943
|
+
const resolvedCandidate = path3.resolve(candidatePath);
|
|
944
|
+
const relative = path3.relative(resolvedRoot, resolvedCandidate);
|
|
945
|
+
return relative !== "" && !relative.startsWith("..") && !path3.isAbsolute(relative);
|
|
946
|
+
}
|
|
947
|
+
var LocalCiStateStore = class {
|
|
948
|
+
constructor(root, hooks = {}) {
|
|
949
|
+
this.root = root;
|
|
950
|
+
this.hooks = hooks;
|
|
951
|
+
}
|
|
952
|
+
root;
|
|
953
|
+
hooks;
|
|
954
|
+
repositoryDirectory(repository) {
|
|
955
|
+
return path3.join(this.root, repoStateKey(repository));
|
|
956
|
+
}
|
|
957
|
+
filePath(repository, sha) {
|
|
958
|
+
return path3.join(this.repositoryDirectory(repository), `${sha.toLowerCase()}.json`);
|
|
959
|
+
}
|
|
960
|
+
logPath(repository, sha) {
|
|
961
|
+
return path3.join(this.repositoryDirectory(repository), `${sha.toLowerCase()}.log`);
|
|
962
|
+
}
|
|
963
|
+
workerClaimPath(repository, sha) {
|
|
964
|
+
return path3.join(this.repositoryDirectory(repository), `${sha.toLowerCase()}.worker.lock`);
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Synthetic proper-lockfile target for worker-claim mutations (never created).
|
|
968
|
+
* Distinct from the claim payload path and from the record JSON path.
|
|
969
|
+
*/
|
|
970
|
+
workerClaimMutexTarget(repository, sha) {
|
|
971
|
+
return path3.join(
|
|
972
|
+
this.repositoryDirectory(repository),
|
|
973
|
+
`${sha.toLowerCase()}.worker.claim.mutex`
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Serialize claim create/stale-reclaim, transfer, and release for one SHA.
|
|
978
|
+
*
|
|
979
|
+
* Lock order (mandatory, never reverse):
|
|
980
|
+
* 1. record mutation lock (when proving authorized generation)
|
|
981
|
+
* 2. claim mutation lock
|
|
982
|
+
*
|
|
983
|
+
* claimWorker nests claim under record. transfer/release take claim only and
|
|
984
|
+
* must never acquire the record lock while holding claim.
|
|
985
|
+
*
|
|
986
|
+
* Uses fenced proper-lockfile ownership (owner.token) so a resumed predecessor
|
|
987
|
+
* after stale reclaim cannot unlock/remove a successor's lock path.
|
|
988
|
+
*/
|
|
989
|
+
async withWorkerClaimMutationLock(repository, sha, fn) {
|
|
990
|
+
const lockTarget = this.workerClaimMutexTarget(repository, sha);
|
|
991
|
+
await mkdir(path3.dirname(lockTarget), { recursive: true, mode: 448 });
|
|
992
|
+
const acquired = await acquireFencedFsLock(lockTarget, this.resolveFsLockOptions());
|
|
993
|
+
try {
|
|
994
|
+
if (this.hooks.afterClaimLockAcquired) {
|
|
995
|
+
await this.hooks.afterClaimLockAcquired();
|
|
996
|
+
}
|
|
997
|
+
await acquired.fence.assertOwned();
|
|
998
|
+
return await fn(acquired.fence);
|
|
999
|
+
} finally {
|
|
1000
|
+
await acquired.release();
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Run `fn` while holding the exclusive per-record mutation lock.
|
|
1005
|
+
* Non-reentrant for the same lock class.
|
|
1006
|
+
*
|
|
1007
|
+
* Locks the stable record JSON path without creating it (`realpath: false`).
|
|
1008
|
+
* May nest the claim mutation lock inside (record → claim only).
|
|
1009
|
+
*
|
|
1010
|
+
* Fenced with owner.token: assert ownership before state-changing writes and
|
|
1011
|
+
* across await boundaries; release is a no-op if superseded.
|
|
1012
|
+
*/
|
|
1013
|
+
async withRecordMutationLock(repository, sha, fn) {
|
|
1014
|
+
const lockTarget = this.filePath(repository, sha);
|
|
1015
|
+
await mkdir(path3.dirname(lockTarget), { recursive: true, mode: 448 });
|
|
1016
|
+
const acquired = await acquireFencedFsLock(lockTarget, this.resolveFsLockOptions());
|
|
1017
|
+
try {
|
|
1018
|
+
if (this.hooks.afterRecordLockAcquired) {
|
|
1019
|
+
await this.hooks.afterRecordLockAcquired();
|
|
1020
|
+
}
|
|
1021
|
+
await acquired.fence.assertOwned();
|
|
1022
|
+
return await fn(acquired.fence);
|
|
1023
|
+
} finally {
|
|
1024
|
+
await acquired.release();
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
/** Shared lock options; test hooks may override floors-respecting timings. */
|
|
1028
|
+
resolveFsLockOptions() {
|
|
1029
|
+
const overrides = this.hooks.lockOptions;
|
|
1030
|
+
if (!overrides) return CROSS_PROCESS_FS_LOCK_OPTIONS;
|
|
1031
|
+
const base = resolveCrossProcessFsLockPolicy();
|
|
1032
|
+
const stale = Math.max(overrides.stale ?? base.stale, PROPER_LOCKFILE_STALE_MIN_MS);
|
|
1033
|
+
const configuredUpdate = overrides.update ?? base.update;
|
|
1034
|
+
const update = Math.max(Math.min(configuredUpdate, stale / 2), PROPER_LOCKFILE_UPDATE_MIN_MS);
|
|
1035
|
+
return {
|
|
1036
|
+
...CROSS_PROCESS_FS_LOCK_OPTIONS,
|
|
1037
|
+
stale,
|
|
1038
|
+
update
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
async readUnlocked(repository, sha) {
|
|
1042
|
+
const filePath = this.filePath(repository, sha);
|
|
1043
|
+
try {
|
|
1044
|
+
const raw = await readFile(filePath, "utf8");
|
|
1045
|
+
return LocalCiRunRecordSchema.parse(JSON.parse(raw));
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
throw stateReadError(filePath, error);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Write a run record under an already-held record mutation fence.
|
|
1052
|
+
*
|
|
1053
|
+
* Awaits (mkdir, temp write) may outlive a predecessor's stale window; re-assert
|
|
1054
|
+
* the fence immediately before the final rename that publishes the record so a
|
|
1055
|
+
* superseded holder cannot overwrite successor state.
|
|
1056
|
+
*/
|
|
1057
|
+
async writeRecordUnlocked(record, fence) {
|
|
1058
|
+
const parsed = LocalCiRunRecordSchema.parse(record);
|
|
1059
|
+
const filePath = this.filePath(parsed.repository, parsed.sha);
|
|
1060
|
+
await mkdir(path3.dirname(filePath), { recursive: true, mode: 448 });
|
|
1061
|
+
const temporary = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
1062
|
+
await writeFile(temporary, `${JSON.stringify(parsed, null, 2)}
|
|
1063
|
+
`, {
|
|
1064
|
+
encoding: "utf8",
|
|
1065
|
+
mode: 384
|
|
1066
|
+
});
|
|
1067
|
+
const attemptId = beginHostPublicationGenerationJournal(parsed);
|
|
1068
|
+
try {
|
|
1069
|
+
await fence.assertOwned();
|
|
1070
|
+
await rename(temporary, filePath);
|
|
1071
|
+
} catch (error) {
|
|
1072
|
+
abandonHostPublicationGenerationJournal(parsed, attemptId);
|
|
1073
|
+
await unlink(temporary).catch(() => void 0);
|
|
1074
|
+
throw error;
|
|
1075
|
+
}
|
|
1076
|
+
syncHostPublicationGeneration(parsed, attemptId);
|
|
1077
|
+
return parsed;
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Acquire exclusive worker ownership for the authorized record generation.
|
|
1081
|
+
*
|
|
1082
|
+
* Lock order: record mutation lock, then claim mutation lock (never reverse).
|
|
1083
|
+
* Under the record lock, re-read the on-disk record and require
|
|
1084
|
+
* {@link isSameLocalCiRecordGeneration} with `authorized`. If the disk
|
|
1085
|
+
* generation differs, return null without touching the claim (prevents a
|
|
1086
|
+
* stale launcher from superseding a fresher claim after the fact).
|
|
1087
|
+
*
|
|
1088
|
+
* Under the claim mutex (only after generation proof):
|
|
1089
|
+
* - same-generation live claim → coalesce (return null)
|
|
1090
|
+
* - different older claim generation → supersede even if child PID is live
|
|
1091
|
+
* - legacy payload (no generationKey) + live pid → do not steal
|
|
1092
|
+
* - legacy/dead payload → reclaimable
|
|
1093
|
+
*/
|
|
1094
|
+
async claimWorker(authorized) {
|
|
1095
|
+
const repository = authorized.repository;
|
|
1096
|
+
const normalizedSha = authorized.sha.toLowerCase();
|
|
1097
|
+
const generationKey = localCiRecordGenerationKey(authorized);
|
|
1098
|
+
return this.withRecordMutationLock(repository, normalizedSha, async (recordFence) => {
|
|
1099
|
+
await recordFence.assertOwned();
|
|
1100
|
+
let onDisk;
|
|
1101
|
+
try {
|
|
1102
|
+
onDisk = await this.readUnlocked(repository, normalizedSha);
|
|
1103
|
+
} catch {
|
|
1104
|
+
return null;
|
|
1105
|
+
}
|
|
1106
|
+
await recordFence.assertOwned();
|
|
1107
|
+
if (!isSameLocalCiRecordGeneration(onDisk, authorized)) {
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1110
|
+
return this.withWorkerClaimMutationLock(repository, normalizedSha, async (claimFence) => {
|
|
1111
|
+
await claimFence.assertOwned();
|
|
1112
|
+
const filePath = this.workerClaimPath(repository, normalizedSha);
|
|
1113
|
+
await mkdir(path3.dirname(filePath), { recursive: true, mode: 448 });
|
|
1114
|
+
await claimFence.assertOwned();
|
|
1115
|
+
try {
|
|
1116
|
+
const current = WorkerClaimPayloadSchema.parse(
|
|
1117
|
+
JSON.parse(await readFile(filePath, "utf8"))
|
|
1118
|
+
);
|
|
1119
|
+
await claimFence.assertOwned();
|
|
1120
|
+
const currentGeneration = current.generationKey;
|
|
1121
|
+
if (currentGeneration === generationKey) {
|
|
1122
|
+
if (processIsAlive(current.pid)) {
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1125
|
+
await claimFence.assertOwned();
|
|
1126
|
+
await unlink(filePath).catch((unlinkError) => {
|
|
1127
|
+
if (!isNodeErrorCode(unlinkError, "ENOENT")) throw unlinkError;
|
|
1128
|
+
});
|
|
1129
|
+
} else if (currentGeneration === void 0) {
|
|
1130
|
+
if (processIsAlive(current.pid)) {
|
|
1131
|
+
return null;
|
|
1132
|
+
}
|
|
1133
|
+
await claimFence.assertOwned();
|
|
1134
|
+
await unlink(filePath).catch((unlinkError) => {
|
|
1135
|
+
if (!isNodeErrorCode(unlinkError, "ENOENT")) throw unlinkError;
|
|
1136
|
+
});
|
|
1137
|
+
} else {
|
|
1138
|
+
await claimFence.assertOwned();
|
|
1139
|
+
await unlink(filePath).catch((unlinkError) => {
|
|
1140
|
+
if (!isNodeErrorCode(unlinkError, "ENOENT")) throw unlinkError;
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
} catch (readError) {
|
|
1144
|
+
if (isNodeErrorCode(readError, "ENOENT")) {
|
|
1145
|
+
} else {
|
|
1146
|
+
await claimFence.assertOwned();
|
|
1147
|
+
await unlink(filePath).catch((unlinkError) => {
|
|
1148
|
+
if (!isNodeErrorCode(unlinkError, "ENOENT")) throw unlinkError;
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
const claim = WorkerClaimPayloadSchema.parse({
|
|
1153
|
+
pid: process.pid,
|
|
1154
|
+
token: randomUUID(),
|
|
1155
|
+
claimedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1156
|
+
generationKey
|
|
1157
|
+
});
|
|
1158
|
+
const temporary = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
1159
|
+
await writeFile(temporary, `${JSON.stringify(claim)}
|
|
1160
|
+
`, {
|
|
1161
|
+
encoding: "utf8",
|
|
1162
|
+
mode: 384
|
|
1163
|
+
});
|
|
1164
|
+
try {
|
|
1165
|
+
await claimFence.assertOwned();
|
|
1166
|
+
await link(temporary, filePath);
|
|
1167
|
+
await unlink(temporary).catch(() => void 0);
|
|
1168
|
+
return {
|
|
1169
|
+
...claim,
|
|
1170
|
+
generationKey,
|
|
1171
|
+
filePath,
|
|
1172
|
+
repository,
|
|
1173
|
+
sha: normalizedSha
|
|
1174
|
+
};
|
|
1175
|
+
} catch (error) {
|
|
1176
|
+
await unlink(temporary).catch(() => void 0);
|
|
1177
|
+
if (isNodeErrorCode(error, "EEXIST")) return null;
|
|
1178
|
+
throw error;
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Transfer claim ownership to a spawned worker PID. Token + generation must
|
|
1185
|
+
* still match under the claim mutex; a superseded claim cannot transfer.
|
|
1186
|
+
*/
|
|
1187
|
+
async transferWorkerClaim(claim, pid) {
|
|
1188
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
1189
|
+
throw new Error("transferWorkerClaim requires a positive integer pid.");
|
|
1190
|
+
}
|
|
1191
|
+
return this.withWorkerClaimMutationLock(claim.repository, claim.sha, async (fence) => {
|
|
1192
|
+
await fence.assertOwned();
|
|
1193
|
+
let current;
|
|
1194
|
+
try {
|
|
1195
|
+
current = WorkerClaimPayloadSchema.parse(
|
|
1196
|
+
JSON.parse(await readFile(claim.filePath, "utf8"))
|
|
1197
|
+
);
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
throw new Error(
|
|
1200
|
+
`Local CI worker claim missing during transfer at ${claim.filePath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
1201
|
+
{ cause: error }
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
await fence.assertOwned();
|
|
1205
|
+
if (current.token !== claim.token) {
|
|
1206
|
+
throw new Error("Local CI worker claim ownership changed.");
|
|
1207
|
+
}
|
|
1208
|
+
if (current.generationKey !== claim.generationKey) {
|
|
1209
|
+
throw new Error("Local CI worker claim generation changed.");
|
|
1210
|
+
}
|
|
1211
|
+
const transferred = WorkerClaimPayloadSchema.parse({
|
|
1212
|
+
...current,
|
|
1213
|
+
pid,
|
|
1214
|
+
generationKey: claim.generationKey
|
|
1215
|
+
});
|
|
1216
|
+
const temporary = `${claim.filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
1217
|
+
await writeFile(temporary, `${JSON.stringify(transferred)}
|
|
1218
|
+
`, {
|
|
1219
|
+
encoding: "utf8",
|
|
1220
|
+
mode: 384
|
|
1221
|
+
});
|
|
1222
|
+
await fence.assertOwned();
|
|
1223
|
+
try {
|
|
1224
|
+
await rename(temporary, claim.filePath);
|
|
1225
|
+
} catch (error) {
|
|
1226
|
+
await unlink(temporary).catch(() => void 0);
|
|
1227
|
+
throw error;
|
|
1228
|
+
}
|
|
1229
|
+
return {
|
|
1230
|
+
...transferred,
|
|
1231
|
+
generationKey: claim.generationKey,
|
|
1232
|
+
filePath: claim.filePath,
|
|
1233
|
+
repository: claim.repository,
|
|
1234
|
+
sha: claim.sha
|
|
1235
|
+
};
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Release a claim only when on-disk owner pid (and optional generation) still
|
|
1240
|
+
* match. Serialized under the claim mutex so release cannot unlink a
|
|
1241
|
+
* generation-superseded successor.
|
|
1242
|
+
*
|
|
1243
|
+
* When `generationKey` is supplied, require exact equality with the on-disk
|
|
1244
|
+
* claim's generationKey (including refusing when the on-disk key is undefined
|
|
1245
|
+
* legacy) so new cleanup cannot delete a legacy claim on PID reuse.
|
|
1246
|
+
*/
|
|
1247
|
+
async releaseWorkerClaim(repository, sha, pid, generationKey) {
|
|
1248
|
+
const normalizedSha = sha.toLowerCase();
|
|
1249
|
+
await this.withWorkerClaimMutationLock(repository, normalizedSha, async (fence) => {
|
|
1250
|
+
await fence.assertOwned();
|
|
1251
|
+
const filePath = this.workerClaimPath(repository, normalizedSha);
|
|
1252
|
+
try {
|
|
1253
|
+
const current = WorkerClaimPayloadSchema.parse(
|
|
1254
|
+
JSON.parse(await readFile(filePath, "utf8"))
|
|
1255
|
+
);
|
|
1256
|
+
await fence.assertOwned();
|
|
1257
|
+
if (current.pid !== pid) return;
|
|
1258
|
+
if (generationKey !== void 0 && current.generationKey !== generationKey) {
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
await fence.assertOwned();
|
|
1262
|
+
await unlink(filePath);
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
if (!isNodeErrorCode(error, "ENOENT")) throw error;
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
rootMappingPath(repoRoot) {
|
|
1269
|
+
return path3.join(this.root, "roots", `${repoRootStateKey(repoRoot)}.json`);
|
|
1270
|
+
}
|
|
1271
|
+
async registerRepositoryRoot(repository, repoRoot) {
|
|
1272
|
+
const filePath = this.rootMappingPath(repoRoot);
|
|
1273
|
+
await mkdir(path3.dirname(filePath), { recursive: true, mode: 448 });
|
|
1274
|
+
await writeFile(
|
|
1275
|
+
filePath,
|
|
1276
|
+
`${JSON.stringify({ repository, repoRoot: path3.resolve(repoRoot) }, null, 2)}
|
|
1277
|
+
`,
|
|
1278
|
+
{ encoding: "utf8", mode: 384 }
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
async repositoryForRoot(repoRoot) {
|
|
1282
|
+
const filePath = this.rootMappingPath(repoRoot);
|
|
1283
|
+
try {
|
|
1284
|
+
const value = z.object({
|
|
1285
|
+
repository: LocalCiRepositoryIdentitySchema,
|
|
1286
|
+
repoRoot: z.string().min(1)
|
|
1287
|
+
}).parse(JSON.parse(await readFile(filePath, "utf8")));
|
|
1288
|
+
return path3.resolve(value.repoRoot) === path3.resolve(repoRoot) ? value.repository : null;
|
|
1289
|
+
} catch (error) {
|
|
1290
|
+
if (isNodeErrorCode(error, "ENOENT")) return null;
|
|
1291
|
+
throw stateReadError(filePath, error);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
async create(record) {
|
|
1295
|
+
const parsed = LocalCiRunRecordSchema.parse(record);
|
|
1296
|
+
const filePath = this.filePath(parsed.repository, parsed.sha);
|
|
1297
|
+
return this.withRecordMutationLock(parsed.repository, parsed.sha, async (fence) => {
|
|
1298
|
+
await fence.assertOwned();
|
|
1299
|
+
await mkdir(path3.dirname(filePath), { recursive: true, mode: 448 });
|
|
1300
|
+
const temporary = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
1301
|
+
await writeFile(temporary, `${JSON.stringify(parsed, null, 2)}
|
|
1302
|
+
`, {
|
|
1303
|
+
encoding: "utf8",
|
|
1304
|
+
mode: 384
|
|
1305
|
+
});
|
|
1306
|
+
try {
|
|
1307
|
+
await fence.assertOwned();
|
|
1308
|
+
await access(filePath);
|
|
1309
|
+
await unlink(temporary).catch(() => void 0);
|
|
1310
|
+
return {
|
|
1311
|
+
created: false,
|
|
1312
|
+
record: await this.readUnlocked(parsed.repository, parsed.sha),
|
|
1313
|
+
filePath
|
|
1314
|
+
};
|
|
1315
|
+
} catch (error) {
|
|
1316
|
+
if (!isNodeErrorCode(error, "ENOENT")) {
|
|
1317
|
+
await unlink(temporary).catch(() => void 0);
|
|
1318
|
+
throw error;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
const attemptId = beginHostPublicationGenerationJournal(parsed);
|
|
1322
|
+
try {
|
|
1323
|
+
await fence.assertOwned();
|
|
1324
|
+
await link(temporary, filePath);
|
|
1325
|
+
await unlink(temporary).catch(() => void 0);
|
|
1326
|
+
} catch (error) {
|
|
1327
|
+
abandonHostPublicationGenerationJournal(parsed, attemptId);
|
|
1328
|
+
await unlink(temporary).catch(() => void 0);
|
|
1329
|
+
if (!isNodeErrorCode(error, "EEXIST")) throw error;
|
|
1330
|
+
return {
|
|
1331
|
+
created: false,
|
|
1332
|
+
record: await this.readUnlocked(parsed.repository, parsed.sha),
|
|
1333
|
+
filePath
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
syncHostPublicationGeneration(parsed, attemptId);
|
|
1337
|
+
return { created: true, record: parsed, filePath };
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
async read(repository, sha) {
|
|
1341
|
+
return this.readUnlocked(repository, sha);
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* Read the durable record and apply any leftover host-generation journal
|
|
1345
|
+
* under the per-record mutation lock so a concurrent writer cannot activate
|
|
1346
|
+
* a newer obligation that this reader then retires.
|
|
1347
|
+
*/
|
|
1348
|
+
async readAndRecover(repository, sha) {
|
|
1349
|
+
return this.withRecordMutationLock(repository, sha, async (fence) => {
|
|
1350
|
+
await fence.assertOwned();
|
|
1351
|
+
const current = await this.readUnlocked(repository, sha);
|
|
1352
|
+
await fence.assertOwned();
|
|
1353
|
+
recoverHostPublicationGeneration(current);
|
|
1354
|
+
return current;
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
async save(record) {
|
|
1358
|
+
const parsed = LocalCiRunRecordSchema.parse(record);
|
|
1359
|
+
return this.withRecordMutationLock(parsed.repository, parsed.sha, async (fence) => {
|
|
1360
|
+
await fence.assertOwned();
|
|
1361
|
+
return this.writeRecordUnlocked(parsed, fence);
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* Conditional save (CAS): write `next` only when the on-disk record still
|
|
1366
|
+
* matches `expected` generation (status, createdAt, updatedAt, obligation).
|
|
1367
|
+
* Cross-process safe under the per-record mutation lock.
|
|
1368
|
+
*/
|
|
1369
|
+
async compareAndSave(expected, next) {
|
|
1370
|
+
const expectedParsed = LocalCiRunRecordSchema.parse(expected);
|
|
1371
|
+
const nextParsed = LocalCiRunRecordSchema.parse(next);
|
|
1372
|
+
if (expectedParsed.repository !== nextParsed.repository || expectedParsed.sha.toLowerCase() !== nextParsed.sha.toLowerCase()) {
|
|
1373
|
+
throw new Error("compareAndSave requires expected and next to share repository/sha.");
|
|
1374
|
+
}
|
|
1375
|
+
if (this.hooks.beforeCompareAndSave) {
|
|
1376
|
+
await this.hooks.beforeCompareAndSave(expectedParsed, nextParsed);
|
|
1377
|
+
}
|
|
1378
|
+
return this.withRecordMutationLock(
|
|
1379
|
+
expectedParsed.repository,
|
|
1380
|
+
expectedParsed.sha,
|
|
1381
|
+
async (fence) => {
|
|
1382
|
+
await fence.assertOwned();
|
|
1383
|
+
return this.compareAndWriteUnlocked(expectedParsed, nextParsed, fence);
|
|
1384
|
+
}
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* CAS write under an already-held record mutation fence.
|
|
1389
|
+
*
|
|
1390
|
+
* Re-asserts ownership after the generation read and immediately before the
|
|
1391
|
+
* final record rename (via {@link writeRecordUnlocked}). A predecessor that
|
|
1392
|
+
* suspends after its first assertion and resumes after reclaim must fail closed
|
|
1393
|
+
* rather than overwrite successor state.
|
|
1394
|
+
*/
|
|
1395
|
+
async compareAndWriteUnlocked(expected, next, fence) {
|
|
1396
|
+
await fence.assertOwned();
|
|
1397
|
+
let current;
|
|
1398
|
+
try {
|
|
1399
|
+
current = await this.readUnlocked(expected.repository, expected.sha);
|
|
1400
|
+
} catch {
|
|
1401
|
+
return { applied: false, record: expected };
|
|
1402
|
+
}
|
|
1403
|
+
await fence.assertOwned();
|
|
1404
|
+
if (!isSameLocalCiRecordGeneration(current, expected)) {
|
|
1405
|
+
return { applied: false, record: current };
|
|
1406
|
+
}
|
|
1407
|
+
if (this.hooks.afterCompareAndWriteRead) {
|
|
1408
|
+
await this.hooks.afterCompareAndWriteRead(current);
|
|
1409
|
+
await fence.assertOwned();
|
|
1410
|
+
}
|
|
1411
|
+
const saved = await this.writeRecordUnlocked(next, fence);
|
|
1412
|
+
return { applied: true, record: saved };
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Hold the per-record mutation lock for a multi-step exclusive section
|
|
1416
|
+
* (e.g. remote failure publish + readback + clear). Nested save/compareAndSave
|
|
1417
|
+
* /deleteRetentionCandidates deadlock — use only the unlocked helpers provided
|
|
1418
|
+
* to `fn`.
|
|
1419
|
+
*
|
|
1420
|
+
* Callers that await (remote publish/readback) must re-assert fence ownership
|
|
1421
|
+
* before any further state change. Fail closed if ownership is lost.
|
|
1422
|
+
*/
|
|
1423
|
+
async withExclusiveRecordAccess(repository, sha, fn) {
|
|
1424
|
+
return this.withRecordMutationLock(repository, sha, async (fence) => {
|
|
1425
|
+
await fence.assertOwned();
|
|
1426
|
+
return fn({
|
|
1427
|
+
read: async () => {
|
|
1428
|
+
await fence.assertOwned();
|
|
1429
|
+
const record = await this.readUnlocked(repository, sha);
|
|
1430
|
+
await fence.assertOwned();
|
|
1431
|
+
return record;
|
|
1432
|
+
},
|
|
1433
|
+
writeIfSameGeneration: async (expected, next) => {
|
|
1434
|
+
await fence.assertOwned();
|
|
1435
|
+
return this.compareAndWriteUnlocked(expected, next, fence);
|
|
1436
|
+
},
|
|
1437
|
+
assertOwned: () => fence.assertOwned()
|
|
1438
|
+
});
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Persist a spawned worker PID only when the on-disk generation still matches
|
|
1443
|
+
* `expected` (the generation that authorized the spawn). Used after requeue CAS
|
|
1444
|
+
* so a concurrent same-SHA resubmit is never overwritten with a stale PID/save.
|
|
1445
|
+
*/
|
|
1446
|
+
async assignWorkerPid(expected, pid) {
|
|
1447
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
1448
|
+
throw new Error("assignWorkerPid requires a positive integer pid.");
|
|
1449
|
+
}
|
|
1450
|
+
const next = {
|
|
1451
|
+
...expected,
|
|
1452
|
+
pid,
|
|
1453
|
+
status: "queued",
|
|
1454
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1455
|
+
};
|
|
1456
|
+
return this.compareAndSave(expected, next);
|
|
1457
|
+
}
|
|
1458
|
+
async list(repository) {
|
|
1459
|
+
const directory = this.repositoryDirectory(repository);
|
|
1460
|
+
let entries;
|
|
1461
|
+
try {
|
|
1462
|
+
const { readdir } = await import("fs/promises");
|
|
1463
|
+
entries = await readdir(directory);
|
|
1464
|
+
} catch (error) {
|
|
1465
|
+
if (isNodeErrorCode(error, "ENOENT")) return [];
|
|
1466
|
+
throw error;
|
|
1467
|
+
}
|
|
1468
|
+
const records = [];
|
|
1469
|
+
for (const entry of entries.filter((value) => value.endsWith(".json")).sort()) {
|
|
1470
|
+
const sha = entry.slice(0, -".json".length);
|
|
1471
|
+
if (!/^[a-f0-9]{40}$/i.test(sha)) continue;
|
|
1472
|
+
records.push(await this.read(repository, sha));
|
|
1473
|
+
}
|
|
1474
|
+
return records.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
1475
|
+
}
|
|
1476
|
+
async listQueuedForBranch(repository, branch) {
|
|
1477
|
+
const records = await this.list(repository);
|
|
1478
|
+
return records.filter((record) => record.branch === branch && record.status === "queued").sort(compareSubmissionsNewestFirst);
|
|
1479
|
+
}
|
|
1480
|
+
async supersedeOlderQueued(input) {
|
|
1481
|
+
const newerSha = input.newerSha.toLowerCase();
|
|
1482
|
+
const updatedAt = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1483
|
+
const queued = await this.listQueuedForBranch(input.repository, input.branch);
|
|
1484
|
+
const newerRecord = queued.find((record) => record.sha === newerSha);
|
|
1485
|
+
const superseded = [];
|
|
1486
|
+
for (const candidate of queued) {
|
|
1487
|
+
if (candidate.sha === newerSha) continue;
|
|
1488
|
+
if (newerRecord) {
|
|
1489
|
+
const ordering = compareSubmissionsNewestFirst(candidate, newerRecord);
|
|
1490
|
+
if (ordering <= 0) continue;
|
|
1491
|
+
}
|
|
1492
|
+
let current;
|
|
1493
|
+
try {
|
|
1494
|
+
current = await this.read(input.repository, candidate.sha);
|
|
1495
|
+
} catch {
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
if (current.status !== "queued") continue;
|
|
1499
|
+
if (current.branch !== input.branch) continue;
|
|
1500
|
+
const next = {
|
|
1501
|
+
...current,
|
|
1502
|
+
status: "superseded",
|
|
1503
|
+
updatedAt
|
|
1504
|
+
};
|
|
1505
|
+
const saved = await this.save(next);
|
|
1506
|
+
superseded.push(saved);
|
|
1507
|
+
}
|
|
1508
|
+
return superseded;
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* Count consecutive terminal `failed` runs for a branch, newest submission
|
|
1512
|
+
* first. A success-family terminal run (validation passed) resets the count;
|
|
1513
|
+
* active runs and `superseded`/`skipped` runs carry no validation verdict and
|
|
1514
|
+
* are ignored. Derived from on-disk records on every call so the count cannot
|
|
1515
|
+
* drift; the terminal retention window (newest-first, obligations held)
|
|
1516
|
+
* always preserves enough history for this small threshold.
|
|
1517
|
+
*/
|
|
1518
|
+
async countConsecutiveTerminalFailures(repository, branch) {
|
|
1519
|
+
const records = await this.list(repository);
|
|
1520
|
+
const sameBranch = records.filter((record) => record.branch === branch).sort(compareSubmissionsNewestFirst);
|
|
1521
|
+
const failedShas = [];
|
|
1522
|
+
for (const record of sameBranch) {
|
|
1523
|
+
if (isLocalCiActiveStatus(record.status)) continue;
|
|
1524
|
+
if (record.status === "superseded" || record.status === "skipped") continue;
|
|
1525
|
+
if (record.status === "failed") {
|
|
1526
|
+
failedShas.push(record.sha);
|
|
1527
|
+
continue;
|
|
1528
|
+
}
|
|
1529
|
+
break;
|
|
1530
|
+
}
|
|
1531
|
+
return { count: failedShas.length, failedShas };
|
|
1532
|
+
}
|
|
1533
|
+
async hasNewerSubmission(input) {
|
|
1534
|
+
const sha = input.sha.toLowerCase();
|
|
1535
|
+
const submittedAt = input.submittedAt ?? input.createdAt;
|
|
1536
|
+
const records = await this.list(input.repository);
|
|
1537
|
+
const sameBranch = records.filter((record) => record.branch === input.branch);
|
|
1538
|
+
for (const record of sameBranch) {
|
|
1539
|
+
const candidateSubmittedAt = submissionTime(record);
|
|
1540
|
+
if (record.sha === sha && candidateSubmittedAt === submittedAt) continue;
|
|
1541
|
+
if (candidateSubmittedAt > submittedAt) return true;
|
|
1542
|
+
if (candidateSubmittedAt === submittedAt && record.sha !== sha) {
|
|
1543
|
+
if (record.sha.localeCompare(sha) > 0) return true;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
return false;
|
|
1547
|
+
}
|
|
1548
|
+
async toRetentionCandidate(record) {
|
|
1549
|
+
const recordPath = this.filePath(record.repository, record.sha);
|
|
1550
|
+
const logPath = this.logPath(record.repository, record.sha);
|
|
1551
|
+
let logExists = false;
|
|
1552
|
+
try {
|
|
1553
|
+
await access(logPath);
|
|
1554
|
+
logExists = true;
|
|
1555
|
+
} catch {
|
|
1556
|
+
logExists = false;
|
|
1557
|
+
}
|
|
1558
|
+
return { record, recordPath, logPath, logExists };
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Select terminal records to preserve vs delete under a global terminal cap.
|
|
1562
|
+
*
|
|
1563
|
+
* Active (queued/running) records are always preserved and never count toward
|
|
1564
|
+
* the terminal cap. Records with an outstanding remote-status publication or
|
|
1565
|
+
* retained-worktree cleanup obligation are always preserved and never count
|
|
1566
|
+
* toward the terminal cap.
|
|
1567
|
+
* Among remaining terminal records, keep the newest globally up to
|
|
1568
|
+
* `maxTerminalRecords` (default {@link DEFAULT_LOCAL_CI_MAX_TERMINAL_RECORDS});
|
|
1569
|
+
* older free terminals are deletable regardless of historical branch count.
|
|
1570
|
+
*/
|
|
1571
|
+
async selectTerminalRetention(repository, options) {
|
|
1572
|
+
const records = await this.list(repository);
|
|
1573
|
+
if (records.length === 0) {
|
|
1574
|
+
return { preserve: [], deletable: [] };
|
|
1575
|
+
}
|
|
1576
|
+
const activeRecords = records.filter((record) => isLocalCiActiveStatus(record.status));
|
|
1577
|
+
const terminalRecords = records.filter((record) => isLocalCiTerminalStatus(record.status)).sort(compareRecordsNewestFirst);
|
|
1578
|
+
const obligationHeld = terminalRecords.filter(
|
|
1579
|
+
(record) => hasOutstandingRemoteStatusPublication(record) || record.worktreePath !== void 0
|
|
1580
|
+
);
|
|
1581
|
+
const freeTerminals = terminalRecords.filter(
|
|
1582
|
+
(record) => !hasOutstandingRemoteStatusPublication(record) && record.worktreePath === void 0
|
|
1583
|
+
);
|
|
1584
|
+
const maxTerminal = resolveMaxTerminalRecords(options?.maxTerminalRecords);
|
|
1585
|
+
const preserveTerminals = [...obligationHeld, ...freeTerminals.slice(0, maxTerminal)];
|
|
1586
|
+
const deletableRecords = freeTerminals.slice(maxTerminal);
|
|
1587
|
+
const preserveRecords = [...activeRecords, ...preserveTerminals];
|
|
1588
|
+
const [preserve, deletable] = await Promise.all([
|
|
1589
|
+
Promise.all(preserveRecords.map((record) => this.toRetentionCandidate(record))),
|
|
1590
|
+
Promise.all(deletableRecords.map((record) => this.toRetentionCandidate(record)))
|
|
1591
|
+
]);
|
|
1592
|
+
return { preserve, deletable };
|
|
1593
|
+
}
|
|
1594
|
+
async deleteRetentionCandidates(candidates) {
|
|
1595
|
+
const deletedRecords = [];
|
|
1596
|
+
const deletedLogs = [];
|
|
1597
|
+
const rootResolved = path3.resolve(this.root);
|
|
1598
|
+
for (const candidate of candidates) {
|
|
1599
|
+
const selected = candidate.record;
|
|
1600
|
+
const recordPath = this.filePath(selected.repository, selected.sha);
|
|
1601
|
+
const logPath = this.logPath(selected.repository, selected.sha);
|
|
1602
|
+
if (!isPathUnderRoot(rootResolved, recordPath) || !isPathUnderRoot(rootResolved, logPath)) {
|
|
1603
|
+
continue;
|
|
1604
|
+
}
|
|
1605
|
+
const deleted = await this.withRecordMutationLock(
|
|
1606
|
+
selected.repository,
|
|
1607
|
+
selected.sha,
|
|
1608
|
+
async (fence) => {
|
|
1609
|
+
await fence.assertOwned();
|
|
1610
|
+
let current;
|
|
1611
|
+
try {
|
|
1612
|
+
current = await this.readUnlocked(selected.repository, selected.sha);
|
|
1613
|
+
} catch {
|
|
1614
|
+
return false;
|
|
1615
|
+
}
|
|
1616
|
+
await fence.assertOwned();
|
|
1617
|
+
if (isLocalCiActiveStatus(current.status)) return false;
|
|
1618
|
+
if (!isLocalCiTerminalStatus(current.status)) return false;
|
|
1619
|
+
if (hasOutstandingRemoteStatusPublication(current)) return false;
|
|
1620
|
+
if (current.worktreePath !== void 0) return false;
|
|
1621
|
+
if (current.repository !== selected.repository || current.sha !== selected.sha || current.status !== selected.status || current.updatedAt !== selected.updatedAt || current.createdAt !== selected.createdAt) {
|
|
1622
|
+
return false;
|
|
1623
|
+
}
|
|
1624
|
+
if (this.hooks.afterRetentionDeleteCheck) {
|
|
1625
|
+
await this.hooks.afterRetentionDeleteCheck(current);
|
|
1626
|
+
await fence.assertOwned();
|
|
1627
|
+
try {
|
|
1628
|
+
current = await this.readUnlocked(selected.repository, selected.sha);
|
|
1629
|
+
} catch {
|
|
1630
|
+
return false;
|
|
1631
|
+
}
|
|
1632
|
+
await fence.assertOwned();
|
|
1633
|
+
if (isLocalCiActiveStatus(current.status)) return false;
|
|
1634
|
+
if (!isLocalCiTerminalStatus(current.status)) return false;
|
|
1635
|
+
if (hasOutstandingRemoteStatusPublication(current)) return false;
|
|
1636
|
+
if (current.worktreePath !== void 0) return false;
|
|
1637
|
+
if (current.repository !== selected.repository || current.sha !== selected.sha || current.status !== selected.status || current.updatedAt !== selected.updatedAt || current.createdAt !== selected.createdAt) {
|
|
1638
|
+
return false;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
await fence.assertOwned();
|
|
1642
|
+
try {
|
|
1643
|
+
await unlink(recordPath);
|
|
1644
|
+
deletedRecords.push(recordPath);
|
|
1645
|
+
} catch (error) {
|
|
1646
|
+
if (!isNodeErrorCode(error, "ENOENT")) throw error;
|
|
1647
|
+
}
|
|
1648
|
+
await fence.assertOwned();
|
|
1649
|
+
try {
|
|
1650
|
+
await unlink(logPath);
|
|
1651
|
+
deletedLogs.push(logPath);
|
|
1652
|
+
} catch (error) {
|
|
1653
|
+
if (!isNodeErrorCode(error, "ENOENT")) throw error;
|
|
1654
|
+
}
|
|
1655
|
+
return true;
|
|
1656
|
+
}
|
|
1657
|
+
);
|
|
1658
|
+
void deleted;
|
|
1659
|
+
}
|
|
1660
|
+
return { deletedRecords, deletedLogs };
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
function selectCommandsForLocalCi(input) {
|
|
1664
|
+
const candidates = input.explicit?.length ? input.explicit : input.resolved.map((entry) => entry.command);
|
|
1665
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1666
|
+
const commands = [];
|
|
1667
|
+
for (const candidate of candidates) {
|
|
1668
|
+
const command = candidate.trim();
|
|
1669
|
+
if (!command || seen.has(command)) continue;
|
|
1670
|
+
seen.add(command);
|
|
1671
|
+
commands.push(command);
|
|
1672
|
+
}
|
|
1673
|
+
return commands;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
// src/core/ci/local-ci-runtime.ts
|
|
1677
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1678
|
+
import {
|
|
1679
|
+
link as link2,
|
|
1680
|
+
mkdir as mkdir2,
|
|
1681
|
+
mkdtemp,
|
|
1682
|
+
readFile as readFile2,
|
|
1683
|
+
rename as rename2,
|
|
1684
|
+
rm,
|
|
1685
|
+
stat,
|
|
1686
|
+
unlink as unlink2,
|
|
1687
|
+
writeFile as writeFile2
|
|
1688
|
+
} from "fs/promises";
|
|
1689
|
+
import os from "os";
|
|
1690
|
+
import path5 from "path";
|
|
1691
|
+
import { setTimeout as delay } from "timers/promises";
|
|
1692
|
+
import { execa } from "execa";
|
|
1693
|
+
import { z as z2 } from "zod";
|
|
1694
|
+
|
|
1695
|
+
// src/core/project/validation-authority.ts
|
|
1696
|
+
import { readFileSync } from "fs";
|
|
1697
|
+
import path4 from "path";
|
|
1698
|
+
var AUTHORITY_EXACT_PATHS = /* @__PURE__ */ new Set([
|
|
1699
|
+
".poetic/config/poetic.config.jsonc",
|
|
1700
|
+
"poetic.config.jsonc",
|
|
1701
|
+
"package.json",
|
|
1702
|
+
"pyproject.toml",
|
|
1703
|
+
"setup.py",
|
|
1704
|
+
"setup.cfg",
|
|
1705
|
+
"tox.ini",
|
|
1706
|
+
"pytest.ini",
|
|
1707
|
+
"go.mod",
|
|
1708
|
+
"cargo.toml",
|
|
1709
|
+
"pom.xml",
|
|
1710
|
+
"gradle.properties",
|
|
1711
|
+
"package.swift",
|
|
1712
|
+
"pubspec.yaml",
|
|
1713
|
+
"gemfile",
|
|
1714
|
+
"rakefile",
|
|
1715
|
+
"composer.json",
|
|
1716
|
+
"makefile",
|
|
1717
|
+
"cmakelists.txt",
|
|
1718
|
+
"cmakepresets.json",
|
|
1719
|
+
"meson.build",
|
|
1720
|
+
"deno.json",
|
|
1721
|
+
"deno.jsonc",
|
|
1722
|
+
"turbo.json",
|
|
1723
|
+
"justfile",
|
|
1724
|
+
"taskfile.yml",
|
|
1725
|
+
"taskfile.yaml"
|
|
1726
|
+
]);
|
|
1727
|
+
var AUTHORITY_PREFIXES = [".githooks/", ".poetic/config/", ".poetic/guard/"];
|
|
1728
|
+
var POETIC_SOURCE_AUTHORITY_PREFIXES = [
|
|
1729
|
+
"src/core/ci/",
|
|
1730
|
+
"src/core/project/",
|
|
1731
|
+
"src/core/verification/",
|
|
1732
|
+
"scripts/release/"
|
|
1733
|
+
];
|
|
1734
|
+
var SHELL_INTERPRETERS = /* @__PURE__ */ new Set(["sh", "bash", "zsh", "dash", "ksh", "fish"]);
|
|
1735
|
+
var PYTHON_INTERPRETERS = /* @__PURE__ */ new Set(["python", "python2", "python3", "pypy", "pypy3"]);
|
|
1736
|
+
var LANGUAGE_INTERPRETERS = /* @__PURE__ */ new Set([
|
|
1737
|
+
"node",
|
|
1738
|
+
"nodejs",
|
|
1739
|
+
"deno",
|
|
1740
|
+
"bun",
|
|
1741
|
+
"ruby",
|
|
1742
|
+
"perl",
|
|
1743
|
+
"php",
|
|
1744
|
+
"lua",
|
|
1745
|
+
"luajit",
|
|
1746
|
+
"raku",
|
|
1747
|
+
"racket",
|
|
1748
|
+
"julia",
|
|
1749
|
+
...PYTHON_INTERPRETERS
|
|
1750
|
+
]);
|
|
1751
|
+
var DIRECT_TEST_RUNNERS = /* @__PURE__ */ new Set([
|
|
1752
|
+
"pytest",
|
|
1753
|
+
"py.test",
|
|
1754
|
+
"unittest",
|
|
1755
|
+
"nose",
|
|
1756
|
+
"nose2",
|
|
1757
|
+
"vitest",
|
|
1758
|
+
"jest",
|
|
1759
|
+
"mocha",
|
|
1760
|
+
"ava",
|
|
1761
|
+
"tap",
|
|
1762
|
+
"rspec",
|
|
1763
|
+
"phpunit",
|
|
1764
|
+
"prove",
|
|
1765
|
+
"bats",
|
|
1766
|
+
"cram"
|
|
1767
|
+
]);
|
|
1768
|
+
function normalize(filePath) {
|
|
1769
|
+
return filePath.replace(/\\/g, "/").replace(/^\.\//, "").toLowerCase();
|
|
1770
|
+
}
|
|
1771
|
+
function normalizeRepoRelativePath(filePath) {
|
|
1772
|
+
const raw = filePath.trim();
|
|
1773
|
+
if (!raw || raw.includes("\0")) return null;
|
|
1774
|
+
if (raw.startsWith("/") || raw.startsWith("\\")) return null;
|
|
1775
|
+
if (raw.startsWith("\\\\")) return null;
|
|
1776
|
+
if (/^[a-zA-Z]:/.test(raw)) return null;
|
|
1777
|
+
let withForward = raw.replace(/\\/g, "/");
|
|
1778
|
+
if (withForward.startsWith("/")) return null;
|
|
1779
|
+
if (withForward.startsWith("//")) return null;
|
|
1780
|
+
if (/^[a-zA-Z]:/.test(withForward)) return null;
|
|
1781
|
+
withForward = withForward.replace(/^\.\//, "");
|
|
1782
|
+
if (withForward.length === 0) return null;
|
|
1783
|
+
const hadTrailingSlash = withForward.endsWith("/") && withForward.length > 1;
|
|
1784
|
+
const segments = withForward.split("/");
|
|
1785
|
+
for (const segment of segments) {
|
|
1786
|
+
if (segment === "..") return null;
|
|
1787
|
+
}
|
|
1788
|
+
const cleaned = segments.filter((segment) => segment.length > 0 && segment !== ".").join("/");
|
|
1789
|
+
if (!cleaned || cleaned === ".") return null;
|
|
1790
|
+
const result = `${cleaned}${hadTrailingSlash ? "/" : ""}`;
|
|
1791
|
+
if (result === ".." || result.startsWith("../") || result.includes("/../") || result.endsWith("/..")) {
|
|
1792
|
+
return null;
|
|
1793
|
+
}
|
|
1794
|
+
return result;
|
|
1795
|
+
}
|
|
1796
|
+
function normalizeAuthorityPath(filePath) {
|
|
1797
|
+
const normalized = normalizeRepoRelativePath(filePath);
|
|
1798
|
+
return normalized === null ? null : normalized.toLowerCase();
|
|
1799
|
+
}
|
|
1800
|
+
function commandReferencedPaths(commands) {
|
|
1801
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1802
|
+
for (const command of commands) {
|
|
1803
|
+
if (opaqueCommandReason(command)) continue;
|
|
1804
|
+
for (const raw of command.split(/\s+/)) {
|
|
1805
|
+
const token = raw.replace(/^['"]|['"]$/g, "").replace(/[;,]$/, "");
|
|
1806
|
+
if (!token || token.startsWith("-") || token.includes("://")) continue;
|
|
1807
|
+
if (token.startsWith("./") || token.includes("/") || /\.[a-z0-9]{1,8}$/i.test(token)) {
|
|
1808
|
+
const normalized = normalize(token);
|
|
1809
|
+
if (!normalized.startsWith("../") && !path4.isAbsolute(token)) paths.add(normalized);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
return [...paths];
|
|
1814
|
+
}
|
|
1815
|
+
function isPoeticSourceCheckout(projectRoot) {
|
|
1816
|
+
if (!projectRoot) return false;
|
|
1817
|
+
try {
|
|
1818
|
+
const manifest = JSON.parse(readFileSync(path4.join(projectRoot, "package.json"), "utf8"));
|
|
1819
|
+
return manifest.name === "@poetic-ai/poetic";
|
|
1820
|
+
} catch {
|
|
1821
|
+
return false;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
function opaqueCommandReason(command) {
|
|
1825
|
+
const trimmed = command.trim();
|
|
1826
|
+
if (!trimmed) return "empty verification command";
|
|
1827
|
+
if (/['"]/.test(trimmed) || /\\/.test(trimmed)) {
|
|
1828
|
+
return `opaque quoted or escaped shell tokens: ${trimmed}`;
|
|
1829
|
+
}
|
|
1830
|
+
if (/[\n|&;`$<>()]/.test(trimmed)) {
|
|
1831
|
+
return `opaque shell composition: ${trimmed}`;
|
|
1832
|
+
}
|
|
1833
|
+
if (parseProjectDependencySetupCommand(trimmed)) {
|
|
1834
|
+
return null;
|
|
1835
|
+
}
|
|
1836
|
+
const tokens = trimmed.split(/\s+/).filter(Boolean);
|
|
1837
|
+
const head = (tokens[0] ?? "").toLowerCase();
|
|
1838
|
+
const headBase = head.includes("/") ? head.split("/").pop() ?? head : head;
|
|
1839
|
+
if (SHELL_INTERPRETERS.has(headBase)) {
|
|
1840
|
+
return `opaque shell interpreter: ${trimmed}`;
|
|
1841
|
+
}
|
|
1842
|
+
if (PYTHON_INTERPRETERS.has(headBase)) {
|
|
1843
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
1844
|
+
const token = tokens[index] ?? "";
|
|
1845
|
+
if (token === "-m") {
|
|
1846
|
+
return `opaque interpreter module: ${trimmed}`;
|
|
1847
|
+
}
|
|
1848
|
+
if (token && !token.startsWith("-")) break;
|
|
1849
|
+
}
|
|
1850
|
+
return `opaque interpreter: ${trimmed}`;
|
|
1851
|
+
}
|
|
1852
|
+
if (head === "npm" || head === "pnpm" || head === "yarn" || head === "bun") {
|
|
1853
|
+
const verb = (tokens[1] ?? "").toLowerCase();
|
|
1854
|
+
if (verb === "ci" || verb === "install") {
|
|
1855
|
+
return `opaque package-manager install variant: ${trimmed}`;
|
|
1856
|
+
}
|
|
1857
|
+
if (verb === "run" || verb === "test" || verb === "start" || verb === "exec") {
|
|
1858
|
+
return `opaque package-manager script: ${trimmed}`;
|
|
1859
|
+
}
|
|
1860
|
+
if (head === "yarn" && verb && !verb.startsWith("-")) {
|
|
1861
|
+
return `opaque package-manager script: ${trimmed}`;
|
|
1862
|
+
}
|
|
1863
|
+
if (verb === "run-script") {
|
|
1864
|
+
return `opaque package-manager script: ${trimmed}`;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
if (head === "npx" || head === "pnpx" || head === "bunx") {
|
|
1868
|
+
return `opaque package-manager launcher: ${trimmed}`;
|
|
1869
|
+
}
|
|
1870
|
+
if (head === "make" || head === "just" || head === "task" || head === "turbo" || head === "nx") {
|
|
1871
|
+
return `opaque task-runner command: ${trimmed}`;
|
|
1872
|
+
}
|
|
1873
|
+
if (headBase === "go" && (tokens[1] ?? "").toLowerCase() === "test") {
|
|
1874
|
+
return `opaque test runner: ${trimmed}`;
|
|
1875
|
+
}
|
|
1876
|
+
if (headBase === "dotnet" && (tokens[1] ?? "").toLowerCase() === "test") {
|
|
1877
|
+
return `opaque test runner: ${trimmed}`;
|
|
1878
|
+
}
|
|
1879
|
+
if (headBase === "cargo" && (tokens[1] ?? "").toLowerCase() === "test") {
|
|
1880
|
+
return `opaque test runner: ${trimmed}`;
|
|
1881
|
+
}
|
|
1882
|
+
if (DIRECT_TEST_RUNNERS.has(headBase)) {
|
|
1883
|
+
return `opaque test runner: ${trimmed}`;
|
|
1884
|
+
}
|
|
1885
|
+
if (LANGUAGE_INTERPRETERS.has(headBase)) {
|
|
1886
|
+
return `opaque interpreter: ${trimmed}`;
|
|
1887
|
+
}
|
|
1888
|
+
return `opaque unresolved command: ${trimmed}`;
|
|
1889
|
+
}
|
|
1890
|
+
function staticAuthorityPaths(poeticSourceCheckout) {
|
|
1891
|
+
const paths = /* @__PURE__ */ new Set([...AUTHORITY_EXACT_PATHS, ...AUTHORITY_PREFIXES]);
|
|
1892
|
+
if (poeticSourceCheckout) {
|
|
1893
|
+
for (const prefix of POETIC_SOURCE_AUTHORITY_PREFIXES) paths.add(prefix);
|
|
1894
|
+
}
|
|
1895
|
+
return [...paths].sort();
|
|
1896
|
+
}
|
|
1897
|
+
function resolveProjectVerificationAuthority(trustedCommands, options = {}) {
|
|
1898
|
+
const poeticSourceCheckout = isPoeticSourceCheckout(options.projectRoot);
|
|
1899
|
+
const pathSet = new Set(staticAuthorityPaths(poeticSourceCheckout));
|
|
1900
|
+
for (const referenced of commandReferencedPaths(trustedCommands)) {
|
|
1901
|
+
pathSet.add(referenced);
|
|
1902
|
+
}
|
|
1903
|
+
const normalizedAdditional = [];
|
|
1904
|
+
for (const raw of options.additionalAuthorityPaths ?? []) {
|
|
1905
|
+
const normalized = normalizeAuthorityPath(raw);
|
|
1906
|
+
if (!normalized) continue;
|
|
1907
|
+
pathSet.add(normalized);
|
|
1908
|
+
normalizedAdditional.push(normalized);
|
|
1909
|
+
}
|
|
1910
|
+
const reasons = [];
|
|
1911
|
+
for (const command of trustedCommands) {
|
|
1912
|
+
const reason = opaqueCommandReason(command);
|
|
1913
|
+
if (reason) reasons.push(reason);
|
|
1914
|
+
}
|
|
1915
|
+
const assertedComplete = options.authorityPathsComplete === true && normalizedAdditional.length > 0;
|
|
1916
|
+
return {
|
|
1917
|
+
paths: [...pathSet].sort(),
|
|
1918
|
+
complete: reasons.length === 0 || assertedComplete,
|
|
1919
|
+
reasons
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
// src/core/ci/local-ci-change-set.ts
|
|
1924
|
+
var InvalidGitRawDiffPathEncodingError = class extends Error {
|
|
1925
|
+
invalidPathBytes;
|
|
1926
|
+
constructor(invalidPathBytes) {
|
|
1927
|
+
super("Changed path is not valid UTF-8; exact Git path identity required.");
|
|
1928
|
+
this.name = "InvalidGitRawDiffPathEncodingError";
|
|
1929
|
+
this.invalidPathBytes = invalidPathBytes;
|
|
1930
|
+
}
|
|
1931
|
+
};
|
|
1932
|
+
var RAW_META_RE = /^:([0-7]{6}) ([0-7]{6}) ([0-9a-f]{40}|0{40}) ([0-9a-f]{40}|0{40}) ([A-Z])(\d*)$/i;
|
|
1933
|
+
function decodeStrictUtf8Path(part) {
|
|
1934
|
+
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
1935
|
+
try {
|
|
1936
|
+
return decoder.decode(part);
|
|
1937
|
+
} catch {
|
|
1938
|
+
throw new InvalidGitRawDiffPathEncodingError(Buffer.from(part));
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
function splitNulFields(stdout) {
|
|
1942
|
+
if (stdout.length === 0) return [];
|
|
1943
|
+
const parts = [];
|
|
1944
|
+
let start = 0;
|
|
1945
|
+
for (let i = 0; i < stdout.length; i++) {
|
|
1946
|
+
if (stdout[i] === 0) {
|
|
1947
|
+
parts.push(stdout.subarray(start, i));
|
|
1948
|
+
start = i + 1;
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
if (parts.length > 0 && parts[parts.length - 1].length === 0) {
|
|
1952
|
+
parts.pop();
|
|
1953
|
+
}
|
|
1954
|
+
return parts;
|
|
1955
|
+
}
|
|
1956
|
+
function parseGitRawDiffChangeSet(stdout) {
|
|
1957
|
+
if (stdout.length === 0) {
|
|
1958
|
+
return { complete: true, entries: [] };
|
|
1959
|
+
}
|
|
1960
|
+
if (stdout[stdout.length - 1] !== 0) {
|
|
1961
|
+
return {
|
|
1962
|
+
complete: false,
|
|
1963
|
+
reason: "Malformed raw diff: nonempty -z output lacks final NUL terminator",
|
|
1964
|
+
entries: []
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
let parts;
|
|
1968
|
+
try {
|
|
1969
|
+
parts = splitNulFields(stdout);
|
|
1970
|
+
} catch (error) {
|
|
1971
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1972
|
+
return { complete: false, reason, entries: [] };
|
|
1973
|
+
}
|
|
1974
|
+
const entries = [];
|
|
1975
|
+
let index = 0;
|
|
1976
|
+
while (index < parts.length) {
|
|
1977
|
+
const metaBuf = parts[index];
|
|
1978
|
+
if (!metaBuf || metaBuf.length === 0) {
|
|
1979
|
+
return {
|
|
1980
|
+
complete: false,
|
|
1981
|
+
reason: "Malformed raw diff: empty metadata field",
|
|
1982
|
+
entries
|
|
1983
|
+
};
|
|
1984
|
+
}
|
|
1985
|
+
let meta;
|
|
1986
|
+
try {
|
|
1987
|
+
meta = decodeStrictUtf8Path(metaBuf);
|
|
1988
|
+
} catch (error) {
|
|
1989
|
+
if (error instanceof InvalidGitRawDiffPathEncodingError) {
|
|
1990
|
+
return {
|
|
1991
|
+
complete: false,
|
|
1992
|
+
reason: "Raw diff metadata is not valid UTF-8",
|
|
1993
|
+
entries
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
throw error;
|
|
1997
|
+
}
|
|
1998
|
+
const match = RAW_META_RE.exec(meta);
|
|
1999
|
+
if (!match) {
|
|
2000
|
+
return {
|
|
2001
|
+
complete: false,
|
|
2002
|
+
reason: `Malformed raw diff metadata: ${meta.slice(0, 120)}`,
|
|
2003
|
+
entries
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
const oldMode = match[1].toLowerCase();
|
|
2007
|
+
const newMode = match[2].toLowerCase();
|
|
2008
|
+
const status = match[5].toUpperCase();
|
|
2009
|
+
if (match[6] && match[6].length > 0) {
|
|
2010
|
+
return {
|
|
2011
|
+
complete: false,
|
|
2012
|
+
reason: `Raw diff status carries rename/copy score under --no-renames: ${status}${match[6]}`,
|
|
2013
|
+
entries
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
2016
|
+
index += 1;
|
|
2017
|
+
const pathBuf = parts[index];
|
|
2018
|
+
if (!pathBuf || pathBuf.length === 0) {
|
|
2019
|
+
return {
|
|
2020
|
+
complete: false,
|
|
2021
|
+
reason: "Malformed raw diff: missing path after metadata",
|
|
2022
|
+
entries
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
let pathText;
|
|
2026
|
+
try {
|
|
2027
|
+
pathText = decodeStrictUtf8Path(pathBuf);
|
|
2028
|
+
} catch (error) {
|
|
2029
|
+
if (error instanceof InvalidGitRawDiffPathEncodingError) {
|
|
2030
|
+
return {
|
|
2031
|
+
complete: false,
|
|
2032
|
+
reason: "Changed path is not valid UTF-8; exact Git path identity required.",
|
|
2033
|
+
entries
|
|
2034
|
+
};
|
|
2035
|
+
}
|
|
2036
|
+
throw error;
|
|
2037
|
+
}
|
|
2038
|
+
entries.push({
|
|
2039
|
+
path: pathText,
|
|
2040
|
+
status,
|
|
2041
|
+
oldMode,
|
|
2042
|
+
newMode
|
|
2043
|
+
});
|
|
2044
|
+
index += 1;
|
|
2045
|
+
}
|
|
2046
|
+
return { complete: true, entries };
|
|
2047
|
+
}
|
|
2048
|
+
function changeSetPathsMatchNameOnly(changedFiles, changeSet) {
|
|
2049
|
+
if (!changeSet.complete) return false;
|
|
2050
|
+
if (changedFiles.length !== changeSet.entries.length) return false;
|
|
2051
|
+
const nameOnly = [...changedFiles].sort();
|
|
2052
|
+
const raw = changeSet.entries.map((entry) => entry.path).sort();
|
|
2053
|
+
for (let i = 0; i < nameOnly.length; i += 1) {
|
|
2054
|
+
if (nameOnly[i] !== raw[i]) return false;
|
|
2055
|
+
}
|
|
2056
|
+
return true;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// src/core/ci/local-ci-profile.ts
|
|
2060
|
+
var LOCAL_CI_FULL_VALIDATION_PROFILE = "full";
|
|
2061
|
+
var LOCAL_CI_VALIDATION_PROFILE_REASON_MAX = 500;
|
|
2062
|
+
var LOCAL_CI_MAX_VALIDATION_PROFILES = 8;
|
|
2063
|
+
var LOCAL_CI_MAX_PROFILE_INCLUDE_PATHS = 32;
|
|
2064
|
+
var LOCAL_CI_MAX_PROFILE_EXTENSIONS = 16;
|
|
2065
|
+
var PROFILE_ID_RE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
2066
|
+
var PROFILE_EXTENSION_RE = /^\.[A-Za-z0-9_+.-]{1,32}$/;
|
|
2067
|
+
function boundLocalCiValidationProfileReason(reason) {
|
|
2068
|
+
if (reason.length <= LOCAL_CI_VALIDATION_PROFILE_REASON_MAX) return reason;
|
|
2069
|
+
return reason.slice(0, LOCAL_CI_VALIDATION_PROFILE_REASON_MAX);
|
|
2070
|
+
}
|
|
2071
|
+
function full(reason) {
|
|
2072
|
+
return {
|
|
2073
|
+
validationProfile: LOCAL_CI_FULL_VALIDATION_PROFILE,
|
|
2074
|
+
reason: boundLocalCiValidationProfileReason(reason)
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
function resolveLocalCiValidationProfiles(raw) {
|
|
2078
|
+
if (!Array.isArray(raw)) return [];
|
|
2079
|
+
const profiles = [];
|
|
2080
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
2081
|
+
for (const entry of raw) {
|
|
2082
|
+
if (profiles.length >= LOCAL_CI_MAX_VALIDATION_PROFILES) break;
|
|
2083
|
+
if (!entry || typeof entry !== "object") continue;
|
|
2084
|
+
const record = entry;
|
|
2085
|
+
if (typeof record.id !== "string") continue;
|
|
2086
|
+
const id = record.id.trim();
|
|
2087
|
+
if (!PROFILE_ID_RE.test(id) || id === LOCAL_CI_FULL_VALIDATION_PROFILE) continue;
|
|
2088
|
+
if (seenIds.has(id)) {
|
|
2089
|
+
return [];
|
|
2090
|
+
}
|
|
2091
|
+
const include = normalizeIncludeList(record.include);
|
|
2092
|
+
if (include.length === 0) continue;
|
|
2093
|
+
const extensions = normalizeExtensionList(record.extensions);
|
|
2094
|
+
if (extensions.length === 0) continue;
|
|
2095
|
+
seenIds.add(id);
|
|
2096
|
+
profiles.push({ id, include, extensions });
|
|
2097
|
+
}
|
|
2098
|
+
if (profilesHaveOverlappingIncludes(profiles)) {
|
|
2099
|
+
return [];
|
|
2100
|
+
}
|
|
2101
|
+
return profiles;
|
|
2102
|
+
}
|
|
2103
|
+
function normalizeIncludeList(value) {
|
|
2104
|
+
if (!Array.isArray(value)) return [];
|
|
2105
|
+
const paths = [];
|
|
2106
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2107
|
+
for (const entry of value) {
|
|
2108
|
+
if (typeof entry !== "string") continue;
|
|
2109
|
+
if (paths.length >= LOCAL_CI_MAX_PROFILE_INCLUDE_PATHS) break;
|
|
2110
|
+
const normalized = normalizeRepoRelativePath(entry);
|
|
2111
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
2112
|
+
seen.add(normalized);
|
|
2113
|
+
paths.push(normalized);
|
|
2114
|
+
}
|
|
2115
|
+
return paths;
|
|
2116
|
+
}
|
|
2117
|
+
function normalizeExtensionList(value) {
|
|
2118
|
+
if (!Array.isArray(value)) return [];
|
|
2119
|
+
const extensions = [];
|
|
2120
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2121
|
+
for (const entry of value) {
|
|
2122
|
+
if (typeof entry !== "string") continue;
|
|
2123
|
+
if (extensions.length >= LOCAL_CI_MAX_PROFILE_EXTENSIONS) break;
|
|
2124
|
+
const trimmed = entry.trim();
|
|
2125
|
+
if (!PROFILE_EXTENSION_RE.test(trimmed) || seen.has(trimmed)) continue;
|
|
2126
|
+
seen.add(trimmed);
|
|
2127
|
+
extensions.push(trimmed);
|
|
2128
|
+
}
|
|
2129
|
+
return extensions;
|
|
2130
|
+
}
|
|
2131
|
+
function profilesHaveOverlappingIncludes(profiles) {
|
|
2132
|
+
for (let i = 0; i < profiles.length; i += 1) {
|
|
2133
|
+
for (let j = i + 1; j < profiles.length; j += 1) {
|
|
2134
|
+
const a = profiles[i];
|
|
2135
|
+
const b = profiles[j];
|
|
2136
|
+
for (const includeA of a.include) {
|
|
2137
|
+
for (const includeB of b.include) {
|
|
2138
|
+
if (includesOverlap(includeA, includeB)) return true;
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
return false;
|
|
2144
|
+
}
|
|
2145
|
+
function includesOverlap(a, b) {
|
|
2146
|
+
if (a === b) return true;
|
|
2147
|
+
const aPrefix = a.endsWith("/");
|
|
2148
|
+
const bPrefix = b.endsWith("/");
|
|
2149
|
+
if (aPrefix && bPrefix) {
|
|
2150
|
+
return a.startsWith(b) || b.startsWith(a);
|
|
2151
|
+
}
|
|
2152
|
+
if (aPrefix) {
|
|
2153
|
+
return b === a.slice(0, -1) || b.startsWith(a);
|
|
2154
|
+
}
|
|
2155
|
+
if (bPrefix) {
|
|
2156
|
+
return a === b.slice(0, -1) || a.startsWith(b);
|
|
2157
|
+
}
|
|
2158
|
+
return false;
|
|
2159
|
+
}
|
|
2160
|
+
function pathMatchesInclude(filePath, include) {
|
|
2161
|
+
if (include.endsWith("/")) {
|
|
2162
|
+
return filePath === include.slice(0, -1) || filePath.startsWith(include);
|
|
2163
|
+
}
|
|
2164
|
+
return filePath === include;
|
|
2165
|
+
}
|
|
2166
|
+
function fileExtension(filePath) {
|
|
2167
|
+
const base = filePath.includes("/") ? filePath.slice(filePath.lastIndexOf("/") + 1) : filePath;
|
|
2168
|
+
const dot = base.lastIndexOf(".");
|
|
2169
|
+
if (dot <= 0 || dot === base.length - 1) return null;
|
|
2170
|
+
return base.slice(dot);
|
|
2171
|
+
}
|
|
2172
|
+
function gitPathIsSafeRepoRelativeLiteral(filePath) {
|
|
2173
|
+
const safeForm = normalizeRepoRelativePath(filePath);
|
|
2174
|
+
return safeForm !== null && safeForm === filePath;
|
|
2175
|
+
}
|
|
2176
|
+
function entryMatchesProfile(entry, profile) {
|
|
2177
|
+
if (!gitPathIsSafeRepoRelativeLiteral(entry.path)) return false;
|
|
2178
|
+
const extension = fileExtension(entry.path);
|
|
2179
|
+
if (!extension || !profile.extensions.includes(extension)) return false;
|
|
2180
|
+
return profile.include.some((include) => pathMatchesInclude(entry.path, include));
|
|
2181
|
+
}
|
|
2182
|
+
function changePathIntersectsAuthority(filePath, authorityPaths) {
|
|
2183
|
+
const folded = normalizeAuthorityPath(filePath);
|
|
2184
|
+
if (!folded) return true;
|
|
2185
|
+
for (const auth of authorityPaths) {
|
|
2186
|
+
if (auth.endsWith("/")) {
|
|
2187
|
+
if (folded === auth.slice(0, -1) || folded.startsWith(auth)) return true;
|
|
2188
|
+
} else if (folded === auth || folded.startsWith(`${auth}/`)) {
|
|
2189
|
+
return true;
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
return false;
|
|
2193
|
+
}
|
|
2194
|
+
function entryIsRegularAddOrModify(entry) {
|
|
2195
|
+
if (entry.status !== "A" && entry.status !== "M") return false;
|
|
2196
|
+
if (entry.newMode !== "100644") return false;
|
|
2197
|
+
if (entry.status === "A") {
|
|
2198
|
+
return entry.oldMode === "000000";
|
|
2199
|
+
}
|
|
2200
|
+
return entry.oldMode === "100644";
|
|
2201
|
+
}
|
|
2202
|
+
function selectLocalCiValidationProfile(input) {
|
|
2203
|
+
if (!input.trusted) {
|
|
2204
|
+
return full("Authority is not trusted; full validation required.");
|
|
2205
|
+
}
|
|
2206
|
+
if (!input.verificationAuthority.complete) {
|
|
2207
|
+
return full("Verification authority is incomplete; full validation required.");
|
|
2208
|
+
}
|
|
2209
|
+
if (input.changedFiles.length === 0) {
|
|
2210
|
+
return full("Change set is empty; full validation required.");
|
|
2211
|
+
}
|
|
2212
|
+
if (!input.changeSet.complete) {
|
|
2213
|
+
return full(
|
|
2214
|
+
input.changeSet.reason || "Raw change-set metadata is incomplete; full validation required."
|
|
2215
|
+
);
|
|
2216
|
+
}
|
|
2217
|
+
if (!changeSetPathsMatchNameOnly(input.changedFiles, input.changeSet)) {
|
|
2218
|
+
return full("Name-only and raw change-set paths disagree; full validation required.");
|
|
2219
|
+
}
|
|
2220
|
+
if (input.profiles.length === 0) {
|
|
2221
|
+
return full("No trusted-base validation profiles configured; full validation required.");
|
|
2222
|
+
}
|
|
2223
|
+
for (const entry of input.changeSet.entries) {
|
|
2224
|
+
if (!entryIsRegularAddOrModify(entry)) {
|
|
2225
|
+
return full(
|
|
2226
|
+
`Change ${entry.status} ${entry.oldMode}->${entry.newMode} on ${entry.path} is not a regular A/M 100644 edit; full validation required.`
|
|
2227
|
+
);
|
|
2228
|
+
}
|
|
2229
|
+
if (changePathIntersectsAuthority(entry.path, input.verificationAuthority.paths)) {
|
|
2230
|
+
return full(
|
|
2231
|
+
`Change path intersects validation authority (${entry.path}); full validation required.`
|
|
2232
|
+
);
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
let selectedId;
|
|
2236
|
+
for (const entry of input.changeSet.entries) {
|
|
2237
|
+
const matches = input.profiles.filter((profile) => entryMatchesProfile(entry, profile));
|
|
2238
|
+
if (matches.length === 0) {
|
|
2239
|
+
return full(`No validation profile matches ${entry.path}; full validation required.`);
|
|
2240
|
+
}
|
|
2241
|
+
if (matches.length > 1) {
|
|
2242
|
+
return full(
|
|
2243
|
+
`Ambiguous validation profiles for ${entry.path}: ${matches.map((p) => p.id).join(", ")}; full validation required.`
|
|
2244
|
+
);
|
|
2245
|
+
}
|
|
2246
|
+
const id = matches[0].id;
|
|
2247
|
+
if (selectedId === void 0) {
|
|
2248
|
+
selectedId = id;
|
|
2249
|
+
} else if (selectedId !== id) {
|
|
2250
|
+
return full(`Mixed validation profiles (${selectedId} vs ${id}); full validation required.`);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
if (!selectedId) {
|
|
2254
|
+
return full("Unable to select a validation profile; full validation required.");
|
|
2255
|
+
}
|
|
2256
|
+
return {
|
|
2257
|
+
validationProfile: selectedId,
|
|
2258
|
+
reason: boundLocalCiValidationProfileReason(
|
|
2259
|
+
`All changes match trusted-base validation profile '${selectedId}'.`
|
|
2260
|
+
)
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
2263
|
+
function resolveLocalCiValidationProfileEnv(value) {
|
|
2264
|
+
if (typeof value !== "string") return LOCAL_CI_FULL_VALIDATION_PROFILE;
|
|
2265
|
+
const trimmed = value.trim();
|
|
2266
|
+
if (!trimmed) return LOCAL_CI_FULL_VALIDATION_PROFILE;
|
|
2267
|
+
if (trimmed === LOCAL_CI_FULL_VALIDATION_PROFILE) return LOCAL_CI_FULL_VALIDATION_PROFILE;
|
|
2268
|
+
if (!PROFILE_ID_RE.test(trimmed)) return LOCAL_CI_FULL_VALIDATION_PROFILE;
|
|
2269
|
+
return trimmed;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
// src/core/ci/local-ci-runtime.ts
|
|
2273
|
+
var SlotRecordSchema = z2.object({
|
|
2274
|
+
pid: z2.number().int().positive(),
|
|
2275
|
+
token: z2.string().uuid(),
|
|
2276
|
+
acquiredAt: z2.iso.datetime()
|
|
2277
|
+
});
|
|
2278
|
+
var RepositoryIdSchema = z2.string().refine((value) => parseLocalCiRepositoryIdentity(value) !== null, {
|
|
2279
|
+
message: "Invalid local CI repository identity"
|
|
2280
|
+
});
|
|
2281
|
+
var Sha40Schema = z2.string().regex(/^[a-f0-9]{40}$/i);
|
|
2282
|
+
var MaxConcurrentSchema = z2.number().int().min(1).max(8);
|
|
2283
|
+
var TARGET_VALIDATION_ENV_KEYS = [
|
|
2284
|
+
"PATH",
|
|
2285
|
+
"HOME",
|
|
2286
|
+
"USER",
|
|
2287
|
+
"LOGNAME",
|
|
2288
|
+
"TMPDIR",
|
|
2289
|
+
"TEMP",
|
|
2290
|
+
"TMP",
|
|
2291
|
+
"LANG",
|
|
2292
|
+
"LC_ALL",
|
|
2293
|
+
"LC_CTYPE",
|
|
2294
|
+
"TERM",
|
|
2295
|
+
"COLORTERM",
|
|
2296
|
+
"SHELL",
|
|
2297
|
+
"XDG_CACHE_HOME",
|
|
2298
|
+
"XDG_CONFIG_HOME",
|
|
2299
|
+
"XDG_DATA_HOME",
|
|
2300
|
+
"XDG_STATE_HOME"
|
|
2301
|
+
];
|
|
2302
|
+
var SchedulingEnvelopeSchema = z2.object({
|
|
2303
|
+
schemaVersion: z2.literal(1),
|
|
2304
|
+
maxConcurrent: MaxConcurrentSchema,
|
|
2305
|
+
updatedFromBaseSha: Sha40Schema,
|
|
2306
|
+
updatedAt: z2.iso.datetime()
|
|
2307
|
+
});
|
|
2308
|
+
var UNPARSABLE_SLOT_LOCK_STALE_MS = 10 * 60 * 1e3;
|
|
2309
|
+
var DEFAULT_ENVELOPE_BASE_SHA = "0".repeat(40);
|
|
2310
|
+
function createLocalCiDetachedChildEnv(overrides = {}) {
|
|
2311
|
+
const env = createCleanChildEnv(overrides);
|
|
2312
|
+
const ghToken = process.env.GH_TOKEN;
|
|
2313
|
+
const githubToken = process.env.GITHUB_TOKEN;
|
|
2314
|
+
if (typeof ghToken === "string" && ghToken.length > 0) {
|
|
2315
|
+
env.GH_TOKEN = ghToken;
|
|
2316
|
+
}
|
|
2317
|
+
if (typeof githubToken === "string" && githubToken.length > 0) {
|
|
2318
|
+
env.GITHUB_TOKEN = githubToken;
|
|
2319
|
+
}
|
|
2320
|
+
const approvedEntrypointSha = readEnv(POETIC_LOCAL_CI_APPROVE_ENTRYPOINT_SHA).value;
|
|
2321
|
+
delete env.POETIC_LOCAL_CI_APPROVE_ENTRYPOINT_SHA;
|
|
2322
|
+
if (/^[a-f0-9]{40}$/i.test(approvedEntrypointSha ?? "")) {
|
|
2323
|
+
env.POETIC_LOCAL_CI_APPROVE_ENTRYPOINT_SHA = approvedEntrypointSha;
|
|
2324
|
+
}
|
|
2325
|
+
return env;
|
|
2326
|
+
}
|
|
2327
|
+
function createLocalCiTargetValidationEnv(source = process.env, options = {}) {
|
|
2328
|
+
const env = {};
|
|
2329
|
+
for (const key of TARGET_VALIDATION_ENV_KEYS) {
|
|
2330
|
+
const value = source[key];
|
|
2331
|
+
if (typeof value === "string" && value.length > 0) {
|
|
2332
|
+
env[key] = value;
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
env.CI = "1";
|
|
2336
|
+
env.npm_config_script_shell = "/bin/sh";
|
|
2337
|
+
const nodeExtraCaCerts = source.NODE_EXTRA_CA_CERTS;
|
|
2338
|
+
if (typeof nodeExtraCaCerts === "string" && nodeExtraCaCerts.length > 0) {
|
|
2339
|
+
env.NODE_EXTRA_CA_CERTS = nodeExtraCaCerts;
|
|
2340
|
+
}
|
|
2341
|
+
if (options.trustedBaseSha !== void 0) {
|
|
2342
|
+
env.POETIC_RELEASE_GATE_TRUSTED_BASE = Sha40Schema.parse(options.trustedBaseSha).toLowerCase();
|
|
2343
|
+
}
|
|
2344
|
+
env.POETIC_LOCAL_CI_PROFILE = resolveLocalCiValidationProfileEnv(options.validationProfile);
|
|
2345
|
+
if (options.gateRetry !== void 0) {
|
|
2346
|
+
env.POETIC_GATE_RETRY_MANIFEST_DIGEST = options.gateRetry.manifestDigest;
|
|
2347
|
+
env.POETIC_GATE_RETRY_LABELS = JSON.stringify(options.gateRetry.rerunLabels);
|
|
2348
|
+
env.POETIC_GATE_REUSED_LABELS = JSON.stringify(options.gateRetry.reusedLabels);
|
|
2349
|
+
}
|
|
2350
|
+
return env;
|
|
2351
|
+
}
|
|
2352
|
+
function isNodeErrorCode2(error, code) {
|
|
2353
|
+
return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
|
|
2354
|
+
}
|
|
2355
|
+
function normalizeMaxConcurrent(value) {
|
|
2356
|
+
return MaxConcurrentSchema.parse(value);
|
|
2357
|
+
}
|
|
2358
|
+
function defaultSchedulingEnvelope(now) {
|
|
2359
|
+
return {
|
|
2360
|
+
schemaVersion: 1,
|
|
2361
|
+
maxConcurrent: DEFAULT_LOCAL_CI_MAX_CONCURRENT,
|
|
2362
|
+
updatedFromBaseSha: DEFAULT_ENVELOPE_BASE_SHA,
|
|
2363
|
+
updatedAt: (now ?? /* @__PURE__ */ new Date()).toISOString()
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
async function readSchedulingEnvelope(input) {
|
|
2367
|
+
const stateRoot = input.stateRoot ?? getLocalCiStateRoot();
|
|
2368
|
+
const filePath = localCiSchedulingEnvelopePath(stateRoot, repoStateKey(input.repository));
|
|
2369
|
+
try {
|
|
2370
|
+
const raw = await readFile2(filePath, "utf8");
|
|
2371
|
+
return SchedulingEnvelopeSchema.parse(JSON.parse(raw));
|
|
2372
|
+
} catch {
|
|
2373
|
+
return defaultSchedulingEnvelope();
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
async function writeSchedulingEnvelope(input) {
|
|
2377
|
+
const repository = parseLocalCiRepositoryIdentity(RepositoryIdSchema.parse(input.repository));
|
|
2378
|
+
if (!repository) {
|
|
2379
|
+
throw new Error(`Invalid local CI repository identity: ${input.repository}`);
|
|
2380
|
+
}
|
|
2381
|
+
const envelope = SchedulingEnvelopeSchema.parse({
|
|
2382
|
+
schemaVersion: 1,
|
|
2383
|
+
maxConcurrent: input.maxConcurrent,
|
|
2384
|
+
updatedFromBaseSha: input.updatedFromBaseSha.toLowerCase(),
|
|
2385
|
+
updatedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString()
|
|
2386
|
+
});
|
|
2387
|
+
const stateRoot = input.stateRoot ?? getLocalCiStateRoot();
|
|
2388
|
+
const filePath = localCiSchedulingEnvelopePath(stateRoot, repoStateKey(repository));
|
|
2389
|
+
await mkdir2(path5.dirname(filePath), { recursive: true, mode: 448 });
|
|
2390
|
+
const temporary = `${filePath}.${process.pid}.${randomUUID2()}.tmp`;
|
|
2391
|
+
try {
|
|
2392
|
+
await writeFile2(temporary, `${JSON.stringify(envelope, null, 2)}
|
|
2393
|
+
`, {
|
|
2394
|
+
encoding: "utf8",
|
|
2395
|
+
mode: 384
|
|
2396
|
+
});
|
|
2397
|
+
await rename2(temporary, filePath);
|
|
2398
|
+
} catch (error) {
|
|
2399
|
+
await unlink2(temporary).catch(() => void 0);
|
|
2400
|
+
throw error;
|
|
2401
|
+
}
|
|
2402
|
+
return envelope;
|
|
2403
|
+
}
|
|
2404
|
+
async function resolveEffectiveMaxConcurrent(input) {
|
|
2405
|
+
if (input.maxConcurrent !== void 0) {
|
|
2406
|
+
return normalizeMaxConcurrent(input.maxConcurrent);
|
|
2407
|
+
}
|
|
2408
|
+
const envelope = await readSchedulingEnvelope({
|
|
2409
|
+
repository: input.repository,
|
|
2410
|
+
stateRoot: input.stateRoot
|
|
2411
|
+
});
|
|
2412
|
+
return envelope.maxConcurrent;
|
|
2413
|
+
}
|
|
2414
|
+
async function acquireLocalCiSlot(input) {
|
|
2415
|
+
const stateRoot = input.stateRoot ?? getLocalCiStateRoot();
|
|
2416
|
+
const effectiveCap = await resolveEffectiveMaxConcurrent({
|
|
2417
|
+
repository: input.repository,
|
|
2418
|
+
stateRoot,
|
|
2419
|
+
maxConcurrent: input.maxConcurrent
|
|
2420
|
+
});
|
|
2421
|
+
const slotsDirectory = path5.join(stateRoot, repoStateKey(input.repository), "slots");
|
|
2422
|
+
await mkdir2(slotsDirectory, { recursive: true, mode: 448 });
|
|
2423
|
+
const token = randomUUID2();
|
|
2424
|
+
const deadline = Date.now() + (input.timeoutMs ?? 6 * 60 * 60 * 1e3);
|
|
2425
|
+
while (Date.now() < deadline) {
|
|
2426
|
+
for (let slot = 0; slot < effectiveCap; slot += 1) {
|
|
2427
|
+
const filePath = path5.join(slotsDirectory, `${slot}.lock`);
|
|
2428
|
+
const payload = `${JSON.stringify({
|
|
2429
|
+
pid: process.pid,
|
|
2430
|
+
token,
|
|
2431
|
+
acquiredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2432
|
+
})}
|
|
2433
|
+
`;
|
|
2434
|
+
const temporary = `${filePath}.${process.pid}.${randomUUID2()}.tmp`;
|
|
2435
|
+
try {
|
|
2436
|
+
try {
|
|
2437
|
+
await writeFile2(temporary, payload, { encoding: "utf8", mode: 384 });
|
|
2438
|
+
await link2(temporary, filePath);
|
|
2439
|
+
} catch (error) {
|
|
2440
|
+
await unlink2(temporary).catch(() => void 0);
|
|
2441
|
+
throw error;
|
|
2442
|
+
}
|
|
2443
|
+
await unlink2(temporary).catch(() => void 0);
|
|
2444
|
+
return {
|
|
2445
|
+
slot,
|
|
2446
|
+
maxConcurrent: effectiveCap,
|
|
2447
|
+
async release() {
|
|
2448
|
+
try {
|
|
2449
|
+
const current = SlotRecordSchema.parse(JSON.parse(await readFile2(filePath, "utf8")));
|
|
2450
|
+
if (current.token === token && current.pid === process.pid) await unlink2(filePath);
|
|
2451
|
+
} catch (error) {
|
|
2452
|
+
if (!isNodeErrorCode2(error, "ENOENT")) {
|
|
2453
|
+
throw error;
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
};
|
|
2458
|
+
} catch (error) {
|
|
2459
|
+
if (!isNodeErrorCode2(error, "EEXIST")) {
|
|
2460
|
+
throw error;
|
|
2461
|
+
}
|
|
2462
|
+
try {
|
|
2463
|
+
const current = SlotRecordSchema.parse(JSON.parse(await readFile2(filePath, "utf8")));
|
|
2464
|
+
if (!processIsAlive(current.pid)) await unlink2(filePath);
|
|
2465
|
+
} catch (readError) {
|
|
2466
|
+
if (isNodeErrorCode2(readError, "ENOENT")) {
|
|
2467
|
+
continue;
|
|
2468
|
+
}
|
|
2469
|
+
try {
|
|
2470
|
+
const metadata = await stat(filePath);
|
|
2471
|
+
if (Date.now() - metadata.mtimeMs > UNPARSABLE_SLOT_LOCK_STALE_MS) {
|
|
2472
|
+
await unlink2(filePath).catch((unlinkError) => {
|
|
2473
|
+
if (!isNodeErrorCode2(unlinkError, "ENOENT")) throw unlinkError;
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
} catch (statError) {
|
|
2477
|
+
if (!isNodeErrorCode2(statError, "ENOENT")) throw statError;
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
await delay(input.pollMs ?? 2e3);
|
|
2483
|
+
}
|
|
2484
|
+
throw new Error("Timed out waiting for a local CI execution slot.");
|
|
2485
|
+
}
|
|
2486
|
+
async function createLocalCiWorktree(repoRoot, sha) {
|
|
2487
|
+
const parent = await mkdtemp(path5.join(os.tmpdir(), "poetic-local-ci-"));
|
|
2488
|
+
const worktreePath = path5.join(parent, "worktree");
|
|
2489
|
+
const result = await execGit(["worktree", "add", "--detach", worktreePath, sha], {
|
|
2490
|
+
cwd: repoRoot
|
|
2491
|
+
});
|
|
2492
|
+
if (result.exitCode !== 0) {
|
|
2493
|
+
await rm(parent, { recursive: true, force: true });
|
|
2494
|
+
throw new Error(`Unable to create local CI worktree: ${result.stderr || result.stdout}`);
|
|
2495
|
+
}
|
|
2496
|
+
return worktreePath;
|
|
2497
|
+
}
|
|
2498
|
+
async function removeLocalCiWorktree(repoRoot, worktreePath) {
|
|
2499
|
+
const resolved = path5.resolve(worktreePath);
|
|
2500
|
+
const temporaryRoot = path5.resolve(os.tmpdir());
|
|
2501
|
+
if (!resolved.startsWith(`${temporaryRoot}${path5.sep}poetic-local-ci-`)) {
|
|
2502
|
+
throw new Error(`Refusing to remove non-local-CI worktree path: ${resolved}`);
|
|
2503
|
+
}
|
|
2504
|
+
const removal = await execGit(["worktree", "remove", "--force", resolved], { cwd: repoRoot });
|
|
2505
|
+
await execGit(["worktree", "prune"], { cwd: repoRoot });
|
|
2506
|
+
await rm(path5.dirname(resolved), { recursive: true, force: true });
|
|
2507
|
+
if (removal.exitCode !== 0 && !/not a working tree|is not a working tree/i.test(removal.stderr)) {
|
|
2508
|
+
throw new Error(`Unable to unregister local CI worktree: ${removal.stderr || removal.stdout}`);
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
async function pruneLocalCiWorktrees(repoRoot) {
|
|
2512
|
+
const result = await execGit(["worktree", "prune"], { cwd: repoRoot });
|
|
2513
|
+
if (result.exitCode !== 0) {
|
|
2514
|
+
throw new Error(`Unable to prune local CI worktrees: ${result.stderr || result.stdout}`);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
var DEFAULT_LOCAL_CI_COMMAND_TIMEOUT_MS = 60 * 60 * 1e3;
|
|
2518
|
+
async function runLocalCiVerificationCommand(command, cwd, options) {
|
|
2519
|
+
const timeoutMs = options?.timeoutMs ?? DEFAULT_LOCAL_CI_COMMAND_TIMEOUT_MS;
|
|
2520
|
+
const env = createLocalCiTargetValidationEnv(process.env, {
|
|
2521
|
+
trustedBaseSha: options?.trustedBaseSha,
|
|
2522
|
+
validationProfile: options?.validationProfile ?? LOCAL_CI_FULL_VALIDATION_PROFILE,
|
|
2523
|
+
gateRetry: options?.gateRetry
|
|
2524
|
+
});
|
|
2525
|
+
const setup = parseProjectDependencySetupCommand(command);
|
|
2526
|
+
const result = setup ? await runVerificationArgv(setup.file, setup.args, {
|
|
2527
|
+
cwd,
|
|
2528
|
+
timeoutMs,
|
|
2529
|
+
env,
|
|
2530
|
+
// Argv already constrained by parseProjectDependencySetupCommand;
|
|
2531
|
+
// verify safety rejects install verbs, so mark structured setup explicitly.
|
|
2532
|
+
dependencySetup: true
|
|
2533
|
+
}) : await runVerificationCommand(command, {
|
|
2534
|
+
cwd,
|
|
2535
|
+
timeoutMs,
|
|
2536
|
+
env
|
|
2537
|
+
});
|
|
2538
|
+
const output = String(result.all ?? [result.stdout, result.stderr].filter(Boolean).join("\n"));
|
|
2539
|
+
return { exitCode: result.exitCode ?? 1, output: output.slice(-1e5) };
|
|
2540
|
+
}
|
|
2541
|
+
async function resolveCanonicalMainRepoRoot(cwd) {
|
|
2542
|
+
const result = await execGit(["rev-parse", "--path-format=absolute", "--git-common-dir"], {
|
|
2543
|
+
cwd
|
|
2544
|
+
});
|
|
2545
|
+
if (result.exitCode !== 0) {
|
|
2546
|
+
throw new Error(
|
|
2547
|
+
`Unable to resolve canonical repository root from ${cwd}: ${result.stderr || result.stdout}`
|
|
2548
|
+
);
|
|
2549
|
+
}
|
|
2550
|
+
const commonDir = result.stdout.trim();
|
|
2551
|
+
if (!commonDir) {
|
|
2552
|
+
throw new Error(`git rev-parse returned an empty --git-common-dir for ${cwd}`);
|
|
2553
|
+
}
|
|
2554
|
+
const candidate = path5.dirname(commonDir);
|
|
2555
|
+
const validation = await execGit(["rev-parse", "--path-format=absolute", "--show-toplevel"], {
|
|
2556
|
+
cwd: candidate
|
|
2557
|
+
});
|
|
2558
|
+
if (validation.exitCode !== 0) {
|
|
2559
|
+
throw new Error(
|
|
2560
|
+
`Resolved candidate repository root ${candidate} (from ${cwd}) is not a valid working tree: ${validation.stderr || validation.stdout}`
|
|
2561
|
+
);
|
|
2562
|
+
}
|
|
2563
|
+
const topLevel = validation.stdout.trim();
|
|
2564
|
+
if (!topLevel || path5.resolve(topLevel) !== path5.resolve(candidate)) {
|
|
2565
|
+
throw new Error(
|
|
2566
|
+
`Resolved candidate repository root ${candidate} (from ${cwd}) does not match its own working-tree root ${topLevel}; unsupported git topology (e.g. separate-git-dir or submodule).`
|
|
2567
|
+
);
|
|
2568
|
+
}
|
|
2569
|
+
return candidate;
|
|
2570
|
+
}
|
|
2571
|
+
function keepMacAwakeForCurrentProcess() {
|
|
2572
|
+
if (process.platform !== "darwin") return;
|
|
2573
|
+
const child = execa("caffeinate", ["-i", "-w", String(process.pid)], {
|
|
2574
|
+
reject: false,
|
|
2575
|
+
stdio: "ignore"
|
|
2576
|
+
});
|
|
2577
|
+
child.unref();
|
|
2578
|
+
void child.catch(() => void 0);
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
export {
|
|
2582
|
+
getLocalCiStateRoot,
|
|
2583
|
+
localCiRootMappingPath,
|
|
2584
|
+
processIsAlive,
|
|
2585
|
+
normalizeAuthorityPath,
|
|
2586
|
+
resolveProjectVerificationAuthority,
|
|
2587
|
+
parseGitRawDiffChangeSet,
|
|
2588
|
+
LOCAL_CI_FULL_VALIDATION_PROFILE,
|
|
2589
|
+
boundLocalCiValidationProfileReason,
|
|
2590
|
+
resolveLocalCiValidationProfiles,
|
|
2591
|
+
selectLocalCiValidationProfile,
|
|
2592
|
+
DEFAULT_LOCAL_CI_STATUS_CONTEXT,
|
|
2593
|
+
DEFAULT_LOCAL_CI_MAX_CONCURRENT,
|
|
2594
|
+
hasOutstandingRemoteStatusPublication,
|
|
2595
|
+
isPublicationPending,
|
|
2596
|
+
outstandingStatusPublicationMatchesRecord,
|
|
2597
|
+
hasLiveFailurePublicationLease,
|
|
2598
|
+
createFailurePublicationLease,
|
|
2599
|
+
matchesFailurePublicationLease,
|
|
2600
|
+
createOutstandingStatusPublication,
|
|
2601
|
+
createOutstandingFailureStatusPublication,
|
|
2602
|
+
DEFAULT_LOCAL_CI_MAX_TERMINAL_RECORDS,
|
|
2603
|
+
createQueuedRun,
|
|
2604
|
+
LocalCiStateStore,
|
|
2605
|
+
selectCommandsForLocalCi,
|
|
2606
|
+
createLocalCiDetachedChildEnv,
|
|
2607
|
+
writeSchedulingEnvelope,
|
|
2608
|
+
acquireLocalCiSlot,
|
|
2609
|
+
createLocalCiWorktree,
|
|
2610
|
+
removeLocalCiWorktree,
|
|
2611
|
+
pruneLocalCiWorktrees,
|
|
2612
|
+
runLocalCiVerificationCommand,
|
|
2613
|
+
resolveCanonicalMainRepoRoot,
|
|
2614
|
+
keepMacAwakeForCurrentProcess,
|
|
2615
|
+
renderOrdinaryHelp,
|
|
2616
|
+
renderFullTieredHelp,
|
|
2617
|
+
resolveCliCommand,
|
|
2618
|
+
buildRoutedArgv,
|
|
2619
|
+
matchPlanNestedCommand,
|
|
2620
|
+
isObservationalProcessesHealthInvocation,
|
|
2621
|
+
isObservationalProcessesJsonInvocation,
|
|
2622
|
+
shouldOwnStartupCleanup,
|
|
2623
|
+
isObservationalInvocation,
|
|
2624
|
+
resolveStartupPoeticDir
|
|
2625
|
+
};
|
|
2626
|
+
//# sourceMappingURL=chunk-JRVNSWSQ.js.map
|