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