@recombine-ai/bosun 0.16.2 → 0.17.1

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 (524) hide show
  1. package/README.md +24 -0
  2. package/bin/bosun.js +153 -20
  3. package/bin/git-mode.js +1139 -0
  4. package/build/client/_app/immutable/assets/0.D9HpHezk.css +1 -0
  5. package/build/client/_app/immutable/assets/0.D9HpHezk.css.br +0 -0
  6. package/build/client/_app/immutable/assets/0.D9HpHezk.css.gz +0 -0
  7. package/build/client/_app/immutable/assets/{2.DtxQDOXx.css → 2.Bc0Sf3YD.css} +1 -1
  8. package/build/client/_app/immutable/assets/2.Bc0Sf3YD.css.br +0 -0
  9. package/build/client/_app/immutable/assets/2.Bc0Sf3YD.css.gz +0 -0
  10. package/build/client/_app/immutable/chunks/{DneQQ7D5.js → -3ZOIHDA.js} +1 -1
  11. package/build/client/_app/immutable/chunks/-3ZOIHDA.js.br +0 -0
  12. package/build/client/_app/immutable/chunks/-3ZOIHDA.js.gz +0 -0
  13. package/build/client/_app/immutable/chunks/7hV1SZxK.js +1 -0
  14. package/build/client/_app/immutable/chunks/7hV1SZxK.js.br +2 -0
  15. package/build/client/_app/immutable/chunks/7hV1SZxK.js.gz +0 -0
  16. package/build/client/_app/immutable/chunks/B-dTD_wm.js +1 -0
  17. package/build/client/_app/immutable/chunks/B-dTD_wm.js.br +0 -0
  18. package/build/client/_app/immutable/chunks/B-dTD_wm.js.gz +0 -0
  19. package/build/client/_app/immutable/chunks/{UqjCSZmA.js → B-iasAJv.js} +1 -1
  20. package/build/client/_app/immutable/chunks/B-iasAJv.js.br +0 -0
  21. package/build/client/_app/immutable/chunks/B-iasAJv.js.gz +0 -0
  22. package/build/client/_app/immutable/chunks/BDijIo6D.js +33 -0
  23. package/build/client/_app/immutable/chunks/BDijIo6D.js.br +0 -0
  24. package/build/client/_app/immutable/chunks/BDijIo6D.js.gz +0 -0
  25. package/build/client/_app/immutable/chunks/BHB0Pj1h.js +2 -0
  26. package/build/client/_app/immutable/chunks/BHB0Pj1h.js.br +0 -0
  27. package/build/client/_app/immutable/chunks/BHB0Pj1h.js.gz +0 -0
  28. package/build/client/_app/immutable/chunks/{Bog028VR.js → BKfmN1Rj.js} +1 -1
  29. package/build/client/_app/immutable/chunks/BKfmN1Rj.js.br +0 -0
  30. package/build/client/_app/immutable/chunks/BKfmN1Rj.js.gz +0 -0
  31. package/build/client/_app/immutable/chunks/{pPpIn6tB.js → BlFOg-Sd.js} +1 -1
  32. package/build/client/_app/immutable/chunks/BlFOg-Sd.js.br +0 -0
  33. package/build/client/_app/immutable/chunks/BlFOg-Sd.js.gz +0 -0
  34. package/build/client/_app/immutable/chunks/{CJcZ2GVC.js → Byg3jr1u.js} +1 -1
  35. package/build/client/_app/immutable/chunks/Byg3jr1u.js.br +0 -0
  36. package/build/client/_app/immutable/chunks/Byg3jr1u.js.gz +0 -0
  37. package/build/client/_app/immutable/chunks/{Bmd5zhWC.js → BzxxLgHi.js} +1 -1
  38. package/build/client/_app/immutable/chunks/BzxxLgHi.js.br +0 -0
  39. package/build/client/_app/immutable/chunks/BzxxLgHi.js.gz +0 -0
  40. package/build/client/_app/immutable/chunks/{CEHsomMo.js → C4UC6AgE.js} +1 -1
  41. package/build/client/_app/immutable/chunks/C4UC6AgE.js.br +0 -0
  42. package/build/client/_app/immutable/chunks/{CEHsomMo.js.gz → C4UC6AgE.js.gz} +0 -0
  43. package/build/client/_app/immutable/chunks/C60AG6QQ.js +1 -0
  44. package/build/client/_app/immutable/chunks/C60AG6QQ.js.br +0 -0
  45. package/build/client/_app/immutable/chunks/C60AG6QQ.js.gz +0 -0
  46. package/build/client/_app/immutable/chunks/CAIfVN23.js +1 -0
  47. package/build/client/_app/immutable/chunks/CAIfVN23.js.br +0 -0
  48. package/build/client/_app/immutable/chunks/CAIfVN23.js.gz +0 -0
  49. package/build/client/_app/immutable/chunks/{JcG4w2gF.js → CB9iDxwI.js} +1 -1
  50. package/build/client/_app/immutable/chunks/CB9iDxwI.js.br +0 -0
  51. package/build/client/_app/immutable/chunks/CB9iDxwI.js.gz +0 -0
  52. package/build/client/_app/immutable/chunks/{DoD3q5w5.js → CD_KdWs9.js} +1 -1
  53. package/build/client/_app/immutable/chunks/CD_KdWs9.js.br +0 -0
  54. package/build/client/_app/immutable/chunks/CD_KdWs9.js.gz +0 -0
  55. package/build/client/_app/immutable/chunks/CDy1LBR5.js +1 -0
  56. package/build/client/_app/immutable/chunks/CDy1LBR5.js.br +0 -0
  57. package/build/client/_app/immutable/chunks/CDy1LBR5.js.gz +0 -0
  58. package/build/client/_app/immutable/chunks/{DCXjBDmY.js → CMFNmSvo.js} +1 -1
  59. package/build/client/_app/immutable/chunks/CMFNmSvo.js.br +0 -0
  60. package/build/client/_app/immutable/chunks/CMFNmSvo.js.gz +0 -0
  61. package/build/client/_app/immutable/chunks/{Bv1mNXs0.js → CNIn4L55.js} +1 -1
  62. package/build/client/_app/immutable/chunks/CNIn4L55.js.br +0 -0
  63. package/build/client/_app/immutable/chunks/CNIn4L55.js.gz +0 -0
  64. package/build/client/_app/immutable/chunks/CUKoVL36.js +1 -0
  65. package/build/client/_app/immutable/chunks/CUKoVL36.js.br +0 -0
  66. package/build/client/_app/immutable/chunks/CUKoVL36.js.gz +0 -0
  67. package/build/client/_app/immutable/chunks/CYGbCr1i.js +3 -0
  68. package/build/client/_app/immutable/chunks/CYGbCr1i.js.br +0 -0
  69. package/build/client/_app/immutable/chunks/CYGbCr1i.js.gz +0 -0
  70. package/build/client/_app/immutable/chunks/CYr0FGgn.js +1 -0
  71. package/build/client/_app/immutable/chunks/CYr0FGgn.js.br +0 -0
  72. package/build/client/_app/immutable/chunks/CYr0FGgn.js.gz +0 -0
  73. package/build/client/_app/immutable/chunks/{DtXXy94i.js → Ce6a22qU.js} +1 -1
  74. package/build/client/_app/immutable/chunks/Ce6a22qU.js.br +0 -0
  75. package/build/client/_app/immutable/chunks/Ce6a22qU.js.gz +0 -0
  76. package/build/client/_app/immutable/chunks/{DfcDjAv-.js → CeoQ-OhK.js} +1 -1
  77. package/build/client/_app/immutable/chunks/CeoQ-OhK.js.br +0 -0
  78. package/build/client/_app/immutable/chunks/CeoQ-OhK.js.gz +0 -0
  79. package/build/client/_app/immutable/chunks/{D8mA-uZ4.js → CupXtwVS.js} +1 -1
  80. package/build/client/_app/immutable/chunks/CupXtwVS.js.br +0 -0
  81. package/build/client/_app/immutable/chunks/CupXtwVS.js.gz +0 -0
  82. package/build/client/_app/immutable/chunks/{B5zf3x62.js → Cy2Uze-c.js} +1 -1
  83. package/build/client/_app/immutable/chunks/Cy2Uze-c.js.br +0 -0
  84. package/build/client/_app/immutable/chunks/Cy2Uze-c.js.gz +0 -0
  85. package/build/client/_app/immutable/chunks/{Dg0A8wPA.js → D8Lmn3S5.js} +1 -1
  86. package/build/client/_app/immutable/chunks/D8Lmn3S5.js.br +0 -0
  87. package/build/client/_app/immutable/chunks/D8Lmn3S5.js.gz +0 -0
  88. package/build/client/_app/immutable/chunks/D8doM1Y6.js +1 -0
  89. package/build/client/_app/immutable/chunks/D8doM1Y6.js.br +2 -0
  90. package/build/client/_app/immutable/chunks/D8doM1Y6.js.gz +0 -0
  91. package/build/client/_app/immutable/chunks/{BHHfrjbV.js → D9BWOD7V.js} +1 -1
  92. package/build/client/_app/immutable/chunks/D9BWOD7V.js.br +3 -0
  93. package/build/client/_app/immutable/chunks/D9BWOD7V.js.gz +0 -0
  94. package/build/client/_app/immutable/chunks/{Cp_st-64.js → DBB88ZB7.js} +1 -1
  95. package/build/client/_app/immutable/chunks/DBB88ZB7.js.br +2 -0
  96. package/build/client/_app/immutable/chunks/DBB88ZB7.js.gz +0 -0
  97. package/build/client/_app/immutable/chunks/{Dm49pcMm.js → DC7lPrn4.js} +1 -1
  98. package/build/client/_app/immutable/chunks/DC7lPrn4.js.br +0 -0
  99. package/build/client/_app/immutable/chunks/DC7lPrn4.js.gz +0 -0
  100. package/build/client/_app/immutable/chunks/DCwWNTpf.js +9 -0
  101. package/build/client/_app/immutable/chunks/DCwWNTpf.js.br +0 -0
  102. package/build/client/_app/immutable/chunks/DCwWNTpf.js.gz +0 -0
  103. package/build/client/_app/immutable/chunks/{wcTQUnZO.js → DEnUeQNt.js} +1 -1
  104. package/build/client/_app/immutable/chunks/DEnUeQNt.js.br +0 -0
  105. package/build/client/_app/immutable/chunks/DEnUeQNt.js.gz +0 -0
  106. package/build/client/_app/immutable/chunks/D_laxq3h.js +1 -0
  107. package/build/client/_app/immutable/chunks/D_laxq3h.js.br +0 -0
  108. package/build/client/_app/immutable/chunks/D_laxq3h.js.gz +0 -0
  109. package/build/client/_app/immutable/chunks/{Ddv5LgEc.js → DfkYlKBk.js} +1 -1
  110. package/build/client/_app/immutable/chunks/DfkYlKBk.js.br +0 -0
  111. package/build/client/_app/immutable/chunks/DfkYlKBk.js.gz +0 -0
  112. package/build/client/_app/immutable/chunks/DnMS2X6M.js +2 -0
  113. package/build/client/_app/immutable/chunks/DnMS2X6M.js.br +0 -0
  114. package/build/client/_app/immutable/chunks/DnMS2X6M.js.gz +0 -0
  115. package/build/client/_app/immutable/chunks/{B1HdnQRQ.js → DrE1ro9S.js} +1 -1
  116. package/build/client/_app/immutable/chunks/DrE1ro9S.js.br +0 -0
  117. package/build/client/_app/immutable/chunks/DrE1ro9S.js.gz +0 -0
  118. package/build/client/_app/immutable/chunks/DzY_RPb9.js +1 -0
  119. package/build/client/_app/immutable/chunks/DzY_RPb9.js.br +0 -0
  120. package/build/client/_app/immutable/chunks/DzY_RPb9.js.gz +0 -0
  121. package/build/client/_app/immutable/chunks/PrCSpCPg.js +1 -0
  122. package/build/client/_app/immutable/chunks/PrCSpCPg.js.br +0 -0
  123. package/build/client/_app/immutable/chunks/PrCSpCPg.js.gz +0 -0
  124. package/build/client/_app/immutable/chunks/{CN555stc.js → eMofpOrR.js} +1 -1
  125. package/build/client/_app/immutable/chunks/eMofpOrR.js.br +0 -0
  126. package/build/client/_app/immutable/chunks/eMofpOrR.js.gz +0 -0
  127. package/build/client/_app/immutable/entry/app.BQUuHkSR.js +2 -0
  128. package/build/client/_app/immutable/entry/app.BQUuHkSR.js.br +0 -0
  129. package/build/client/_app/immutable/entry/app.BQUuHkSR.js.gz +0 -0
  130. package/build/client/_app/immutable/entry/start.DoeTycdu.js +1 -0
  131. package/build/client/_app/immutable/entry/start.DoeTycdu.js.br +2 -0
  132. package/build/client/_app/immutable/entry/start.DoeTycdu.js.gz +0 -0
  133. package/build/client/_app/immutable/nodes/0.DBnFpwRM.js +4 -0
  134. package/build/client/_app/immutable/nodes/0.DBnFpwRM.js.br +0 -0
  135. package/build/client/_app/immutable/nodes/0.DBnFpwRM.js.gz +0 -0
  136. package/build/client/_app/immutable/nodes/1.D-kI-fLd.js +1 -0
  137. package/build/client/_app/immutable/nodes/1.D-kI-fLd.js.br +0 -0
  138. package/build/client/_app/immutable/nodes/1.D-kI-fLd.js.gz +0 -0
  139. package/build/client/_app/immutable/nodes/10.DWb5H7vX.js +28 -0
  140. package/build/client/_app/immutable/nodes/10.DWb5H7vX.js.br +0 -0
  141. package/build/client/_app/immutable/nodes/10.DWb5H7vX.js.gz +0 -0
  142. package/build/client/_app/immutable/nodes/{15.D2I0y3ys.js → 15.CGc3_07c.js} +1 -1
  143. package/build/client/_app/immutable/nodes/15.CGc3_07c.js.br +3 -0
  144. package/build/client/_app/immutable/nodes/15.CGc3_07c.js.gz +0 -0
  145. package/build/client/_app/immutable/nodes/17.BIba62hW.js +3 -0
  146. package/build/client/_app/immutable/nodes/17.BIba62hW.js.br +0 -0
  147. package/build/client/_app/immutable/nodes/17.BIba62hW.js.gz +0 -0
  148. package/build/client/_app/immutable/nodes/18.Clxr6FwI.js +11 -0
  149. package/build/client/_app/immutable/nodes/18.Clxr6FwI.js.br +0 -0
  150. package/build/client/_app/immutable/nodes/18.Clxr6FwI.js.gz +0 -0
  151. package/build/client/_app/immutable/nodes/2.cbpnR8be.js +35 -0
  152. package/build/client/_app/immutable/nodes/2.cbpnR8be.js.br +0 -0
  153. package/build/client/_app/immutable/nodes/2.cbpnR8be.js.gz +0 -0
  154. package/build/client/_app/immutable/nodes/3.DsP759xk.js +1 -0
  155. package/build/client/_app/immutable/nodes/3.DsP759xk.js.br +0 -0
  156. package/build/client/_app/immutable/nodes/3.DsP759xk.js.gz +0 -0
  157. package/build/client/_app/immutable/nodes/{4.C8VxVbVn.js → 4.BIGLAIDe.js} +1 -1
  158. package/build/client/_app/immutable/nodes/4.BIGLAIDe.js.br +0 -0
  159. package/build/client/_app/immutable/nodes/4.BIGLAIDe.js.gz +0 -0
  160. package/build/client/_app/immutable/nodes/{5.BjrnKYaA.js → 5.zMHOyhDv.js} +1 -1
  161. package/build/client/_app/immutable/nodes/5.zMHOyhDv.js.br +0 -0
  162. package/build/client/_app/immutable/nodes/5.zMHOyhDv.js.gz +0 -0
  163. package/build/client/_app/immutable/nodes/6.DFQ1IZsQ.js +1 -0
  164. package/build/client/_app/immutable/nodes/6.DFQ1IZsQ.js.br +0 -0
  165. package/build/client/_app/immutable/nodes/6.DFQ1IZsQ.js.gz +0 -0
  166. package/build/client/_app/immutable/nodes/8.CTgEdr7L.js +2 -0
  167. package/build/client/_app/immutable/nodes/8.CTgEdr7L.js.br +0 -0
  168. package/build/client/_app/immutable/nodes/8.CTgEdr7L.js.gz +0 -0
  169. package/build/client/_app/immutable/nodes/9.BidF5Uvq.js +2 -0
  170. package/build/client/_app/immutable/nodes/9.BidF5Uvq.js.br +0 -0
  171. package/build/client/_app/immutable/nodes/9.BidF5Uvq.js.gz +0 -0
  172. package/build/client/_app/version.json +1 -1
  173. package/build/client/_app/version.json.br +0 -0
  174. package/build/client/_app/version.json.gz +0 -0
  175. package/build/server/chunks/0-BZOFUIfF.js +9 -0
  176. package/build/server/chunks/0-BZOFUIfF.js.map +1 -0
  177. package/build/server/chunks/1-XwGU5VVN.js +9 -0
  178. package/build/server/chunks/{1-C0HdTvXW.js.map → 1-XwGU5VVN.js.map} +1 -1
  179. package/build/server/chunks/10-BgFSx4ib.js +9 -0
  180. package/build/server/chunks/10-BgFSx4ib.js.map +1 -0
  181. package/build/server/chunks/15-BjTTC1jf.js +9 -0
  182. package/build/server/chunks/{15-BQcb9C_y.js.map → 15-BjTTC1jf.js.map} +1 -1
  183. package/build/server/chunks/17-BEkWCfzc.js +9 -0
  184. package/build/server/chunks/17-BEkWCfzc.js.map +1 -0
  185. package/build/server/chunks/18-C-S0wKdp.js +9 -0
  186. package/build/server/chunks/18-C-S0wKdp.js.map +1 -0
  187. package/build/server/chunks/2-o-cIkIjs.js +9 -0
  188. package/build/server/chunks/2-o-cIkIjs.js.map +1 -0
  189. package/build/server/chunks/3-B-ABlyFO.js +9 -0
  190. package/build/server/chunks/3-B-ABlyFO.js.map +1 -0
  191. package/build/server/chunks/4-joAFA_Y8.js +9 -0
  192. package/build/server/chunks/4-joAFA_Y8.js.map +1 -0
  193. package/build/server/chunks/5-DIhC9k4l.js +9 -0
  194. package/build/server/chunks/5-DIhC9k4l.js.map +1 -0
  195. package/build/server/chunks/6-CMuAQIcW.js +9 -0
  196. package/build/server/chunks/6-CMuAQIcW.js.map +1 -0
  197. package/build/server/chunks/8-CbETTVRG.js +9 -0
  198. package/build/server/chunks/8-CbETTVRG.js.map +1 -0
  199. package/build/server/chunks/9-nMV90uVU.js +9 -0
  200. package/build/server/chunks/9-nMV90uVU.js.map +1 -0
  201. package/build/server/chunks/{AgentSidebar-BWBnqHnV.js → AgentSidebar-BEbSdZdN.js} +4 -4
  202. package/build/server/chunks/{AgentSidebar-BWBnqHnV.js.map → AgentSidebar-BEbSdZdN.js.map} +1 -1
  203. package/build/server/chunks/{Button-DH7gdU0s.js → Button-Dc9CR4Wh.js} +2 -2
  204. package/build/server/chunks/{Button-DH7gdU0s.js.map → Button-Dc9CR4Wh.js.map} +1 -1
  205. package/build/server/chunks/{ChatStepTrace-BivoNYOY.js → ChatStepTrace-OM96X4g8.js} +18 -7
  206. package/build/server/chunks/ChatStepTrace-OM96X4g8.js.map +1 -0
  207. package/build/server/chunks/{Dropdown-DnARl86Q.js → Dropdown-Cgd7pG-k.js} +2 -2
  208. package/build/server/chunks/{Dropdown-DnARl86Q.js.map → Dropdown-Cgd7pG-k.js.map} +1 -1
  209. package/build/server/chunks/GitControlsButton-BLcNzrDt.js +81 -0
  210. package/build/server/chunks/GitControlsButton-BLcNzrDt.js.map +1 -0
  211. package/build/server/chunks/{Icon-DP1YKCRV.js → Icon-Bogvl2n9.js} +2 -2
  212. package/build/server/chunks/{Icon-DP1YKCRV.js.map → Icon-Bogvl2n9.js.map} +1 -1
  213. package/build/server/chunks/{Input-CUJ9Cg7b.js → Input-7hpw1WLW.js} +2 -2
  214. package/build/server/chunks/{Input-CUJ9Cg7b.js.map → Input-7hpw1WLW.js.map} +1 -1
  215. package/build/server/chunks/{Modal-CGJuNpZ0.js → Modal-7IIIfFEG.js} +5 -5
  216. package/build/server/chunks/{Modal-CGJuNpZ0.js.map → Modal-7IIIfFEG.js.map} +1 -1
  217. package/build/server/chunks/{Panel-zvjxe036.js → Panel-Cj1mXx9-.js} +2 -2
  218. package/build/server/chunks/{Panel-zvjxe036.js.map → Panel-Cj1mXx9-.js.map} +1 -1
  219. package/build/server/chunks/{Tab-CUkyLqbN.js → Tab-bf4ZDs5y.js} +4 -4
  220. package/build/server/chunks/{Tab-CUkyLqbN.js.map → Tab-bf4ZDs5y.js.map} +1 -1
  221. package/build/server/chunks/{_layout.svelte-BnrWYuLv.js → _layout.svelte-C71luIwv.js} +10 -10
  222. package/build/server/chunks/{_layout.svelte-BnrWYuLv.js.map → _layout.svelte-C71luIwv.js.map} +1 -1
  223. package/build/server/chunks/{_layout.svelte-wi6o5Ecu.js → _layout.svelte-CFhHgXI_.js} +24 -18
  224. package/build/server/chunks/_layout.svelte-CFhHgXI_.js.map +1 -0
  225. package/build/server/chunks/_layout.svelte-CJd_pmkx.js +431 -0
  226. package/build/server/chunks/_layout.svelte-CJd_pmkx.js.map +1 -0
  227. package/build/server/chunks/{_layout.svelte-B88WMyk0.js → _layout.svelte-D6rhwm98.js} +445 -70
  228. package/build/server/chunks/_layout.svelte-D6rhwm98.js.map +1 -0
  229. package/build/server/chunks/{_page.svelte-CeS_21Ho.js → _page.svelte-BBvDMiTV.js} +25 -21
  230. package/build/server/chunks/_page.svelte-BBvDMiTV.js.map +1 -0
  231. package/build/server/chunks/{_page.svelte-h_J4lNT9.js → _page.svelte-BQs9dEuq.js} +8 -7
  232. package/build/server/chunks/{_page.svelte-h_J4lNT9.js.map → _page.svelte-BQs9dEuq.js.map} +1 -1
  233. package/build/server/chunks/{_page.svelte-DhFNFMnl.js → _page.svelte-BoO2LWVX.js} +34 -14
  234. package/build/server/chunks/_page.svelte-BoO2LWVX.js.map +1 -0
  235. package/build/server/chunks/{_page.svelte-CHgQjBQj.js → _page.svelte-DUCTSdv-.js} +2 -2
  236. package/build/server/chunks/{_page.svelte-CHgQjBQj.js.map → _page.svelte-DUCTSdv-.js.map} +1 -1
  237. package/build/server/chunks/_page.svelte-FXYLKyMw.js +110 -0
  238. package/build/server/chunks/_page.svelte-FXYLKyMw.js.map +1 -0
  239. package/build/server/chunks/{_page.svelte-57pyhszV.js → _page.svelte-KA1hqKj2.js} +20 -107
  240. package/build/server/chunks/_page.svelte-KA1hqKj2.js.map +1 -0
  241. package/build/server/chunks/{_page.svelte-BSVJ2d2G.js → _page.svelte-y8CZFL8a.js} +210 -106
  242. package/build/server/chunks/_page.svelte-y8CZFL8a.js.map +1 -0
  243. package/build/server/chunks/{_page.svelte-DHSYVLoj.js → _page.svelte-yIHSIEW6.js} +21 -15
  244. package/build/server/chunks/_page.svelte-yIHSIEW6.js.map +1 -0
  245. package/build/server/chunks/_server.ts-Cp9DgLcE.js +336 -0
  246. package/build/server/chunks/_server.ts-Cp9DgLcE.js.map +1 -0
  247. package/build/server/chunks/{agents-context-CbFsZmi5.js → agents-context-owgusUPR.js} +2 -2
  248. package/build/server/chunks/{agents-context-CbFsZmi5.js.map → agents-context-owgusUPR.js.map} +1 -1
  249. package/build/server/chunks/{client-DO-hqMbT.js → client-P-7bDULt.js} +4 -4
  250. package/build/server/chunks/{client-DO-hqMbT.js.map → client-P-7bDULt.js.map} +1 -1
  251. package/build/server/chunks/{conversation-view-memory-CtbUZd_L.js → conversation-view-memory-SdpdqTR7.js} +2 -2
  252. package/build/server/chunks/{conversation-view-memory-CtbUZd_L.js.map → conversation-view-memory-SdpdqTR7.js.map} +1 -1
  253. package/build/server/chunks/{edit-workspace-context-tyHBPujC.js → edit-workspace-context-hanFnXR3.js} +2 -2
  254. package/build/server/chunks/{edit-workspace-context-tyHBPujC.js.map → edit-workspace-context-hanFnXR3.js.map} +1 -1
  255. package/build/server/chunks/error.svelte-yI9RS6H4.js +15 -0
  256. package/build/server/chunks/{error.svelte-DmKKN1RU.js.map → error.svelte-yI9RS6H4.js.map} +1 -1
  257. package/build/server/chunks/git-mode-context-BpNsAUGF.js +18 -0
  258. package/build/server/chunks/git-mode-context-BpNsAUGF.js.map +1 -0
  259. package/build/server/chunks/{index-LMsd1ih2.js → index-DGtbUu8Y.js} +2 -2
  260. package/build/server/chunks/{index-LMsd1ih2.js.map → index-DGtbUu8Y.js.map} +1 -1
  261. package/build/server/chunks/{index-server-BRr5mZEq.js → index-server-l8-WsVn4.js} +2 -2
  262. package/build/server/chunks/{index-server-BRr5mZEq.js.map → index-server-l8-WsVn4.js.map} +1 -1
  263. package/build/server/chunks/{index2-ClgfzZ89.js → index2-CtgLiJVO.js} +3 -3
  264. package/build/server/chunks/{index2-ClgfzZ89.js.map → index2-CtgLiJVO.js.map} +1 -1
  265. package/build/server/chunks/{index3-BdRUAffj.js → index3-4hbbCqW0.js} +2 -2
  266. package/build/server/chunks/{index3-BdRUAffj.js.map → index3-4hbbCqW0.js.map} +1 -1
  267. package/build/server/chunks/{root-4q2Pr7ck.js → root-sWPnlpi3.js} +2 -2
  268. package/build/server/chunks/{root-4q2Pr7ck.js.map → root-sWPnlpi3.js.map} +1 -1
  269. package/build/server/chunks/{source-selection-CcN1cgXc.js → source-selection-DSkaPGfZ.js} +7 -2
  270. package/build/server/chunks/source-selection-DSkaPGfZ.js.map +1 -0
  271. package/build/server/chunks/{state.svelte-Bd1i-u49.js → state.svelte-BkvaElc6.js} +3 -3
  272. package/build/server/chunks/{state.svelte-Bd1i-u49.js.map → state.svelte-BkvaElc6.js.map} +1 -1
  273. package/build/server/chunks/trpc-client-Cr8RJ4UO.js +260 -0
  274. package/build/server/chunks/trpc-client-Cr8RJ4UO.js.map +1 -0
  275. package/build/server/index.js +4 -4
  276. package/build/server/index.js.map +1 -1
  277. package/build/server/manifest.js +15 -15
  278. package/build/server/manifest.js.map +1 -1
  279. package/package.json +1 -1
  280. package/src/backend/git-mode/command.spec.ts +172 -0
  281. package/src/backend/git-mode/command.ts +405 -0
  282. package/src/backend/git-mode/config.spec.ts +45 -0
  283. package/src/backend/git-mode/config.ts +90 -0
  284. package/src/backend/git-mode/index.ts +20 -0
  285. package/src/backend/git-mode/repository-write-gate.spec.ts +116 -0
  286. package/src/backend/git-mode/repository-write-gate.ts +260 -0
  287. package/src/backend/git-mode/router.ts +59 -0
  288. package/src/backend/git-mode/service.spec.ts +1653 -0
  289. package/src/backend/git-mode/service.ts +2183 -0
  290. package/src/backend/git-mode/types.ts +82 -0
  291. package/src/backend/git-mode/write-tracker.spec.ts +46 -0
  292. package/src/backend/git-mode/write-tracker.ts +39 -0
  293. package/src/backend/lib/backend-restart.spec.ts +27 -0
  294. package/src/backend/lib/backend-restart.ts +16 -2
  295. package/src/backend/lib/middleware/cors.spec.ts +67 -0
  296. package/src/backend/lib/middleware/cors.ts +40 -12
  297. package/src/backend/roleplay/agent-loader.ts +49 -0
  298. package/src/backend/roleplay/old-server.ts +106 -56
  299. package/src/backend/roleplay/prompt-edit-proposals.ts +38 -4
  300. package/src/backend/roleplay/prompt-file.ts +59 -0
  301. package/src/backend/roleplay/router.spec.ts +75 -0
  302. package/src/backend/roleplay/router.ts +123 -69
  303. package/src/backend/roleplay/suggest-file-edit.ts +160 -0
  304. package/src/backend/roleplay/suggest-prompt-edit.ts +9 -138
  305. package/src/backend/roleplay/suggest-test-fix.spec.ts +50 -0
  306. package/src/backend/roleplay/suggest-test-fix.ts +65 -0
  307. package/src/backend/roleplay/test-fix-proposal-acceptance.spec.ts +253 -0
  308. package/src/backend/server.ts +116 -13
  309. package/src/backend/tests/academy.ts +17 -3
  310. package/src/backend/tests/data.spec.ts +81 -2
  311. package/src/backend/tests/data.ts +74 -4
  312. package/src/backend/tests/router.ts +101 -36
  313. package/src/backend/tests/suggest-test-history-edit.spec.ts +20 -0
  314. package/src/backend/tests/suggest-test-history-edit.ts +52 -0
  315. package/src/backend/tests/test-edit-source.spec.ts +61 -0
  316. package/src/backend/tests/test-edit-source.ts +34 -0
  317. package/src/backend/tests/test-history-edit-proposals.ts +69 -0
  318. package/src/backend/tests/test-history-overrides.spec.ts +24 -0
  319. package/src/backend/tests/test-selection.ts +27 -0
  320. package/src/lib/api-telescope/source-selection.spec.ts +44 -0
  321. package/src/lib/api-telescope/source-selection.ts +28 -0
  322. package/src/lib/components/Modal.svelte +1 -1
  323. package/src/lib/edit/edit-workspace-context.ts +33 -3
  324. package/src/lib/edit/test-ai-edit.spec.ts +31 -0
  325. package/src/lib/edit/test-ai-edit.ts +52 -0
  326. package/src/lib/git-mode/BlockingGitDialog.svelte +118 -0
  327. package/src/lib/git-mode/GitControlsButton.svelte +96 -0
  328. package/src/lib/git-mode/client-admission.spec.ts +304 -0
  329. package/src/lib/git-mode/client-admission.ts +343 -0
  330. package/src/lib/git-mode/errors.ts +2 -0
  331. package/src/lib/git-mode/git-mode-context.ts +70 -0
  332. package/src/lib/git-mode/refresh-policy.spec.ts +95 -0
  333. package/src/lib/git-mode/refresh-policy.ts +39 -0
  334. package/src/lib/git-mode/status-recovery.spec.ts +86 -0
  335. package/src/lib/git-mode/status-recovery.ts +33 -0
  336. package/src/lib/monaco/bosun-theme.ts +6 -83
  337. package/src/lib/monaco/editor-appearance.spec.ts +84 -0
  338. package/src/lib/monaco/editor-appearance.ts +154 -0
  339. package/src/lib/monaco/editor-decorations.spec.ts +91 -0
  340. package/src/lib/monaco/editor-decorations.ts +133 -0
  341. package/src/lib/tests/assertion-types.ts +74 -0
  342. package/src/lib/trpc-client.ts +15 -1
  343. package/src/lib/ws-client.spec.ts +208 -0
  344. package/src/lib/ws-client.ts +196 -14
  345. package/src/lib/ws-message.ts +24 -0
  346. package/build/client/_app/immutable/assets/0.Rrc0O--C.css +0 -1
  347. package/build/client/_app/immutable/assets/0.Rrc0O--C.css.br +0 -0
  348. package/build/client/_app/immutable/assets/0.Rrc0O--C.css.gz +0 -0
  349. package/build/client/_app/immutable/assets/2.DtxQDOXx.css.br +0 -0
  350. package/build/client/_app/immutable/assets/2.DtxQDOXx.css.gz +0 -0
  351. package/build/client/_app/immutable/chunks/B1HdnQRQ.js.br +0 -0
  352. package/build/client/_app/immutable/chunks/B1HdnQRQ.js.gz +0 -0
  353. package/build/client/_app/immutable/chunks/B5zf3x62.js.br +0 -0
  354. package/build/client/_app/immutable/chunks/B5zf3x62.js.gz +0 -0
  355. package/build/client/_app/immutable/chunks/BHHfrjbV.js.br +0 -2
  356. package/build/client/_app/immutable/chunks/BHHfrjbV.js.gz +0 -0
  357. package/build/client/_app/immutable/chunks/BU7aeYn4.js +0 -1
  358. package/build/client/_app/immutable/chunks/BU7aeYn4.js.br +0 -0
  359. package/build/client/_app/immutable/chunks/BU7aeYn4.js.gz +0 -0
  360. package/build/client/_app/immutable/chunks/BbGYcfPY.js +0 -3
  361. package/build/client/_app/immutable/chunks/BbGYcfPY.js.br +0 -0
  362. package/build/client/_app/immutable/chunks/BbGYcfPY.js.gz +0 -0
  363. package/build/client/_app/immutable/chunks/Bmd5zhWC.js.br +0 -0
  364. package/build/client/_app/immutable/chunks/Bmd5zhWC.js.gz +0 -0
  365. package/build/client/_app/immutable/chunks/BnqH7p3U.js +0 -33
  366. package/build/client/_app/immutable/chunks/BnqH7p3U.js.br +0 -0
  367. package/build/client/_app/immutable/chunks/BnqH7p3U.js.gz +0 -0
  368. package/build/client/_app/immutable/chunks/Bog028VR.js.br +0 -0
  369. package/build/client/_app/immutable/chunks/Bog028VR.js.gz +0 -0
  370. package/build/client/_app/immutable/chunks/Bv1mNXs0.js.br +0 -0
  371. package/build/client/_app/immutable/chunks/Bv1mNXs0.js.gz +0 -0
  372. package/build/client/_app/immutable/chunks/BvjXTrOz.js +0 -1
  373. package/build/client/_app/immutable/chunks/BvjXTrOz.js.br +0 -0
  374. package/build/client/_app/immutable/chunks/BvjXTrOz.js.gz +0 -0
  375. package/build/client/_app/immutable/chunks/C1R-s4aD.js +0 -1
  376. package/build/client/_app/immutable/chunks/C1R-s4aD.js.br +0 -0
  377. package/build/client/_app/immutable/chunks/C1R-s4aD.js.gz +0 -0
  378. package/build/client/_app/immutable/chunks/CC1OcqhR.js +0 -1
  379. package/build/client/_app/immutable/chunks/CC1OcqhR.js.br +0 -0
  380. package/build/client/_app/immutable/chunks/CC1OcqhR.js.gz +0 -0
  381. package/build/client/_app/immutable/chunks/CEHsomMo.js.br +0 -0
  382. package/build/client/_app/immutable/chunks/CJcZ2GVC.js.br +0 -0
  383. package/build/client/_app/immutable/chunks/CJcZ2GVC.js.gz +0 -0
  384. package/build/client/_app/immutable/chunks/CN555stc.js.br +0 -0
  385. package/build/client/_app/immutable/chunks/CN555stc.js.gz +0 -0
  386. package/build/client/_app/immutable/chunks/CQg2Mme0.js +0 -9
  387. package/build/client/_app/immutable/chunks/CQg2Mme0.js.br +0 -0
  388. package/build/client/_app/immutable/chunks/CQg2Mme0.js.gz +0 -0
  389. package/build/client/_app/immutable/chunks/Cp_st-64.js.br +0 -0
  390. package/build/client/_app/immutable/chunks/Cp_st-64.js.gz +0 -0
  391. package/build/client/_app/immutable/chunks/D8mA-uZ4.js.br +0 -0
  392. package/build/client/_app/immutable/chunks/D8mA-uZ4.js.gz +0 -0
  393. package/build/client/_app/immutable/chunks/DCXjBDmY.js.br +0 -0
  394. package/build/client/_app/immutable/chunks/DCXjBDmY.js.gz +0 -0
  395. package/build/client/_app/immutable/chunks/DSLyVE1P.js +0 -1
  396. package/build/client/_app/immutable/chunks/DSLyVE1P.js.br +0 -0
  397. package/build/client/_app/immutable/chunks/DSLyVE1P.js.gz +0 -0
  398. package/build/client/_app/immutable/chunks/Ddv5LgEc.js.br +0 -0
  399. package/build/client/_app/immutable/chunks/Ddv5LgEc.js.gz +0 -0
  400. package/build/client/_app/immutable/chunks/DfcDjAv-.js.br +0 -0
  401. package/build/client/_app/immutable/chunks/DfcDjAv-.js.gz +0 -0
  402. package/build/client/_app/immutable/chunks/Dg0A8wPA.js.br +0 -0
  403. package/build/client/_app/immutable/chunks/Dg0A8wPA.js.gz +0 -0
  404. package/build/client/_app/immutable/chunks/Dm49pcMm.js.br +0 -0
  405. package/build/client/_app/immutable/chunks/Dm49pcMm.js.gz +0 -0
  406. package/build/client/_app/immutable/chunks/DneQQ7D5.js.br +0 -0
  407. package/build/client/_app/immutable/chunks/DneQQ7D5.js.gz +0 -0
  408. package/build/client/_app/immutable/chunks/DoD3q5w5.js.br +0 -0
  409. package/build/client/_app/immutable/chunks/DoD3q5w5.js.gz +0 -0
  410. package/build/client/_app/immutable/chunks/DtXXy94i.js.br +0 -0
  411. package/build/client/_app/immutable/chunks/DtXXy94i.js.gz +0 -0
  412. package/build/client/_app/immutable/chunks/Dy2CFyD6.js +0 -2
  413. package/build/client/_app/immutable/chunks/Dy2CFyD6.js.br +0 -0
  414. package/build/client/_app/immutable/chunks/Dy2CFyD6.js.gz +0 -0
  415. package/build/client/_app/immutable/chunks/DyBwTDJp.js +0 -2
  416. package/build/client/_app/immutable/chunks/DyBwTDJp.js.br +0 -0
  417. package/build/client/_app/immutable/chunks/DyBwTDJp.js.gz +0 -0
  418. package/build/client/_app/immutable/chunks/ILTCveYH.js +0 -1
  419. package/build/client/_app/immutable/chunks/ILTCveYH.js.br +0 -0
  420. package/build/client/_app/immutable/chunks/ILTCveYH.js.gz +0 -0
  421. package/build/client/_app/immutable/chunks/JcG4w2gF.js.br +0 -0
  422. package/build/client/_app/immutable/chunks/JcG4w2gF.js.gz +0 -0
  423. package/build/client/_app/immutable/chunks/UqjCSZmA.js.br +0 -0
  424. package/build/client/_app/immutable/chunks/UqjCSZmA.js.gz +0 -0
  425. package/build/client/_app/immutable/chunks/_YcrHZ6P.js +0 -1
  426. package/build/client/_app/immutable/chunks/_YcrHZ6P.js.br +0 -2
  427. package/build/client/_app/immutable/chunks/_YcrHZ6P.js.gz +0 -0
  428. package/build/client/_app/immutable/chunks/i6ygh6h8.js +0 -1
  429. package/build/client/_app/immutable/chunks/i6ygh6h8.js.br +0 -2
  430. package/build/client/_app/immutable/chunks/i6ygh6h8.js.gz +0 -0
  431. package/build/client/_app/immutable/chunks/oRi68ch3.js +0 -1
  432. package/build/client/_app/immutable/chunks/oRi68ch3.js.br +0 -0
  433. package/build/client/_app/immutable/chunks/oRi68ch3.js.gz +0 -0
  434. package/build/client/_app/immutable/chunks/pPpIn6tB.js.br +0 -0
  435. package/build/client/_app/immutable/chunks/pPpIn6tB.js.gz +0 -0
  436. package/build/client/_app/immutable/chunks/r0sHOWrM.js +0 -1
  437. package/build/client/_app/immutable/chunks/r0sHOWrM.js.br +0 -0
  438. package/build/client/_app/immutable/chunks/r0sHOWrM.js.gz +0 -0
  439. package/build/client/_app/immutable/chunks/wcTQUnZO.js.br +0 -0
  440. package/build/client/_app/immutable/chunks/wcTQUnZO.js.gz +0 -0
  441. package/build/client/_app/immutable/entry/app.DdO0gkPg.js +0 -2
  442. package/build/client/_app/immutable/entry/app.DdO0gkPg.js.br +0 -0
  443. package/build/client/_app/immutable/entry/app.DdO0gkPg.js.gz +0 -0
  444. package/build/client/_app/immutable/entry/start.eAjDq0c9.js +0 -1
  445. package/build/client/_app/immutable/entry/start.eAjDq0c9.js.br +0 -2
  446. package/build/client/_app/immutable/entry/start.eAjDq0c9.js.gz +0 -0
  447. package/build/client/_app/immutable/nodes/0.nG3DdvIX.js +0 -3
  448. package/build/client/_app/immutable/nodes/0.nG3DdvIX.js.br +0 -0
  449. package/build/client/_app/immutable/nodes/0.nG3DdvIX.js.gz +0 -0
  450. package/build/client/_app/immutable/nodes/1.Ceu0XS6v.js +0 -1
  451. package/build/client/_app/immutable/nodes/1.Ceu0XS6v.js.br +0 -0
  452. package/build/client/_app/immutable/nodes/1.Ceu0XS6v.js.gz +0 -0
  453. package/build/client/_app/immutable/nodes/10.dm2dgy31.js +0 -13
  454. package/build/client/_app/immutable/nodes/10.dm2dgy31.js.br +0 -0
  455. package/build/client/_app/immutable/nodes/10.dm2dgy31.js.gz +0 -0
  456. package/build/client/_app/immutable/nodes/15.D2I0y3ys.js.br +0 -0
  457. package/build/client/_app/immutable/nodes/15.D2I0y3ys.js.gz +0 -0
  458. package/build/client/_app/immutable/nodes/17.CKxGp0E7.js +0 -3
  459. package/build/client/_app/immutable/nodes/17.CKxGp0E7.js.br +0 -0
  460. package/build/client/_app/immutable/nodes/17.CKxGp0E7.js.gz +0 -0
  461. package/build/client/_app/immutable/nodes/18.DHJxyQpw.js +0 -11
  462. package/build/client/_app/immutable/nodes/18.DHJxyQpw.js.br +0 -0
  463. package/build/client/_app/immutable/nodes/18.DHJxyQpw.js.gz +0 -0
  464. package/build/client/_app/immutable/nodes/2.DesR2keg.js +0 -35
  465. package/build/client/_app/immutable/nodes/2.DesR2keg.js.br +0 -0
  466. package/build/client/_app/immutable/nodes/2.DesR2keg.js.gz +0 -0
  467. package/build/client/_app/immutable/nodes/3.BUKrXGR_.js +0 -1
  468. package/build/client/_app/immutable/nodes/3.BUKrXGR_.js.br +0 -0
  469. package/build/client/_app/immutable/nodes/3.BUKrXGR_.js.gz +0 -0
  470. package/build/client/_app/immutable/nodes/4.C8VxVbVn.js.br +0 -0
  471. package/build/client/_app/immutable/nodes/4.C8VxVbVn.js.gz +0 -0
  472. package/build/client/_app/immutable/nodes/5.BjrnKYaA.js.br +0 -0
  473. package/build/client/_app/immutable/nodes/5.BjrnKYaA.js.gz +0 -0
  474. package/build/client/_app/immutable/nodes/6.C_hEzPrU.js +0 -1
  475. package/build/client/_app/immutable/nodes/6.C_hEzPrU.js.br +0 -0
  476. package/build/client/_app/immutable/nodes/6.C_hEzPrU.js.gz +0 -0
  477. package/build/client/_app/immutable/nodes/8.BTYaOiDC.js +0 -2
  478. package/build/client/_app/immutable/nodes/8.BTYaOiDC.js.br +0 -0
  479. package/build/client/_app/immutable/nodes/8.BTYaOiDC.js.gz +0 -0
  480. package/build/client/_app/immutable/nodes/9.DVspuDYY.js +0 -2
  481. package/build/client/_app/immutable/nodes/9.DVspuDYY.js.br +0 -0
  482. package/build/client/_app/immutable/nodes/9.DVspuDYY.js.gz +0 -0
  483. package/build/server/chunks/0-D_duQDWy.js +0 -9
  484. package/build/server/chunks/0-D_duQDWy.js.map +0 -1
  485. package/build/server/chunks/1-C0HdTvXW.js +0 -9
  486. package/build/server/chunks/10-DcfhCR4R.js +0 -9
  487. package/build/server/chunks/10-DcfhCR4R.js.map +0 -1
  488. package/build/server/chunks/15-BQcb9C_y.js +0 -9
  489. package/build/server/chunks/17-DuVCTioj.js +0 -9
  490. package/build/server/chunks/17-DuVCTioj.js.map +0 -1
  491. package/build/server/chunks/18-CT0VFW_p.js +0 -9
  492. package/build/server/chunks/18-CT0VFW_p.js.map +0 -1
  493. package/build/server/chunks/2-DgTzKQAP.js +0 -9
  494. package/build/server/chunks/2-DgTzKQAP.js.map +0 -1
  495. package/build/server/chunks/3-eQMSMzXP.js +0 -9
  496. package/build/server/chunks/3-eQMSMzXP.js.map +0 -1
  497. package/build/server/chunks/4-OaAJtDqP.js +0 -9
  498. package/build/server/chunks/4-OaAJtDqP.js.map +0 -1
  499. package/build/server/chunks/5-CBdMutbv.js +0 -9
  500. package/build/server/chunks/5-CBdMutbv.js.map +0 -1
  501. package/build/server/chunks/6-FoEi7ggm.js +0 -9
  502. package/build/server/chunks/6-FoEi7ggm.js.map +0 -1
  503. package/build/server/chunks/8-B44yRkfX.js +0 -9
  504. package/build/server/chunks/8-B44yRkfX.js.map +0 -1
  505. package/build/server/chunks/9-eUjFpHzI.js +0 -9
  506. package/build/server/chunks/9-eUjFpHzI.js.map +0 -1
  507. package/build/server/chunks/ChatStepTrace-BivoNYOY.js.map +0 -1
  508. package/build/server/chunks/_layout.svelte-B-0DHjLB.js +0 -45
  509. package/build/server/chunks/_layout.svelte-B-0DHjLB.js.map +0 -1
  510. package/build/server/chunks/_layout.svelte-B88WMyk0.js.map +0 -1
  511. package/build/server/chunks/_layout.svelte-wi6o5Ecu.js.map +0 -1
  512. package/build/server/chunks/_page.svelte-57pyhszV.js.map +0 -1
  513. package/build/server/chunks/_page.svelte-BSVJ2d2G.js.map +0 -1
  514. package/build/server/chunks/_page.svelte-BoHt2cpr.js +0 -61
  515. package/build/server/chunks/_page.svelte-BoHt2cpr.js.map +0 -1
  516. package/build/server/chunks/_page.svelte-CeS_21Ho.js.map +0 -1
  517. package/build/server/chunks/_page.svelte-DHSYVLoj.js.map +0 -1
  518. package/build/server/chunks/_page.svelte-DhFNFMnl.js.map +0 -1
  519. package/build/server/chunks/_server.ts-D_eo_OAK.js +0 -139
  520. package/build/server/chunks/_server.ts-D_eo_OAK.js.map +0 -1
  521. package/build/server/chunks/error.svelte-DmKKN1RU.js +0 -15
  522. package/build/server/chunks/source-selection-CcN1cgXc.js.map +0 -1
  523. package/build/server/chunks/trpc-client-D8L6vWf4.js +0 -14
  524. package/build/server/chunks/trpc-client-D8L6vWf4.js.map +0 -1
@@ -0,0 +1,405 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
3
+ import { readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'
4
+
5
+ export interface CommandResult {
6
+ stdout: string
7
+ stderr: string
8
+ stdoutBuffer?: Buffer
9
+ stderrBuffer?: Buffer
10
+ exitCode: number
11
+ }
12
+
13
+ export interface CommandOptions {
14
+ cwd: string
15
+ env?: NodeJS.ProcessEnv
16
+ input?: string | Buffer
17
+ timeoutMs?: number
18
+ acceptedExitCodes?: readonly number[]
19
+ }
20
+
21
+ export type CommandRunner = (
22
+ executable: string,
23
+ args: readonly string[],
24
+ options: CommandOptions
25
+ ) => Promise<CommandResult>
26
+
27
+ export interface CommandSupervisor {
28
+ run: CommandRunner
29
+ shutdown(): Promise<void>
30
+ activeCount(): number
31
+ }
32
+
33
+ export class CommandError extends Error {
34
+ readonly executable: string
35
+ readonly args: readonly string[]
36
+ readonly result: CommandResult
37
+
38
+ constructor(executable: string, args: readonly string[], result: CommandResult) {
39
+ super(
40
+ `${executable} exited with code ${result.exitCode}${result.stderr.trim() ? `: ${result.stderr.trim()}` : ''}`
41
+ )
42
+ this.name = 'CommandError'
43
+ this.executable = executable
44
+ this.args = args
45
+ this.result = result
46
+ }
47
+ }
48
+
49
+ interface CommandLedgerProcess {
50
+ pid: number
51
+ processGroupId: number
52
+ executable: string
53
+ startedAt: string
54
+ }
55
+
56
+ interface CommandLedger {
57
+ version: 1
58
+ leaseId: string
59
+ launchId: string
60
+ repositoryRoot: string
61
+ processes: CommandLedgerProcess[]
62
+ }
63
+
64
+ interface ActiveCommand {
65
+ child: ChildProcessWithoutNullStreams
66
+ closed: Promise<void>
67
+ resolveClosed: () => void
68
+ ledgerProcess: CommandLedgerProcess
69
+ }
70
+
71
+ const DEFAULT_TIMEOUT_MS = 2 * 60 * 1000
72
+ const MAX_OUTPUT_BYTES = 4 * 1024 * 1024
73
+ const COMPLETION_GRACE_MS = 3_000
74
+ const TERMINATE_GRACE_MS = 3_000
75
+ const KILL_CONFIRM_MS = 5_000
76
+ const DESCENDANT_EXIT_GRACE_MS = 100
77
+
78
+ export function createCommandSupervisor(options?: {
79
+ commandStatePath?: string
80
+ leaseId?: string
81
+ completionGraceMs?: number
82
+ terminateGraceMs?: number
83
+ killConfirmMs?: number
84
+ }): CommandSupervisor {
85
+ const commandStatePath = options?.commandStatePath?.trim() || null
86
+ const leaseId = options?.leaseId?.trim() || null
87
+ if (Boolean(commandStatePath) !== Boolean(leaseId)) {
88
+ throw new Error('Git command supervision requires both a command-state path and lease ID.')
89
+ }
90
+ if (commandStatePath && leaseId) readCommandLedger(commandStatePath, leaseId)
91
+
92
+ const active = new Map<number, ActiveCommand>()
93
+ let accepting = true
94
+ let shutdownPromise: Promise<void> | null = null
95
+
96
+ const persistActiveCommands = (): void => {
97
+ if (!commandStatePath || !leaseId) return
98
+ const ledger = readCommandLedger(commandStatePath, leaseId)
99
+ ledger.processes = [...active.values()].map((entry) => entry.ledgerProcess)
100
+ writeCommandLedger(commandStatePath, ledger)
101
+ }
102
+
103
+ const run: CommandRunner = (executable, args, commandOptions) => {
104
+ if (!accepting) {
105
+ return Promise.reject(new Error(`Bosun is shutting down; ${executable} was not started.`))
106
+ }
107
+
108
+ return new Promise((resolve, reject) => {
109
+ const child = spawn(executable, [...args], {
110
+ cwd: commandOptions.cwd,
111
+ env: commandOptions.env,
112
+ detached: process.platform !== 'win32',
113
+ stdio: ['pipe', 'pipe', 'pipe'],
114
+ windowsHide: true
115
+ })
116
+ const stdoutChunks: Buffer[] = []
117
+ const stderrChunks: Buffer[] = []
118
+ let outputBytes = 0
119
+ let settled = false
120
+ let timedOut = false
121
+ let outputLimitExceeded = false
122
+ let pendingError: Error | null = null
123
+
124
+ let resolveClosed = () => {}
125
+ const closed = new Promise<void>((resolveClose) => {
126
+ resolveClosed = resolveClose
127
+ })
128
+ const pid = child.pid
129
+ if (pid !== undefined) {
130
+ active.set(pid, {
131
+ child,
132
+ closed,
133
+ resolveClosed,
134
+ ledgerProcess: {
135
+ pid,
136
+ processGroupId: pid,
137
+ executable,
138
+ startedAt: new Date().toISOString()
139
+ }
140
+ })
141
+ try {
142
+ persistActiveCommands()
143
+ } catch (error) {
144
+ pendingError = asError(error)
145
+ signalChild(child, 'SIGKILL')
146
+ }
147
+ }
148
+
149
+ const timer = setTimeout(() => {
150
+ timedOut = true
151
+ signalChild(child, 'SIGKILL')
152
+ }, commandOptions.timeoutMs ?? DEFAULT_TIMEOUT_MS)
153
+ timer.unref()
154
+
155
+ const capture = (target: Buffer[], chunk: Buffer) => {
156
+ outputBytes += chunk.length
157
+ if (outputBytes > MAX_OUTPUT_BYTES) {
158
+ outputLimitExceeded = true
159
+ signalChild(child, 'SIGKILL')
160
+ return
161
+ }
162
+ target.push(chunk)
163
+ }
164
+
165
+ child.stdout.on('data', (chunk: Buffer) => capture(stdoutChunks, chunk))
166
+ child.stderr.on('data', (chunk: Buffer) => capture(stderrChunks, chunk))
167
+ child.stdin.on('error', (error: NodeJS.ErrnoException) => {
168
+ if (error.code === 'EPIPE' || settled) return
169
+ pendingError = error
170
+ signalChild(child, 'SIGKILL')
171
+ })
172
+ child.once('error', (error) => {
173
+ pendingError = error
174
+ if (child.pid === undefined) void finish(null, null)
175
+ else {
176
+ signalChild(child, 'SIGKILL')
177
+ }
178
+ })
179
+ child.once('close', (code, signal) => void finish(code, signal))
180
+
181
+ if (commandOptions.input === undefined) child.stdin.end()
182
+ else child.stdin.end(commandOptions.input)
183
+
184
+ async function finish(code: number | null, signal: NodeJS.Signals | null): Promise<void> {
185
+ if (settled) return
186
+ settled = true
187
+ clearTimeout(timer)
188
+ let processGroupError: Error | null = null
189
+ if (pid !== undefined && process.platform !== 'win32') {
190
+ await waitForProcessGroupExit(pid, DESCENDANT_EXIT_GRACE_MS)
191
+ if (isProcessGroupAlive(pid)) {
192
+ const backgroundProcessError = new Error(
193
+ `${executable} left background processes in repository command group ${pid}; Bosun terminated them.`
194
+ )
195
+ pendingError ??= backgroundProcessError
196
+ signalProcessGroup(pid, 'SIGTERM')
197
+ await waitForProcessGroupExit(pid, options?.terminateGraceMs ?? TERMINATE_GRACE_MS)
198
+ if (isProcessGroupAlive(pid)) {
199
+ signalProcessGroup(pid, 'SIGKILL')
200
+ await waitForProcessGroupExit(pid, options?.killConfirmMs ?? KILL_CONFIRM_MS)
201
+ }
202
+ if (isProcessGroupAlive(pid)) {
203
+ processGroupError = new Error(
204
+ `Bosun could not confirm termination of repository command process group ${pid}.`
205
+ )
206
+ }
207
+ }
208
+ }
209
+
210
+ if (processGroupError) {
211
+ pendingError = processGroupError
212
+ reject(processGroupError)
213
+ return
214
+ }
215
+ if (pid !== undefined) active.delete(pid)
216
+ let ledgerError: Error | null = null
217
+ try {
218
+ persistActiveCommands()
219
+ } catch (error) {
220
+ ledgerError = asError(error)
221
+ }
222
+ resolveClosed()
223
+
224
+ if (ledgerError) {
225
+ reject(ledgerError)
226
+ return
227
+ }
228
+ if (pendingError) {
229
+ reject(pendingError)
230
+ return
231
+ }
232
+ if (timedOut) {
233
+ reject(
234
+ new Error(
235
+ `${executable} timed out after ${commandOptions.timeoutMs ?? DEFAULT_TIMEOUT_MS} ms`
236
+ )
237
+ )
238
+ return
239
+ }
240
+ if (outputLimitExceeded) {
241
+ reject(new Error(`${executable} produced more than ${MAX_OUTPUT_BYTES} bytes of output`))
242
+ return
243
+ }
244
+
245
+ const stdoutBuffer = Buffer.concat(stdoutChunks)
246
+ const stderrBuffer = Buffer.concat(stderrChunks)
247
+ const result: CommandResult = {
248
+ stdout: stdoutBuffer.toString('utf8'),
249
+ stderr: stderrBuffer.toString('utf8'),
250
+ stdoutBuffer,
251
+ stderrBuffer,
252
+ exitCode: code ?? (signal ? 128 : 1)
253
+ }
254
+ if (!(commandOptions.acceptedExitCodes ?? [0]).includes(result.exitCode)) {
255
+ reject(new CommandError(executable, args, result))
256
+ return
257
+ }
258
+ resolve(result)
259
+ }
260
+ })
261
+ }
262
+
263
+ const shutdown = (): Promise<void> => {
264
+ if (shutdownPromise) return shutdownPromise
265
+ accepting = false
266
+ shutdownPromise = (async () => {
267
+ // Starting the next command is already forbidden. Give a command that is currently at a
268
+ // Give the current Git/pnpm boundary a short opportunity to finish naturally, then stop
269
+ // the whole process group. Once every group is confirmed dead and the ledger is empty,
270
+ // shutdown is safe; interrupted operations themselves still reject their caller.
271
+ await waitForActiveCommands(active, options?.completionGraceMs ?? COMPLETION_GRACE_MS)
272
+ for (const command of active.values()) signalChild(command.child, 'SIGTERM')
273
+ await waitForActiveCommands(active, options?.terminateGraceMs ?? TERMINATE_GRACE_MS)
274
+
275
+ if (active.size > 0) {
276
+ for (const command of active.values()) signalChild(command.child, 'SIGKILL')
277
+ await waitForActiveCommands(active, options?.killConfirmMs ?? KILL_CONFIRM_MS)
278
+ }
279
+
280
+ if (active.size > 0) {
281
+ throw new Error(
282
+ `Bosun could not confirm termination of repository command PIDs ${[...active.keys()].join(', ')}.`
283
+ )
284
+ }
285
+ persistActiveCommands()
286
+ })()
287
+ return shutdownPromise
288
+ }
289
+
290
+ return { run, shutdown, activeCount: () => active.size }
291
+ }
292
+
293
+ const defaultSupervisor = createCommandSupervisor()
294
+ export const runCommand: CommandRunner = defaultSupervisor.run
295
+
296
+ async function waitForActiveCommands(
297
+ active: Map<number, ActiveCommand>,
298
+ timeoutMs: number
299
+ ): Promise<void> {
300
+ const closures = [...active.values()].map((entry) => entry.closed)
301
+ if (closures.length === 0) return
302
+ await Promise.race([
303
+ Promise.allSettled(closures),
304
+ new Promise<void>((resolve) => {
305
+ const timer = setTimeout(resolve, timeoutMs)
306
+ timer.unref()
307
+ })
308
+ ])
309
+ }
310
+
311
+ function signalChild(child: ChildProcessWithoutNullStreams, signal: NodeJS.Signals): boolean {
312
+ const pid = child.pid
313
+ if (pid === undefined) return false
314
+ if (process.platform !== 'win32') return signalProcessGroup(pid, signal)
315
+ try {
316
+ return child.kill(signal)
317
+ } catch (error) {
318
+ return isNodeError(error, 'ESRCH')
319
+ }
320
+ }
321
+
322
+ function signalProcessGroup(processGroupId: number, signal: NodeJS.Signals): boolean {
323
+ try {
324
+ process.kill(-processGroupId, signal)
325
+ return true
326
+ } catch (error) {
327
+ return isNodeError(error, 'ESRCH')
328
+ }
329
+ }
330
+
331
+ function isProcessGroupAlive(processGroupId: number): boolean {
332
+ try {
333
+ process.kill(-processGroupId, 0)
334
+ return true
335
+ } catch (error) {
336
+ return !isNodeError(error, 'ESRCH')
337
+ }
338
+ }
339
+
340
+ async function waitForProcessGroupExit(processGroupId: number, timeoutMs: number): Promise<void> {
341
+ const deadline = Date.now() + timeoutMs
342
+ while (isProcessGroupAlive(processGroupId) && Date.now() < deadline) {
343
+ await new Promise((resolve) => setTimeout(resolve, 20))
344
+ }
345
+ }
346
+
347
+ function readCommandLedger(path: string, expectedLeaseId: string): CommandLedger {
348
+ const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
349
+ if (!isCommandLedger(parsed) || parsed.leaseId !== expectedLeaseId) {
350
+ throw new Error(`Bosun cannot verify repository command ownership at ${path}.`)
351
+ }
352
+ return parsed
353
+ }
354
+
355
+ function writeCommandLedger(path: string, ledger: CommandLedger): void {
356
+ const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`
357
+ let operationError: Error | null = null
358
+ try {
359
+ writeFileSync(temporaryPath, `${JSON.stringify(ledger)}\n`, {
360
+ encoding: 'utf8',
361
+ flag: 'wx',
362
+ mode: 0o600
363
+ })
364
+ renameSync(temporaryPath, path)
365
+ } catch (error) {
366
+ operationError = asError(error)
367
+ }
368
+ try {
369
+ unlinkSync(temporaryPath)
370
+ } catch (error) {
371
+ if (!isNodeError(error, 'ENOENT')) operationError = asError(error)
372
+ }
373
+ if (operationError) throw operationError
374
+ }
375
+
376
+ function isCommandLedger(value: unknown): value is CommandLedger {
377
+ if (!value || typeof value !== 'object') return false
378
+ const ledger = value as Partial<CommandLedger>
379
+ return (
380
+ ledger.version === 1 &&
381
+ typeof ledger.leaseId === 'string' &&
382
+ typeof ledger.launchId === 'string' &&
383
+ typeof ledger.repositoryRoot === 'string' &&
384
+ Array.isArray(ledger.processes) &&
385
+ ledger.processes.every(
386
+ (entry) =>
387
+ entry &&
388
+ typeof entry === 'object' &&
389
+ Number.isSafeInteger(entry.pid) &&
390
+ entry.pid > 0 &&
391
+ Number.isSafeInteger(entry.processGroupId) &&
392
+ entry.processGroupId > 0 &&
393
+ typeof entry.executable === 'string' &&
394
+ typeof entry.startedAt === 'string'
395
+ )
396
+ )
397
+ }
398
+
399
+ function asError(error: unknown): Error {
400
+ return error instanceof Error ? error : new Error(String(error))
401
+ }
402
+
403
+ function isNodeError(error: unknown, code: string): boolean {
404
+ return error instanceof Error && 'code' in error && error.code === code
405
+ }
@@ -0,0 +1,45 @@
1
+ import path from 'node:path'
2
+
3
+ import { describe, expect, it } from 'vitest'
4
+
5
+ import { readGitModeConfig } from './config'
6
+
7
+ describe('readGitModeConfig', () => {
8
+ it('does not require Git settings while the launcher has not enabled Git mode', () => {
9
+ expect(readGitModeConfig({ env: {}, projectDir: '/tmp/project' })).toMatchObject({
10
+ enabled: false,
11
+ projectDir: path.resolve('/tmp/project')
12
+ })
13
+ })
14
+
15
+ it('reads the normalized launcher-only environment contract', () => {
16
+ const config = readGitModeConfig({
17
+ env: {
18
+ BOSUN_GIT_MODE: 'true',
19
+ BOSUN_CURRENT_BRANCH: 'next-release',
20
+ BOSUN_PERSONAL_BRANCH: 'sb/bosun',
21
+ BOSUN_GIT_ACTIVATION_IDLE_MS: '1234',
22
+ BOSUN_GIT_REPOSITORY_ROOT: '/tmp/project',
23
+ BOSUN_GIT_CURRENT_UPSTREAM: 'origin/next-release',
24
+ BOSUN_GIT_CURRENT_UPSTREAM_REMOTE: 'origin',
25
+ BOSUN_GIT_CURRENT_UPSTREAM_BRANCH: 'next-release',
26
+ BOSUN_GIT_PERSONAL_UPSTREAM: 'origin/sb/bosun',
27
+ BOSUN_GIT_PERSONAL_UPSTREAM_REMOTE: 'origin',
28
+ BOSUN_GIT_PERSONAL_UPSTREAM_BRANCH: 'sb/bosun',
29
+ BOSUN_GIT_LAUNCH_ID: 'launch-id',
30
+ BOSUN_GIT_RUNTIME_STATE_PATH: '/tmp/runtime/state.json',
31
+ BOSUN_GIT_ACTIVATION_RESTART_EXIT_CODE: '76'
32
+ }
33
+ })
34
+
35
+ expect(config).toMatchObject({
36
+ enabled: true,
37
+ idleMs: 1234,
38
+ currentBranch: 'next-release',
39
+ personalBranch: 'sb/bosun',
40
+ currentUpstreamRemote: 'origin',
41
+ personalUpstreamRemote: 'origin',
42
+ restartExitCode: 76
43
+ })
44
+ })
45
+ })
@@ -0,0 +1,90 @@
1
+ import path from 'node:path'
2
+
3
+ import type { GitModeConfig } from './types'
4
+
5
+ const DEFAULT_IDLE_MS = 10 * 60 * 1000
6
+ const DEFAULT_RESTART_EXIT_CODE = 76
7
+
8
+ export function readGitModeConfig(options?: {
9
+ env?: NodeJS.ProcessEnv
10
+ projectDir?: string
11
+ }): GitModeConfig {
12
+ const env = options?.env ?? process.env
13
+ const enabled = env.BOSUN_GIT_MODE === 'true'
14
+ const projectDir = path.resolve(
15
+ enabled
16
+ ? requireValue(env, 'BOSUN_GIT_REPOSITORY_ROOT')
17
+ : (options?.projectDir ?? process.cwd())
18
+ )
19
+
20
+ if (!enabled) {
21
+ return {
22
+ enabled: false,
23
+ projectDir,
24
+ currentBranch: '',
25
+ personalBranch: '',
26
+ idleMs: DEFAULT_IDLE_MS,
27
+ currentUpstream: '',
28
+ currentUpstreamRemote: '',
29
+ currentUpstreamBranch: '',
30
+ personalUpstream: '',
31
+ personalUpstreamRemote: '',
32
+ personalUpstreamBranch: '',
33
+ launchId: '',
34
+ runtimeStatePath: '',
35
+ restartExitCode: DEFAULT_RESTART_EXIT_CODE
36
+ }
37
+ }
38
+
39
+ const idleMs = readPositiveInteger(env.BOSUN_GIT_ACTIVATION_IDLE_MS, DEFAULT_IDLE_MS, {
40
+ name: 'BOSUN_GIT_ACTIVATION_IDLE_MS',
41
+ allowZero: true
42
+ })
43
+ const restartExitCode = readPositiveInteger(
44
+ env.BOSUN_GIT_ACTIVATION_RESTART_EXIT_CODE,
45
+ DEFAULT_RESTART_EXIT_CODE,
46
+ { name: 'BOSUN_GIT_ACTIVATION_RESTART_EXIT_CODE', max: 255 }
47
+ )
48
+
49
+ return {
50
+ enabled: true,
51
+ projectDir,
52
+ currentBranch: requireValue(env, 'BOSUN_CURRENT_BRANCH'),
53
+ personalBranch: requireValue(env, 'BOSUN_PERSONAL_BRANCH'),
54
+ idleMs,
55
+ currentUpstream: requireValue(env, 'BOSUN_GIT_CURRENT_UPSTREAM'),
56
+ currentUpstreamRemote: requireValue(env, 'BOSUN_GIT_CURRENT_UPSTREAM_REMOTE'),
57
+ currentUpstreamBranch: requireValue(env, 'BOSUN_GIT_CURRENT_UPSTREAM_BRANCH'),
58
+ personalUpstream: requireValue(env, 'BOSUN_GIT_PERSONAL_UPSTREAM'),
59
+ personalUpstreamRemote: requireValue(env, 'BOSUN_GIT_PERSONAL_UPSTREAM_REMOTE'),
60
+ personalUpstreamBranch: requireValue(env, 'BOSUN_GIT_PERSONAL_UPSTREAM_BRANCH'),
61
+ launchId: requireValue(env, 'BOSUN_GIT_LAUNCH_ID'),
62
+ runtimeStatePath: path.resolve(requireValue(env, 'BOSUN_GIT_RUNTIME_STATE_PATH')),
63
+ restartExitCode
64
+ }
65
+ }
66
+
67
+ function requireValue(env: NodeJS.ProcessEnv, name: string): string {
68
+ const value = env[name]?.trim()
69
+ if (!value) throw new Error(`${name} is required when BOSUN_GIT_MODE=true`)
70
+ return value
71
+ }
72
+
73
+ function readPositiveInteger(
74
+ raw: string | undefined,
75
+ fallback: number,
76
+ options: { name: string; allowZero?: boolean; max?: number }
77
+ ): number {
78
+ if (raw === undefined || raw === '') return fallback
79
+
80
+ const value = Number(raw)
81
+ const minimum = options.allowZero ? 0 : 1
82
+ if (
83
+ !Number.isSafeInteger(value) ||
84
+ value < minimum ||
85
+ value > (options.max ?? Number.MAX_SAFE_INTEGER)
86
+ ) {
87
+ throw new Error(`Invalid ${options.name}: ${raw}`)
88
+ }
89
+ return value
90
+ }
@@ -0,0 +1,20 @@
1
+ export { readGitModeConfig } from './config'
2
+ export { createGitModeRouter } from './router'
3
+ export {
4
+ createGitModeService,
5
+ createPullRequestUrl,
6
+ parseGitHubRepository,
7
+ type GitModeServiceDependencies
8
+ } from './service'
9
+ export type {
10
+ ActivationReason,
11
+ ChangeAction,
12
+ ChangeChoice,
13
+ GitModeConfig,
14
+ GitModeFatal,
15
+ GitModeFatalKind,
16
+ GitModePhase,
17
+ GitModePreparationStage,
18
+ GitModeService,
19
+ GitModeStatus
20
+ } from './types'
@@ -0,0 +1,116 @@
1
+ import { mkdir, mkdtemp, readFile, rm, unlink, writeFile } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import path from 'node:path'
4
+
5
+ import { afterEach, describe, expect, it, vi } from 'vitest'
6
+
7
+ import { RepositoryWriteGate, RepositoryWritesPausedError } from './repository-write-gate'
8
+
9
+ const temporaryDirectories: string[] = []
10
+
11
+ afterEach(async () => {
12
+ await Promise.all(
13
+ temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true }))
14
+ )
15
+ })
16
+
17
+ describe('RepositoryWriteGate', () => {
18
+ it('locks out new cross-process writes and drains a previously admitted write', async () => {
19
+ const config = await createConfig('lease-a', 'launch-a')
20
+ const backendGate = new RepositoryWriteGate(config)
21
+ const frontendGate = new RepositoryWriteGate(config)
22
+ await backendGate.initializeBackend()
23
+
24
+ let releaseWrite!: () => void
25
+ let markStarted!: () => void
26
+ const started = new Promise<void>((resolve) => (markStarted = resolve))
27
+ const write = frontendGate.withWriteAdmission(async () => {
28
+ markStarted()
29
+ await new Promise<void>((resolve) => (releaseWrite = resolve))
30
+ })
31
+ await started
32
+
33
+ const drained = vi.fn()
34
+ const locking = backendGate.lockAndDrain().then(drained)
35
+ await new Promise((resolve) => setTimeout(resolve, 50))
36
+ expect(drained).not.toHaveBeenCalled()
37
+ await expect(frontendGate.withWriteAdmission(async () => {})).rejects.toBeInstanceOf(
38
+ RepositoryWritesPausedError
39
+ )
40
+
41
+ releaseWrite()
42
+ await Promise.all([write, locking])
43
+ expect(drained).toHaveBeenCalledOnce()
44
+ })
45
+
46
+ it('idempotently unlocks its owned gate and admits writes again', async () => {
47
+ const config = await createConfig('lease-a', 'launch-a')
48
+ const backendGate = new RepositoryWriteGate(config)
49
+ const frontendGate = new RepositoryWriteGate(config)
50
+ await backendGate.initializeBackend()
51
+ await backendGate.lockAndDrain()
52
+
53
+ await expect(frontendGate.withWriteAdmission(async () => {})).rejects.toBeInstanceOf(
54
+ RepositoryWritesPausedError
55
+ )
56
+ await backendGate.unlock()
57
+ await backendGate.unlock()
58
+
59
+ await expect(frontendGate.withWriteAdmission(async () => 'written')).resolves.toBe('written')
60
+ })
61
+
62
+ it('refuses to remove a lock whose ownership changed while it was held', async () => {
63
+ const config = await createConfig('lease-a', 'launch-a')
64
+ const gate = new RepositoryWriteGate(config)
65
+ await gate.initializeBackend()
66
+ await gate.lockAndDrain()
67
+ const lockPath = path.join(config.projectDir, '.git', 'bosun-write-gate.lock.json')
68
+ const replacement = {
69
+ version: 1,
70
+ leaseId: 'different-lease',
71
+ launchId: 'different-launch',
72
+ pid: process.pid,
73
+ createdAt: new Date().toISOString()
74
+ }
75
+ await writeFile(lockPath, `${JSON.stringify(replacement)}\n`)
76
+
77
+ await expect(gate.unlock()).rejects.toThrow(/no longer owns/)
78
+ expect(JSON.parse(await readFile(lockPath, 'utf8'))).toEqual(replacement)
79
+ })
80
+
81
+ it('fails closed if its owned lock disappears before unlock', async () => {
82
+ const config = await createConfig('lease-a', 'launch-a')
83
+ const gate = new RepositoryWriteGate(config)
84
+ await gate.initializeBackend()
85
+ await gate.lockAndDrain()
86
+ await unlink(path.join(config.projectDir, '.git', 'bosun-write-gate.lock.json'))
87
+
88
+ await expect(gate.unlock()).rejects.toThrow(/no longer owns/)
89
+ })
90
+
91
+ it('recovers a persisted lock after a new launcher lease following shutdown', async () => {
92
+ const firstConfig = await createConfig('lease-before-shutdown', 'launch-before-shutdown')
93
+ const firstBackend = new RepositoryWriteGate(firstConfig)
94
+ await firstBackend.initializeBackend()
95
+ await firstBackend.lockAndDrain()
96
+
97
+ const restartedConfig = {
98
+ ...firstConfig,
99
+ leaseId: 'lease-after-restart',
100
+ launchId: 'launch-after-restart'
101
+ }
102
+ const restartedBackend = new RepositoryWriteGate(restartedConfig)
103
+ await restartedBackend.initializeBackend()
104
+
105
+ await expect(
106
+ new RepositoryWriteGate(restartedConfig).withWriteAdmission(async () => 'written')
107
+ ).resolves.toBe('written')
108
+ })
109
+ })
110
+
111
+ async function createConfig(leaseId: string, launchId: string) {
112
+ const projectDir = await mkdtemp(path.join(tmpdir(), 'bosun-write-gate-'))
113
+ temporaryDirectories.push(projectDir)
114
+ await mkdir(path.join(projectDir, '.git'))
115
+ return { enabled: true, projectDir, leaseId, launchId }
116
+ }