@ebrindley/build 0.0.0-stage → 1.42.1-rc28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (878) hide show
  1. package/.build-project/providers/catalog.json +13951 -0
  2. package/.build-project/providers/pricing.json +1978 -0
  3. package/.build-project/providers/registry.json +4103 -0
  4. package/.github/SECURITY.md +75 -0
  5. package/.nvmrc +1 -0
  6. package/CHANGELOG.md +661 -0
  7. package/CODE_OF_CONDUCT.md +44 -0
  8. package/CONTRIBUTING.md +24 -0
  9. package/INSTALL.md +623 -0
  10. package/LICENSE +21 -0
  11. package/README.md +118 -2
  12. package/THIRD_PARTY_NOTICES.txt +26 -0
  13. package/dist/ai-judge-UE5UOYOO.js +191 -0
  14. package/dist/allowlist-grounding-KPLN6NC5.js +16 -0
  15. package/dist/allowlist-utils-MZZKKXRD.js +31 -0
  16. package/dist/apply-completion-mode-EM37UZHB.js +110 -0
  17. package/dist/artifact-migrator-WPACNSEP.js +276 -0
  18. package/dist/ask-2OZ4JFQR.js +176 -0
  19. package/dist/auth-6UKBTJUQ.js +185 -0
  20. package/dist/auth-H5KJL6HL.js +165 -0
  21. package/dist/auth-LVJRJDMU.js +12 -0
  22. package/dist/auth-NHZOCNRR.js +94 -0
  23. package/dist/auth-PBXHWXGW.js +95 -0
  24. package/dist/auth-T7Y5DCAH.js +91 -0
  25. package/dist/auth-TVVKCHQW.js +13 -0
  26. package/dist/auth-UHB2UCSS.js +97 -0
  27. package/dist/auth-VACA3VUL.js +98 -0
  28. package/dist/auth-VVLC77S3.js +44 -0
  29. package/dist/auth-liveness-XO4AGDDD.js +53 -0
  30. package/dist/autoloop-cleanup-SI3VGAAR.js +106 -0
  31. package/dist/autoloop-evidence-gates-J5GG5YQB.js +39 -0
  32. package/dist/autoloop-gate-policy-SUNAEP6Q.js +169 -0
  33. package/dist/autoloop-helpers-VQGQNTT6.js +52 -0
  34. package/dist/autoloop-ledger-P6TIDP7C.js +96 -0
  35. package/dist/autoloop-ledger-subscriber-HFAGOZXH.js +161 -0
  36. package/dist/autoloop-run-defaults-GWGZBNT7.js +27 -0
  37. package/dist/autoloop-run-lock-GBWRM5A2.js +197 -0
  38. package/dist/autoloop-success-C32QVWOP.js +93 -0
  39. package/dist/autoloop-termination-summary-GI6IBQWD.js +58 -0
  40. package/dist/autoloop-trajectory-AMKSQISD.js +70 -0
  41. package/dist/autoloop-wall-clock-T7ZVIQOC.js +12 -0
  42. package/dist/automatic-branch-cleanup-R62X2C5O.js +49 -0
  43. package/dist/autonomous-loop-controller-2WL363LW.js +4386 -0
  44. package/dist/backend-YUTFDZXZ.js +30 -0
  45. package/dist/background-executor-Z3ODTMD7.js +260 -0
  46. package/dist/backlog-75QPEAMW.js +403 -0
  47. package/dist/backlog-execution-intent-GWUV5TUK.js +19 -0
  48. package/dist/backup-active-store-XHWLN34M.js +16 -0
  49. package/dist/backup-machine-stores-UI4LTJOY.js +251 -0
  50. package/dist/backup-telemetry-db-Z4ICYCNW.js +26 -0
  51. package/dist/branch-archive-manager-FMSQ7YJF.js +18 -0
  52. package/dist/branch-cleanup-manager-ST6FJGND.js +35 -0
  53. package/dist/build-gate-7NKWPY62.js +68 -0
  54. package/dist/build-root-S7DINODY.js +18 -0
  55. package/dist/change-summary-R6CB6VXJ.js +50 -0
  56. package/dist/chunk-24MNKKOM.js +173 -0
  57. package/dist/chunk-2H6VZFIO.js +235 -0
  58. package/dist/chunk-2JIX3EKV.js +39 -0
  59. package/dist/chunk-2KO7HVZM.js +106 -0
  60. package/dist/chunk-2L36U25O.js +100 -0
  61. package/dist/chunk-2NMNFUQA.js +475 -0
  62. package/dist/chunk-2OUAEH67.js +83 -0
  63. package/dist/chunk-2VLJUXJU.js +115 -0
  64. package/dist/chunk-2VUEDIUM.js +77 -0
  65. package/dist/chunk-2Y2IIHPI.js +430 -0
  66. package/dist/chunk-2YRG3LHZ.js +220 -0
  67. package/dist/chunk-2Z5Q3SUY.js +65 -0
  68. package/dist/chunk-32IYXIE5.js +873 -0
  69. package/dist/chunk-33IGPPUI.js +292 -0
  70. package/dist/chunk-352PCJLK.js +283 -0
  71. package/dist/chunk-35J4NE6S.js +226 -0
  72. package/dist/chunk-3C7WBIVK.js +1016 -0
  73. package/dist/chunk-3H3EY6U7.js +1225 -0
  74. package/dist/chunk-3H4MWYIC.js +198 -0
  75. package/dist/chunk-3HHWQR3N.js +2479 -0
  76. package/dist/chunk-3JKNRYEE.js +14 -0
  77. package/dist/chunk-3LECCYZY.js +128 -0
  78. package/dist/chunk-3LOY6YZI.js +852 -0
  79. package/dist/chunk-3M57DADF.js +44 -0
  80. package/dist/chunk-3NOTLVLG.js +3803 -0
  81. package/dist/chunk-3OPP2OZB.js +1480 -0
  82. package/dist/chunk-3P5IN5E7.js +9 -0
  83. package/dist/chunk-3PL22IYZ.js +20 -0
  84. package/dist/chunk-3REDAJUJ.js +14 -0
  85. package/dist/chunk-3S2R4XJI.js +203 -0
  86. package/dist/chunk-3UBZI5DW.js +61 -0
  87. package/dist/chunk-3VV5OBGG.js +308 -0
  88. package/dist/chunk-3WY5TU2O.js +823 -0
  89. package/dist/chunk-43OKHTW4.js +82 -0
  90. package/dist/chunk-45KUJBDI.js +112 -0
  91. package/dist/chunk-47X5LM7B.js +224 -0
  92. package/dist/chunk-4B6FQJ77.js +268 -0
  93. package/dist/chunk-4BTDUT2S.js +42 -0
  94. package/dist/chunk-4DJL2VXA.js +24 -0
  95. package/dist/chunk-4EBSXZ73.js +48 -0
  96. package/dist/chunk-4QKP6I4C.js +294 -0
  97. package/dist/chunk-4U6XK2DG.js +300 -0
  98. package/dist/chunk-4UMRUFBD.js +132 -0
  99. package/dist/chunk-4US4SYJ4.js +569 -0
  100. package/dist/chunk-4ZPLWEZO.js +154 -0
  101. package/dist/chunk-56WPYDJH.js +624 -0
  102. package/dist/chunk-57WRCJJW.js +50 -0
  103. package/dist/chunk-5AW4RMNG.js +24 -0
  104. package/dist/chunk-5EQN56OZ.js +155 -0
  105. package/dist/chunk-5F43TUIN.js +38 -0
  106. package/dist/chunk-5FKHIKTS.js +115 -0
  107. package/dist/chunk-5HDP7XZG.js +91 -0
  108. package/dist/chunk-5KQETTNS.js +152 -0
  109. package/dist/chunk-5PDVN2KO.js +498 -0
  110. package/dist/chunk-5UBIFVRD.js +509 -0
  111. package/dist/chunk-5YDE3Q62.js +46 -0
  112. package/dist/chunk-626AJVMU.js +302 -0
  113. package/dist/chunk-633XZ7UF.js +3583 -0
  114. package/dist/chunk-63DHNHGA.js +236 -0
  115. package/dist/chunk-666656AR.js +568 -0
  116. package/dist/chunk-67YRJPJO.js +19 -0
  117. package/dist/chunk-6CAU7VBA.js +20 -0
  118. package/dist/chunk-6CB3KJRY.js +21 -0
  119. package/dist/chunk-6DBMALXL.js +29 -0
  120. package/dist/chunk-6E25C4IA.js +269 -0
  121. package/dist/chunk-6FIBNIQU.js +73 -0
  122. package/dist/chunk-6G57FYBV.js +189 -0
  123. package/dist/chunk-6KZKRTG3.js +4885 -0
  124. package/dist/chunk-6LAH2UV2.js +525 -0
  125. package/dist/chunk-6PZY2TTU.js +921 -0
  126. package/dist/chunk-6T6NHXK4.js +230 -0
  127. package/dist/chunk-6TBEX4R4.js +432 -0
  128. package/dist/chunk-6TRD7TEE.js +911 -0
  129. package/dist/chunk-6XCP2RAI.js +47 -0
  130. package/dist/chunk-726G6WDL.js +351 -0
  131. package/dist/chunk-75EMT7BF.js +616 -0
  132. package/dist/chunk-762ACDZJ.js +4696 -0
  133. package/dist/chunk-77I6G4CO.js +1 -0
  134. package/dist/chunk-7DJ6SQH3.js +510 -0
  135. package/dist/chunk-7HPCIOMA.js +1383 -0
  136. package/dist/chunk-7HYBSRPZ.js +17503 -0
  137. package/dist/chunk-7KBEOPVB.js +73 -0
  138. package/dist/chunk-7LHJDJB3.js +2222 -0
  139. package/dist/chunk-7NVJMH52.js +306 -0
  140. package/dist/chunk-7RMARDVH.js +33 -0
  141. package/dist/chunk-7SEWU4QG.js +36 -0
  142. package/dist/chunk-7TUTZTI2.js +911 -0
  143. package/dist/chunk-7VBDFPUM.js +340 -0
  144. package/dist/chunk-7VRJHTGZ.js +6406 -0
  145. package/dist/chunk-7YAOUUKK.js +493 -0
  146. package/dist/chunk-AAAODV76.js +860 -0
  147. package/dist/chunk-AB4HUJVQ.js +339 -0
  148. package/dist/chunk-AEQWV746.js +611 -0
  149. package/dist/chunk-AHU3FKPA.js +11639 -0
  150. package/dist/chunk-AISTV5XU.js +226 -0
  151. package/dist/chunk-AJBTKGJZ.js +48 -0
  152. package/dist/chunk-AJJNW7XT.js +248 -0
  153. package/dist/chunk-AJN5Y5YZ.js +160 -0
  154. package/dist/chunk-ALDTFNZ3.js +150 -0
  155. package/dist/chunk-ARARVIZK.js +281 -0
  156. package/dist/chunk-ATBM6AW5.js +53 -0
  157. package/dist/chunk-ATKEKM3E.js +206 -0
  158. package/dist/chunk-AUJLW6JZ.js +92 -0
  159. package/dist/chunk-AY3MHEFM.js +24 -0
  160. package/dist/chunk-B7PYTV72.js +236 -0
  161. package/dist/chunk-B7YRSULE.js +1423 -0
  162. package/dist/chunk-BAWJNMWF.js +339 -0
  163. package/dist/chunk-BBPIILO2.js +88 -0
  164. package/dist/chunk-BD3PGW6A.js +957 -0
  165. package/dist/chunk-BDPYFAB4.js +44 -0
  166. package/dist/chunk-BIUV23HH.js +729 -0
  167. package/dist/chunk-BQ3VAGXH.js +243 -0
  168. package/dist/chunk-BQJOGAWT.js +31 -0
  169. package/dist/chunk-BT7EG7CO.js +68 -0
  170. package/dist/chunk-BV3XQQLU.js +27 -0
  171. package/dist/chunk-BY6ODHNZ.js +78 -0
  172. package/dist/chunk-BZRLSZNV.js +79 -0
  173. package/dist/chunk-C3DH3NCY.js +2773 -0
  174. package/dist/chunk-C3KCMAJP.js +756 -0
  175. package/dist/chunk-C7AGUJ5K.js +267 -0
  176. package/dist/chunk-CBSWKQCX.js +342 -0
  177. package/dist/chunk-CE5OZBYY.js +21 -0
  178. package/dist/chunk-CFCVYHSW.js +373 -0
  179. package/dist/chunk-CHA2HDE5.js +126 -0
  180. package/dist/chunk-CIHWPGS5.js +146 -0
  181. package/dist/chunk-CJBSZX3F.js +127 -0
  182. package/dist/chunk-CLA6QIJM.js +43 -0
  183. package/dist/chunk-CPSVQR2I.js +365 -0
  184. package/dist/chunk-CRPRVNQ4.js +254 -0
  185. package/dist/chunk-CSIE4Y7C.js +1093 -0
  186. package/dist/chunk-CSOPSYDY.js +142 -0
  187. package/dist/chunk-CSXLLEGB.js +205 -0
  188. package/dist/chunk-CTDLERWU.js +293 -0
  189. package/dist/chunk-CULBXMSK.js +110 -0
  190. package/dist/chunk-D2M2Y5ID.js +21 -0
  191. package/dist/chunk-D37WY7SH.js +1060 -0
  192. package/dist/chunk-D4VJFCGZ.js +89 -0
  193. package/dist/chunk-D6WTZMZB.js +48 -0
  194. package/dist/chunk-DBHOWDUH.js +976 -0
  195. package/dist/chunk-DCACX7BV.js +19364 -0
  196. package/dist/chunk-DFHX4XYA.js +128 -0
  197. package/dist/chunk-DIKBRRKE.js +176 -0
  198. package/dist/chunk-DJG5RIOR.js +9 -0
  199. package/dist/chunk-DNIOIISW.js +616 -0
  200. package/dist/chunk-DOA5G7IA.js +315 -0
  201. package/dist/chunk-DP7FKP6W.js +855 -0
  202. package/dist/chunk-DS2JZ677.js +79 -0
  203. package/dist/chunk-DSRLVYBS.js +82 -0
  204. package/dist/chunk-EAEVKNJA.js +209 -0
  205. package/dist/chunk-EEUESN3T.js +575 -0
  206. package/dist/chunk-EJ4LUU5Y.js +73 -0
  207. package/dist/chunk-EJIWTPSZ.js +1063 -0
  208. package/dist/chunk-EJVCDTAN.js +116 -0
  209. package/dist/chunk-EKLUBDBQ.js +627 -0
  210. package/dist/chunk-EN62WLFD.js +849 -0
  211. package/dist/chunk-ENYRCL43.js +26 -0
  212. package/dist/chunk-EP22ZMFO.js +273 -0
  213. package/dist/chunk-EP4SVAVI.js +87 -0
  214. package/dist/chunk-EPAMVGI3.js +1562 -0
  215. package/dist/chunk-EPRMH6HO.js +24 -0
  216. package/dist/chunk-ES6NWJ36.js +4057 -0
  217. package/dist/chunk-ETA2K4KI.js +30 -0
  218. package/dist/chunk-ETAD4NKD.js +31 -0
  219. package/dist/chunk-EUWIIDQ7.js +93 -0
  220. package/dist/chunk-EVE3OD2W.js +141 -0
  221. package/dist/chunk-EXA26FFC.js +459 -0
  222. package/dist/chunk-EXSPAA35.js +244 -0
  223. package/dist/chunk-F55YABKV.js +258 -0
  224. package/dist/chunk-F5DNGU5W.js +27 -0
  225. package/dist/chunk-F622MJYV.js +103 -0
  226. package/dist/chunk-F6NIQD74.js +5111 -0
  227. package/dist/chunk-FAWJTC34.js +43 -0
  228. package/dist/chunk-FAZBRHGG.js +47 -0
  229. package/dist/chunk-FIEUSGNJ.js +39 -0
  230. package/dist/chunk-FKCG7K6H.js +1146 -0
  231. package/dist/chunk-FOVVXV45.js +229 -0
  232. package/dist/chunk-FRBPKBFE.js +30 -0
  233. package/dist/chunk-FSA5BPFI.js +268 -0
  234. package/dist/chunk-FSJ5TTZ3.js +29 -0
  235. package/dist/chunk-FT7SGLXT.js +193 -0
  236. package/dist/chunk-FUUVZZUB.js +62 -0
  237. package/dist/chunk-FYMRVUNZ.js +1697 -0
  238. package/dist/chunk-FYTAA5FG.js +162 -0
  239. package/dist/chunk-G3VB2UCZ.js +3892 -0
  240. package/dist/chunk-G6OIMVXH.js +405 -0
  241. package/dist/chunk-G7PQCXVH.js +228 -0
  242. package/dist/chunk-GAB6UW52.js +1087 -0
  243. package/dist/chunk-GAFJEYLK.js +29 -0
  244. package/dist/chunk-GC4CRMTS.js +642 -0
  245. package/dist/chunk-GFHSIWHL.js +5297 -0
  246. package/dist/chunk-GI5M67YA.js +735 -0
  247. package/dist/chunk-GIJWHUWF.js +191 -0
  248. package/dist/chunk-GO6DRECI.js +1 -0
  249. package/dist/chunk-GOFVBTMN.js +314 -0
  250. package/dist/chunk-GQIP72ZJ.js +13 -0
  251. package/dist/chunk-GQXLXRFA.js +28 -0
  252. package/dist/chunk-GT6HFKJB.js +48 -0
  253. package/dist/chunk-GVJGSSKM.js +174 -0
  254. package/dist/chunk-GWHGTG37.js +36 -0
  255. package/dist/chunk-GZAJHLKO.js +18 -0
  256. package/dist/chunk-GZK62I3N.js +314 -0
  257. package/dist/chunk-H3HGMISG.js +483 -0
  258. package/dist/chunk-H3ISZJMJ.js +403 -0
  259. package/dist/chunk-H4HXVWEC.js +9 -0
  260. package/dist/chunk-H6MJGKAW.js +120 -0
  261. package/dist/chunk-HBTTGVR5.js +353 -0
  262. package/dist/chunk-HBZ322WZ.js +35 -0
  263. package/dist/chunk-HEP3ILP4.js +34 -0
  264. package/dist/chunk-HGFXODKO.js +106 -0
  265. package/dist/chunk-HHWDTB6S.js +59 -0
  266. package/dist/chunk-HKO45NLZ.js +286 -0
  267. package/dist/chunk-HLVTEA6A.js +89 -0
  268. package/dist/chunk-HS3SY6HN.js +160 -0
  269. package/dist/chunk-HT5OCGKN.js +899 -0
  270. package/dist/chunk-HXIIEMGF.js +78 -0
  271. package/dist/chunk-I56TH7VM.js +46 -0
  272. package/dist/chunk-I6FN3R5D.js +352 -0
  273. package/dist/chunk-I7SCBHJN.js +655 -0
  274. package/dist/chunk-IJM4GKAT.js +529 -0
  275. package/dist/chunk-IMEYU36R.js +344 -0
  276. package/dist/chunk-INJ3SS6A.js +936 -0
  277. package/dist/chunk-IUKOPCCE.js +441 -0
  278. package/dist/chunk-IVEPZQJO.js +33 -0
  279. package/dist/chunk-IVZEONFC.js +43 -0
  280. package/dist/chunk-IWBHRSG7.js +936 -0
  281. package/dist/chunk-IYY3HQAD.js +484 -0
  282. package/dist/chunk-J2ICMYDA.js +48 -0
  283. package/dist/chunk-J2QCFLUN.js +505 -0
  284. package/dist/chunk-J5HM6V5P.js +1039 -0
  285. package/dist/chunk-J6NZX6QH.js +398 -0
  286. package/dist/chunk-JBBB2Y47.js +432 -0
  287. package/dist/chunk-JDCZQZU7.js +42 -0
  288. package/dist/chunk-JEDNXGSS.js +169 -0
  289. package/dist/chunk-JHP7OT6K.js +89 -0
  290. package/dist/chunk-JLS65NUQ.js +14 -0
  291. package/dist/chunk-JNMWYMC6.js +38 -0
  292. package/dist/chunk-JNSTWNNW.js +25 -0
  293. package/dist/chunk-JOSK6GEU.js +9564 -0
  294. package/dist/chunk-JQUMSIGC.js +100 -0
  295. package/dist/chunk-JVHGMURQ.js +1106 -0
  296. package/dist/chunk-JYXS47FQ.js +951 -0
  297. package/dist/chunk-K2ZMW2Z5.js +2147 -0
  298. package/dist/chunk-K7FAFORM.js +14 -0
  299. package/dist/chunk-KBJXX3ON.js +333 -0
  300. package/dist/chunk-KHFKVQWY.js +486 -0
  301. package/dist/chunk-KHMEI3DQ.js +173 -0
  302. package/dist/chunk-KHW3UD7B.js +69 -0
  303. package/dist/chunk-KKQKWAMF.js +365 -0
  304. package/dist/chunk-KOKXJV2U.js +455 -0
  305. package/dist/chunk-KQ4NGOJI.js +14 -0
  306. package/dist/chunk-KQYQOXMS.js +342 -0
  307. package/dist/chunk-KTMW65UX.js +14 -0
  308. package/dist/chunk-KUEWV2JH.js +593 -0
  309. package/dist/chunk-KXYDV3HP.js +1523 -0
  310. package/dist/chunk-KZ4JMGZ5.js +96 -0
  311. package/dist/chunk-KZWGBUIO.js +2659 -0
  312. package/dist/chunk-L2CBG5IX.js +1892 -0
  313. package/dist/chunk-L2S235CU.js +314 -0
  314. package/dist/chunk-L32WD57F.js +98 -0
  315. package/dist/chunk-LDJCKAJP.js +123 -0
  316. package/dist/chunk-LDWNBEN7.js +541 -0
  317. package/dist/chunk-LJVH57G2.js +241 -0
  318. package/dist/chunk-LPQ7NZEN.js +209 -0
  319. package/dist/chunk-LRGH3CCV.js +148 -0
  320. package/dist/chunk-LULOGMMM.js +1133 -0
  321. package/dist/chunk-LWGKN2GY.js +8284 -0
  322. package/dist/chunk-LX4F24ZH.js +157 -0
  323. package/dist/chunk-M4BUEXXF.js +839 -0
  324. package/dist/chunk-M6UHOC4I.js +553 -0
  325. package/dist/chunk-MCBNWZNO.js +657 -0
  326. package/dist/chunk-MEMSY6VY.js +223 -0
  327. package/dist/chunk-MHLEZQQI.js +145 -0
  328. package/dist/chunk-MJ6WNSIX.js +192 -0
  329. package/dist/chunk-MKASIKWZ.js +442 -0
  330. package/dist/chunk-MNOVBYFU.js +60 -0
  331. package/dist/chunk-MO2BYAAF.js +2896 -0
  332. package/dist/chunk-MP6P7Y2A.js +180 -0
  333. package/dist/chunk-MQ4TFJAG.js +3365 -0
  334. package/dist/chunk-MT2WAIR3.js +131 -0
  335. package/dist/chunk-MTU7VTNN.js +1122 -0
  336. package/dist/chunk-MULMXX4V.js +150 -0
  337. package/dist/chunk-MW2DWWVA.js +147 -0
  338. package/dist/chunk-MY3ZPUN4.js +151 -0
  339. package/dist/chunk-MYLH2S4G.js +64 -0
  340. package/dist/chunk-MZ5WYKNA.js +28 -0
  341. package/dist/chunk-N3Q2JNPV.js +249 -0
  342. package/dist/chunk-N5DX4JAK.js +12 -0
  343. package/dist/chunk-NAINGKHF.js +35 -0
  344. package/dist/chunk-NEQUQHOX.js +223 -0
  345. package/dist/chunk-NHNPEZS4.js +1408 -0
  346. package/dist/chunk-NHTIXJCR.js +17 -0
  347. package/dist/chunk-NKLPMBNV.js +1154 -0
  348. package/dist/chunk-NMVZ5YES.js +199 -0
  349. package/dist/chunk-NRFRY3GE.js +256 -0
  350. package/dist/chunk-NTD6CSI6.js +1266 -0
  351. package/dist/chunk-NUO7B3SH.js +318 -0
  352. package/dist/chunk-NVOHIJKG.js +1319 -0
  353. package/dist/chunk-NXH3RX7O.js +523 -0
  354. package/dist/chunk-NXZ3DURF.js +257 -0
  355. package/dist/chunk-O26L6F7F.js +209 -0
  356. package/dist/chunk-O5RRIUEP.js +124 -0
  357. package/dist/chunk-O5X6BKAO.js +10 -0
  358. package/dist/chunk-O6ZQUVTR.js +31 -0
  359. package/dist/chunk-O7FE75QZ.js +306 -0
  360. package/dist/chunk-OC7Z6AJQ.js +81 -0
  361. package/dist/chunk-OH7IEZ63.js +307 -0
  362. package/dist/chunk-OI734GRS.js +115 -0
  363. package/dist/chunk-OJBH62DK.js +684 -0
  364. package/dist/chunk-OKJLVSTJ.js +247 -0
  365. package/dist/chunk-OMX5VL43.js +79 -0
  366. package/dist/chunk-ONHAMBN6.js +12 -0
  367. package/dist/chunk-ONYPLIVE.js +753 -0
  368. package/dist/chunk-OSK7QWPD.js +637 -0
  369. package/dist/chunk-OTXLMJKP.js +438 -0
  370. package/dist/chunk-OV5PKQMA.js +213 -0
  371. package/dist/chunk-OXK5HA4B.js +31 -0
  372. package/dist/chunk-OYDWFQA4.js +272 -0
  373. package/dist/chunk-OZAUKWHB.js +22 -0
  374. package/dist/chunk-P5P2EKGH.js +94 -0
  375. package/dist/chunk-P6SSWAKY.js +29 -0
  376. package/dist/chunk-P7CQGPLS.js +30 -0
  377. package/dist/chunk-P7RVCHQV.js +197 -0
  378. package/dist/chunk-PEBWDYIW.js +726 -0
  379. package/dist/chunk-PFCXFO7A.js +9193 -0
  380. package/dist/chunk-PFLDWMX6.js +168 -0
  381. package/dist/chunk-PHZGLQSK.js +15883 -0
  382. package/dist/chunk-PNFTPNSR.js +20 -0
  383. package/dist/chunk-PPHXX4W7.js +175 -0
  384. package/dist/chunk-PPZESPJT.js +262 -0
  385. package/dist/chunk-PRDHI2L5.js +296 -0
  386. package/dist/chunk-PSTKWHHN.js +32 -0
  387. package/dist/chunk-PV53ODOT.js +555 -0
  388. package/dist/chunk-PVJBTI7P.js +24 -0
  389. package/dist/chunk-PXKERBKX.js +100 -0
  390. package/dist/chunk-PZ5AY32C.js +10 -0
  391. package/dist/chunk-Q3DHEZA5.js +85 -0
  392. package/dist/chunk-Q5RZDVQK.js +204 -0
  393. package/dist/chunk-Q64IR2Z2.js +4933 -0
  394. package/dist/chunk-Q6Z5UGPL.js +398 -0
  395. package/dist/chunk-QAMAV4DT.js +627 -0
  396. package/dist/chunk-QBWF4444.js +112 -0
  397. package/dist/chunk-QDKYT4CI.js +22805 -0
  398. package/dist/chunk-QFLBRWCJ.js +126 -0
  399. package/dist/chunk-QILAQACE.js +107 -0
  400. package/dist/chunk-QJ6OEH4K.js +3683 -0
  401. package/dist/chunk-QK3NLYTV.js +320 -0
  402. package/dist/chunk-QOFLUS24.js +535 -0
  403. package/dist/chunk-QOIASNEJ.js +98 -0
  404. package/dist/chunk-QP4UTSOV.js +3587 -0
  405. package/dist/chunk-QP5ST7RI.js +904 -0
  406. package/dist/chunk-QS5AGRZN.js +167 -0
  407. package/dist/chunk-QTAOU53N.js +107 -0
  408. package/dist/chunk-QTCLJHLZ.js +351 -0
  409. package/dist/chunk-QTOLTG4L.js +119 -0
  410. package/dist/chunk-QWDV4OSL.js +142 -0
  411. package/dist/chunk-QWMQ74KD.js +508 -0
  412. package/dist/chunk-QX6LOMMH.js +158 -0
  413. package/dist/chunk-QXBKIHY7.js +238 -0
  414. package/dist/chunk-QXBKTJT6.js +270 -0
  415. package/dist/chunk-QYQOVOLR.js +196 -0
  416. package/dist/chunk-R2CKS6WT.js +73 -0
  417. package/dist/chunk-R2TIYG7U.js +68 -0
  418. package/dist/chunk-R2W3LESH.js +465 -0
  419. package/dist/chunk-R5EXHMUN.js +100 -0
  420. package/dist/chunk-RBBTBCHX.js +289 -0
  421. package/dist/chunk-RBKLOUXA.js +121 -0
  422. package/dist/chunk-RCP2Y6DQ.js +86 -0
  423. package/dist/chunk-RCYEJSD5.js +260 -0
  424. package/dist/chunk-RFDVSI4N.js +134 -0
  425. package/dist/chunk-RGJU63FV.js +1717 -0
  426. package/dist/chunk-RI4EX2QE.js +18 -0
  427. package/dist/chunk-RIMARPCF.js +152 -0
  428. package/dist/chunk-RJ6RSNT6.js +42 -0
  429. package/dist/chunk-RNI2GUIF.js +131 -0
  430. package/dist/chunk-RPHUWJ3K.js +264 -0
  431. package/dist/chunk-RPIMAMV7.js +361 -0
  432. package/dist/chunk-RQFX6YRN.js +49 -0
  433. package/dist/chunk-RRC5BL7B.js +285 -0
  434. package/dist/chunk-RSYTSBOO.js +79 -0
  435. package/dist/chunk-RWQQIKIC.js +35 -0
  436. package/dist/chunk-RZUBOZH2.js +373 -0
  437. package/dist/chunk-S2E632FQ.js +380 -0
  438. package/dist/chunk-S2VQCZO4.js +22 -0
  439. package/dist/chunk-S2XIMQJQ.js +681 -0
  440. package/dist/chunk-S47ISY27.js +183 -0
  441. package/dist/chunk-S6I4KISU.js +142 -0
  442. package/dist/chunk-S6TAFQI5.js +2403 -0
  443. package/dist/chunk-SBBNT5ZJ.js +27 -0
  444. package/dist/chunk-SDJV2DTQ.js +491 -0
  445. package/dist/chunk-SDPIFNLD.js +33 -0
  446. package/dist/chunk-SF6YK3JI.js +835 -0
  447. package/dist/chunk-SK3ZFONO.js +976 -0
  448. package/dist/chunk-SKTFRFQ7.js +1775 -0
  449. package/dist/chunk-SNGM452K.js +2002 -0
  450. package/dist/chunk-STW7OFOA.js +581 -0
  451. package/dist/chunk-SU6UFUQY.js +1351 -0
  452. package/dist/chunk-SVJM3LJA.js +27 -0
  453. package/dist/chunk-SVUNZOPG.js +129 -0
  454. package/dist/chunk-T5XLUYV3.js +34 -0
  455. package/dist/chunk-T7E5JS6N.js +57 -0
  456. package/dist/chunk-TA3N5MMO.js +450 -0
  457. package/dist/chunk-TAJPCXSB.js +13 -0
  458. package/dist/chunk-TDEMDRN2.js +1639 -0
  459. package/dist/chunk-TDSEX5CF.js +24 -0
  460. package/dist/chunk-TGCVL74Y.js +28 -0
  461. package/dist/chunk-TGXXYDL4.js +1012 -0
  462. package/dist/chunk-TIKTYUJO.js +296 -0
  463. package/dist/chunk-TIZAJD4M.js +6029 -0
  464. package/dist/chunk-TLSEZZCG.js +5549 -0
  465. package/dist/chunk-TMBDRHAX.js +19 -0
  466. package/dist/chunk-TRURQUBP.js +1217 -0
  467. package/dist/chunk-TSQ256N5.js +152 -0
  468. package/dist/chunk-TUQMMFEQ.js +2244 -0
  469. package/dist/chunk-U424MJLC.js +81 -0
  470. package/dist/chunk-U4DRS6ZW.js +11 -0
  471. package/dist/chunk-U5KS22IN.js +204 -0
  472. package/dist/chunk-UAPHFJPN.js +673 -0
  473. package/dist/chunk-UB7LQITD.js +1040 -0
  474. package/dist/chunk-UBVRHGAZ.js +237 -0
  475. package/dist/chunk-UDNNAQN5.js +98 -0
  476. package/dist/chunk-UJP3XM54.js +342 -0
  477. package/dist/chunk-UJYDH2MW.js +305 -0
  478. package/dist/chunk-UKMPASIC.js +399 -0
  479. package/dist/chunk-UKUL3EHK.js +763 -0
  480. package/dist/chunk-UPHU64NB.js +1327 -0
  481. package/dist/chunk-URMSZXAD.js +43 -0
  482. package/dist/chunk-US3RT6CY.js +525 -0
  483. package/dist/chunk-UTDEOPKV.js +30 -0
  484. package/dist/chunk-UUUDKV7N.js +13 -0
  485. package/dist/chunk-UVKWU3SM.js +39 -0
  486. package/dist/chunk-V2TVPO3X.js +521 -0
  487. package/dist/chunk-V3KLVK7B.js +398 -0
  488. package/dist/chunk-V3YFFHDR.js +17 -0
  489. package/dist/chunk-VAG6GUBR.js +1162 -0
  490. package/dist/chunk-VBENAHDA.js +1814 -0
  491. package/dist/chunk-VBNCKCCI.js +16 -0
  492. package/dist/chunk-VCGSWN5M.js +1300 -0
  493. package/dist/chunk-VE2O2I7M.js +115 -0
  494. package/dist/chunk-VIFHYTI5.js +31 -0
  495. package/dist/chunk-VJER3FUS.js +5080 -0
  496. package/dist/chunk-VLMDM56Q.js +268 -0
  497. package/dist/chunk-VMFZEVVX.js +99 -0
  498. package/dist/chunk-VNDPQVUZ.js +1853 -0
  499. package/dist/chunk-VSPJRLGA.js +4783 -0
  500. package/dist/chunk-VSWC5ZXI.js +103 -0
  501. package/dist/chunk-VYI4DSZP.js +2017 -0
  502. package/dist/chunk-W4BAIQ6L.js +1212 -0
  503. package/dist/chunk-W7VQAL2K.js +2203 -0
  504. package/dist/chunk-WEI3ZDZH.js +33 -0
  505. package/dist/chunk-WEIUS5NT.js +26 -0
  506. package/dist/chunk-WEW7UFWE.js +546 -0
  507. package/dist/chunk-WGQAXLNA.js +384 -0
  508. package/dist/chunk-WJAL4JL3.js +206 -0
  509. package/dist/chunk-WKFOJ5ZG.js +141 -0
  510. package/dist/chunk-WMWQEFLN.js +2177 -0
  511. package/dist/chunk-WNECKKO2.js +154 -0
  512. package/dist/chunk-WPKLYOAM.js +195 -0
  513. package/dist/chunk-WSH37YQN.js +243 -0
  514. package/dist/chunk-WV3KQDPE.js +84 -0
  515. package/dist/chunk-WZFC4V4U.js +389 -0
  516. package/dist/chunk-XCOIBNET.js +13 -0
  517. package/dist/chunk-XDM65LBX.js +53 -0
  518. package/dist/chunk-XGU6PBO5.js +645 -0
  519. package/dist/chunk-XHOI4V62.js +43 -0
  520. package/dist/chunk-XIBVFMLQ.js +6858 -0
  521. package/dist/chunk-XJ2E7YEJ.js +117 -0
  522. package/dist/chunk-XON4ZCYX.js +69 -0
  523. package/dist/chunk-XQPEWJPI.js +127 -0
  524. package/dist/chunk-XSJTIFUY.js +31 -0
  525. package/dist/chunk-XT7OVWM3.js +64 -0
  526. package/dist/chunk-XTXYIISX.js +1104 -0
  527. package/dist/chunk-XV7NZL4B.js +57 -0
  528. package/dist/chunk-XWIZ6B5S.js +109 -0
  529. package/dist/chunk-XWLIKOBX.js +240 -0
  530. package/dist/chunk-XZOXEIN4.js +156 -0
  531. package/dist/chunk-XZRS7I7W.js +3673 -0
  532. package/dist/chunk-Y4ZDDHB6.js +44 -0
  533. package/dist/chunk-Y733RB7T.js +194 -0
  534. package/dist/chunk-YBHUB4XR.js +14931 -0
  535. package/dist/chunk-YKC75XK4.js +147 -0
  536. package/dist/chunk-YKMXFVK7.js +27 -0
  537. package/dist/chunk-YO5BAJ55.js +225 -0
  538. package/dist/chunk-YPDCLXNZ.js +568 -0
  539. package/dist/chunk-YSQPGVFK.js +116 -0
  540. package/dist/chunk-YTVVTIWJ.js +716 -0
  541. package/dist/chunk-YVCMDVBB.js +698 -0
  542. package/dist/chunk-YYTOA6DP.js +114 -0
  543. package/dist/chunk-Z5WGNULV.js +88 -0
  544. package/dist/chunk-Z6NGVVXG.js +66 -0
  545. package/dist/chunk-Z7CDQNH3.js +20 -0
  546. package/dist/chunk-ZH4G3FU5.js +187 -0
  547. package/dist/chunk-ZJB6SQPA.js +257 -0
  548. package/dist/chunk-ZL6E4HMY.js +42 -0
  549. package/dist/chunk-ZMLOC3DW.js +62 -0
  550. package/dist/chunk-ZORW64QH.js +101 -0
  551. package/dist/chunk-ZVVD2XGV.js +1369 -0
  552. package/dist/cli-utils-ER33ODC5.js +31 -0
  553. package/dist/cli-validation-6CHPTANV.js +146 -0
  554. package/dist/commit-utils-3STAY7C7.js +49 -0
  555. package/dist/compatibility-notice-IHNXR2JR.js +21 -0
  556. package/dist/compete-config-resolver-V3GDNJMM.js +125 -0
  557. package/dist/compete-results-XUM3PJHS.js +318 -0
  558. package/dist/competition-outcome-tracker-NWSMVUPG.js +62 -0
  559. package/dist/config-4HHE4OIK.js +238 -0
  560. package/dist/config-D6YAZASH.js +10 -0
  561. package/dist/config-MIPRDYYV.js +46 -0
  562. package/dist/config-audit-GA5DCUAI.js +371 -0
  563. package/dist/config-explain-4LMO556H.js +71 -0
  564. package/dist/config-loader-RGP634PC.js +51 -0
  565. package/dist/config-manager-J2UTO635.js +114 -0
  566. package/dist/config-merger-TVUJBZTS.js +14 -0
  567. package/dist/config-validator-H4ZVIE56.js +122 -0
  568. package/dist/cost-HSIV5U2Y.js +85 -0
  569. package/dist/data-migration-state-OAUL2WPQ.js +35 -0
  570. package/dist/db-migrator-NUOW5NKR.js +34 -0
  571. package/dist/default-paths-UILB62BN.js +49 -0
  572. package/dist/discovery-V6GYUJUC.js +53 -0
  573. package/dist/doctor-BBSMCDWE.js +271 -0
  574. package/dist/doctor-context-blocks-LPY75VZA.js +231 -0
  575. package/dist/doctor-instructions-import-JXHDDOE5.js +70 -0
  576. package/dist/doctor-purpose-6YJOXPBE.js +30 -0
  577. package/dist/domain-analyzer-S35ABUNA.js +48 -0
  578. package/dist/ensure-initialized-Q6PEU55O.js +78 -0
  579. package/dist/entry.js +922 -0
  580. package/dist/error-parser-core-MZFMFANN.js +70 -0
  581. package/dist/evaluation-context-GQBYDAEV.js +63 -0
  582. package/dist/evidence-plan-resolver-5FUAP3SA.js +111 -0
  583. package/dist/execution-data-writer-YCG5UH37.js +72 -0
  584. package/dist/execution-policy-applier-PWZM5LNA.js +63 -0
  585. package/dist/execution-preflight-DASJWWQF.js +223 -0
  586. package/dist/execution-roles-KNSNNB2D.js +112 -0
  587. package/dist/exit-code-error-QIY5DOKF.js +18 -0
  588. package/dist/external-temp-cleanup-CAW7LQ6R.js +360 -0
  589. package/dist/factory-YCYCB7AE.js +209 -0
  590. package/dist/file-utils-ZQYNBQBW.js +45 -0
  591. package/dist/flywheel-allowlist-revert-H22HA6JE.js +21 -0
  592. package/dist/flywheel-autoloop-executor-PDOASJEZ.js +316 -0
  593. package/dist/flywheel-autoloop-unverified-TOKYFCDC.js +36 -0
  594. package/dist/flywheel-competition-executor-YJUMMEJO.js +320 -0
  595. package/dist/flywheel-executor-JWU43KAY.js +437 -0
  596. package/dist/flywheel-git-isolation-FQ2TRWMB.js +56 -0
  597. package/dist/flywheel-manifest-HDPNMWWF.js +114 -0
  598. package/dist/flywheel-model-defaults-UHAKTZT3.js +110 -0
  599. package/dist/flywheel-preflight-III2C7TH.js +163 -0
  600. package/dist/flywheel-resume-2O45MS7Y.js +31 -0
  601. package/dist/flywheel-resume-execution-lease-P2BK257J.js +59 -0
  602. package/dist/flywheel-safety-AVAVFXIJ.js +19 -0
  603. package/dist/flywheel-scope-decision-ALJUUNGG.js +14 -0
  604. package/dist/get-telemetry-logger-MPDVO2JA.js +155 -0
  605. package/dist/git-message-style-KCXGMTDT.js +104 -0
  606. package/dist/guidance-profile-config-G3K5YQOO.js +113 -0
  607. package/dist/guidance-profile-renderer-TX4TNDFL.js +23 -0
  608. package/dist/identity-XLQJ4YJF.js +25 -0
  609. package/dist/index-query-6NUY4FKX.js +37 -0
  610. package/dist/index.js +85588 -0
  611. package/dist/indexer-DL54CM3K.js +44 -0
  612. package/dist/init-OUZ72344.js +245 -0
  613. package/dist/instructions-import-offer-NSYXSITC.js +363 -0
  614. package/dist/instructions-run-NBII7W36.js +114 -0
  615. package/dist/interrupted-work-E4TLKJLE.js +43 -0
  616. package/dist/invoke-provider-auth-NEWEIHCI.js +38 -0
  617. package/dist/judge-scoring.json +64 -0
  618. package/dist/lab-mode-YXQ2Y3NG.js +44 -0
  619. package/dist/lab-utils-5QAMDJR5.js +29 -0
  620. package/dist/linux-host-class-YDWQX65O.js +31 -0
  621. package/dist/llm-judge-executor-OJSNCTFJ.js +179 -0
  622. package/dist/local-ci-config-VZ4WWDT5.js +48 -0
  623. package/dist/local-executor-DCMDDRX2.js +280 -0
  624. package/dist/logger-5KNKS3O4.js +154 -0
  625. package/dist/matrix-prompt-builder-4ZBOGZSB.js +527 -0
  626. package/dist/matrix-result-parser-EZJM4YXP.js +14 -0
  627. package/dist/merge-batch-RTEEMVDB.js +313 -0
  628. package/dist/merge-batch-members-VIWGVRHP.js +24 -0
  629. package/dist/merge-queue-TIIUSOY6.js +126 -0
  630. package/dist/merge-queue-trust-DUNZ4J65.js +21 -0
  631. package/dist/metadata-UKSMWM6J.js +29 -0
  632. package/dist/model-catalog-readiness-R5KJBP5M.js +202 -0
  633. package/dist/model-invocations-db-XTUPPQ34.js +70 -0
  634. package/dist/model-resolver-JFZ2MHOJ.js +39 -0
  635. package/dist/model-resolver-SCQ57KAQ.js +217 -0
  636. package/dist/model-roles-D3OR4NAH.js +110 -0
  637. package/dist/monitor-F4SIU5G2.js +96 -0
  638. package/dist/nested-worktrees-2WUWQ75N.js +23 -0
  639. package/dist/next-S3IRLVER.js +96 -0
  640. package/dist/next-UKPFRXOO.js +148 -0
  641. package/dist/opportunistic-TL4S26VX.js +86 -0
  642. package/dist/optimization-config-resolver-NAEIYU6A.js +148 -0
  643. package/dist/package-HJR4QJT4.js +10 -0
  644. package/dist/parse-git-status-7PIYFPS2.js +21 -0
  645. package/dist/path-security-5WJSY56G.js +41 -0
  646. package/dist/paths-JUI7C4KU.js +45 -0
  647. package/dist/plan-backlog-list-fast-SSTQL5XP.js +89 -0
  648. package/dist/plan-backlog-show-fast-PFRAQ3JB.js +114 -0
  649. package/dist/plan-sprint-list-fast-HD2KBZSY.js +110 -0
  650. package/dist/plan-sprint-status-fast-YFYH7WQF.js +116 -0
  651. package/dist/plan-task-status-fast-JU5PNLUU.js +114 -0
  652. package/dist/plan-to-flywheel-PY5MSY2H.js +529 -0
  653. package/dist/preflight-PJHGTJNZ.js +12 -0
  654. package/dist/process-registry-B4ZUCN7T.js +28 -0
  655. package/dist/process-scanner-TU3QHWVB.js +23 -0
  656. package/dist/processes-6FHVGT72.js +28 -0
  657. package/dist/processes-json-fast-YWTMUWXB.js +72 -0
  658. package/dist/profile-HWTC56SG.js +209 -0
  659. package/dist/project-runtime-paths-2UQNG5Y4.js +41 -0
  660. package/dist/protected-branches-N36LDBFY.js +71 -0
  661. package/dist/provider-aware-resource-manager-YTFAHNGJ.js +40 -0
  662. package/dist/provider-matrix-2KO2V5OM.js +214 -0
  663. package/dist/provider-matrix-renderer-MEC3PER7.js +139 -0
  664. package/dist/provider-output-forwarding-2AHL7YMA.js +26 -0
  665. package/dist/provider-registry-WCZ65QHC.js +136 -0
  666. package/dist/provider-temp-cleanup-IUNPJCZF.js +14 -0
  667. package/dist/prune-engine-EOQVWEP6.js +448 -0
  668. package/dist/publication-turn-S42RZTQF.js +44 -0
  669. package/dist/quality-gate-JQMVCKOO.js +72 -0
  670. package/dist/quickstart-3O6FRCFB.js +105 -0
  671. package/dist/readiness-GW6ABRHB.js +139 -0
  672. package/dist/readiness-KFZCXDTT.js +215 -0
  673. package/dist/registry-7XG376SY.js +217 -0
  674. package/dist/repair-helpers-TTV5FEF4.js +117 -0
  675. package/dist/repo-root-YG6MQZDW.js +28 -0
  676. package/dist/resolution-engine-FFEK7HQ2.js +187 -0
  677. package/dist/resolve-provider-cli-ZEXVZKET.js +44 -0
  678. package/dist/restore-telemetry-db-E7BR5PO7.js +459 -0
  679. package/dist/routing-events-AW6SRZ6G.js +13 -0
  680. package/dist/routing-history-store-FMDDQL6W.js +39 -0
  681. package/dist/routing-planner-C6OHO3EY.js +221 -0
  682. package/dist/runner-KHDNQ5KU.js +1194 -0
  683. package/dist/runner-PIS2BW7Y.js +1801 -0
  684. package/dist/safety-E3JJVSFL.js +67 -0
  685. package/dist/safety-stanza-HE6GXP4G.js +49 -0
  686. package/dist/sandbox-7LBZC5PE.js +72 -0
  687. package/dist/sandbox-ASWCDYZL.js +80 -0
  688. package/dist/schema-extensions.sql +234 -0
  689. package/dist/security-JI22TBEB.js +17 -0
  690. package/dist/setup-bb-plugin-FQXLA76Y.js +531 -0
  691. package/dist/setup-claude-code-NF4ZSHVE.js +197 -0
  692. package/dist/setup-temp-cleanup-2APZ4HMA.js +29 -0
  693. package/dist/shared-utils-34HJZ6JX.js +40 -0
  694. package/dist/simple-artifact-goal-MQLDDMMX.js +29 -0
  695. package/dist/sprint-N7JFTEGL.js +403 -0
  696. package/dist/sprint-bridge-KJXIBNRJ.js +117 -0
  697. package/dist/sprint-completion-dependents-34SR7JVL.js +43 -0
  698. package/dist/sprint-execution-service-A6JPUBWY.js +376 -0
  699. package/dist/sprint-manager-MU2ZUWN2.js +140 -0
  700. package/dist/sqlite-timeout-config-U2CT4LKU.js +13 -0
  701. package/dist/sqlite-wrapper-FSGJ2QEW.js +19 -0
  702. package/dist/stale-cleanup-orchestrator-4X6324VF.js +71 -0
  703. package/dist/storage-access-I3V5LER7.js +31 -0
  704. package/dist/store-XVG64ZR2.js +25 -0
  705. package/dist/task-classifier-MN2DYE5R.js +558 -0
  706. package/dist/task-list-fast-7AT6LTTA.js +112 -0
  707. package/dist/task-manager-LHPBBRRE.js +136 -0
  708. package/dist/task-report-OLPAWBTK.js +64 -0
  709. package/dist/task-type-detector-6RG3YNZF.js +27 -0
  710. package/dist/task-type-resolver-EQHOUIHT.js +64 -0
  711. package/dist/telemetry-5VYSK772.js +212 -0
  712. package/dist/telemetry-health-tracker-4GFW2EMP.js +12 -0
  713. package/dist/telemetry-path-resolver-H3NFHHQY.js +34 -0
  714. package/dist/telemetry-reliability-URJDTNPZ.js +76 -0
  715. package/dist/transports-NREZQTJK.js +106 -0
  716. package/dist/unified-cost-tracker-5MTBJ2CL.js +59 -0
  717. package/dist/unified-state-cleanup-GGAXLHXF.js +292 -0
  718. package/dist/universal-cost-calculator-JTFEJVWK.js +70 -0
  719. package/dist/user-agents-QLIGPLP2.js +30 -0
  720. package/dist/variant-execution-I4ZZUAWY.js +319 -0
  721. package/dist/variant-opt-parser-JHCXKY2K.js +251 -0
  722. package/dist/variant-request-input-PL37N3AN.js +11 -0
  723. package/dist/variant-state-cleanup-KHMXRBZN.js +287 -0
  724. package/dist/variant-worker-COB5YTJX.js +4517 -0
  725. package/dist/variant-worker.js +26 -0
  726. package/dist/variants-XGDSXYG3.js +267 -0
  727. package/dist/verification-command-YSPU4VBH.js +66 -0
  728. package/dist/verify-command-timeout-DQUYH33Z.js +11 -0
  729. package/dist/verify-commands-LOHMOFKC.js +65 -0
  730. package/dist/warning-JSKSFKBT.js +13 -0
  731. package/dist/work-edge-K65NHVWY.js +50 -0
  732. package/dist/work-item-writer-KY5ZWZWW.js +30 -0
  733. package/dist/worker-JNXXEIT6.js +1504 -0
  734. package/dist/worker.js +21 -0
  735. package/dist/workspace-tree-identity-JO5PSKTW.js +52 -0
  736. package/dist/worktree-environment-WTVABAQW.js +64 -0
  737. package/dist/worktree-metrics-VZRQI26T.js +66 -0
  738. package/dist/writeback-TQFQBJNU.js +50 -0
  739. package/docs/CLI_REFERENCE.md +8287 -0
  740. package/docs/PROVIDER_SETUP.md +1737 -0
  741. package/docs/README.md +96 -0
  742. package/docs/TROUBLESHOOTING.md +2209 -0
  743. package/docs/getting-started/QUICK_START.md +260 -0
  744. package/docs/getting-started/README.md +198 -0
  745. package/docs/getting-started/SETUP.md +25 -0
  746. package/docs/reference/AGENT_CONTEXT.md +84 -0
  747. package/docs/reference/DELIVERY_ONBOARDING.md +198 -0
  748. package/docs/reference/PROVIDER_RELEASE_TIERS.md +34 -0
  749. package/docs/reference/SECURITY.md +291 -0
  750. package/docs/reference/TELEMETRY_PRIVACY.md +259 -0
  751. package/integrations/bb-plugin-build/README.md +391 -0
  752. package/integrations/bb-plugin-build/app.css +431 -0
  753. package/integrations/bb-plugin-build/app.tsx +6210 -0
  754. package/integrations/bb-plugin-build/package.json +45 -0
  755. package/integrations/bb-plugin-build/server.ts +488 -0
  756. package/integrations/bb-plugin-build/src/adapter.ts +3672 -0
  757. package/integrations/bb-plugin-build/src/backlog-authoring-model.ts +150 -0
  758. package/integrations/bb-plugin-build/src/backlog-authoring-schema.ts +458 -0
  759. package/integrations/bb-plugin-build/src/backlog-authoring-service.ts +467 -0
  760. package/integrations/bb-plugin-build/src/backlog-authoring-view.ts +1025 -0
  761. package/integrations/bb-plugin-build/src/command-evidence-schema.ts +13 -0
  762. package/integrations/bb-plugin-build/src/competition-defaults-schema.ts +122 -0
  763. package/integrations/bb-plugin-build/src/competition-defaults-service.ts +105 -0
  764. package/integrations/bb-plugin-build/src/competition-defaults-view.ts +138 -0
  765. package/integrations/bb-plugin-build/src/contract.ts +718 -0
  766. package/integrations/bb-plugin-build/src/execution-activity-view.ts +98 -0
  767. package/integrations/bb-plugin-build/src/finalize-model.ts +205 -0
  768. package/integrations/bb-plugin-build/src/finalize-schema.ts +150 -0
  769. package/integrations/bb-plugin-build/src/finalize-service.ts +330 -0
  770. package/integrations/bb-plugin-build/src/finalize-view.ts +159 -0
  771. package/integrations/bb-plugin-build/src/judge-operations-readback.ts +303 -0
  772. package/integrations/bb-plugin-build/src/judge-operations-schema.ts +33 -0
  773. package/integrations/bb-plugin-build/src/judge-operations-view.ts +459 -0
  774. package/integrations/bb-plugin-build/src/model.ts +1318 -0
  775. package/integrations/bb-plugin-build/src/monitor-service.ts +233 -0
  776. package/integrations/bb-plugin-build/src/panel-read-ux.ts +97 -0
  777. package/integrations/bb-plugin-build/src/patch-preview-schema.ts +166 -0
  778. package/integrations/bb-plugin-build/src/patch-preview-service.ts +480 -0
  779. package/integrations/bb-plugin-build/src/patch-preview-view.ts +109 -0
  780. package/integrations/bb-plugin-build/src/planning-readback.ts +228 -0
  781. package/integrations/bb-plugin-build/src/planning-service.ts +647 -0
  782. package/integrations/bb-plugin-build/src/planning-sync-evidence.ts +37 -0
  783. package/integrations/bb-plugin-build/src/planning-workspace-readback.ts +33 -0
  784. package/integrations/bb-plugin-build/src/planning-workspace-schema.ts +131 -0
  785. package/integrations/bb-plugin-build/src/planning-workspace.ts +333 -0
  786. package/integrations/bb-plugin-build/src/poll-handoff.ts +136 -0
  787. package/integrations/bb-plugin-build/src/project-target-server.ts +21 -0
  788. package/integrations/bb-plugin-build/src/project-target.ts +45 -0
  789. package/integrations/bb-plugin-build/src/reference-index.ts +218 -0
  790. package/integrations/bb-plugin-build/src/repository-identity.ts +17 -0
  791. package/integrations/bb-plugin-build/src/repository-schema.ts +30 -0
  792. package/integrations/bb-plugin-build/src/result-explorer-view.ts +597 -0
  793. package/integrations/bb-plugin-build/src/result-readback-model.ts +163 -0
  794. package/integrations/bb-plugin-build/src/result-readback-schema.ts +319 -0
  795. package/integrations/bb-plugin-build/src/result-readback-service.ts +333 -0
  796. package/integrations/bb-plugin-build/src/run-page-view.ts +332 -0
  797. package/integrations/bb-plugin-build/src/setup-config-schema.ts +293 -0
  798. package/integrations/bb-plugin-build/src/setup-config-service.ts +620 -0
  799. package/integrations/bb-plugin-build/src/setup-config-view.ts +388 -0
  800. package/integrations/bb-plugin-build/src/sprint-close-model.ts +150 -0
  801. package/integrations/bb-plugin-build/src/sprint-close-schema.ts +128 -0
  802. package/integrations/bb-plugin-build/src/sprint-close-service.ts +293 -0
  803. package/integrations/bb-plugin-build/src/sprint-close-view.ts +116 -0
  804. package/integrations/bb-plugin-build/src/sprint-composition-model.ts +709 -0
  805. package/integrations/bb-plugin-build/src/sprint-composition-readback.ts +231 -0
  806. package/integrations/bb-plugin-build/src/sprint-composition-schema.ts +597 -0
  807. package/integrations/bb-plugin-build/src/sprint-composition-service.ts +773 -0
  808. package/integrations/bb-plugin-build/src/sprint-composition-view.ts +1221 -0
  809. package/integrations/bb-plugin-build/src/sprint-execution-schema.ts +188 -0
  810. package/integrations/bb-plugin-build/src/sprint-execution-service.ts +289 -0
  811. package/integrations/bb-plugin-build/src/sprint-execution-view.ts +54 -0
  812. package/integrations/bb-plugin-build/src/task-authoring-model.ts +563 -0
  813. package/integrations/bb-plugin-build/src/task-authoring-schema.ts +629 -0
  814. package/integrations/bb-plugin-build/src/task-authoring-service.ts +842 -0
  815. package/integrations/bb-plugin-build/src/task-authoring-view.ts +1227 -0
  816. package/integrations/bb-plugin-build/src/task-catalog-schema.ts +46 -0
  817. package/integrations/bb-plugin-build/src/task-catalog-service.ts +44 -0
  818. package/integrations/bb-plugin-build/src/task-detail-readback.ts +56 -0
  819. package/integrations/bb-plugin-build/src/task-detail-schema.ts +171 -0
  820. package/integrations/bb-plugin-build/src/task-navigation.ts +446 -0
  821. package/integrations/bb-plugin-build/src/workbench-route.ts +74 -0
  822. package/package.json +306 -4
  823. package/schemas/README.md +80 -0
  824. package/schemas/build.config.schema.json +2110 -0
  825. package/schemas/config-v1.schema.json +136 -0
  826. package/schemas/execution-config.schema.json +47 -0
  827. package/schemas/judge-scoring.schema.json +257 -0
  828. package/schemas/provider-config.schema.json +179 -0
  829. package/schemas/telemetry-config.schema.json +79 -0
  830. package/schemas/user-preferences.schema.json +141 -0
  831. package/scripts/assert-node-runtime.mjs +146 -0
  832. package/scripts/preflight-native.mjs +49 -0
  833. package/scripts/setup-git-hooks.mjs +39 -0
  834. package/scripts/sync.sh +2871 -0
  835. package/scripts/write-node-launcher.sh +138 -0
  836. package/src/resources/gemini/slash-packs/default/plan.toml +15 -0
  837. package/src/resources/gemini/slash-packs/default/summary.toml +16 -0
  838. package/src/resources/gemini/slash-packs/default/tests.toml +16 -0
  839. package/templates/.build-project/README.md +41 -0
  840. package/templates/.build-project/agents/README.md +50 -0
  841. package/templates/.build-project/agents/architect.md +10 -0
  842. package/templates/.build-project/agents/code-reviewer.md +10 -0
  843. package/templates/.build-project/agents/debugger.md +10 -0
  844. package/templates/.build-project/agents/documenter.md +10 -0
  845. package/templates/.build-project/agents/test-automator.md +10 -0
  846. package/templates/.build-project/config/build.config.jsonc +64 -0
  847. package/templates/.build-project/config/project-context.template.json +6 -0
  848. package/templates/.build-project/config/task-type-aliases.presets/kanban.yaml +14 -0
  849. package/templates/.build-project/config/task-type-aliases.presets/scrum.yaml +17 -0
  850. package/templates/.build-project/config/task-type-aliases.presets/xp.yaml +12 -0
  851. package/templates/.build-project/config/task-type-aliases.yaml +29 -0
  852. package/templates/.build-project/gitignore.template +70 -0
  853. package/templates/.build-project/task-types/analysis.yaml +38 -0
  854. package/templates/.build-project/task-types/architecture.yaml +35 -0
  855. package/templates/.build-project/task-types/doc.yaml +34 -0
  856. package/templates/.build-project/task-types/feature.yaml +22 -0
  857. package/templates/.build-project/task-types/general.yaml +4 -0
  858. package/templates/.build-project/task-types/security.yaml +38 -0
  859. package/templates/AGENTS.template.md +85 -0
  860. package/templates/CLAUDE.template.md +1 -0
  861. package/templates/GEMINI.template.md +1 -0
  862. package/templates/builtin-workflows/code-review.yaml +73 -0
  863. package/templates/builtin-workflows/skills/code-review/SKILL.md +61 -0
  864. package/templates/builtin-workflows/skills/execution-contract-analysis/SKILL.md +24 -0
  865. package/templates/builtin-workflows/skills/execution-contract-code/SKILL.md +19 -0
  866. package/templates/builtin-workflows/skills/execution-contract-structured/SKILL.md +17 -0
  867. package/templates/ci/incident-intake.mjs +787 -0
  868. package/templates/ci/incidents.yml.template +103 -0
  869. package/templates/ci/node.yml.template +17 -0
  870. package/templates/ci/python.yml.template +17 -0
  871. package/templates/guard-kit/GUARD_SETUP.md.template +258 -0
  872. package/templates/guard-kit/build-guard.yml.template +189 -0
  873. package/templates/guard-kit/check.mjs.template +932 -0
  874. package/templates/guard-kit/config.json.template +7 -0
  875. package/templates/guard-kit/policy.json.template +1 -0
  876. package/templates/profiles/README.md +57 -0
  877. package/templates/profiles/frontier-claude.json +23 -0
  878. package/templates/profiles/frontier-codex.json +23 -0
@@ -0,0 +1,2871 @@
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 `build 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. BUILD_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. BUILD_PROTECT_MAIN env var → sets mode
71
+ # 3. build config get --scope user sync.protectMain → sets mode
72
+ # 4. Default: confirm
73
+ # - In confirm mode: BUILD_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
+ USER_DRY_RUN=false
99
+ SYNC_MODE=false
100
+ SYNC_ONLY=false
101
+ PR_ONLY=false
102
+ STATUS_ONLY=false
103
+ NO_SYNC_PUSH=false
104
+ PUBLISH=false
105
+ NO_PUBLISH=false
106
+ PUSH_ALL_BRANCHES=false
107
+ ENSURE_PR=false
108
+ ROUTE_MAIN_FORCE=false
109
+ NO_ROUTE_MAIN=false
110
+ STAY_ON_MAIN=false
111
+ REBASE=true # Default to rebase for cleaner history
112
+ ALL_BRANCHES=false
113
+ SKIP_TESTS=false
114
+ VERBOSE=false
115
+ ALLOW_PROTECTED=false # Guardrail: require explicit flag for protected branches (legacy)
116
+ NO_PROTECT_MAIN=false # Guardrail 1: bypass protected branch prompt for this run
117
+ NO_ORPHAN_WARN=false # Guardrail 2: suppress orphan branch warnings
118
+ FORCE=false # Guardrail: skip ALL safety checks (nuclear option)
119
+ CREATE_PR=false # Create PR instead of direct push (enables CI/auto-fix)
120
+ NO_PR=false # Force direct push even on protected branches
121
+ IS_PROTECTED=false # Guardrail 1: whether current branch is protected (set in guard block)
122
+
123
+ # State tracking variables (Phase 1 improvements)
124
+ UPSTREAM_EXISTS=false
125
+ UPSTREAM_BRANCH=""
126
+ PUSH_SUCCESS=false
127
+ SYNC_SUCCESS=true
128
+ HAS_WARNINGS=false
129
+ HAS_FAILURE=false
130
+ STASHED=false # Track whether we actually stashed changes
131
+ AUTO_STASH_MSG="" # Unique message of the stash this run created
132
+ ROUTE_MAIN_NEEDED=false
133
+ ROUTE_MAIN_AHEAD_COUNT=0
134
+ ROUTE_MAIN_MODE_ACTIVE=false
135
+ ROUTE_MAIN_WIP_BRANCH=""
136
+ CREATED_PR_URL="" # Track PR URL to show helpful note when switching back to main
137
+ CREATED_COMMIT_SHA="" # Commit owned by this invocation (if Step 3 created one)
138
+ CREATED_COMMIT_PARENT_SHA=""
139
+
140
+ print_sync_usage() {
141
+ cat <<'EOF'
142
+ Commit, Merge & Sync - Multi-environment git workflow automation
143
+ Usage: ./sync.sh [commit-message] [options]
144
+
145
+ Options:
146
+ --sync Sync mode (pull + push-if-ahead, no commit)
147
+ --sync-only Sync only (skip PR phase)
148
+ --pr-only PR status/ensure only (skip sync + commit)
149
+ --no-sync-push In sync mode, never push after pull (alias: --pull-only)
150
+ --pull-only Alias for --no-sync-push (backward compat)
151
+ --ensure-pr Create PR if missing (default is show-only)
152
+ --route-main Route main commits via deterministic PR branch
153
+ --no-route-main Disable route-main for this run (overrides config)
154
+ --stay-on-main (deprecated, now default) Return to main after routing
155
+ --publish Force publishing new branches (push -u) when no upstream exists
156
+ --no-publish Disable publishing new branches for this run (overrides auto-publish)
157
+ --push-all-branches With --all-branches, also push branches that are ahead
158
+ --status Preflight-only status (no changes)
159
+ --rebase Use rebase strategy (default)
160
+ --merge Use merge strategy
161
+ --all-branches Sync all tracked branches
162
+ --dry-run Preview without making changes
163
+ --pr Create a PR instead of pushing directly (enables CI/auto-fix flow)
164
+ --no-pr Force direct push even on protected branches
165
+ --allow-protected Allow commits to main/master/release branches (legacy)
166
+ --no-protect-main Bypass protected branch prompt for this run
167
+ --no-orphan-warn Suppress orphan branch warnings (Guardrail 2)
168
+ --force Skip ALL guardrails (nuclear option, use sparingly)
169
+ --verbose Show detailed output
170
+ -h, --help Print usage and exit
171
+
172
+ Commit semantics:
173
+ When committing staged changes, the local commit is created before any push
174
+ attempt (--sync and --sync-only do not commit). If the push fails, the commit
175
+ remains on your current branch. Before the first mutating step, a checkpoint
176
+ branch backup/pre-sync-<epoch> is created for recovery; failed runs keep theirs.
177
+ Stale checkpoints are pruned per BUILD_SYNC_BACKUP_RETENTION_DAYS (see script header).
178
+
179
+ Exit codes:
180
+ 0 Journey completed (sync and/or commit/push succeeded; warnings may appear above)
181
+ 1 Failure — an operation did not complete (commit/push/merge/guard refusal, etc.)
182
+ EOF
183
+ }
184
+
185
+ for arg in "$@"; do
186
+ case $arg in
187
+ -h|--help)
188
+ print_sync_usage
189
+ exit 0
190
+ ;;
191
+ --dry-run)
192
+ DRY_RUN=true
193
+ USER_DRY_RUN=true
194
+ ;;
195
+ --status)
196
+ STATUS_ONLY=true
197
+ ;;
198
+ --sync)
199
+ SYNC_MODE=true
200
+ ;;
201
+ --sync-only)
202
+ SYNC_MODE=true
203
+ SYNC_ONLY=true
204
+ ;;
205
+ --pr-only)
206
+ PR_ONLY=true
207
+ ;;
208
+ --rebase)
209
+ REBASE=true
210
+ ;;
211
+ --merge)
212
+ REBASE=false
213
+ ;;
214
+ --all-branches)
215
+ ALL_BRANCHES=true
216
+ ;;
217
+ --push-all-branches)
218
+ PUSH_ALL_BRANCHES=true
219
+ ;;
220
+ --no-sync-push)
221
+ NO_SYNC_PUSH=true
222
+ ;;
223
+ --pull-only)
224
+ NO_SYNC_PUSH=true
225
+ ;;
226
+ --publish)
227
+ PUBLISH=true
228
+ ;;
229
+ --no-publish)
230
+ NO_PUBLISH=true
231
+ ;;
232
+ --ensure-pr)
233
+ ENSURE_PR=true
234
+ ;;
235
+ --route-main)
236
+ ROUTE_MAIN_FORCE=true
237
+ ;;
238
+ --no-route-main)
239
+ NO_ROUTE_MAIN=true
240
+ ;;
241
+ --stay-on-main)
242
+ # Deprecated: return-to-main is now the default behavior.
243
+ # Kept for backward compatibility; flag is a no-op.
244
+ STAY_ON_MAIN=true
245
+ ;;
246
+ --skip-tests)
247
+ SKIP_TESTS=true
248
+ ;;
249
+ --verbose)
250
+ VERBOSE=true
251
+ ;;
252
+ --allow-protected)
253
+ ALLOW_PROTECTED=true
254
+ NO_PROTECT_MAIN=true # --allow-protected implies --no-protect-main
255
+ ;;
256
+ --no-protect-main)
257
+ NO_PROTECT_MAIN=true
258
+ # Log bypass to stderr for visibility
259
+ printf " ${YELLOW}%-16s %s${NC}\n" "Protect-main" "bypassed (--no-protect-main)" >&2
260
+ ;;
261
+ --no-orphan-warn)
262
+ NO_ORPHAN_WARN=true
263
+ ;;
264
+ --force)
265
+ FORCE=true
266
+ ALLOW_PROTECTED=true # --force implies --allow-protected
267
+ NO_PROTECT_MAIN=true # --force implies --no-protect-main
268
+ NO_ORPHAN_WARN=true # --force implies --no-orphan-warn
269
+ ;;
270
+ --pr)
271
+ CREATE_PR=true
272
+ ;;
273
+ --no-pr)
274
+ NO_PR=true
275
+ ;;
276
+ --message=*|-m=*)
277
+ printf " ${RED}%-16s %s${NC}\n" "Error" "unknown option: $arg"
278
+ printf " %-16s %s\n" "Hint" 'Use positional form: build sync "your message" (not --message)'
279
+ exit 1
280
+ ;;
281
+ --message|-m)
282
+ printf " ${RED}%-16s %s${NC}\n" "Error" "unknown option: $arg"
283
+ printf " %-16s %s\n" "Hint" 'Use positional form: build sync "your message" (not --message)'
284
+ exit 1
285
+ ;;
286
+ --*)
287
+ printf " ${RED}%-16s %s${NC}\n" "Error" "unknown option: $arg"
288
+ exit 1
289
+ ;;
290
+ *)
291
+ COMMIT_MESSAGE="$arg"
292
+ ;;
293
+ esac
294
+ done
295
+
296
+ if [ "$PUBLISH" = true ] && [ "$NO_PUBLISH" = true ]; then
297
+ printf " ${RED}%-16s %s${NC}\n" "Error" "conflicting flags: --publish and --no-publish"
298
+ exit 1
299
+ fi
300
+
301
+ # Header
302
+ echo ""
303
+ echo " sync — $(basename "$REPO_ROOT")"
304
+ printf ' %0.s' $(seq 1 40); echo ""
305
+ echo ""
306
+
307
+ # Helper functions — 2-space indent, 16-char left-aligned label column
308
+ log_ok() { printf " %-16s ${GREEN}%s${NC}\n" "$1" "$2"; }
309
+ log_warn() { printf " %-16s ${YELLOW}%s${NC}\n" "$1" "$2"; }
310
+ log_err() { printf " ${RED}%-16s %s${NC}\n" "$1" "$2"; }
311
+ log_dim() { printf " ${DIM}%-16s %s${NC}\n" "$1" "$2"; }
312
+
313
+ LAST_GIT_PUSH_ERROR=""
314
+
315
+ run_git_push() {
316
+ LAST_GIT_PUSH_ERROR=""
317
+
318
+ local err_file
319
+ err_file=$(mktemp 2>/dev/null) || true
320
+ if [ -z "${err_file:-}" ]; then
321
+ git push "$@" >/dev/null 2>&1
322
+ return $?
323
+ fi
324
+
325
+ if git push "$@" >/dev/null 2>"$err_file"; then
326
+ rm -f "$err_file"
327
+ return 0
328
+ fi
329
+
330
+ local filtered_error
331
+ filtered_error=$(
332
+ {
333
+ grep -Eiv '^(To |remote: (Resolving deltas|Compressing objects|Counting objects))' "$err_file" || true
334
+ } | sed 's/^remote: //' | sed '/^[[:space:]]*$/d'
335
+ )
336
+ LAST_GIT_PUSH_ERROR=$(printf '%s\n' "$filtered_error" | grep -Eiv '^hint:' | tail -n 3 || true)
337
+ if [ -z "$LAST_GIT_PUSH_ERROR" ]; then
338
+ LAST_GIT_PUSH_ERROR=$(printf '%s\n' "$filtered_error" | tail -n 3)
339
+ fi
340
+ rm -f "$err_file"
341
+ return 1
342
+ }
343
+
344
+ log_git_push_reason() {
345
+ if [ -z "$LAST_GIT_PUSH_ERROR" ]; then
346
+ return 0
347
+ fi
348
+
349
+ local line
350
+ while IFS= read -r line; do
351
+ [ -n "$line" ] || continue
352
+ log_warn "Reason" "$line"
353
+ done <<< "$LAST_GIT_PUSH_ERROR"
354
+ }
355
+
356
+ move_lock_blocked_route_ref() {
357
+ local branch="$1"
358
+
359
+ case "$branch" in
360
+ wip/route-main/*) ;;
361
+ *) return 1 ;;
362
+ esac
363
+ if ! git check-ref-format --branch "$branch" >/dev/null 2>&1; then
364
+ return 1
365
+ fi
366
+
367
+ local current_branch
368
+ current_branch=$(git branch --show-current 2>/dev/null || true)
369
+ if [ "$branch" = "$current_branch" ]; then
370
+ return 1
371
+ fi
372
+
373
+ local ref_path
374
+ ref_path=$(git rev-parse --git-path "refs/heads/$branch" 2>/dev/null || true)
375
+ if [ -z "$ref_path" ] || [ ! -f "$ref_path" ]; then
376
+ return 1
377
+ fi
378
+
379
+ local archive_root archive_path safe_name
380
+ archive_root="${TMPDIR:-/tmp}/build-merged-refs"
381
+ safe_name=$(echo "$branch" | sed 's#[^A-Za-z0-9._-]#_#g')
382
+ archive_path="$archive_root/$safe_name-$(date +%s)"
383
+
384
+ mkdir -p "$archive_root" 2>/dev/null || return 1
385
+ if mv "$ref_path" "$archive_path" 2>/dev/null; then
386
+ log_warn "Cleanup" "local ref lock blocked; moved merged $branch"
387
+ log_dim "Ref backup" "$archive_path"
388
+ return 0
389
+ fi
390
+
391
+ return 1
392
+ }
393
+
394
+ delete_local_route_branch() {
395
+ local branch="$1"
396
+ local err_file
397
+ err_file=$(mktemp 2>/dev/null) || true
398
+
399
+ if [ -z "${err_file:-}" ]; then
400
+ git branch -D "$branch" >/dev/null 2>&1
401
+ return $?
402
+ fi
403
+
404
+ if git branch -D "$branch" >/dev/null 2>"$err_file"; then
405
+ rm -f "$err_file"
406
+ return 0
407
+ fi
408
+
409
+ local delete_error
410
+ delete_error=$(cat "$err_file" 2>/dev/null || true)
411
+ rm -f "$err_file"
412
+
413
+ if echo "$delete_error" | grep -qi 'cannot lock ref' && \
414
+ echo "$delete_error" | grep -qi 'Operation not permitted'; then
415
+ move_lock_blocked_route_ref "$branch"
416
+ return $?
417
+ fi
418
+
419
+ return 1
420
+ }
421
+
422
+ # Extract Build backlog IDs from arbitrary text.
423
+ # Format: <YYYYMMDDTHHMMSSZ>-<uuidHex12>
424
+ extract_build_backlog_ids() {
425
+ local text="${1:-}"
426
+ if [ -z "$text" ]; then
427
+ return 0
428
+ fi
429
+ echo "$text" | grep -Eo '[0-9]{8}T[0-9]{6}Z-[0-9a-f]{12}' 2>/dev/null | sort -u || true
430
+ }
431
+
432
+ is_generated_sync_artifact_path() {
433
+ local file="$1"
434
+ case "$file" in
435
+ schemas/build.config.schema.json|schemas/judge-scoring.schema.json|providers/pricing.json|providers/registry.json|.build-project/providers/pricing.json|.build-project/providers/registry.json)
436
+ return 0
437
+ ;;
438
+ src/core/providers/generated/*|src/core/providers/routing/generated/*)
439
+ return 0
440
+ ;;
441
+ *)
442
+ return 1
443
+ ;;
444
+ esac
445
+ }
446
+
447
+ filter_generated_sync_artifacts() {
448
+ local file
449
+ while IFS= read -r file; do
450
+ [ -n "$file" ] || continue
451
+ if is_generated_sync_artifact_path "$file"; then
452
+ echo "$file"
453
+ fi
454
+ done
455
+ }
456
+
457
+ get_staged_generated_artifacts() {
458
+ git diff --cached --name-only 2>/dev/null | filter_generated_sync_artifacts
459
+ }
460
+
461
+ get_last_commit_generated_artifacts() {
462
+ git diff-tree --no-commit-id --name-only -r HEAD 2>/dev/null | filter_generated_sync_artifacts
463
+ }
464
+
465
+ get_last_commit_subject() {
466
+ git log -1 --pretty=%s 2>/dev/null || echo ""
467
+ }
468
+
469
+ get_last_commit_sha_short() {
470
+ git rev-parse --short=12 HEAD 2>/dev/null || echo ""
471
+ }
472
+
473
+ # Basic PR body template for sync-created PRs.
474
+ # Goals:
475
+ # - Human skimmable (Summary/Verification)
476
+ # - Agent parseable (stable Build key/value lines)
477
+ build_sync_pr_body() {
478
+ local mode="${1:-}"
479
+ local head_branch="${2:-}"
480
+ local base_branch="${3:-}"
481
+
482
+ local subject="${COMMIT_MESSAGE:-}"
483
+ if [ -z "$subject" ]; then
484
+ subject=$(get_last_commit_subject)
485
+ fi
486
+
487
+ local sha
488
+ sha=$(get_last_commit_sha_short)
489
+
490
+ local summary_line_1
491
+ case "$mode" in
492
+ route-main)
493
+ summary_line_1="Route protected-branch updates via PR"
494
+ ;;
495
+ ensure-pr)
496
+ summary_line_1="Open PR for branch updates"
497
+ ;;
498
+ pr)
499
+ summary_line_1="Run CI/auto-fix via PR workflow"
500
+ ;;
501
+ *)
502
+ summary_line_1="Update"
503
+ ;;
504
+ esac
505
+
506
+ local summary_line_2=""
507
+ if [ -n "$subject" ]; then
508
+ summary_line_2="Commit: $subject"
509
+ if [ -n "$sha" ]; then
510
+ summary_line_2="$summary_line_2 ($sha)"
511
+ fi
512
+ fi
513
+
514
+ local backlog_ids
515
+ backlog_ids=$(extract_build_backlog_ids "$subject" | tr '\n' ' ' | sed 's/[[:space:]]*$//') || true
516
+
517
+ echo "Summary:"
518
+ echo "- $summary_line_1"
519
+ if [ -n "$summary_line_2" ]; then
520
+ echo "- $summary_line_2"
521
+ fi
522
+ echo ""
523
+
524
+ if [ "$mode" = "route-main" ]; then
525
+ echo "Why:"
526
+ echo "- Avoid direct commits to protected branches"
527
+ echo ""
528
+ fi
529
+
530
+ echo "Verification:"
531
+ echo "- not run (created by build sync; rely on CI)"
532
+
533
+ local generated_artifacts
534
+ generated_artifacts=$(get_last_commit_generated_artifacts)
535
+ if [ -n "$generated_artifacts" ]; then
536
+ echo ""
537
+ echo "Generated artifacts:"
538
+ echo "- Sync/build hooks updated committed generated files"
539
+ echo "$generated_artifacts" | sed 's/^/- /'
540
+ fi
541
+
542
+ if [ -n "$backlog_ids" ]; then
543
+ echo ""
544
+ echo "Links:"
545
+ echo "- Backlog: $backlog_ids"
546
+ fi
547
+
548
+ echo ""
549
+ echo "Build:"
550
+ echo "- Mode: $mode"
551
+ echo "- Branch: $head_branch"
552
+ echo "- Base: $base_branch"
553
+
554
+ # Include Refs: and Build-Sync: lines from commit message for task linkage
555
+ # This enables `build sync reconcile` to find merged PRs by task ID
556
+ local commit_body
557
+ commit_body=$(git log -1 --pretty=%B 2>/dev/null || true)
558
+ local refs_line build_sync_line
559
+ refs_line=$(echo "$commit_body" | grep -E '^Refs: task/' || true)
560
+ build_sync_line=$(echo "$commit_body" | grep -E '^Build-Sync:' || true)
561
+ if [ -n "$refs_line" ] || [ -n "$build_sync_line" ]; then
562
+ echo ""
563
+ [ -n "$refs_line" ] && echo "$refs_line"
564
+ [ -n "$build_sync_line" ] && echo "$build_sync_line"
565
+ fi
566
+ }
567
+
568
+
569
+ # Project-local post-sync hooks (optional extension points):
570
+ # - $REPO_ROOT/.claude/hooks/post-sync.sh
571
+ # - $REPO_ROOT/.claude/hooks/post-sync-cleanup.sh
572
+ # Notes:
573
+ # - Hooks are skipped in --dry-run mode.
574
+ # - Hook failures never fail the main sync run.
575
+ run_post_sync_hooks() {
576
+ if [ "${CLAUDE_SYNC_HOOKS_DISABLED:-}" = "1" ]; then
577
+ if [ "${VERBOSE}" = true ]; then
578
+ log_dim "Hooks" "disabled (CLAUDE_SYNC_HOOKS_DISABLED=1)"
579
+ fi
580
+ return 0
581
+ fi
582
+
583
+ local hook_paths=(
584
+ "$REPO_ROOT/.claude/hooks/post-sync.sh"
585
+ "$REPO_ROOT/.claude/hooks/post-sync-cleanup.sh"
586
+ )
587
+
588
+ local found_any=false
589
+ local seen_hooks=$'\n'
590
+ for hook in "${hook_paths[@]}"; do
591
+ if [ ! -f "$hook" ]; then
592
+ continue
593
+ fi
594
+ if [[ "$seen_hooks" == *$'\n'"$hook"$'\n'* ]]; then
595
+ continue
596
+ fi
597
+ seen_hooks+=$'\n'"${hook}"$'\n'
598
+
599
+ found_any=true
600
+ if [ ! -x "$hook" ]; then
601
+ log_warn "Hook" "not executable: $hook"
602
+ continue
603
+ fi
604
+
605
+ log_dim "Hook" "${hook#$REPO_ROOT/}"
606
+ if CLAUDE_SYNC_REPO_ROOT="$REPO_ROOT" \
607
+ CLAUDE_SYNC_DRY_RUN="$DRY_RUN" \
608
+ CLAUDE_SYNC_MODE="$SYNC_MODE" \
609
+ CLAUDE_SYNC_ONLY="$SYNC_ONLY" \
610
+ CLAUDE_SYNC_PR_ONLY="$PR_ONLY" \
611
+ CLAUDE_SYNC_STATUS_ONLY="$STATUS_ONLY" \
612
+ CLAUDE_SYNC_PUSH_SUCCESS="$PUSH_SUCCESS" \
613
+ CLAUDE_SYNC_HAS_WARNINGS="$HAS_WARNINGS" \
614
+ CLAUDE_SYNC_ALREADY_FETCHED="${SYNC_FETCH_DONE:-false}" \
615
+ CLAUDE_SYNC_BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")" \
616
+ bash "$hook" ${ORIGINAL_ARGS[@]+"${ORIGINAL_ARGS[@]}"}; then
617
+ :
618
+ else
619
+ log_warn "Hook" "failed: ${hook#$REPO_ROOT/}"
620
+ HAS_WARNINGS=true
621
+ fi
622
+ done
623
+
624
+ if [ "$found_any" = false ] && [ "${VERBOSE}" = true ]; then
625
+ log_dim "Hooks" "none found"
626
+ fi
627
+ }
628
+
629
+ # Normalize common truthy values to bash-friendly true/false
630
+ is_truthy() {
631
+ case "${1:-}" in
632
+ 1|true|TRUE|yes|YES|on|ON) return 0 ;;
633
+ *) return 1 ;;
634
+ esac
635
+ }
636
+
637
+ has_tracked_changes() {
638
+ local tracked
639
+ tracked=$(git status --porcelain | grep -Ev "^[\\?!]" || true)
640
+ [ -n "$tracked" ]
641
+ }
642
+
643
+ # ============================================================================
644
+ # CONFIG CACHE - load all sync settings once at startup (avoids repeated CLI spawns)
645
+ # ============================================================================
646
+ # Priority per key: 1. env var (highest) 2. build config --scope user (routeMain: project scope first) 3. default
647
+ #
648
+ # Cache is populated lazily on first access. Uses `build config get --scope user --json`
649
+ # which returns {"path":"...","scope":"user","value":...} when set, or omits "value" when not.
650
+ _SYNC_CONFIG_LOADED=false
651
+ _CFG_ROUTE_MAIN=""
652
+ _CFG_AUTO_PUBLISH=""
653
+ _CFG_PROTECT_MAIN=""
654
+ _CFG_CHECKOUT_MAIN=""
655
+
656
+ _load_sync_config() {
657
+ if [ "$_SYNC_CONFIG_LOADED" = true ]; then
658
+ return
659
+ fi
660
+ _SYNC_CONFIG_LOADED=true
661
+
662
+ # Only attempt CLI reads if build is available
663
+ if ! command -v build >/dev/null 2>&1; then
664
+ return
665
+ fi
666
+
667
+ # Batch-read all 4 sync keys via `build config get` (portable; no tsx/source dependency).
668
+ local batch_json
669
+ batch_json=$(
670
+ build config get sync --scope user --json 2>/dev/null | node -e '
671
+ let s = "";
672
+ process.stdin.on("data", d => s += d);
673
+ process.stdin.on("end", () => {
674
+ try {
675
+ const parsed = JSON.parse(s);
676
+ process.stdout.write(JSON.stringify(parsed.value ?? {}));
677
+ } catch {
678
+ process.stdout.write("{}");
679
+ }
680
+ });
681
+ ' 2>/dev/null || echo "{}"
682
+ )
683
+
684
+ # Project scope wins over user scope for routeMain (env var still wins over both)
685
+ local project_route
686
+ project_route=$(build config get sync.routeMain --scope project --json 2>/dev/null \
687
+ | tr -d '\n' | sed -n 's/.*"found"[[:space:]]*:[[:space:]]*true.*"value"[[:space:]]*:[[:space:]]*\([a-z]*\).*/\1/p' || true)
688
+
689
+ # Parse batch result (single jq-free parse via grep)
690
+ if [ -n "$batch_json" ] && [ "$batch_json" != "{}" ]; then
691
+ if [ -z "${BUILD_PR_ROUTE_MAIN:-}" ]; then
692
+ _CFG_ROUTE_MAIN=$(echo "$batch_json" | sed -n 's/.*"routeMain"[[:space:]]*:[[:space:]]*\([^,}]*\).*/\1/p' | tr -d '"' || true)
693
+ fi
694
+ if [ -z "${BUILD_SYNC_AUTO_PUBLISH:-}" ]; then
695
+ _CFG_AUTO_PUBLISH=$(echo "$batch_json" | sed -n 's/.*"autoPublish"[[:space:]]*:[[:space:]]*\([^,}]*\).*/\1/p' | tr -d '"' || true)
696
+ fi
697
+ if [ -z "${BUILD_PROTECT_MAIN:-}" ]; then
698
+ _CFG_PROTECT_MAIN=$(echo "$batch_json" | sed -n 's/.*"protectMain"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' || true)
699
+ fi
700
+ if [ -z "${BUILD_SYNC_CHECKOUT_MAIN:-}" ]; then
701
+ _CFG_CHECKOUT_MAIN=$(echo "$batch_json" | sed -n 's/.*"checkoutMain"[[:space:]]*:[[:space:]]*\([^,}]*\).*/\1/p' | tr -d '"' || true)
702
+ fi
703
+ fi
704
+ if [ -z "${BUILD_PR_ROUTE_MAIN:-}" ] && [ -n "$project_route" ]; then
705
+ _CFG_ROUTE_MAIN="$project_route"
706
+ fi
707
+ }
708
+
709
+ # Get route-main default from config or environment
710
+ # Priority: 1. BUILD_PR_ROUTE_MAIN env var 2. build config 3. default false
711
+ # Default is false: direct-to-main push. Enable via config for PR-centric CI workflows.
712
+ get_route_main_default() {
713
+ if [ -n "${BUILD_PR_ROUTE_MAIN:-}" ]; then
714
+ echo "${BUILD_PR_ROUTE_MAIN}"
715
+ return
716
+ fi
717
+ _load_sync_config
718
+ if [ -n "$_CFG_ROUTE_MAIN" ]; then
719
+ echo "$_CFG_ROUTE_MAIN"
720
+ return
721
+ fi
722
+ echo "false"
723
+ }
724
+
725
+ # Get sync auto-publish default from config or environment
726
+ # Priority: 1. BUILD_SYNC_AUTO_PUBLISH env var 2. build config 3. default false
727
+ get_sync_auto_publish_default() {
728
+ if [ -n "${BUILD_SYNC_AUTO_PUBLISH:-}" ]; then
729
+ echo "${BUILD_SYNC_AUTO_PUBLISH}"
730
+ return
731
+ fi
732
+ _load_sync_config
733
+ if [ -n "$_CFG_AUTO_PUBLISH" ]; then
734
+ echo "$_CFG_AUTO_PUBLISH"
735
+ return
736
+ fi
737
+ echo "false"
738
+ }
739
+
740
+ # Get sync checkout-main default from config or environment
741
+ # Priority: 1. BUILD_SYNC_CHECKOUT_MAIN env var 2. build config 3. default false
742
+ get_sync_checkout_main_default() {
743
+ if [ -n "${BUILD_SYNC_CHECKOUT_MAIN:-}" ]; then
744
+ echo "${BUILD_SYNC_CHECKOUT_MAIN}"
745
+ return
746
+ fi
747
+ _load_sync_config
748
+ if [ -n "$_CFG_CHECKOUT_MAIN" ]; then
749
+ echo "$_CFG_CHECKOUT_MAIN"
750
+ return
751
+ fi
752
+ echo "false"
753
+ }
754
+
755
+ is_wip_named_branch() {
756
+ local branch="$1"
757
+ case "$branch" in
758
+ local/*|wip/*|tmp/*|scratch/*|throwaway/*|validation/*) return 0 ;;
759
+ *) return 1 ;;
760
+ esac
761
+ }
762
+
763
+ should_publish_new_branch() {
764
+ local branch="$1"
765
+
766
+ if [ "$NO_PUBLISH" = true ]; then
767
+ return 1
768
+ fi
769
+ if [ "$PUBLISH" = true ]; then
770
+ return 0
771
+ fi
772
+ if is_wip_named_branch "$branch"; then
773
+ return 1
774
+ fi
775
+
776
+ local auto_publish_default
777
+ auto_publish_default=$(get_sync_auto_publish_default)
778
+ if is_truthy "$auto_publish_default"; then
779
+ return 0
780
+ fi
781
+
782
+ return 1
783
+ }
784
+
785
+ # Get protect-main mode from config or environment
786
+ # Priority: 1. BUILD_PROTECT_MAIN env var 2. build config 3. default "confirm"
787
+ get_protect_main_mode() {
788
+ if [ -n "${BUILD_PROTECT_MAIN:-}" ]; then
789
+ echo "${BUILD_PROTECT_MAIN}"
790
+ return
791
+ fi
792
+ _load_sync_config
793
+ if [ -n "$_CFG_PROTECT_MAIN" ]; then
794
+ echo "$_CFG_PROTECT_MAIN"
795
+ return
796
+ fi
797
+ echo "confirm"
798
+ }
799
+
800
+ # Generate commit message from staged changes
801
+ # Format: type(scope): subject
802
+ # Types: feat, fix, chore, docs, refactor, test
803
+ # Research: based on the task-sync design recommendations
804
+ generate_commit_message() {
805
+ # If Build repo with build CLI, try intelligent task-aware generation first
806
+ # Contract: build sync message reads --cached, returns message on stdout, exits 0
807
+ # On failure, exits non-zero and we fall back to diff-based generation
808
+ if [ -d ".build-project" ] && command -v build >/dev/null 2>&1; then
809
+ local build_msg
810
+ local build_status=0
811
+ local force_flag=""
812
+ if [ "$FORCE" = true ]; then force_flag="--force"; fi
813
+ build_msg=$(build sync message $force_flag 2>/dev/null) || build_status=$?
814
+ if [ "$build_status" -eq 0 ] && [ -n "$build_msg" ]; then
815
+ echo "$build_msg"
816
+ return
817
+ fi
818
+ if [ "$build_status" -eq 2 ]; then
819
+ return 2
820
+ fi
821
+ fi
822
+
823
+ local staged_files
824
+ staged_files=$(git diff --cached --name-only 2>/dev/null)
825
+
826
+ if [ -z "$staged_files" ]; then
827
+ # Fallback to timestamp if no staged files
828
+ echo "chore: checkpoint $(date '+%Y-%m-%d %H:%M')"
829
+ return
830
+ fi
831
+
832
+ local file_count
833
+ file_count=$(echo "$staged_files" | wc -l | tr -d ' ')
834
+
835
+ # Analyze file types and changes
836
+ local has_src=false has_test=false has_config=false has_docs=false has_build=false
837
+ local src_count=0 test_count=0 doc_count=0 config_count=0
838
+ local scope="" commit_type=""
839
+
840
+ while IFS= read -r file; do
841
+ case "$file" in
842
+ src/*|lib/*|app/*|components/*|utils/*|services/*|core/*|modules/*)
843
+ has_src=true
844
+ src_count=$((src_count + 1))
845
+ # Extract scope from path (first directory after src/)
846
+ if [ -z "$scope" ]; then
847
+ scope=$(echo "$file" | sed 's|^[^/]*/||' | cut -d'/' -f1)
848
+ fi
849
+ ;;
850
+ test/*|tests/*|__tests__/*|*.test.*|*.spec.*|*_test.*|*_spec.*)
851
+ has_test=true
852
+ test_count=$((test_count + 1))
853
+ ;;
854
+ *.md|docs/*|README*|CHANGELOG*|CONTRIBUTING*)
855
+ has_docs=true
856
+ doc_count=$((doc_count + 1))
857
+ ;;
858
+ package.json|package-lock.json|*.config.js|*.config.ts|tsconfig.json|.gitignore|.npmignore)
859
+ has_config=true
860
+ config_count=$((config_count + 1))
861
+ ;;
862
+ scripts/*|build/*|Makefile|Dockerfile|*.sh)
863
+ has_build=true
864
+ ;;
865
+ esac
866
+ done <<< "$staged_files"
867
+
868
+ # Detect change type (add/modify/delete)
869
+ local primary_action="update"
870
+ local added_count deleted_count modified_count
871
+ added_count=$(git diff --cached --name-only --diff-filter=A | wc -l | tr -d ' ')
872
+ deleted_count=$(git diff --cached --name-only --diff-filter=D | wc -l | tr -d ' ')
873
+ modified_count=$(git diff --cached --name-only --diff-filter=M | wc -l | tr -d ' ')
874
+
875
+ if [ "$added_count" -gt "$deleted_count" ] && [ "$added_count" -gt "$modified_count" ]; then
876
+ primary_action="add"
877
+ elif [ "$deleted_count" -gt "$added_count" ] && [ "$deleted_count" -gt "$modified_count" ]; then
878
+ primary_action="remove"
879
+ fi
880
+
881
+ # Classify commit type using rule-based heuristics.
882
+ # Source changes take precedence over test majorities so mixed implementation
883
+ # commits do not get mislabeled as test-only work.
884
+ if [ "$has_docs" = true ] && [ "$doc_count" -ge "$((file_count * 70 / 100))" ]; then
885
+ # 70%+ docs files
886
+ commit_type="docs"
887
+ scope=""
888
+ elif [ "$has_test" = true ] && \
889
+ [ "$has_src" = false ] && \
890
+ [ "$has_build" = false ] && \
891
+ [ "$has_config" = false ] && \
892
+ [ "$test_count" -ge "$((file_count * 70 / 100))" ]; then
893
+ # 70%+ test files with no implementation/build/config changes
894
+ commit_type="test"
895
+ elif [ "$has_config" = true ] && [ "$config_count" -ge "$((file_count * 50 / 100))" ]; then
896
+ # 50%+ config files
897
+ commit_type="chore"
898
+ scope="config"
899
+ elif [ "$has_build" = true ] && [ "$file_count" -le 5 ]; then
900
+ # Build/script changes (when small batches)
901
+ commit_type="chore"
902
+ scope="build"
903
+ elif [ "$has_src" = true ]; then
904
+ # Source code changes - detect feat vs fix vs refactor
905
+ local diff_content
906
+ diff_content=$(git diff --cached 2>/dev/null)
907
+
908
+ # Heuristic: look for patterns in diff to classify feat/fix/refactor
909
+ if echo "$diff_content" | grep -qiE "^[+].*\b(bug|fix|issue|error|crash|fail)\b" || \
910
+ echo "$diff_content" | grep -qiE "^[+].*\b(correct|repair|resolve)\b"; then
911
+ commit_type="fix"
912
+ elif echo "$diff_content" | grep -qiE "^[+].*\b(refactor|rename|restructure|reorganize)\b" || \
913
+ [ "$added_count" -gt 0 ] && [ "$deleted_count" -gt 0 ] && [ "$added_count" -eq "$deleted_count" ]; then
914
+ commit_type="refactor"
915
+ elif [ "$primary_action" = "add" ] || echo "$diff_content" | grep -qE "^\+[^+].*\b(new|add|create|implement)\b"; then
916
+ commit_type="feat"
917
+ else
918
+ # Default for src changes
919
+ commit_type="feat"
920
+ fi
921
+ else
922
+ # Uncertain classification - fallback to chore
923
+ commit_type="chore"
924
+ fi
925
+
926
+ # Generate human-readable summary
927
+ local summary=""
928
+
929
+ if [ "$file_count" -eq 1 ]; then
930
+ # Single file - use filename
931
+ local filename
932
+ filename=$(basename "$(echo "$staged_files" | head -1)" | sed 's/\.[^.]*$//')
933
+ summary="${primary_action} ${filename}"
934
+ elif [ "$has_docs" = true ] && [ "$doc_count" = "$file_count" ]; then
935
+ # All docs
936
+ if [ "$file_count" -eq 1 ]; then
937
+ summary="update documentation"
938
+ else
939
+ summary="update documentation (${file_count} files)"
940
+ fi
941
+ elif [ "$has_test" = true ] && [ "$test_count" = "$file_count" ]; then
942
+ # All tests
943
+ summary="${primary_action} tests for ${scope:-components}"
944
+ elif [ "$has_config" = true ] && [ "$config_count" = "$file_count" ]; then
945
+ # All config
946
+ summary="update configuration"
947
+ else
948
+ # Mixed changes - summarize by dominant type
949
+ local parts=""
950
+ if [ "$src_count" -gt 0 ]; then
951
+ parts="source"
952
+ if [ -n "$scope" ] && [ "$src_count" -le 3 ]; then
953
+ parts="${scope}"
954
+ fi
955
+ fi
956
+ if [ "$test_count" -gt 0 ]; then
957
+ [ -n "$parts" ] && parts="${parts} + tests" || parts="tests"
958
+ fi
959
+ if [ "$doc_count" -gt 0 ]; then
960
+ [ -n "$parts" ] && parts="${parts} + docs" || parts="docs"
961
+ fi
962
+ if [ "$config_count" -gt 0 ]; then
963
+ [ -n "$parts" ] && parts="${parts} + config" || parts="config"
964
+ fi
965
+
966
+ if [ -z "$parts" ]; then
967
+ parts="${file_count} files"
968
+ fi
969
+
970
+ summary="${primary_action} ${parts}"
971
+ fi
972
+
973
+ # Build final message in conventional commit format
974
+ local message
975
+ if [ -n "$scope" ] && [ "$commit_type" != "docs" ]; then
976
+ message="${commit_type}(${scope}): ${summary}"
977
+ else
978
+ message="${commit_type}: ${summary}"
979
+ fi
980
+
981
+ # Truncate if too long (git recommends <72 chars for subject, we target 50-60)
982
+ if [ ${#message} -gt 72 ]; then
983
+ # Keep type and scope, truncate summary
984
+ local prefix
985
+ if [ -n "$scope" ] && [ "$commit_type" != "docs" ]; then
986
+ prefix="${commit_type}(${scope}): "
987
+ else
988
+ prefix="${commit_type}: "
989
+ fi
990
+ local max_summary_len=$((72 - ${#prefix} - 3)) # -3 for "..."
991
+ summary="${summary:0:$max_summary_len}..."
992
+ message="${prefix}${summary}"
993
+ fi
994
+
995
+ echo "$message"
996
+ }
997
+
998
+ staged_has_mixed_system_and_code() {
999
+ local staged_files
1000
+ staged_files=$(git diff --cached --name-only 2>/dev/null || true)
1001
+
1002
+ local has_system=false
1003
+ local has_code=false
1004
+
1005
+ while IFS= read -r file; do
1006
+ [ -z "$file" ] && continue
1007
+ case "$file" in
1008
+ task/*|sprint/*|backlog/*)
1009
+ has_system=true
1010
+ ;;
1011
+ *)
1012
+ has_code=true
1013
+ ;;
1014
+ esac
1015
+
1016
+ if [ "$has_system" = true ] && [ "$has_code" = true ]; then
1017
+ return 0
1018
+ fi
1019
+ done <<EOF
1020
+ $staged_files
1021
+ EOF
1022
+
1023
+ return 1
1024
+ }
1025
+
1026
+ # Authoritative mixed planning/code stage-set check.
1027
+ #
1028
+ # The shell heuristic above (staged_has_mixed_system_and_code) only recognizes
1029
+ # task/sprint/backlog as system paths and only fires when the shell's own
1030
+ # IS_PROTECTED (main/master/release/*) is set. The TypeScript validator
1031
+ # (validateSyncStageSet -> isSystemWritePath, src/core/utils/repo-path-classifier.ts)
1032
+ # is broader on both axes: it also treats .build-project/planning, .build-project/plans, .build-project/tmp
1033
+ # and other managed/runtime paths as system writes, and its protected-branch set
1034
+ # (isProtectedPipelineBranch) covers develop/staging/production, hotfix/*, origin/*
1035
+ # and BUILD_PROTECTED_BRANCHES. `build sync message` runs that same validator and
1036
+ # exits 2 exactly when the staged set mixes system-write and code paths on a protected
1037
+ # branch, so it is the single source of truth and needs no shell IS_PROTECTED gate.
1038
+ #
1039
+ # The auto-generated-message path already runs this validator inside
1040
+ # generate_commit_message (its exit 2 aborts under set -e). The gap this closes is
1041
+ # `build sync "explicit message"`, which skips message generation; the caller invokes
1042
+ # this only when an explicit COMMIT_MESSAGE was supplied, so bare syncs pay no extra
1043
+ # CLI call. --force is honored by the caller.
1044
+ #
1045
+ # Best-effort limitation: when `build` is not on PATH this returns non-zero and the
1046
+ # guard falls back to staged_has_mixed_system_and_code, which is narrower on both axes
1047
+ # (only task/sprint/backlog paths, only the main/master/release/* branch set). sync.sh
1048
+ # is normally invoked by `build sync`, so the CLI is present and authoritative; a
1049
+ # direct `./scripts/sync.sh "message"` invocation without build installed on a branch
1050
+ # outside the shell-protected set is the only path that stays best-effort. Fully closing
1051
+ # it would require duplicating the TS path classifier and protected-branch set in shell
1052
+ # (a drift risk), so it is intentionally left to the authoritative CLI check.
1053
+ cli_refuses_mixed_stage_set() {
1054
+ [ -d ".build-project" ] || return 1
1055
+ command -v build >/dev/null 2>&1 || return 1
1056
+ local rc=0
1057
+ build sync message >/dev/null 2>&1 || rc=$?
1058
+ [ "$rc" -eq 2 ]
1059
+ }
1060
+
1061
+ # Print "<commit> stash@{N}" for the stash this run created (matched by its
1062
+ # unique message). The stash list is shared by every worktree, so never assume
1063
+ # stash@{0}; commit id and index come from one listing so they always agree.
1064
+ own_stash_entry() {
1065
+ [ -n "$AUTO_STASH_MSG" ] || return 0
1066
+ git stash list --format='%H %gd %gs' 2>/dev/null | awk -v m="$AUTO_STASH_MSG" '$NF == m { print $1, $2; exit }'
1067
+ }
1068
+ own_stash_sha() { own_stash_entry | cut -d' ' -f1; }
1069
+ own_stash_ref() { own_stash_entry | cut -d' ' -f2; }
1070
+
1071
+ # Drop this run's stash (commit $1) only when it is the sole stash entry, with a
1072
+ # compare-and-delete of refs/stash: if another worktree stashes in the meantime
1073
+ # the ref no longer equals $1 and the delete fails without touching its entry.
1074
+ # `git stash drop` takes a mutable index, so with other entries present this run
1075
+ # never drops by index; the entry is retained (see note_retained_stash).
1076
+ drop_own_stash() {
1077
+ local sha="$1"
1078
+ [ -n "$sha" ] && [ "$(git stash list --format=%H 2>/dev/null)" = "$sha" ] || return 1
1079
+ git update-ref -d refs/stash "$sha" >/dev/null 2>&1
1080
+ }
1081
+
1082
+ # Explain a retained auto-stash entry (commit $1) and how to drop it by hand.
1083
+ note_retained_stash() {
1084
+ log_warn "Stash" "entry kept in 'git stash list' (other stashes present, not dropped by index)"
1085
+ echo " Entry: $1 ($AUTO_STASH_MSG)"
1086
+ echo " Drop it: git stash list, then git stash drop stash@{N} for that entry"
1087
+ HAS_WARNINGS=true
1088
+ }
1089
+
1090
+ # Print "rebase" or "merge" when one is in progress in this worktree, else nothing.
1091
+ git_op_in_progress() {
1092
+ if [ -d "$(git rev-parse --git-path rebase-merge)" ] || [ -d "$(git rev-parse --git-path rebase-apply)" ]; then
1093
+ echo rebase
1094
+ elif git rev-parse -q --verify MERGE_HEAD >/dev/null 2>&1; then
1095
+ echo merge
1096
+ fi
1097
+ }
1098
+
1099
+ # Stash recovery helper (DRY - used in multiple places)
1100
+ handle_stash_conflict() {
1101
+ local branch_context="$1"
1102
+ log_err "Stash" "conflict when re-applying changes"
1103
+ HAS_WARNINGS=true
1104
+
1105
+ # Auto-recovery: Create recovery branch from stash
1106
+ RECOVERY_BRANCH="recovery/stash-conflict-$(date +%s)"
1107
+ local own_sha
1108
+ own_sha=$(own_stash_sha)
1109
+ if [ -n "$own_sha" ] && git stash branch "$RECOVERY_BRANCH" "$own_sha" >/dev/null 2>&1; then
1110
+ drop_own_stash "$own_sha" || note_retained_stash "$own_sha"
1111
+ log_ok "Recovery" "$RECOVERY_BRANCH"
1112
+ echo ""
1113
+ echo "Note: You are now on $RECOVERY_BRANCH"
1114
+ echo "To merge back to $branch_context:"
1115
+ echo " 1. git checkout $branch_context"
1116
+ echo " 2. git merge $RECOVERY_BRANCH"
1117
+ echo " 3. Resolve conflicts manually"
1118
+ else
1119
+ log_err "Recovery" "failed to create branch"
1120
+ local orphan_ref
1121
+ orphan_ref=$(own_stash_ref)
1122
+ if [ -n "$orphan_ref" ]; then
1123
+ echo " Your uncommitted changes are in stash: $orphan_ref"
1124
+ echo " Inspect: git stash show -p $orphan_ref"
1125
+ echo " Restore: git stash pop $orphan_ref"
1126
+ echo " Discard: git stash drop $orphan_ref"
1127
+ else
1128
+ echo "Your changes are safe in stash. Use: git stash list"
1129
+ fi
1130
+ fi
1131
+ exit 1
1132
+ }
1133
+
1134
+ # ============================================================================
1135
+ # SHARED PULL HELPER - stash, pull/rebase, conflict recovery, unstash
1136
+ # ============================================================================
1137
+ # Encapsulates the stash → pull → conflict-abort → unstash flow used by
1138
+ # both sync mode and commit mode.
1139
+ #
1140
+ # Usage:
1141
+ # do_pull_with_stash # normal pull from upstream
1142
+ # do_pull_with_stash --rebase-onto origin/main # rebase onto ref (no upstream)
1143
+ #
1144
+ # Globals read:
1145
+ # HAS_TRACKED_CHANGES, HAS_UNTRACKED_ONLY, REBASE, CURRENT_BRANCH
1146
+ #
1147
+ # Globals written:
1148
+ # STASHED (true if changes were stashed and need external restore on early return)
1149
+ #
1150
+ # Returns:
1151
+ # 0 = success (stash restored on success path)
1152
+ # 1 = conflict (rebase/merge aborted, stash restored, caller should print
1153
+ # resolution instructions and exit)
1154
+ #
1155
+ do_pull_with_stash() {
1156
+ local rebase_onto=""
1157
+ while [ $# -gt 0 ]; do
1158
+ case "$1" in
1159
+ --rebase-onto) rebase_onto="$2"; shift 2 ;;
1160
+ *) shift ;;
1161
+ esac
1162
+ done
1163
+
1164
+ # --- Stash ---
1165
+ STASHED=false
1166
+ if [ "$HAS_TRACKED_CHANGES" = true ]; then
1167
+ ensure_backup_branch
1168
+ AUTO_STASH_MSG="cm-auto-stash-$(date +%s)-$$-${RANDOM}"
1169
+ if git stash push -u -q -m "$AUTO_STASH_MSG" && [ -n "$(own_stash_ref)" ]; then
1170
+ STASHED=true
1171
+ log_dim "Stash" "saved"
1172
+ else
1173
+ log_warn "Stash" "failed, continuing without"
1174
+ fi
1175
+ elif [ "$HAS_UNTRACKED_ONLY" = true ]; then
1176
+ log_dim "Stash" "not needed (untracked only)"
1177
+ fi
1178
+
1179
+ # --- Pull strategy ---
1180
+ local pull_strategy="--rebase=merges"
1181
+ if [ "$REBASE" = false ]; then
1182
+ pull_strategy="--merge"
1183
+ fi
1184
+
1185
+ # --- Execute pull or rebase-onto ---
1186
+ local pull_ok=false
1187
+
1188
+ if [ -n "$rebase_onto" ]; then
1189
+ # No upstream: rebase or merge onto an explicit ref (route-main path)
1190
+ if [ "$REBASE" = true ]; then
1191
+ if git rebase --rebase-merges -q "$rebase_onto" >/dev/null 2>&1; then
1192
+ pull_ok=true
1193
+ log_ok "Rebase" "onto $rebase_onto"
1194
+ else
1195
+ log_err "Rebase" "failed — $rebase_onto"
1196
+ git rebase --abort 2>/dev/null || true
1197
+ fi
1198
+ else
1199
+ if git merge -q "$rebase_onto" --no-edit >/dev/null 2>&1; then
1200
+ pull_ok=true
1201
+ log_ok "Merge" "from $rebase_onto"
1202
+ else
1203
+ log_err "Merge" "failed — $rebase_onto"
1204
+ git merge --abort 2>/dev/null || true
1205
+ fi
1206
+ fi
1207
+ elif git rev-parse --abbrev-ref --symbolic-full-name "@{u}" >/dev/null 2>&1; then
1208
+ # Normal upstream pull
1209
+ ensure_backup_branch
1210
+ if git pull $pull_strategy >/dev/null 2>&1; then
1211
+ pull_ok=true
1212
+ log_ok "Pull" "done"
1213
+ else
1214
+ log_err "Pull" "conflict detected"
1215
+ git rebase --abort 2>/dev/null || git merge --abort 2>/dev/null || true
1216
+ fi
1217
+ else
1218
+ # No upstream, no rebase-onto: nothing to pull
1219
+ pull_ok=true
1220
+ fi
1221
+
1222
+ # --- Restore stash ---
1223
+ # Apply this run's own stash by commit id (another worktree may have pushed
1224
+ # a newer entry meanwhile), then drop that entry without touching others.
1225
+ if [ "$STASHED" = true ]; then
1226
+ local own_sha
1227
+ own_sha=$(own_stash_sha)
1228
+ if [ -n "$own_sha" ] && git stash apply -q "$own_sha" >/dev/null 2>&1; then
1229
+ drop_own_stash "$own_sha" || note_retained_stash "$own_sha"
1230
+ log_ok "Stash" "restored"
1231
+ STASHED=false
1232
+ else
1233
+ if [ "$pull_ok" = true ]; then
1234
+ # Pull succeeded but stash pop conflicted
1235
+ handle_stash_conflict "$CURRENT_BRANCH"
1236
+ # handle_stash_conflict exits; control does not return here
1237
+ else
1238
+ # Pull also failed; restore stash best-effort and report below
1239
+ log_warn "Stash" "restore failed — use 'git stash list' to recover"
1240
+ fi
1241
+ fi
1242
+ fi
1243
+
1244
+ if [ "$pull_ok" = true ]; then
1245
+ return 0
1246
+ else
1247
+ return 1
1248
+ fi
1249
+ }
1250
+
1251
+ # Try to find an open PR URL for a head branch (returns empty string if none / gh missing)
1252
+ # TODO: For fork workflows, add --repo flag to scope to intended base repo
1253
+ # e.g., gh pr list --repo owner/repo --head ...
1254
+ get_open_pr_url_for_head() {
1255
+ local head_branch="$1"
1256
+
1257
+ if ! command -v gh >/dev/null 2>&1; then
1258
+ echo ""
1259
+ return
1260
+ fi
1261
+
1262
+ gh pr list \
1263
+ --head "$head_branch" \
1264
+ --state open \
1265
+ --limit 1 \
1266
+ --json url \
1267
+ --jq '.[0].url' 2>/dev/null || echo ""
1268
+ }
1269
+
1270
+ ensure_pr_for_head() {
1271
+ local head_branch="$1"
1272
+ local base_branch="$2"
1273
+ local title="$3"
1274
+ local body="$4"
1275
+
1276
+ if ! command -v gh >/dev/null 2>&1; then
1277
+ log_warn "PR" "gh cli not found"
1278
+ HAS_WARNINGS=true
1279
+ return 1
1280
+ fi
1281
+
1282
+ local existing_url
1283
+ existing_url=$(get_open_pr_url_for_head "$head_branch")
1284
+ if [ -n "$existing_url" ]; then
1285
+ log_ok "PR" "$existing_url (existing)"
1286
+ CREATED_PR_URL="$existing_url"
1287
+ return 0
1288
+ fi
1289
+
1290
+ local pr_url
1291
+ pr_url=$(gh pr create --head "$head_branch" --base "$base_branch" --title "$title" --body "$body" 2>&1) || true
1292
+ if echo "$pr_url" | grep -Eq "^https?://"; then
1293
+ log_ok "PR" "$pr_url"
1294
+ CREATED_PR_URL="$pr_url"
1295
+ return 0
1296
+ fi
1297
+
1298
+ log_warn "PR" "creation failed: $pr_url"
1299
+ HAS_WARNINGS=true
1300
+ return 1
1301
+ }
1302
+
1303
+ # After the route branch is pushed, move local base branch back to its upstream so
1304
+ # the routed commits are not also left on main. Rewinds only when every commit on the
1305
+ # local base is contained in the route branch (base is an ancestor of, or equal to,
1306
+ # routed_sha) and the upstream is an ancestor of base. Not checked out: compare-and-swap
1307
+ # update-ref against the base's current tip. Checked out: HEAD must still be that tip,
1308
+ # then one reset --keep, which refuses (changing nothing) if local changes conflict.
1309
+ rewind_base_after_route() {
1310
+ local base_branch="$1" routed_sha="$2" up_sha base_sha
1311
+ up_sha=$(git rev-parse -q --verify "$base_branch@{u}" 2>/dev/null || echo "")
1312
+ [ -n "$up_sha" ] && [ -n "$routed_sha" ] || return 0
1313
+ base_sha=$(git rev-parse -q --verify "refs/heads/$base_branch" 2>/dev/null || echo "")
1314
+ [ -n "$base_sha" ] || return 0
1315
+ [ "$up_sha" != "$base_sha" ] || return 0
1316
+ git merge-base --is-ancestor "$base_sha" "$routed_sha" 2>/dev/null || return 0
1317
+ git merge-base --is-ancestor "$up_sha" "$base_sha" 2>/dev/null || return 0
1318
+ if [ "$(git symbolic-ref -q --short HEAD 2>/dev/null)" = "$base_branch" ]; then
1319
+ if [ "$(git rev-parse HEAD 2>/dev/null)" = "$base_sha" ] \
1320
+ && git reset --keep "$up_sha" >/dev/null 2>&1; then
1321
+ log_dim "Rewind" "$base_branch to upstream (commits are on the route branch)"
1322
+ else
1323
+ log_warn "Rewind" "$base_branch left at $base_sha (changed or local changes block the reset); commits are on the route branch"
1324
+ HAS_WARNINGS=true
1325
+ fi
1326
+ elif git update-ref "refs/heads/$base_branch" "$up_sha" "$base_sha" 2>/dev/null; then
1327
+ log_dim "Rewind" "$base_branch to upstream (commits are on the route branch)"
1328
+ else
1329
+ log_warn "Rewind" "$base_branch changed concurrently; left as is"
1330
+ HAS_WARNINGS=true
1331
+ fi
1332
+ }
1333
+
1334
+ # Print the PR number from a PR URL (build ci merge takes a number, not a URL)
1335
+ pr_number_from_url() {
1336
+ echo "${1:-}" | sed -n 's|.*/pull/\([0-9][0-9]*\).*|\1|p'
1337
+ }
1338
+
1339
+ route_main_head_to_pr() {
1340
+ local base_branch="$1" # main/master
1341
+ local stay_on_main="$2" # true/false
1342
+ local short_sha
1343
+ local route_branch
1344
+ local pr_title
1345
+ local pr_body
1346
+ local routed_sha
1347
+
1348
+ short_sha=$(git rev-parse --short=7 HEAD 2>/dev/null || echo "")
1349
+ if [ -z "$short_sha" ]; then
1350
+ log_err "Route-main" "failed to resolve HEAD"
1351
+ return 1
1352
+ fi
1353
+
1354
+ route_branch="wip/route-main/$short_sha"
1355
+ routed_sha=$(git rev-parse HEAD 2>/dev/null || echo "")
1356
+
1357
+ # Create/reset local route branch at current HEAD, then checkout
1358
+ if ! git checkout -B "$route_branch" HEAD >/dev/null 2>&1; then
1359
+ log_err "Route-main" "checkout failed: $route_branch"
1360
+ return 1
1361
+ fi
1362
+ CURRENT_BRANCH="$route_branch"
1363
+
1364
+ # Publish route branch (idempotent for same HEAD)
1365
+ if ! run_git_push -u origin "$route_branch"; then
1366
+ log_err "Push" "failed: origin/$route_branch"
1367
+ log_git_push_reason
1368
+ # Do not strand the checkout on the ephemeral route branch; nothing was published,
1369
+ # so base is not rewound and the commits stay on the local route branch.
1370
+ if git checkout "$base_branch" >/dev/null 2>&1; then
1371
+ CURRENT_BRANCH="$base_branch"
1372
+ log_dim "Route-main" "commits kept on local branch $route_branch; $base_branch checked out"
1373
+ else
1374
+ log_err "Checkout" "failed: $base_branch (stranded on $route_branch)"
1375
+ HAS_WARNINGS=true
1376
+ fi
1377
+ return 1
1378
+ fi
1379
+ log_ok "Push" "origin/$route_branch"
1380
+ UPSTREAM_EXISTS=true
1381
+ UPSTREAM_BRANCH="origin/$route_branch"
1382
+
1383
+ # Ensure PR for the route branch (always ensure; routing implies intent)
1384
+ pr_title="Route ${base_branch}: $(git log -1 --pretty=%s 2>/dev/null || echo "$short_sha")"
1385
+ pr_body=$(build_sync_pr_body "route-main" "$route_branch" "$base_branch")
1386
+ local pr_rc=0
1387
+ ensure_pr_for_head "$route_branch" "$base_branch" "$pr_title" "$pr_body" || pr_rc=$?
1388
+
1389
+ # Return to base branch so agents are not stranded on an ephemeral wip/route-main/* branch.
1390
+ if ! git checkout "$base_branch" >/dev/null 2>&1; then
1391
+ log_err "Checkout" "failed: $base_branch (stranded on $route_branch)"
1392
+ HAS_WARNINGS=true
1393
+ else
1394
+ log_dim "Checkout" "$base_branch"
1395
+ CURRENT_BRANCH="$base_branch"
1396
+ rewind_base_after_route "$base_branch" "$routed_sha"
1397
+ # Fast-forward local base branch if behind origin (fetch already ran)
1398
+ local behind_count
1399
+ behind_count=$(git rev-list --count "HEAD..origin/$base_branch" 2>/dev/null || echo "0")
1400
+ if [ "$behind_count" -gt 0 ]; then
1401
+ if git merge --ff-only "origin/$base_branch" >/dev/null 2>&1; then
1402
+ log_dim "Sync" "$base_branch (ff +$behind_count)"
1403
+ else
1404
+ log_warn "Sync" "$base_branch diverged from origin (ff-only rejected)"
1405
+ HAS_WARNINGS=true
1406
+ fi
1407
+ fi
1408
+ fi
1409
+
1410
+ if [ "$pr_rc" -ne 0 ]; then
1411
+ log_err "Route-main" "incomplete: PR not created; commits are on origin/$route_branch"
1412
+ log_dim "Next" "gh pr create --head $route_branch --base $base_branch"
1413
+ return 1
1414
+ fi
1415
+ log_dim "Next" "after review: build ci merge $(pr_number_from_url "$CREATED_PR_URL") --sha $routed_sha"
1416
+
1417
+ return 0
1418
+ }
1419
+
1420
+ # Reset PR URL tracking for this sync run (prevents stale values from previous iterations)
1421
+ CREATED_PR_URL=""
1422
+
1423
+ # Status/PR-only should never mutate the repo
1424
+ if [ "$STATUS_ONLY" = true ] || [ "$PR_ONLY" = true ]; then
1425
+ DRY_RUN=true
1426
+ fi
1427
+
1428
+ # Get current branch
1429
+ CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || true)
1430
+ DETACHED_HEAD=false
1431
+ DETACHED_HEAD_SHA=""
1432
+ if [ -z "$CURRENT_BRANCH" ]; then
1433
+ if [ "$STATUS_ONLY" = true ]; then
1434
+ DETACHED_HEAD=true
1435
+ DETACHED_HEAD_SHA=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
1436
+ CURRENT_BRANCH="HEAD"
1437
+ log_dim "Branch" "detached HEAD ($DETACHED_HEAD_SHA)"
1438
+ else
1439
+ log_err "Branch" "detached HEAD"
1440
+ exit 1
1441
+ fi
1442
+ fi
1443
+
1444
+ # Create safety backup branch (before any operations)
1445
+ BACKUP_BRANCH=""
1446
+ BACKUP_BRANCH_CREATED=false
1447
+ RECOVERY_BRANCH=""
1448
+
1449
+ # Create backup branch lazily (only before first mutating operation)
1450
+ ensure_backup_branch() {
1451
+ if [ "$BACKUP_BRANCH_CREATED" = true ]; then return 0; fi
1452
+ if [ "$DRY_RUN" = true ]; then return 0; fi
1453
+ BACKUP_BRANCH="backup/pre-sync-$(date +%s)"
1454
+ if git branch "$BACKUP_BRANCH" >/dev/null 2>&1; then
1455
+ BACKUP_BRANCH_CREATED=true
1456
+ log_dim "Checkpoint" "$BACKUP_BRANCH"
1457
+ else
1458
+ log_err "Checkpoint" "failed to create backup"
1459
+ exit 1
1460
+ fi
1461
+ }
1462
+
1463
+ # Print recovery hint: use backup branch if created, otherwise point to reflog/ORIG_HEAD
1464
+ print_recovery_hint() {
1465
+ echo ""
1466
+ if [ "$BACKUP_BRANCH_CREATED" = true ]; then
1467
+ echo "Your work is safe in: $BACKUP_BRANCH"
1468
+ else
1469
+ echo "Your work is safe in git reflog (run: git reflog show HEAD)"
1470
+ fi
1471
+ echo ""
1472
+ }
1473
+
1474
+ # Retention window for pruning stale backup branches (env-only knob; not a config key).
1475
+ # unset/empty -> 7 days; off|never -> pruning disabled; integer (0 allowed) -> days,
1476
+ # where 0 disables the age gate. Anything else falls back to 7 (fail-closed default).
1477
+ get_backup_retention_days() {
1478
+ local v="${BUILD_SYNC_BACKUP_RETENTION_DAYS:-}"
1479
+ case "$v" in
1480
+ "") echo 7 ;;
1481
+ off|never) echo disabled ;;
1482
+ *)
1483
+ # Digit cap avoids arithmetic overflow; 10# forces base-10 so leading
1484
+ # zeroes ("08") cannot trigger bash octal-parse errors mid-cleanup.
1485
+ if [[ "$v" =~ ^[0-9]{1,4}$ ]]; then
1486
+ echo "$((10#$v))"
1487
+ else
1488
+ echo 7
1489
+ fi
1490
+ ;;
1491
+ esac
1492
+ }
1493
+
1494
+ # Reap backup/pre-sync-* branches orphaned by prior failed/interrupted runs.
1495
+ # Lossless-only: a branch is deleted ONLY when its content is provably preserved —
1496
+ # tip is an ancestor of HEAD, or a linear (merge-free) range whose every commit is
1497
+ # patch-equivalent to one in HEAD. Any safety-check failure retains the branch
1498
+ # (false-keep is harmless; false-delete is forbidden). Never touches the current
1499
+ # run's checkpoint, branches checked out in other worktrees, or exit/warning state.
1500
+ prune_stale_backup_branches() {
1501
+ local retention_days now cutoff pruned=0 kept=""
1502
+ retention_days=$(get_backup_retention_days)
1503
+ [ "$retention_days" = "disabled" ] && return 0
1504
+ now=$(date +%s)
1505
+ cutoff=$((now - retention_days * 86400))
1506
+
1507
+ # Fail-closed: if the worktree list cannot be read, skip pruning this run
1508
+ # (we cannot prove a candidate branch is not checked out elsewhere).
1509
+ local wt_list
1510
+ wt_list=$(git worktree list --porcelain 2>/dev/null) || return 0
1511
+
1512
+ local name epoch tip safe merges cherry_out
1513
+ while IFS= read -r name; do
1514
+ [ -n "$name" ] || continue
1515
+ [ "$name" = "${BACKUP_BRANCH:-}" ] && continue
1516
+ epoch="${name##*-}"
1517
+ [[ "$epoch" =~ ^[0-9]{1,12}$ ]] || continue
1518
+ [ "$epoch" -le "$cutoff" ] || continue
1519
+ tip=$(git rev-parse --verify --quiet "refs/heads/$name^{commit}") || continue
1520
+
1521
+ safe=false
1522
+ if git merge-base --is-ancestor "$tip" HEAD 2>/dev/null; then
1523
+ safe=true
1524
+ else
1525
+ # cherry is patch-id based and blind to merge commits: only trust it on
1526
+ # merge-free ranges, and only when it ran successfully (fail-closed).
1527
+ merges=$(git rev-list --merges "HEAD..$tip" 2>/dev/null) || merges="unknown"
1528
+ if [ -z "$merges" ]; then
1529
+ if cherry_out=$(git cherry HEAD "$tip" 2>/dev/null); then
1530
+ # here-string, not a pipeline: under pipefail a grep -q early
1531
+ # exit can SIGPIPE the writer and invert the safety verdict
1532
+ if ! grep -q '^+' <<<"$cherry_out"; then
1533
+ safe=true
1534
+ fi
1535
+ fi
1536
+ fi
1537
+ fi
1538
+ if [ "$safe" != true ]; then
1539
+ kept="$kept $name"
1540
+ continue
1541
+ fi
1542
+
1543
+ # Skip branches checked out in any worktree (shared branch namespace), then
1544
+ # compare-and-swap delete against the verified tip so a moved ref is never pruned.
1545
+ if grep -qxF "branch refs/heads/$name" <<<"$wt_list"; then
1546
+ continue
1547
+ fi
1548
+ if git update-ref -d "refs/heads/$name" "$tip" 2>/dev/null; then
1549
+ pruned=$((pruned + 1))
1550
+ fi
1551
+ done < <(git for-each-ref --format='%(refname:short)' 'refs/heads/backup/pre-sync-*' 2>/dev/null)
1552
+
1553
+ if [ "$pruned" -gt 0 ]; then
1554
+ log_dim "Cleanup" "pruned $pruned stale backup branch(es)"
1555
+ fi
1556
+ if [ -n "$kept" ]; then
1557
+ log_dim "Cleanup" "kept stale backup(s) with unique commits:$kept (inspect: git log HEAD..<name>)"
1558
+ fi
1559
+ return 0
1560
+ }
1561
+
1562
+ # Get main branch name (main or master)
1563
+ # TODO: Support exotic default branches (e.g., develop) via:
1564
+ # git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'
1565
+ # or: gh repo view --json defaultBranchRef -q .defaultBranchRef.name
1566
+ if git show-ref --verify --quiet refs/heads/main; then
1567
+ MAIN_BRANCH="main"
1568
+ elif git show-ref --verify --quiet refs/heads/master; then
1569
+ MAIN_BRANCH="master"
1570
+ else
1571
+ log_err "Branch" "no main or master found"
1572
+ exit 1
1573
+ fi
1574
+
1575
+ # Resolve route-main effective setting (flag overrides config)
1576
+ ROUTE_MAIN_EFFECTIVE=false
1577
+ if [ "$NO_ROUTE_MAIN" = true ]; then
1578
+ ROUTE_MAIN_EFFECTIVE=false
1579
+ elif [ "$ROUTE_MAIN_FORCE" = true ]; then
1580
+ ROUTE_MAIN_EFFECTIVE=true
1581
+ else
1582
+ ROUTE_MAIN_DEFAULT=$(get_route_main_default)
1583
+ if is_truthy "$ROUTE_MAIN_DEFAULT"; then
1584
+ ROUTE_MAIN_EFFECTIVE=true
1585
+ fi
1586
+ fi
1587
+
1588
+ # Backup branch is created lazily via ensure_backup_branch() before first mutating op
1589
+
1590
+ # Check for uncommitted changes
1591
+ CHANGED_FILES=$(git status --porcelain)
1592
+ HAS_CHANGES=false
1593
+ HAS_TRACKED_CHANGES=false
1594
+ HAS_UNTRACKED_ONLY=false
1595
+
1596
+ if [ -n "$CHANGED_FILES" ]; then
1597
+ HAS_CHANGES=true
1598
+
1599
+ # Distinguish tracked changes from untracked-only (??) or ignored (!!)
1600
+ # Git porcelain format: XY where X=index status, Y=worktree status
1601
+ # Blacklist approach: exclude lines starting with ? or ! (covers all tracked codes)
1602
+ TRACKED_CHANGES=$(echo "$CHANGED_FILES" | grep -Ev "^[\?!]" || true)
1603
+ if [ -n "$TRACKED_CHANGES" ]; then
1604
+ HAS_TRACKED_CHANGES=true
1605
+ else
1606
+ HAS_UNTRACKED_ONLY=true
1607
+ fi
1608
+ fi
1609
+
1610
+ # Auto-detect mode: if clean and no message, assume sync mode
1611
+ if [ "$PR_ONLY" = false ] && [ "$STATUS_ONLY" = false ] && [ "$HAS_CHANGES" = false ] && [ -z "$COMMIT_MESSAGE" ] && [ "$SYNC_MODE" = false ]; then
1612
+ log_dim "Mode" "clean — switching to sync"
1613
+ SYNC_MODE=true
1614
+ fi
1615
+
1616
+ # Status/PR-only: force non-mutating "sync-style" control flow (skip commit/push phases)
1617
+ if [ "$STATUS_ONLY" = true ] || [ "$PR_ONLY" = true ]; then
1618
+ SYNC_MODE=true
1619
+ fi
1620
+
1621
+ # ============================================================================
1622
+ # Route-main pre-commit guardrail bypass (commit on a non-protected branch)
1623
+ # ============================================================================
1624
+ # If route-main is enabled and we're on main/master in commit mode, switch to a
1625
+ # temporary route branch BEFORE the protected-branch check so we never commit on
1626
+ # main/master (avoids requiring BUILD_ALLOW_MAIN_COMMIT in non-interactive runs).
1627
+ if [ "$DRY_RUN" = false ] && [ "$SYNC_MODE" = false ] && [ "$STATUS_ONLY" = false ] && [ "$PR_ONLY" = false ]; then
1628
+ if [ "$CURRENT_BRANCH" = "$MAIN_BRANCH" ] && [ "$ROUTE_MAIN_EFFECTIVE" = true ]; then
1629
+ ensure_backup_branch
1630
+ ROUTE_MAIN_MODE_ACTIVE=true
1631
+ ROUTE_MAIN_WIP_BRANCH="wip/route-main/wip-$(date +%s)"
1632
+ if ! git checkout -B "$ROUTE_MAIN_WIP_BRANCH" >/dev/null 2>&1; then
1633
+ log_err "Route-main" "branch creation failed"
1634
+ exit 1
1635
+ fi
1636
+ CURRENT_BRANCH="$ROUTE_MAIN_WIP_BRANCH"
1637
+ fi
1638
+ fi
1639
+
1640
+ # ============================================================================
1641
+ # GUARDRAIL 1: Protected Branch Check (Soft by Default)
1642
+ # ============================================================================
1643
+ # Modes: off (no message), warn (print warning, proceed), confirm (prompt y/N), block (always block)
1644
+ # Configure: BUILD_PROTECT_MAIN=<mode> or build config set --scope user sync.protectMain <mode>
1645
+ # Default: confirm (interactive prompt when TTY, block when non-TTY unless BUILD_ALLOW_MAIN_COMMIT=1)
1646
+
1647
+ if [ "$FORCE" = false ] && [ "$NO_PROTECT_MAIN" = false ]; then
1648
+ # Define protected branches (main, master, and any branch starting with release/)
1649
+ IS_PROTECTED=false
1650
+ if [ "$CURRENT_BRANCH" = "main" ] || [ "$CURRENT_BRANCH" = "master" ]; then
1651
+ IS_PROTECTED=true
1652
+ elif [[ "$CURRENT_BRANCH" == release/* ]]; then
1653
+ IS_PROTECTED=true
1654
+ fi
1655
+
1656
+ if [ "$DRY_RUN" = false ] && [ "$IS_PROTECTED" = true ] && [ "$SYNC_MODE" = false ] && [ "$STATUS_ONLY" = false ] && [ "$PR_ONLY" = false ]; then
1657
+ PROTECT_MODE=$(get_protect_main_mode)
1658
+
1659
+ case "$PROTECT_MODE" in
1660
+ off)
1661
+ # No message, proceed silently
1662
+ ;;
1663
+ warn)
1664
+ # Print warning to stderr, proceed
1665
+ printf " ${YELLOW}%-16s %s${NC}\n" "Protect-main" "committing to '$CURRENT_BRANCH'" >&2
1666
+ ;;
1667
+ block)
1668
+ # Always block (original behavior)
1669
+ echo ""
1670
+ log_err "Blocked" "commits to '$CURRENT_BRANCH' are not allowed"
1671
+ echo ""
1672
+ echo " To change this behavior:"
1673
+ echo " build config set --scope user sync.protectMain confirm # Interactive prompt"
1674
+ echo " build config set --scope user sync.protectMain warn # Warn and proceed"
1675
+ echo " build config set --scope user sync.protectMain off # No protection"
1676
+ echo ""
1677
+ echo " Or bypass for this run:"
1678
+ echo " build sync --no-protect-main"
1679
+ echo ""
1680
+ [ "$BACKUP_BRANCH_CREATED" = true ] && git branch -D "$BACKUP_BRANCH" >/dev/null 2>&1 || true
1681
+ exit 1
1682
+ ;;
1683
+ confirm|*)
1684
+ # Default: Interactive confirm when TTY, block otherwise
1685
+ # Robust TTY check: both stdin AND stdout must be terminals
1686
+ # Note: This is intentionally strict - redirected stdin with TTY stdout
1687
+ # still goes to non-interactive path (safer default for piped input)
1688
+ if [ -t 0 ] && [ -t 1 ]; then
1689
+ # Interactive terminal - prompt user
1690
+ echo ""
1691
+ printf " ${YELLOW}%-16s %s${NC}\n" "Protect-main" "committing to '$CURRENT_BRANCH'"
1692
+ echo -n "Continue? [y/N] "
1693
+
1694
+ # Read with timeout (5s) to prevent hanging on weird stdin states
1695
+ # Empty input or timeout = "No" (safe default)
1696
+ # Note: if statement captures exit code, safe with set -e
1697
+ if read -r -n 1 -t 5 response 2>/dev/null; then
1698
+ echo "" # newline after single char input
1699
+ else
1700
+ # Timeout or read error - treat as "No"
1701
+ echo ""
1702
+ log_dim "Confirm" "timed out — treating as no"
1703
+ response="n"
1704
+ fi
1705
+
1706
+ if [[ ! "$response" =~ ^[Yy]$ ]]; then
1707
+ log_dim "Confirm" "aborted"
1708
+ [ "$BACKUP_BRANCH_CREATED" = true ] && git branch -D "$BACKUP_BRANCH" >/dev/null 2>&1 || true
1709
+ exit 1 # Exit non-zero: user declined or timeout
1710
+ fi
1711
+ else
1712
+ # Non-interactive (CI/script) - check for explicit allow
1713
+ if [ "${BUILD_ALLOW_MAIN_COMMIT:-}" = "1" ]; then
1714
+ printf " ${YELLOW}%-16s %s${NC}\n" "Protect-main" "committing to '$CURRENT_BRANCH' (BUILD_ALLOW_MAIN_COMMIT=1)" >&2
1715
+ else
1716
+ echo ""
1717
+ log_err "Blocked" "non-interactive commit to '$CURRENT_BRANCH'"
1718
+ echo ""
1719
+ echo " For CI/scripts, set:"
1720
+ echo " BUILD_ALLOW_MAIN_COMMIT=1"
1721
+ echo ""
1722
+ echo " Or change default mode:"
1723
+ echo " build config set --scope user sync.protectMain warn"
1724
+ echo ""
1725
+ [ "$BACKUP_BRANCH_CREATED" = true ] && git branch -D "$BACKUP_BRANCH" >/dev/null 2>&1 || true
1726
+ exit 1
1727
+ fi
1728
+ fi
1729
+ ;;
1730
+ esac
1731
+ fi
1732
+ fi
1733
+
1734
+ # ============================================================================
1735
+ # GUARDRAIL 2: Orphaned Branch Work Detection
1736
+ # ============================================================================
1737
+ # Warn if there are local branches with unpushed commits that might be lost
1738
+ # Scoped to: branches ahead of upstream OR commits not reachable from target ref
1739
+ # Filtered to: only branches with activity in last 7 days (reduces noise)
1740
+ # Note: Only runs when committing (HAS_CHANGES=true), not during sync-only
1741
+ # Throttle: only show orphan warning once per 24h (timestamp file under the
1742
+ # user's Build state directory, keyed by a hash of the repo root; without a
1743
+ # hash tool the warning is simply not throttled)
1744
+ if [ -n "${BUILD_HOME:-}" ]; then
1745
+ ORPHAN_BUILD_HOME="$BUILD_HOME"
1746
+ case "$ORPHAN_BUILD_HOME" in
1747
+ '~') ORPHAN_BUILD_HOME="$HOME" ;;
1748
+ '~/'*) ORPHAN_BUILD_HOME="$HOME/${ORPHAN_BUILD_HOME#\~/}" ;;
1749
+ esac
1750
+ ORPHAN_STATE_DIR="${ORPHAN_BUILD_HOME%/}/state/sync"
1751
+ elif [ -n "${XDG_STATE_HOME:-}" ]; then
1752
+ ORPHAN_STATE_DIR="${XDG_STATE_HOME%/}/build/sync"
1753
+ else
1754
+ ORPHAN_STATE_DIR="$HOME/.build/state/sync"
1755
+ fi
1756
+ ORPHAN_REPO_KEY=$(printf '%s' "$REPO_ROOT" | { shasum -a 256 2>/dev/null || sha256sum 2>/dev/null; } | cut -c1-12 || true)
1757
+ ORPHAN_THROTTLE_FILE=""
1758
+ if [ -n "$ORPHAN_REPO_KEY" ]; then
1759
+ ORPHAN_THROTTLE_FILE="$ORPHAN_STATE_DIR/orphan-warn-$ORPHAN_REPO_KEY"
1760
+ fi
1761
+ ORPHAN_THROTTLED=false
1762
+ if [ -n "$ORPHAN_THROTTLE_FILE" ] && [ -f "$ORPHAN_THROTTLE_FILE" ]; then
1763
+ LAST_WARN=$(cat "$ORPHAN_THROTTLE_FILE" 2>/dev/null || echo "0")
1764
+ NOW_TS=$(date +%s)
1765
+ if [ $((NOW_TS - LAST_WARN)) -lt 86400 ]; then
1766
+ ORPHAN_THROTTLED=true
1767
+ fi
1768
+ fi
1769
+
1770
+ if [ "$NO_ORPHAN_WARN" = false ] && [ "$ORPHAN_THROTTLED" = false ] && [ "$HAS_CHANGES" = true ] && [ "$STATUS_ONLY" = false ] && [ "$PR_ONLY" = false ]; then
1771
+ ORPHANED_WORK=""
1772
+ SEVEN_DAYS_AGO=$(( $(date +%s) - 604800 )) # 7 days = 604800 seconds (portable)
1773
+
1774
+ # Check for local branches ahead of their upstream (unpushed work)
1775
+ while IFS= read -r branch; do
1776
+ # Skip current branch and backup branches
1777
+ if [ "$branch" = "$CURRENT_BRANCH" ] || [[ "$branch" == backup/* ]]; then
1778
+ continue
1779
+ fi
1780
+
1781
+ # Filter: Only check branches with recent activity (last 7 days)
1782
+ LAST_COMMIT_TIME=$(git log -1 --format=%ct "$branch" 2>/dev/null || echo "0")
1783
+ if [ "$LAST_COMMIT_TIME" -lt "$SEVEN_DAYS_AGO" ]; then
1784
+ continue # Skip stale branches to reduce noise
1785
+ fi
1786
+
1787
+ # Check if branch has upstream
1788
+ UPSTREAM=$(git rev-parse --abbrev-ref "$branch@{u}" 2>/dev/null || echo "")
1789
+ if [ -n "$UPSTREAM" ]; then
1790
+ # Count commits ahead of upstream
1791
+ AHEAD_COUNT=$(git rev-list --count "$UPSTREAM..$branch" 2>/dev/null || echo "0")
1792
+ if [ "$AHEAD_COUNT" -gt 0 ]; then
1793
+ ORPHANED_WORK="${ORPHANED_WORK} $branch: $AHEAD_COUNT unpushed commit(s)\n"
1794
+ fi
1795
+ else
1796
+ # No upstream - check if it has commits not on current branch
1797
+ UNIQUE_COMMITS=$(git rev-list --count "$CURRENT_BRANCH..$branch" 2>/dev/null || echo "0")
1798
+ if [ "$UNIQUE_COMMITS" -gt 0 ]; then
1799
+ ORPHANED_WORK="${ORPHANED_WORK} $branch: $UNIQUE_COMMITS commit(s) not on $CURRENT_BRANCH (no upstream)\n"
1800
+ fi
1801
+ fi
1802
+ done < <(git for-each-ref --format='%(refname:short)' refs/heads/)
1803
+
1804
+ if [ -n "$ORPHANED_WORK" ]; then
1805
+ echo ""
1806
+ log_warn "Orphaned" "unpushed work on other branches (last 7 days):"
1807
+ echo -e "$ORPHANED_WORK"
1808
+ echo " Current branch: $CURRENT_BRANCH"
1809
+ echo " Suppress: build sync --no-orphan-warn"
1810
+ echo ""
1811
+
1812
+ # Don't block, just warn prominently (user can --no-orphan-warn if intentional)
1813
+ # This is a warning, not a hard block, per Codex's feedback
1814
+ HAS_WARNINGS=true
1815
+ # Throttle: don't show again for 24h
1816
+ if [ -n "$ORPHAN_THROTTLE_FILE" ]; then
1817
+ (umask 077 && mkdir -p "$ORPHAN_STATE_DIR") 2>/dev/null || true
1818
+ date +%s > "$ORPHAN_THROTTLE_FILE" 2>/dev/null || true
1819
+ fi
1820
+ fi
1821
+ fi
1822
+
1823
+ # DRY RUN HEADER
1824
+ if [ "$DRY_RUN" = true ]; then
1825
+ log_warn "Mode" "dry run — no changes will be made"
1826
+ fi
1827
+
1828
+ # STEP 0: Fetch from remote(s) (ALWAYS do this first)
1829
+ # Smart remote: fetch only the upstream's remote (usually origin) when single-remote.
1830
+ # Fall back to --all only when multiple remotes are configured.
1831
+ FETCH_REMOTE="origin"
1832
+ REMOTE_COUNT=$(git remote 2>/dev/null | wc -l | tr -d ' ')
1833
+ if [ "$REMOTE_COUNT" -gt 1 ]; then
1834
+ # Multiple remotes: check if branch's upstream points to a specific one
1835
+ UPSTREAM_REMOTE=$(git config "branch.${CURRENT_BRANCH}.remote" 2>/dev/null || echo "")
1836
+ if [ -n "$UPSTREAM_REMOTE" ]; then
1837
+ FETCH_REMOTE="$UPSTREAM_REMOTE"
1838
+ else
1839
+ FETCH_REMOTE="--all"
1840
+ fi
1841
+ fi
1842
+
1843
+ SYNC_FETCH_DONE=false
1844
+ if [ "$DRY_RUN" = false ]; then
1845
+ if [ "$FETCH_REMOTE" = "--all" ]; then
1846
+ if git fetch --all --prune >/dev/null 2>&1; then
1847
+ log_ok "Fetch" "done (all remotes)"
1848
+ SYNC_FETCH_DONE=true
1849
+ else
1850
+ log_warn "Fetch" "completed with warnings"
1851
+ fi
1852
+ else
1853
+ if git fetch "$FETCH_REMOTE" --prune >/dev/null 2>&1; then
1854
+ log_ok "Fetch" "done"
1855
+ SYNC_FETCH_DONE=true
1856
+ else
1857
+ log_warn "Fetch" "completed with warnings"
1858
+ fi
1859
+ fi
1860
+ else
1861
+ echo "Would fetch from ${FETCH_REMOTE}"
1862
+ fi
1863
+
1864
+ # STEP 1: Check upstream tracking (cache result for reuse)
1865
+ if [ "$DETACHED_HEAD" = true ]; then
1866
+ UPSTREAM_EXISTS=false
1867
+ UPSTREAM_BRANCH=""
1868
+ log_dim "Upstream" "not applicable (detached HEAD)"
1869
+ elif git rev-parse --abbrev-ref --symbolic-full-name "@{u}" >/dev/null 2>&1; then
1870
+ UPSTREAM_EXISTS=true
1871
+ UPSTREAM_BRANCH=$(git rev-parse --abbrev-ref --symbolic-full-name "@{u}")
1872
+ else
1873
+ if [ "$ROUTE_MAIN_MODE_ACTIVE" = true ]; then
1874
+ log_dim "Upstream" "not configured (route-main work branch)"
1875
+ else
1876
+ log_warn "Upstream" "not configured"
1877
+ HAS_WARNINGS=true
1878
+ fi
1879
+
1880
+ if [ "$DRY_RUN" = false ]; then
1881
+ # Use the resolved fetch remote (respects multi-remote setups)
1882
+ UPSTREAM_CANDIDATE="${FETCH_REMOTE}"
1883
+ # If FETCH_REMOTE is --all, fall back to origin for upstream detection
1884
+ [ "$UPSTREAM_CANDIDATE" = "--all" ] && UPSTREAM_CANDIDATE="origin"
1885
+
1886
+ # Check if remote branch exists using local refs (fetch already ran, no network needed)
1887
+ if git show-ref --verify --quiet "refs/remotes/${UPSTREAM_CANDIDATE}/$CURRENT_BRANCH" 2>/dev/null; then
1888
+ log_dim "Upstream" "setting to ${UPSTREAM_CANDIDATE}/$CURRENT_BRANCH"
1889
+ git branch --set-upstream-to="${UPSTREAM_CANDIDATE}/$CURRENT_BRANCH" "$CURRENT_BRANCH"
1890
+ log_ok "Upstream" "configured"
1891
+ UPSTREAM_EXISTS=true
1892
+ UPSTREAM_BRANCH="${UPSTREAM_CANDIDATE}/$CURRENT_BRANCH"
1893
+ else
1894
+ log_dim "Upstream" "remote branch not yet created"
1895
+ fi
1896
+ else
1897
+ echo "Would set upstream to ${FETCH_REMOTE}/$CURRENT_BRANCH (if exists)"
1898
+ fi
1899
+ fi
1900
+
1901
+ # Compute TRACKED_BRANCHES once (both branch and upstream columns) for reuse everywhere.
1902
+ # Format: "branch_name upstream_ref" per line (e.g., "main origin/main")
1903
+ # Includes any branch with a non-empty upstream (not limited to origin).
1904
+ TRACKED_BRANCHES=$(git for-each-ref --format='%(if)%(upstream:short)%(then)%(refname:short) %(upstream:short)%(end)' refs/heads/ | grep -v '^$' || true)
1905
+
1906
+ # SYNC MODE: Pull all branches and report status
1907
+ if [ "$SYNC_MODE" = true ]; then
1908
+ if [ "$DRY_RUN" = false ]; then
1909
+ if git rev-parse --abbrev-ref --symbolic-full-name "@{u}" >/dev/null 2>&1; then
1910
+ # Early-exit optimization: skip pull if already synced
1911
+ AHEAD=$(git rev-list --count "@{u}..HEAD" 2>/dev/null || echo "0")
1912
+ BEHIND=$(git rev-list --count "HEAD..@{u}" 2>/dev/null || echo "0")
1913
+
1914
+ if [ "$AHEAD" -eq 0 ] && [ "$BEHIND" -eq 0 ] && [ "$HAS_CHANGES" = false ] && [ "$ALL_BRANCHES" = false ]; then
1915
+ log_ok "$CURRENT_BRANCH" "up-to-date"
1916
+ else
1917
+ # Pull using shared helper (stash → pull → conflict-abort → unstash)
1918
+ if do_pull_with_stash; then
1919
+ log_ok "Pull" "$CURRENT_BRANCH"
1920
+
1921
+ # Bidirectional sync: push if local is ahead
1922
+ AHEAD_AFTER_PULL=$(git rev-list --count "@{u}..HEAD" 2>/dev/null || echo "0")
1923
+ if [ "$AHEAD_AFTER_PULL" -gt 0 ]; then
1924
+ # --pr on main means "never push main directly": route via a PR branch
1925
+ if [ "$CURRENT_BRANCH" = "$MAIN_BRANCH" ] && [ "$NO_SYNC_PUSH" = false ] && { [ "$ROUTE_MAIN_EFFECTIVE" = true ] || [ "$CREATE_PR" = true ]; }; then
1926
+ ROUTE_MAIN_NEEDED=true
1927
+ ROUTE_MAIN_AHEAD_COUNT="$AHEAD_AFTER_PULL"
1928
+ log_dim "Route-main" "$AHEAD_AFTER_PULL commit(s) ahead — routing via PR"
1929
+ elif [ "$NO_SYNC_PUSH" = false ]; then
1930
+ if run_git_push; then
1931
+ log_ok "Push" "$UPSTREAM_BRANCH"
1932
+ PUSH_SUCCESS=true
1933
+ else
1934
+ log_warn "Push" "failed — push manually"
1935
+ log_git_push_reason
1936
+ HAS_WARNINGS=true
1937
+ fi
1938
+ else
1939
+ log_dim "Push" "skipped ($AHEAD_AFTER_PULL ahead, sync push disabled)"
1940
+ fi
1941
+ fi
1942
+ else
1943
+ SYNC_SUCCESS=false
1944
+ print_recovery_hint
1945
+ echo "To resolve manually:"
1946
+ echo " 1. git pull --rebase (or --merge)"
1947
+ echo " 2. Resolve conflicts in affected files"
1948
+ echo " 3. git add <resolved-files>"
1949
+ echo " 4. git rebase --continue (or git commit)"
1950
+ echo " 5. Run build sync again to push"
1951
+ exit 1
1952
+ fi
1953
+ fi
1954
+ else
1955
+ # No upstream: optionally auto-publish (push -u) to establish tracking
1956
+ if [ "$NO_SYNC_PUSH" = true ]; then
1957
+ log_dim "Pull" "no upstream (--no-sync-push)"
1958
+ else
1959
+ MAIN_REMOTE_REF=""
1960
+ if git show-ref --verify --quiet "refs/remotes/origin/$MAIN_BRANCH"; then
1961
+ MAIN_REMOTE_REF="origin/$MAIN_BRANCH"
1962
+ elif git show-ref --verify --quiet "refs/remotes/origin/master"; then
1963
+ MAIN_REMOTE_REF="origin/master"
1964
+ fi
1965
+ if [ -z "$MAIN_REMOTE_REF" ]; then
1966
+ MAIN_REMOTE_REF="$MAIN_BRANCH"
1967
+ fi
1968
+
1969
+ UNIQUE_TO_MAIN=$(git rev-list --count "$MAIN_REMOTE_REF..HEAD" 2>/dev/null || echo "0")
1970
+
1971
+ if [ "$UNIQUE_TO_MAIN" -gt 0 ] && should_publish_new_branch "$CURRENT_BRANCH"; then
1972
+ if run_git_push -u origin "$CURRENT_BRANCH"; then
1973
+ log_ok "Push" "origin/$CURRENT_BRANCH (set upstream)"
1974
+ PUSH_SUCCESS=true
1975
+ UPSTREAM_EXISTS=true
1976
+ UPSTREAM_BRANCH="origin/$CURRENT_BRANCH"
1977
+ else
1978
+ log_warn "Push" "auto-publish failed"
1979
+ log_git_push_reason
1980
+ HAS_WARNINGS=true
1981
+ fi
1982
+ elif [ "$UNIQUE_TO_MAIN" -eq 0 ]; then
1983
+ log_dim "Push" "skipped (no commits beyond $MAIN_REMOTE_REF)"
1984
+ else
1985
+ log_dim "Push" "publishing disabled (use --publish)"
1986
+ fi
1987
+ fi
1988
+ fi
1989
+
1990
+ # Sync all branches if requested
1991
+ if [ "$ALL_BRANCHES" = true ]; then
1992
+ # Keep strategy local to this block (PULL_STRATEGY is not a global).
1993
+ pull_strategy_all="--rebase=merges"
1994
+ if [ "$REBASE" = false ]; then
1995
+ pull_strategy_all="--merge"
1996
+ fi
1997
+
1998
+ # Iterate branch names from TRACKED_BRANCHES (computed after fetch)
1999
+ # Process substitution keeps the loop in this shell so HAS_WARNINGS survives.
2000
+ while IFS= read -r branch; do
2001
+ [ -z "$branch" ] && continue
2002
+ if [ "$branch" != "$CURRENT_BRANCH" ]; then
2003
+ if git checkout -q "$branch" 2>/dev/null; then
2004
+ op_before=$(git_op_in_progress)
2005
+ if ! git pull -q "$pull_strategy_all" >/dev/null 2>&1; then
2006
+ # Undo only a rebase/merge this pull started, so the loop can move on
2007
+ if [ -z "$op_before" ]; then
2008
+ case "$(git_op_in_progress)" in
2009
+ rebase) git rebase --abort >/dev/null 2>&1 || true ;;
2010
+ merge) git merge --abort >/dev/null 2>&1 || true ;;
2011
+ esac
2012
+ fi
2013
+ log_warn "$branch" "pull failed — skipped"
2014
+ HAS_WARNINGS=true
2015
+ else
2016
+ local_ahead=$(git rev-list --count "@{u}..HEAD" 2>/dev/null || echo "0")
2017
+ if [ "$local_ahead" -gt 0 ]; then
2018
+ if [ "$PUSH_ALL_BRANCHES" = true ] && [ "$NO_SYNC_PUSH" = false ]; then
2019
+ if run_git_push -q; then
2020
+ log_dim "$branch" "pushed $local_ahead commit(s)"
2021
+ else
2022
+ log_warn "$branch" "push failed ($local_ahead ahead)"
2023
+ log_git_push_reason
2024
+ HAS_WARNINGS=true
2025
+ fi
2026
+ else
2027
+ log_dim "$branch" "$local_ahead commit(s) ahead"
2028
+ fi
2029
+ fi
2030
+ fi
2031
+ fi
2032
+ fi
2033
+ done < <(echo "$TRACKED_BRANCHES" | awk '{print $1}')
2034
+
2035
+ # Return to original branch
2036
+ if ! git checkout -q "$CURRENT_BRANCH" 2>/dev/null; then
2037
+ log_err "Sync" "could not return to $CURRENT_BRANCH (now on $(git rev-parse --abbrev-ref HEAD 2>/dev/null))"
2038
+ echo " Check 'git status' for an in-progress rebase or merge"
2039
+ exit 1
2040
+ fi
2041
+ log_ok "Sync" "all branches done"
2042
+ fi
2043
+
2044
+ # Route main only after sync operations complete (keeps stash/branch handling coherent)
2045
+ if [ "$ROUTE_MAIN_NEEDED" = true ]; then
2046
+ if route_main_head_to_pr "$MAIN_BRANCH" "$STAY_ON_MAIN"; then
2047
+ PUSH_SUCCESS=true
2048
+ else
2049
+ log_err "Route-main" "failed"
2050
+ exit 1
2051
+ fi
2052
+ fi
2053
+ else
2054
+ echo "Would pull latest changes using $([ "$REBASE" = true ] && echo "rebase" || echo "merge")"
2055
+ if [ "$ALL_BRANCHES" = true ]; then
2056
+ echo "Would sync all tracked branches"
2057
+ fi
2058
+ fi
2059
+
2060
+ # Skip to status report
2061
+ SKIP_COMMIT=true
2062
+ else
2063
+ SKIP_COMMIT=false
2064
+ fi
2065
+
2066
+ # STEP 2: Pull current branch BEFORE committing (critical for multi-env)
2067
+ if [ "$SKIP_COMMIT" = false ] && [ "$DRY_RUN" = false ]; then
2068
+ if [ "$ROUTE_MAIN_MODE_ACTIVE" = true ]; then
2069
+ # Route-main pre-commit branch has no upstream; rebase/merge onto origin/main
2070
+ PULL_RC=0; do_pull_with_stash --rebase-onto "origin/$MAIN_BRANCH" || PULL_RC=$?
2071
+ else
2072
+ PULL_RC=0; do_pull_with_stash || PULL_RC=$?
2073
+ fi
2074
+
2075
+ if [ "$PULL_RC" -ne 0 ]; then
2076
+ echo ""
2077
+ echo "Please resolve conflicts manually and run build sync again"
2078
+ exit 1
2079
+ fi
2080
+ fi
2081
+
2082
+ # STEP 3: Stage and Commit (if not sync mode)
2083
+ if [ "$SKIP_COMMIT" = false ]; then
2084
+ if [ "$HAS_CHANGES" = false ]; then
2085
+ log_dim "Commit" "nothing to commit"
2086
+ else
2087
+ if [ "$DRY_RUN" = false ]; then
2088
+ # Stage all changes FIRST (so pre-commit checks see what we're about to commit)
2089
+ git add -A
2090
+ STAGED_COUNT=$(git diff --cached --numstat | wc -l | tr -d ' ')
2091
+ log_ok "Staged" "$STAGED_COUNT files"
2092
+
2093
+ # ============================================================================
2094
+ # GUARDRAIL 3: Staged Changes Preview
2095
+ # ============================================================================
2096
+ # Show what's about to be committed for verification
2097
+ if [ "$FORCE" = false ] && [ "$STAGED_COUNT" -gt 10 ]; then
2098
+ echo ""
2099
+ log_warn "Staging" "$STAGED_COUNT files — review below"
2100
+ echo ""
2101
+ # Show summary by directory
2102
+ git diff --cached --stat | tail -20
2103
+ echo ""
2104
+ # Show file types being changed
2105
+ printf " ${DIM}%-16s${NC}\n" "File types:"
2106
+ git diff --cached --name-only | sed 's/.*\.//' | sort | uniq -c | sort -rn | head -5
2107
+ echo ""
2108
+ echo " Verify this matches your intended changes."
2109
+ echo " To abort: Ctrl+C then 'git reset HEAD'"
2110
+ echo " To skip: build sync --force"
2111
+ echo ""
2112
+ # Brief pause for human review (non-blocking in CI)
2113
+ if [ -t 0 ]; then
2114
+ sleep 2
2115
+ fi
2116
+ fi
2117
+
2118
+ # Pre-commit validation
2119
+ log_dim "Pre-commit" "checking..."
2120
+
2121
+ # Check for sensitive filenames (most reliable check)
2122
+ SENSITIVE_FILES=$(git diff --cached --name-only | grep -E '\.(env|key|pem|p12|pfx|credentials|secret)$|^\.env' || true)
2123
+ if [ -n "$SENSITIVE_FILES" ]; then
2124
+ log_err "Blocked" "sensitive files detected"
2125
+ echo "$SENSITIVE_FILES"
2126
+ echo ""
2127
+ echo "To remove: git reset HEAD <file>"
2128
+ exit 1
2129
+ fi
2130
+
2131
+ # Check for actual secret values (pattern: key="value" or key: "value")
2132
+ # Skip known-safe paths (test fixtures, benchmarks, coverage, etc.)
2133
+ SKIP_SECRET_CHECK='\.build-project/|test-results|benchmarks|coverage|\.test\.|__tests__|fixtures|\.snap$'
2134
+ FILES_TO_CHECK=$(git diff --cached --name-only | grep -vE "$SKIP_SECRET_CHECK" || true)
2135
+
2136
+ SECRET_VALUE_PATTERNS=(
2137
+ '["\x27][a-zA-Z0-9_-]{20,}["\x27].*?(api[_-]?key|password|token|secret)'
2138
+ '(api[_-]?key|password|token|secret)["\x27]?\s*[:=]\s*["\x27][^"\x27]{8,}'
2139
+ )
2140
+
2141
+ SECRETS_FOUND=false
2142
+ FLAGGED_FILES=""
2143
+ if [ -n "$FILES_TO_CHECK" ]; then
2144
+ for pattern in "${SECRET_VALUE_PATTERNS[@]}"; do
2145
+ # Only check non-allowlisted files
2146
+ MATCHES=$(git diff --cached -- $FILES_TO_CHECK 2>/dev/null | grep -iE "$pattern" || true)
2147
+ if [ -n "$MATCHES" ]; then
2148
+ FLAGGED_FILES=$(git diff --cached --name-only -- $FILES_TO_CHECK | head -10)
2149
+ SECRETS_FOUND=true
2150
+ break
2151
+ fi
2152
+ done
2153
+ fi
2154
+
2155
+ if [ "$SECRETS_FOUND" = true ]; then
2156
+ # WARN instead of BLOCK - let AI/human review
2157
+ log_warn "Secrets" "possible secrets detected in:"
2158
+ echo "$FLAGGED_FILES"
2159
+ echo ""
2160
+ echo "Review these files: git diff --cached <file>"
2161
+ echo "To unstage if needed: git reset HEAD <file>"
2162
+ echo ""
2163
+ # Continue - don't exit (was: exit 1)
2164
+ HAS_WARNINGS=true
2165
+ fi
2166
+
2167
+ # Check for merge conflict markers
2168
+ if git diff --cached | grep -q "^<<<<<<< \|^======= \|^>>>>>>> "; then
2169
+ log_err "Blocked" "merge conflict markers in staged files"
2170
+ echo "Please resolve conflicts before committing"
2171
+ exit 1
2172
+ fi
2173
+
2174
+ # Check for large files (>10MB) - skip interactive prompt for automated workflows
2175
+ LARGE_FILES=$(git diff --cached --name-only | xargs -I {} find {} -type f -size +10M 2>/dev/null || true)
2176
+ if [ -n "$LARGE_FILES" ]; then
2177
+ log_warn "Large files" "detected (>10MB):"
2178
+ echo "$LARGE_FILES"
2179
+ echo ""
2180
+ echo "Consider using Git LFS for large files."
2181
+ echo "To exclude: git reset HEAD <file>"
2182
+ # Don't block - just warn (for automated end-of-day workflow)
2183
+ fi
2184
+
2185
+ if [ "$FORCE" = false ] && { { [ -n "$COMMIT_MESSAGE" ] && cli_refuses_mixed_stage_set; } || { [ "$IS_PROTECTED" = true ] && staged_has_mixed_system_and_code; }; }; then
2186
+ echo ""
2187
+ log_err "Blocked" "mixed planning metadata and code changes staged on protected branch '$CURRENT_BRANCH'"
2188
+ echo ""
2189
+ echo " This staged set would create a misleading combined sync commit."
2190
+ echo " Move the work onto a feature/run branch, or split the staged changes first."
2191
+ echo ""
2192
+ echo " Recommended:"
2193
+ echo " git reset HEAD task/ sprint/ backlog/"
2194
+ echo " git checkout -b <run-branch>"
2195
+ echo " build sync"
2196
+ echo ""
2197
+ exit 1
2198
+ fi
2199
+
2200
+ GENERATED_SYNC_ARTIFACTS=$(get_staged_generated_artifacts)
2201
+ if [ -n "$GENERATED_SYNC_ARTIFACTS" ]; then
2202
+ generated_count=$(echo "$GENERATED_SYNC_ARTIFACTS" | wc -l | tr -d ' ')
2203
+ log_dim "Generated" "$generated_count generated artifact(s) staged"
2204
+ if [ "$ROUTE_MAIN_MODE_ACTIVE" = true ]; then
2205
+ log_dim "Route-main" "generated artifacts will be routed via PR"
2206
+ fi
2207
+ if [ "$VERBOSE" = true ]; then
2208
+ echo "$GENERATED_SYNC_ARTIFACTS" | while IFS= read -r generated_file; do
2209
+ [ -n "$generated_file" ] || continue
2210
+ log_dim "Generated file" "$generated_file"
2211
+ done
2212
+ fi
2213
+ fi
2214
+
2215
+ # Generate or use commit message
2216
+ if [ -z "$COMMIT_MESSAGE" ]; then
2217
+ # Auto-generate from staged changes
2218
+ COMMIT_MESSAGE=$(generate_commit_message)
2219
+
2220
+ if [ "$VERBOSE" = true ]; then
2221
+ log_dim "Message" "$COMMIT_MESSAGE"
2222
+ fi
2223
+ fi
2224
+
2225
+ # Commit
2226
+ if git commit -m "$COMMIT_MESSAGE" >/dev/null 2>&1; then
2227
+ CREATED_COMMIT_SHA=$(git rev-parse HEAD)
2228
+ CREATED_COMMIT_PARENT_SHA=$(git rev-parse "${CREATED_COMMIT_SHA}^")
2229
+ log_ok "Commit" "$COMMIT_MESSAGE"
2230
+ else
2231
+ log_err "Commit" "failed"
2232
+ HAS_FAILURE=true
2233
+ exit 1
2234
+ fi
2235
+ else
2236
+ echo ""
2237
+ echo "Would stage and commit:"
2238
+ git status --short
2239
+ echo ""
2240
+ if [ -n "$COMMIT_MESSAGE" ]; then
2241
+ echo "Commit message: \"$COMMIT_MESSAGE\""
2242
+ else
2243
+ echo "Commit message: (auto-generated)"
2244
+ fi
2245
+ fi
2246
+ fi
2247
+ fi
2248
+
2249
+ # STEP 4: Merge from main (if not on main)
2250
+ if [ "$CURRENT_BRANCH" != "$MAIN_BRANCH" ] && [ "$DRY_RUN" = false ] && [ "$SKIP_COMMIT" = false ]; then
2251
+ # Rebasing published commits onto a newer main rewrites them; the push is then
2252
+ # rejected and recovery would replay them onto the old upstream. Skip instead.
2253
+ SKIP_MAIN_REBASE=false
2254
+ if [ "$REBASE" = true ] && git rev-parse -q --verify "@{u}" >/dev/null 2>&1 \
2255
+ && ! git merge-base --is-ancestor "@{u}" "origin/$MAIN_BRANCH" 2>/dev/null \
2256
+ && ! git merge-base --is-ancestor "origin/$MAIN_BRANCH" "@{u}" 2>/dev/null; then
2257
+ SKIP_MAIN_REBASE=true
2258
+ log_dim "Rebase" "onto $MAIN_BRANCH skipped (branch already pushed; use --merge to integrate)"
2259
+ fi
2260
+
2261
+ # Use rebase or merge based on flag
2262
+ if [ "$SKIP_MAIN_REBASE" = true ]; then
2263
+ :
2264
+ elif [ "$REBASE" = true ]; then
2265
+ if git rebase --rebase-merges "origin/$MAIN_BRANCH" >/dev/null 2>&1; then
2266
+ log_ok "Rebase" "onto $MAIN_BRANCH"
2267
+ else
2268
+ if git diff --quiet HEAD "origin/$MAIN_BRANCH"; then
2269
+ git rebase --abort 2>/dev/null || true
2270
+ log_dim "Rebase" "up-to-date with $MAIN_BRANCH"
2271
+ else
2272
+ log_err "Rebase" "conflict detected"
2273
+ git rebase --abort 2>/dev/null || true
2274
+ print_recovery_hint
2275
+ echo "Conflicted files:"
2276
+ git diff --name-only --diff-filter=U 2>/dev/null || echo "(run git status to see conflicts)"
2277
+ echo ""
2278
+ echo "To resolve manually:"
2279
+ echo " 1. git rebase origin/$MAIN_BRANCH"
2280
+ echo " 2. Edit conflicted files"
2281
+ echo " 3. git add <resolved-files>"
2282
+ echo " 4. git rebase --continue"
2283
+ echo " 5. Run build sync again to push"
2284
+ exit 1
2285
+ fi
2286
+ fi
2287
+ else
2288
+ if git merge "origin/$MAIN_BRANCH" --no-edit >/dev/null 2>&1; then
2289
+ log_ok "Merge" "from $MAIN_BRANCH"
2290
+ else
2291
+ if git diff --quiet HEAD "origin/$MAIN_BRANCH"; then
2292
+ log_dim "Merge" "up-to-date with $MAIN_BRANCH"
2293
+ else
2294
+ log_err "Merge" "conflict detected"
2295
+ print_recovery_hint
2296
+ echo "Conflicted files:"
2297
+ git diff --name-only --diff-filter=U
2298
+ echo ""
2299
+ echo "To resolve manually:"
2300
+ echo " 1. Edit conflicted files"
2301
+ echo " 2. git add <resolved-files>"
2302
+ echo " 3. git commit"
2303
+ echo " 4. Run build sync again to push"
2304
+ exit 1
2305
+ fi
2306
+ fi
2307
+ fi
2308
+ fi
2309
+
2310
+ # STEP 5: Push to remote or create PR
2311
+ if [ "$DRY_RUN" = false ] && [ "$SKIP_COMMIT" = false ]; then
2312
+
2313
+ # Route-main: if enabled for a main/master commit, route via deterministic PR branch (no direct push to origin/main)
2314
+ if [ "$ROUTE_MAIN_MODE_ACTIVE" = true ] || { [ "$CURRENT_BRANCH" = "$MAIN_BRANCH" ] && [ "$ROUTE_MAIN_EFFECTIVE" = true ]; }; then
2315
+ if route_main_head_to_pr "$MAIN_BRANCH" "$STAY_ON_MAIN"; then
2316
+ PUSH_SUCCESS=true
2317
+ # Route-main already ensured the PR for the route branch, then checked
2318
+ # back out to main. A standalone --ensure-pr (ENSURE_PR=true) would now
2319
+ # re-trigger PR creation in Step 5b while on main and hard-fail with
2320
+ # "cannot create from main". Route-main subsumes ensure-pr, so mark it
2321
+ # satisfied here. (--route-main --ensure-pr must be a no-op overlap, not a trap.)
2322
+ ENSURE_PR=false
2323
+ # Best-effort cleanup: remove temporary WIP branch used to avoid committing on main.
2324
+ if [ -n "${ROUTE_MAIN_WIP_BRANCH:-}" ]; then
2325
+ git branch -D "$ROUTE_MAIN_WIP_BRANCH" >/dev/null 2>&1 || true
2326
+ ROUTE_MAIN_WIP_BRANCH=""
2327
+ fi
2328
+ else
2329
+ log_err "Route-main" "failed"
2330
+ exit 1
2331
+ fi
2332
+ else
2333
+ # If --pr is used on a non-main branch, treat it as an intent to ensure a PR for this branch
2334
+ if [ "$CREATE_PR" = true ] && [ "$CURRENT_BRANCH" != "$MAIN_BRANCH" ]; then
2335
+ ENSURE_PR=true
2336
+ fi
2337
+
2338
+ # Determine if we should create a PR
2339
+ SHOULD_CREATE_PR=false
2340
+ if [ "$CREATE_PR" = true ] && [ "$CURRENT_BRANCH" = "$MAIN_BRANCH" ]; then
2341
+ SHOULD_CREATE_PR=true
2342
+ fi
2343
+
2344
+ if [ "$SHOULD_CREATE_PR" = true ]; then
2345
+ if [ -z "$CREATED_COMMIT_SHA" ] || [ "$(git rev-parse HEAD)" != "$CREATED_COMMIT_SHA" ]; then
2346
+ log_err "PR" "no sync-owned commit available to route"
2347
+ exit 1
2348
+ fi
2349
+
2350
+ # Generate branch name from commit message or timestamp
2351
+ PR_BRANCH_NAME=""
2352
+ if [ -n "$COMMIT_MESSAGE" ]; then
2353
+ # Convert message to branch-safe name
2354
+ 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)"
2355
+ else
2356
+ PR_BRANCH_NAME="pr/changes-$(date +%Y%m%d-%H%M%S)"
2357
+ fi
2358
+
2359
+ # Use only the commit created by this invocation. The original branch
2360
+ # is restored with a compare-and-swap below so foreign ref updates can
2361
+ # never be rewound by this cleanup.
2362
+ COMMIT_SHA="$CREATED_COMMIT_SHA"
2363
+
2364
+ # Create and checkout PR branch from current position
2365
+ if git checkout -b "$PR_BRANCH_NAME" >/dev/null 2>&1; then
2366
+ log_ok "Branch" "$PR_BRANCH_NAME"
2367
+ else
2368
+ log_err "Branch" "creation failed"
2369
+ exit 1
2370
+ fi
2371
+
2372
+ # Remove our temporary commit from the original branch before the
2373
+ # network/PR window. update-ref's old-value check makes this rollback
2374
+ # fail closed if another process has already advanced the branch.
2375
+ if git update-ref "refs/heads/$CURRENT_BRANCH" "$CREATED_COMMIT_PARENT_SHA" "$COMMIT_SHA"; then
2376
+ log_dim "Reset" "$CURRENT_BRANCH (commit on PR branch)"
2377
+ else
2378
+ log_err "Reset" "$CURRENT_BRANCH changed concurrently; preserved $PR_BRANCH_NAME"
2379
+ git checkout "$CURRENT_BRANCH" >/dev/null 2>&1 || true
2380
+ exit 1
2381
+ fi
2382
+
2383
+ # Push PR branch
2384
+ if run_git_push -u origin "$PR_BRANCH_NAME"; then
2385
+ log_ok "Push" "origin/$PR_BRANCH_NAME"
2386
+ else
2387
+ log_err "Push" "failed: $PR_BRANCH_NAME"
2388
+ log_git_push_reason
2389
+ git checkout "$CURRENT_BRANCH" >/dev/null 2>&1
2390
+ log_dim "Recovery" "commit preserved on $PR_BRANCH_NAME"
2391
+ exit 1
2392
+ fi
2393
+
2394
+ # Create PR using gh cli
2395
+ PR_TITLE="${COMMIT_MESSAGE:-$(git log -1 --pretty=%s 2>/dev/null || echo "Sync changes")}"
2396
+ PR_BODY=$(build_sync_pr_body "pr" "$PR_BRANCH_NAME" "$CURRENT_BRANCH")
2397
+
2398
+ if command -v gh >/dev/null 2>&1; then
2399
+ PR_URL=$(gh pr create --title "$PR_TITLE" --body "$PR_BODY" --base "$MAIN_BRANCH" 2>&1)
2400
+ if [ $? -eq 0 ]; then
2401
+ log_ok "PR" "$PR_URL"
2402
+ CREATED_PR_URL="$PR_URL"
2403
+ PUSH_SUCCESS=true
2404
+
2405
+ echo ""
2406
+ log_dim "CI" "Quality Gate will run automatically"
2407
+ log_dim "Merge" "after review: build ci merge $(pr_number_from_url "$PR_URL") --sha $COMMIT_SHA"
2408
+ else
2409
+ log_warn "PR" "creation failed: $PR_URL"
2410
+ log_dim "PR" "branch pushed — create PR manually"
2411
+ PUSH_SUCCESS=true
2412
+ fi
2413
+ else
2414
+ log_warn "PR" "gh cli not found"
2415
+ echo " Branch: $PR_BRANCH_NAME"
2416
+ echo " URL: https://github.com/$(git remote get-url origin | sed 's/.*github.com[:/]//' | sed 's/.git$//')/pull/new/$PR_BRANCH_NAME"
2417
+ PUSH_SUCCESS=true
2418
+ fi
2419
+
2420
+ # Return to original branch
2421
+ git checkout "$CURRENT_BRANCH" >/dev/null 2>&1
2422
+
2423
+ else
2424
+ # Original direct push logic
2425
+
2426
+ # Use cached UPSTREAM_EXISTS instead of rechecking
2427
+ if [ "$UPSTREAM_EXISTS" = false ]; then
2428
+ if ! should_publish_new_branch "$CURRENT_BRANCH"; then
2429
+ log_err "Push" "no upstream — refusing to publish"
2430
+ echo ""
2431
+ if is_wip_named_branch "$CURRENT_BRANCH"; then
2432
+ echo "Note: '$CURRENT_BRANCH' looks like a local-only/WIP branch (local/*, wip/*, tmp/*, validation/*, etc.)"
2433
+ echo ""
2434
+ fi
2435
+ echo "To publish and set upstream (force):"
2436
+ echo " build sync --publish"
2437
+ echo ""
2438
+ echo "To enable auto-publish by default:"
2439
+ echo " build config set --scope user sync.autoPublish true"
2440
+ echo ""
2441
+ echo "Or manually:"
2442
+ echo " git push -u origin $CURRENT_BRANCH"
2443
+ exit 1
2444
+ fi
2445
+
2446
+ if run_git_push -u origin "$CURRENT_BRANCH"; then
2447
+ log_ok "Push" "origin/$CURRENT_BRANCH (set upstream)"
2448
+ PUSH_SUCCESS=true
2449
+ UPSTREAM_EXISTS=true
2450
+ UPSTREAM_BRANCH="origin/$CURRENT_BRANCH"
2451
+ else
2452
+ log_err "Push" "failed (new branch)"
2453
+ log_git_push_reason
2454
+ PUSH_SUCCESS=false
2455
+ HAS_FAILURE=true
2456
+ exit 1
2457
+ fi
2458
+ else
2459
+ # Try push, with automatic retry on rejection
2460
+ if run_git_push; then
2461
+ log_ok "Push" "$UPSTREAM_BRANCH"
2462
+ PUSH_SUCCESS=true
2463
+ else
2464
+ log_warn "Push" "rejected — remote may have changed"
2465
+ log_git_push_reason
2466
+ log_dim "Push" "retrying after rebase..."
2467
+
2468
+ # Auto-recover: pull, rebase, retry push
2469
+ if git pull --rebase=merges >/dev/null 2>&1; then
2470
+ log_ok "Rebase" "onto remote changes"
2471
+
2472
+ if run_git_push; then
2473
+ log_ok "Push" "$UPSTREAM_BRANCH (after rebase)"
2474
+ PUSH_SUCCESS=true
2475
+ else
2476
+ log_err "Push" "still rejected after rebase"
2477
+ log_git_push_reason
2478
+ PUSH_SUCCESS=false
2479
+ HAS_FAILURE=true
2480
+
2481
+ echo ""
2482
+ print_recovery_hint
2483
+ echo "Push failed even after rebase. Possible causes:"
2484
+ echo " - Force push from another location"
2485
+ echo " - Protected branch rules"
2486
+ echo " - Permission issues"
2487
+ echo ""
2488
+ echo "To investigate:"
2489
+ echo " 1. git log --oneline -3 (see your commits)"
2490
+ echo " 2. git status (see current state)"
2491
+ echo " 3. Check branch protection rules"
2492
+ exit 1
2493
+ fi
2494
+ else
2495
+ log_err "Pull" "failed during recovery"
2496
+ PUSH_SUCCESS=false
2497
+ HAS_FAILURE=true
2498
+
2499
+ print_recovery_hint
2500
+ echo "Recovery pull failed. Resolve manually:"
2501
+ echo " 1. git pull --rebase (resolve any conflicts)"
2502
+ echo " 2. git push"
2503
+ exit 1
2504
+ fi
2505
+ fi
2506
+ fi
2507
+ fi
2508
+ fi
2509
+ fi
2510
+
2511
+ # STEP 5b: PR status / ensure (show-only by default)
2512
+ if [ "$SYNC_ONLY" = false ]; then
2513
+ # Avoid PR creation directly from main/master (use route-main instead)
2514
+ if [ "$CURRENT_BRANCH" = "$MAIN_BRANCH" ]; then
2515
+ if [ "$ENSURE_PR" = true ]; then
2516
+ log_err "PR" "cannot create from '$MAIN_BRANCH' — use --route-main"
2517
+ exit 1
2518
+ fi
2519
+ else
2520
+ EXISTING_PR_URL=$(get_open_pr_url_for_head "$CURRENT_BRANCH")
2521
+ if [ -n "$EXISTING_PR_URL" ]; then
2522
+ log_dim "PR" "$EXISTING_PR_URL"
2523
+ CREATED_PR_URL="$EXISTING_PR_URL"
2524
+ elif [ "$ENSURE_PR" = true ]; then
2525
+ PR_TITLE="${COMMIT_MESSAGE:-$(git log -1 --pretty=%s 2>/dev/null || echo "$CURRENT_BRANCH")}"
2526
+ if [ "$USER_DRY_RUN" = true ]; then
2527
+ log_dim "PR" "dry run: would create a PR for '$CURRENT_BRANCH' into '$MAIN_BRANCH'"
2528
+ else
2529
+ PR_BODY=$(build_sync_pr_body "ensure-pr" "$CURRENT_BRANCH" "$MAIN_BRANCH")
2530
+ ensure_pr_for_head "$CURRENT_BRANCH" "$MAIN_BRANCH" "$PR_TITLE" "$PR_BODY" || true
2531
+ fi
2532
+ fi
2533
+ fi
2534
+ fi
2535
+
2536
+ # STEP 6: Cross-Environment Status Report
2537
+ echo ""
2538
+ echo " Branch Status"
2539
+ printf ' %0.s' $(seq 1 40); echo ""
2540
+
2541
+ # === CACHE BRANCH STATUS (Fix 1: Eliminate duplicate git operations) ===
2542
+ # Scan all branches ONCE and cache the results
2543
+ # Note: Associative arrays require bash 4+
2544
+ # Temporarily disable unbound variable check for caching section
2545
+ set +u
2546
+
2547
+ # Check bash version for associative array support
2548
+ BASH_MAJOR="${BASH_VERSION%%.*}"
2549
+ HAS_ASSOC_ARRAYS=false
2550
+ if [ "$BASH_MAJOR" -ge 4 ] 2>/dev/null; then
2551
+ HAS_ASSOC_ARRAYS=true
2552
+ declare -A BRANCH_AHEAD 2>/dev/null || HAS_ASSOC_ARRAYS=false
2553
+ declare -A BRANCH_BEHIND 2>/dev/null || HAS_ASSOC_ARRAYS=false
2554
+ declare -A BRANCH_HAS_UPSTREAM 2>/dev/null || HAS_ASSOC_ARRAYS=false
2555
+ fi
2556
+
2557
+ # Cache current branch status (for bash 4+) or use simple variables (bash 3.2)
2558
+ if [ "$DETACHED_HEAD" = true ]; then
2559
+ CURRENT_HAS_UPSTREAM=false
2560
+ CURRENT_AHEAD=0
2561
+ CURRENT_BEHIND=0
2562
+ elif git rev-parse --abbrev-ref --symbolic-full-name "@{u}" >/dev/null 2>&1; then
2563
+ CURRENT_HAS_UPSTREAM=true
2564
+ CURRENT_AHEAD=$(git rev-list --count "@{u}..HEAD" 2>/dev/null || echo "0")
2565
+ CURRENT_BEHIND=$(git rev-list --count "HEAD..@{u}" 2>/dev/null || echo "0")
2566
+ if [ "$HAS_ASSOC_ARRAYS" = true ]; then
2567
+ BRANCH_HAS_UPSTREAM["$CURRENT_BRANCH"]=true
2568
+ BRANCH_AHEAD["$CURRENT_BRANCH"]="$CURRENT_AHEAD"
2569
+ BRANCH_BEHIND["$CURRENT_BRANCH"]="$CURRENT_BEHIND"
2570
+ fi
2571
+ else
2572
+ CURRENT_HAS_UPSTREAM=false
2573
+ CURRENT_AHEAD=0
2574
+ CURRENT_BEHIND=0
2575
+ if [ "$HAS_ASSOC_ARRAYS" = true ]; then
2576
+ BRANCH_HAS_UPSTREAM["$CURRENT_BRANCH"]=false
2577
+ BRANCH_AHEAD["$CURRENT_BRANCH"]=0
2578
+ BRANCH_BEHIND["$CURRENT_BRANCH"]=0
2579
+ fi
2580
+ fi
2581
+
2582
+ # Cache other branches status (only on bash 4+ and verbose mode)
2583
+ if [ "$HAS_ASSOC_ARRAYS" = true ] && [ "$VERBOSE" = true ] && [ -n "$TRACKED_BRANCHES" ]; then
2584
+ while IFS= read -r line; do
2585
+ branch=$(echo "$line" | awk '{print $1}')
2586
+ upstream=$(echo "$line" | awk '{print $2}')
2587
+
2588
+ if [ "$branch" != "$CURRENT_BRANCH" ]; then
2589
+ BRANCH_HAS_UPSTREAM["$branch"]=true
2590
+ BRANCH_AHEAD["$branch"]=$(git rev-list --count "$upstream..$branch" 2>/dev/null || echo "0")
2591
+ BRANCH_BEHIND["$branch"]=$(git rev-list --count "$branch..$upstream" 2>/dev/null || echo "0")
2592
+ fi
2593
+ done <<< "$TRACKED_BRANCHES"
2594
+ fi
2595
+
2596
+ # === SUMMARY SECTION (using cached values) ===
2597
+ # Note: Keeping set +u active for array access throughout status reporting
2598
+
2599
+ echo " Summary"
2600
+ printf ' %0.s' $(seq 1 40); echo ""
2601
+
2602
+ # Count synced vs warning branches using cached values
2603
+ SYNCED_COUNT=0
2604
+ WARNING_COUNT=0
2605
+
2606
+ # Check current branch (use simple variables - works on all bash versions)
2607
+ if [ "$CURRENT_HAS_UPSTREAM" = true ]; then
2608
+ if [ "$CURRENT_AHEAD" -eq 0 ] && [ "$CURRENT_BEHIND" -eq 0 ]; then
2609
+ SYNCED_COUNT=$((SYNCED_COUNT + 1))
2610
+ else
2611
+ WARNING_COUNT=$((WARNING_COUNT + 1))
2612
+ HAS_WARNINGS=true
2613
+ fi
2614
+ fi
2615
+
2616
+ # Check other branches using cached values (bash 4+ only, verbose only)
2617
+ if [ "$HAS_ASSOC_ARRAYS" = true ] && [ "$VERBOSE" = true ] && [ -n "$TRACKED_BRANCHES" ]; then
2618
+ while IFS= read -r line; do
2619
+ branch=$(echo "$line" | awk '{print $1}')
2620
+
2621
+ if [ "$branch" != "$CURRENT_BRANCH" ] && [ "${BRANCH_HAS_UPSTREAM["$branch"]}" = true ]; then
2622
+ AHEAD="${BRANCH_AHEAD["$branch"]}"
2623
+ BEHIND="${BRANCH_BEHIND["$branch"]}"
2624
+
2625
+ if [ "$AHEAD" -eq 0 ] && [ "$BEHIND" -eq 0 ]; then
2626
+ SYNCED_COUNT=$((SYNCED_COUNT + 1))
2627
+ else
2628
+ WARNING_COUNT=$((WARNING_COUNT + 1))
2629
+ HAS_WARNINGS=true
2630
+ fi
2631
+ fi
2632
+ done <<< "$TRACKED_BRANCHES"
2633
+ fi
2634
+
2635
+ # Display summary counts
2636
+ if [ "$SYNCED_COUNT" -gt 0 ]; then
2637
+ log_ok "Synced" "$SYNCED_COUNT branch(es)"
2638
+ fi
2639
+ if [ "$WARNING_COUNT" -gt 0 ]; then
2640
+ log_warn "Warnings" "$WARNING_COUNT branch(es)"
2641
+ fi
2642
+
2643
+ echo ""
2644
+ echo " Details"
2645
+ printf ' %0.s' $(seq 1 40); echo ""
2646
+
2647
+ # Current branch status (use simple variables - works on all bash versions)
2648
+ if [ "$DETACHED_HEAD" = true ]; then
2649
+ log_dim "detached HEAD" "$DETACHED_HEAD_SHA (no branch)"
2650
+ elif [ "$CURRENT_HAS_UPSTREAM" = true ]; then
2651
+ if [ "$CURRENT_AHEAD" -eq 0 ] && [ "$CURRENT_BEHIND" -eq 0 ]; then
2652
+ log_ok "$CURRENT_BRANCH" "synced"
2653
+ elif [ "$CURRENT_AHEAD" -gt 0 ] && [ "$CURRENT_BEHIND" -eq 0 ]; then
2654
+ log_warn "$CURRENT_BRANCH" "$CURRENT_AHEAD ahead (needs push)"
2655
+ elif [ "$CURRENT_AHEAD" -eq 0 ] && [ "$CURRENT_BEHIND" -gt 0 ]; then
2656
+ log_warn "$CURRENT_BRANCH" "$CURRENT_BEHIND behind (needs pull)"
2657
+ else
2658
+ log_warn "$CURRENT_BRANCH" "$CURRENT_AHEAD ahead, $CURRENT_BEHIND behind (diverged)"
2659
+ fi
2660
+ else
2661
+ log_dim "$CURRENT_BRANCH" "no upstream tracking"
2662
+ fi
2663
+
2664
+ # Other tracked branches + untracked: only show in verbose mode (default: fast)
2665
+ if [ "$VERBOSE" = true ]; then
2666
+ # Other tracked branches (using cached values - bash 4+ only)
2667
+ if [ -n "$TRACKED_BRANCHES" ]; then
2668
+ if [ "$HAS_ASSOC_ARRAYS" = true ]; then
2669
+ echo "$TRACKED_BRANCHES" | while read -r branch upstream; do
2670
+ if [ "$branch" != "$CURRENT_BRANCH" ]; then
2671
+ AHEAD="${BRANCH_AHEAD["$branch"]}"
2672
+ BEHIND="${BRANCH_BEHIND["$branch"]}"
2673
+
2674
+ if [ "$AHEAD" -eq 0 ] && [ "$BEHIND" -eq 0 ]; then
2675
+ log_ok "$branch" "synced"
2676
+ elif [ "$AHEAD" -gt 0 ] && [ "$BEHIND" -eq 0 ]; then
2677
+ log_warn "$branch" "$AHEAD ahead (needs push)"
2678
+ elif [ "$AHEAD" -eq 0 ] && [ "$BEHIND" -gt 0 ]; then
2679
+ log_warn "$branch" "$BEHIND behind (needs pull)"
2680
+ else
2681
+ log_warn "$branch" "+$AHEAD/-$BEHIND (diverged)"
2682
+ fi
2683
+ fi
2684
+ done
2685
+ else
2686
+ # Bash 3.2 fallback: fetch status inline (slower but works)
2687
+ echo "$TRACKED_BRANCHES" | while read -r branch upstream; do
2688
+ if [ "$branch" != "$CURRENT_BRANCH" ]; then
2689
+ AHEAD=$(git rev-list --count "$upstream..$branch" 2>/dev/null || echo "0")
2690
+ BEHIND=$(git rev-list --count "$branch..$upstream" 2>/dev/null || echo "0")
2691
+
2692
+ if [ "$AHEAD" -eq 0 ] && [ "$BEHIND" -eq 0 ]; then
2693
+ log_ok "$branch" "synced"
2694
+ elif [ "$AHEAD" -gt 0 ] && [ "$BEHIND" -eq 0 ]; then
2695
+ log_warn "$branch" "$AHEAD ahead (needs push)"
2696
+ elif [ "$AHEAD" -eq 0 ] && [ "$BEHIND" -gt 0 ]; then
2697
+ log_warn "$branch" "$BEHIND behind (needs pull)"
2698
+ else
2699
+ log_warn "$branch" "+$AHEAD/-$BEHIND (diverged)"
2700
+ fi
2701
+ fi
2702
+ done
2703
+ fi
2704
+ fi
2705
+
2706
+ # Untracked local branches (no per-branch subprocess; single git call)
2707
+ UNTRACKED=$(git for-each-ref --format='%(if)%(upstream)%(then)%(else)%(refname:short)%(end)' refs/heads/ | grep -v '^$' || true)
2708
+ if [ -n "$UNTRACKED" ]; then
2709
+ echo ""
2710
+ printf " ${DIM}%-16s${NC}\n" "Untracked:"
2711
+ echo "$UNTRACKED" | while read -r branch; do
2712
+ log_dim "$branch" "local only"
2713
+ done
2714
+ fi
2715
+ fi # end verbose branch/untracked detail
2716
+
2717
+ # Remote-only branches + tip: verbose only
2718
+ if [ "$VERBOSE" = true ]; then
2719
+ REMOTE_ONLY=$(git branch -r --format='%(refname:short)' | sed 's/origin\///' | grep -v "HEAD" | while read -r rbranch; do
2720
+ if ! git show-ref --verify --quiet "refs/heads/$rbranch"; then
2721
+ echo "$rbranch"
2722
+ fi
2723
+ done)
2724
+ if [ -n "$REMOTE_ONLY" ]; then
2725
+ echo ""
2726
+ printf " ${DIM}%-16s${NC}\n" "Remote-only:"
2727
+ echo "$REMOTE_ONLY" | while read -r rbranch; do
2728
+ log_dim "$rbranch" "not checked out locally"
2729
+ done
2730
+ fi
2731
+ fi
2732
+
2733
+ # Restore strict unbound-variable checking (disabled at status cache section)
2734
+ set -u
2735
+
2736
+ # === CLEANUP BACKUP BRANCH (only if created and successful) ===
2737
+ if [ "$DRY_RUN" = false ] && [ "$BACKUP_BRANCH_CREATED" = true ]; then
2738
+ if [ "$PUSH_SUCCESS" = true ] || [ "$SYNC_MODE" = true ]; then
2739
+ if git branch -D "$BACKUP_BRANCH" >/dev/null 2>&1; then
2740
+ log_dim "Cleanup" "backup branch removed"
2741
+ fi
2742
+ fi
2743
+ fi
2744
+
2745
+ # === PRUNE STALE BACKUP BRANCHES (orphaned by prior failed/interrupted runs) ===
2746
+ if [ "$DRY_RUN" = false ] && [ "$STATUS_ONLY" = false ] && [ "$PR_ONLY" = false ]; then
2747
+ if [ "$PUSH_SUCCESS" = true ] || [ "$SYNC_MODE" = true ]; then
2748
+ prune_stale_backup_branches || true
2749
+ fi
2750
+ fi
2751
+
2752
+ # === POST-SYNC HOOKS (project-local) ===
2753
+ if [ "$DRY_RUN" = false ] && [ "$STATUS_ONLY" = false ] && [ "$PR_ONLY" = false ]; then
2754
+ run_post_sync_hooks
2755
+ fi
2756
+
2757
+ # === AUTO-CHECKOUT MAIN (optional) ===
2758
+ maybe_checkout_main_after_sync() {
2759
+ local checkout_default
2760
+ checkout_default=$(get_sync_checkout_main_default)
2761
+ if ! is_truthy "$checkout_default"; then
2762
+ return 0
2763
+ fi
2764
+
2765
+ # Only after a real sync run with a successful push or sync-mode pull.
2766
+ if [ "$PUSH_SUCCESS" != true ] && [ "$SYNC_MODE" != true ]; then
2767
+ return 0
2768
+ fi
2769
+
2770
+ local branch
2771
+ branch=$(git branch --show-current 2>/dev/null || echo "")
2772
+ if [ "$branch" = "$MAIN_BRANCH" ]; then
2773
+ return 0
2774
+ fi
2775
+
2776
+ if has_tracked_changes; then
2777
+ log_warn "Checkout" "skipped — working tree has changes"
2778
+ return 0
2779
+ fi
2780
+
2781
+ if git checkout "$MAIN_BRANCH" >/dev/null 2>&1; then
2782
+ log_dim "Checkout" "$MAIN_BRANCH"
2783
+ if [ -n "${CREATED_PR_URL:-}" ]; then
2784
+ log_dim "Note" "changes on PR branch, not $MAIN_BRANCH"
2785
+ log_dim "PR" "$CREATED_PR_URL"
2786
+ fi
2787
+ else
2788
+ log_warn "Checkout" "failed: $MAIN_BRANCH"
2789
+ fi
2790
+ }
2791
+
2792
+ if [ "$DRY_RUN" = false ] && [ "$STATUS_ONLY" = false ]; then
2793
+ maybe_checkout_main_after_sync
2794
+ fi
2795
+
2796
+ # === CLEANUP MERGED ROUTE-MAIN BRANCHES ===
2797
+ # After successful sync, clean up local wip/route-main/* branches that have been merged.
2798
+ if [ "$DRY_RUN" = false ]; then
2799
+ if [ "$PUSH_SUCCESS" = true ] || [ "$SYNC_MODE" = true ]; then
2800
+ # Prefer origin's tree (more up-to-date after fetch), fall back to local
2801
+ main_tree=$(git rev-parse "origin/$MAIN_BRANCH^{tree}" 2>/dev/null || git rev-parse "$MAIN_BRANCH^{tree}" 2>/dev/null || true)
2802
+ if [ -n "$main_tree" ]; then
2803
+ current_branch=$(git branch --show-current 2>/dev/null || true)
2804
+ tmpfile=$(mktemp 2>/dev/null) || true
2805
+ if [ -z "${tmpfile:-}" ]; then
2806
+ log_warn "Cleanup" "mktemp failed, skipping"
2807
+ else
2808
+ trap 'rm -f "$tmpfile"' EXIT
2809
+ git branch --list 'wip/route-main/*' 2>/dev/null | sed 's/^[* ]*//' | while IFS= read -r branch; do
2810
+ [ -z "$branch" ] && continue
2811
+ [ "$branch" = "$current_branch" ] && continue # Can't delete current branch
2812
+ branch_tree=$(git rev-parse "$branch^{tree}" 2>/dev/null || true)
2813
+ if [ -n "$branch_tree" ] && [ "$main_tree" = "$branch_tree" ]; then
2814
+ if delete_local_route_branch "$branch"; then
2815
+ echo "$branch" >> "$tmpfile"
2816
+ fi
2817
+ fi
2818
+ done
2819
+ if [ -s "$tmpfile" ]; then
2820
+ cleaned_count=$(wc -l < "$tmpfile" | tr -d ' ')
2821
+ log_dim "Cleanup" "$cleaned_count route-main branch(es) removed"
2822
+ fi
2823
+ fi
2824
+ fi
2825
+ fi
2826
+ fi
2827
+
2828
+ # === ORPHANED AUTO-STASH CHECK ===
2829
+ # If this run stashed changes, verify the stash was actually consumed.
2830
+ # Safety net: catches any path that leaves this run's auto-stash unconsumed.
2831
+ if [ "$DRY_RUN" = false ] && [ "${STASHED:-false}" = true ]; then
2832
+ stash_ref=$(own_stash_ref || true)
2833
+ if [ -n "$stash_ref" ]; then
2834
+ log_err "Stash" "orphaned auto-stash detected: $stash_ref"
2835
+ echo " Your uncommitted changes were stashed but could not be restored."
2836
+ echo " Inspect: git stash show -p $stash_ref"
2837
+ echo " Restore: git stash pop $stash_ref"
2838
+ echo " Discard: git stash drop $stash_ref"
2839
+ HAS_WARNINGS=true
2840
+ fi
2841
+ fi
2842
+
2843
+ # === COMPLETION ===
2844
+ echo ""
2845
+ if [ "$DRY_RUN" = true ]; then
2846
+ log_dim "Done" "dry run complete — no changes made"
2847
+ elif [ "$SYNC_MODE" = true ]; then
2848
+ if [ "$HAS_WARNINGS" = true ]; then
2849
+ log_warn "Done" "synced (warnings above)"
2850
+ else
2851
+ log_ok "Done" "synced"
2852
+ fi
2853
+ else
2854
+ if [ "$PUSH_SUCCESS" = true ]; then
2855
+ if [ "$HAS_WARNINGS" = true ]; then
2856
+ log_warn "Done" "committed and pushed (warnings above)"
2857
+ else
2858
+ log_ok "Done" "committed and pushed"
2859
+ fi
2860
+ else
2861
+ log_warn "Done" "partial success (see above)"
2862
+ fi
2863
+ fi
2864
+ echo ""
2865
+
2866
+ # Explicit exit code: 0 when journey completed; 1 only on failure (warnings do not fail)
2867
+ if [ "$HAS_FAILURE" = true ]; then
2868
+ exit 1
2869
+ else
2870
+ exit 0
2871
+ fi