@maycombhq/atticus 2.0.0-beta.2 → 2.0.0-beta.22

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 (284) hide show
  1. package/dist/bin.js +8 -4
  2. package/dist/chunk-23XVSEV5.js +49 -0
  3. package/dist/{chunk-ZDPAS56H.js → chunk-24OXGB3P.js} +8 -2
  4. package/dist/{chunk-7EOBHUMC.js → chunk-24XLDU72.js} +4 -3
  5. package/dist/{chunk-CR3MTH32.js → chunk-2F4CYZKJ.js} +86 -11
  6. package/dist/{chunk-6IU67AEK.js → chunk-2GF7OLF3.js} +23 -15
  7. package/dist/chunk-2IIMFOPM.js +41 -0
  8. package/dist/chunk-2LGRMVCA.js +617 -0
  9. package/dist/{chunk-XPCM366L.js → chunk-2MMCFP5C.js} +10 -8
  10. package/dist/{chunk-JBTK5OP2.js → chunk-2Q56PQPS.js} +6 -4
  11. package/dist/{chunk-Z6LF7HQN.js → chunk-2Q5EDGEO.js} +7 -5
  12. package/dist/{chunk-3USYZGRF.js → chunk-2RLKFUUL.js} +38 -27
  13. package/dist/chunk-2SKB4O2E.js +59 -0
  14. package/dist/{chunk-SP2I63OO.js → chunk-34S2WGOB.js} +87 -4
  15. package/dist/{chunk-GQ4N4WKF.js → chunk-3NFFJL5D.js} +33 -27
  16. package/dist/{chunk-SJRGMG6A.js → chunk-3NWDLGG6.js} +28 -25
  17. package/dist/{chunk-4WBVUHXN.js → chunk-3RM4UK6W.js} +20 -18
  18. package/dist/chunk-42SIDKEU.js +583 -0
  19. package/dist/{chunk-GLUX4EEP.js → chunk-4EURQWQD.js} +20 -15
  20. package/dist/chunk-4U276ICE.js +39 -0
  21. package/dist/{chunk-ZMFPW66A.js → chunk-53T4O55J.js} +4 -3
  22. package/dist/{chunk-6J5OKA4Y.js → chunk-57TDNMI3.js} +17 -15
  23. package/dist/chunk-5BD2KYLE.js +57 -0
  24. package/dist/{chunk-7EFILTCI.js → chunk-5F7GFN2F.js} +28 -0
  25. package/dist/chunk-5NMKONLP.js +95 -0
  26. package/dist/{chunk-7ZUIMTOU.js → chunk-5SJZW52K.js} +26 -24
  27. package/dist/{chunk-4RS5KZQD.js → chunk-63YRMR6J.js} +6 -5
  28. package/dist/chunk-66PPNOON.js +62 -0
  29. package/dist/chunk-6H3Z5JHH.js +64 -0
  30. package/dist/{chunk-CEXSU4S6.js → chunk-6MUGR4LS.js} +775 -89
  31. package/dist/{chunk-QSXK2RGH.js → chunk-6OJQOPUN.js} +5 -3
  32. package/dist/{chunk-MVZVGYXH.js → chunk-6PPOWY5E.js} +84 -14
  33. package/dist/{chunk-7EKXJZ2Y.js → chunk-6ZGLAACW.js} +299 -38
  34. package/dist/chunk-6ZJ5A7AF.js +222 -0
  35. package/dist/{chunk-L24C55VN.js → chunk-745DAZ4L.js} +142 -30
  36. package/dist/{chunk-Q4D5X7QB.js → chunk-7ASNL4Q4.js} +46 -24
  37. package/dist/chunk-7BR7IPOH.js +35 -0
  38. package/dist/{chunk-7WD63HU2.js → chunk-7DOR27HL.js} +6 -6
  39. package/dist/chunk-7FTTA3HD.js +341 -0
  40. package/dist/{chunk-OLQJTJJU.js → chunk-7GJS6DNX.js} +12 -3
  41. package/dist/{chunk-7JUKDRSZ.js → chunk-7IGKHZ4Z.js} +347 -226
  42. package/dist/{chunk-G2B4ZQGH.js → chunk-7IOWA5D5.js} +11 -6
  43. package/dist/{chunk-EBEW3XXQ.js → chunk-7JITZFJC.js} +7 -5
  44. package/dist/chunk-7MJSQTL5.js +174 -0
  45. package/dist/{chunk-I4JRCD6M.js → chunk-7UOMVMFG.js} +25 -17
  46. package/dist/{chunk-YY6ZQQBP.js → chunk-AFWUDXEM.js} +19 -7
  47. package/dist/chunk-AFZ3QA6W.js +47 -0
  48. package/dist/chunk-AJQ72EBM.js +420 -0
  49. package/dist/chunk-AJYXNTNZ.js +317 -0
  50. package/dist/chunk-AVTBS4OW.js +49 -0
  51. package/dist/{chunk-FHH4I4ZV.js → chunk-BDKC6H5P.js} +12 -6
  52. package/dist/{chunk-IPKHE5D6.js → chunk-BEVTIBRH.js} +1526 -875
  53. package/dist/chunk-BHT7QK4G.js +162 -0
  54. package/dist/{chunk-6JUBZBMW.js → chunk-BIGDQDHD.js} +6 -4
  55. package/dist/{chunk-2BFCZ3RK.js → chunk-BO4KX37X.js} +20 -18
  56. package/dist/chunk-BPK5P77O.js +115 -0
  57. package/dist/chunk-BQ4SQQC6.js +59 -0
  58. package/dist/chunk-BQGDB6ZL.js +7342 -0
  59. package/dist/{chunk-2ANOROX3.js → chunk-BTGPE6CR.js} +11 -9
  60. package/dist/chunk-BTY3HZBO.js +93 -0
  61. package/dist/{chunk-DPZHZQKE.js → chunk-BXYMYEJ4.js} +44 -10
  62. package/dist/{chunk-2RMYG36Q.js → chunk-C3RY6KU6.js} +309 -346
  63. package/dist/chunk-C4R4JLHE.js +40 -0
  64. package/dist/chunk-CADWLE2T.js +18 -0
  65. package/dist/chunk-CBGGOQWU.js +82 -0
  66. package/dist/chunk-CMDXGQTS.js +47 -0
  67. package/dist/{chunk-MC3H7736.js → chunk-CQDGOPGH.js} +58 -18
  68. package/dist/{chunk-HFQCFSJR.js → chunk-CX4RZ2KS.js} +4 -3
  69. package/dist/{chunk-PAMPPUR6.js → chunk-CXJ6FUKN.js} +1852 -207
  70. package/dist/{chunk-ABWHYWNI.js → chunk-D73UEXYW.js} +3 -3
  71. package/dist/chunk-DCZWQLYT.js +1205 -0
  72. package/dist/chunk-DLOBCJL6.js +48 -0
  73. package/dist/{chunk-4BN53TG3.js → chunk-DOBQKHIF.js} +31 -23
  74. package/dist/chunk-E3ISYXO3.js +423 -0
  75. package/dist/{chunk-7SW2QWLU.js → chunk-E6ZIEVOI.js} +16 -13
  76. package/dist/chunk-EBL2R3YG.js +43 -0
  77. package/dist/{chunk-AIOQPLA5.js → chunk-EGQRREQH.js} +41 -15
  78. package/dist/chunk-EHA7RMZ3.js +58 -0
  79. package/dist/{chunk-M766ZJ6W.js → chunk-EIPTLPFC.js} +38 -27
  80. package/dist/chunk-EMRIQUND.js +517 -0
  81. package/dist/{chunk-DFBCSHVT.js → chunk-EPCJQQWD.js} +6 -4
  82. package/dist/{chunk-RZPYKDWS.js → chunk-F3P7N7SF.js} +1 -1
  83. package/dist/{chunk-R77KLRB3.js → chunk-FFLNS6VP.js} +21 -18
  84. package/dist/chunk-FFTRTL6R.js +26 -0
  85. package/dist/{chunk-IHMLFRVV.js → chunk-FPJPLKDW.js} +62 -16
  86. package/dist/{chunk-JCZIKA4C.js → chunk-FSMSFDLD.js} +5 -4
  87. package/dist/{chunk-AVJDSJ2F.js → chunk-FUJM6RTF.js} +8 -7
  88. package/dist/chunk-FZOJFH7Q.js +132 -0
  89. package/dist/{chunk-QRAOANID.js → chunk-G6X77UJZ.js} +23 -21
  90. package/dist/{chunk-3ALD2W2A.js → chunk-GCWRWSU7.js} +10 -4
  91. package/dist/chunk-GIW4ZVDB.js +39 -0
  92. package/dist/{chunk-B2WLAWNJ.js → chunk-GUX3PTYM.js} +3 -3
  93. package/dist/{chunk-DPUYDDQG.js → chunk-GZY6RUZ6.js} +25 -3
  94. package/dist/chunk-H4VO6WG3.js +10 -0
  95. package/dist/{chunk-MMOSZPWJ.js → chunk-H5HBGNXU.js} +5 -4
  96. package/dist/chunk-H7KM7LZE.js +229 -0
  97. package/dist/{chunk-AA6YULYB.js → chunk-HNM2YQT2.js} +7 -7
  98. package/dist/{chunk-3C7EYRFZ.js → chunk-HYU5VLGA.js} +1 -1
  99. package/dist/{chunk-XY3P7JW7.js → chunk-IDKCWSM5.js} +1032 -172
  100. package/dist/chunk-IFKEI4SA.js +237 -0
  101. package/dist/{chunk-F7HYOUFF.js → chunk-IGOE6KPG.js} +67 -22
  102. package/dist/{chunk-TDD55CJH.js → chunk-IH2XURVQ.js} +6 -5
  103. package/dist/chunk-INQHESFB.js +38 -0
  104. package/dist/chunk-IP74LCOQ.js +57 -0
  105. package/dist/chunk-IPHHDDC6.js +45 -0
  106. package/dist/{chunk-T6AIP7H5.js → chunk-ISUAAMGS.js} +110 -153
  107. package/dist/{chunk-A7UPUQFY.js → chunk-IUPG3UKI.js} +624 -319
  108. package/dist/{chunk-L6SGQIR3.js → chunk-IVBOQAGB.js} +7 -3
  109. package/dist/{chunk-27NKUS3X.js → chunk-J2JAKCIW.js} +2205 -796
  110. package/dist/{chunk-V2YTAIQO.js → chunk-J3IOLXWM.js} +24 -7
  111. package/dist/chunk-JA5FI3QV.js +142 -0
  112. package/dist/chunk-JAVI7JSP.js +49 -0
  113. package/dist/{chunk-HLJAXMM4.js → chunk-JB5LINEM.js} +6 -2
  114. package/dist/{chunk-2VHF7OXM.js → chunk-JENSEMXF.js} +1 -1
  115. package/dist/{chunk-6BVONWJ7.js → chunk-JJF6HALY.js} +4 -3
  116. package/dist/chunk-JLUCKAEL.js +38 -0
  117. package/dist/chunk-JRBFANFE.js +221 -0
  118. package/dist/{chunk-R6XF6UR3.js → chunk-KDNTAPLX.js} +4 -4
  119. package/dist/chunk-KFAVQZSX.js +33 -0
  120. package/dist/chunk-KJXD4PNL.js +50 -0
  121. package/dist/chunk-KN5BXMWN.js +44 -0
  122. package/dist/{chunk-PCVDV3UZ.js → chunk-KQGURXFU.js} +9 -7
  123. package/dist/{chunk-WY6PP6AA.js → chunk-KXITGYLF.js} +40 -6
  124. package/dist/{chunk-66YVAGGM.js → chunk-L27SV425.js} +22 -20
  125. package/dist/{chunk-NE2W3OO7.js → chunk-LDLL7LB3.js} +24 -22
  126. package/dist/chunk-LFGTYX5U.js +379 -0
  127. package/dist/{chunk-EYWIDJFU.js → chunk-LGBMTVNT.js} +7 -5
  128. package/dist/{chunk-Q5AMFEWD.js → chunk-LGFL54FP.js} +3 -2
  129. package/dist/{chunk-6TACQN3N.js → chunk-LJXMYKZJ.js} +7 -5
  130. package/dist/{chunk-BKVWTZ6A.js → chunk-LKOR5UET.js} +29 -26
  131. package/dist/chunk-LNBQOP7D.js +294 -0
  132. package/dist/{chunk-O3WI5KWW.js → chunk-LO6P2VYG.js} +2 -2
  133. package/dist/chunk-LQJPN536.js +122 -0
  134. package/dist/{chunk-YEDDOYSC.js → chunk-LYBNHMVZ.js} +29 -5
  135. package/dist/{chunk-A3GY6SOA.js → chunk-LZ7NUC62.js} +20 -18
  136. package/dist/chunk-M32BLWDH.js +37 -0
  137. package/dist/{chunk-VYDBJ73M.js → chunk-M3NANSDZ.js} +17 -15
  138. package/dist/chunk-MFHL5URL.js +31 -0
  139. package/dist/chunk-MFR5OPBD.js +109 -0
  140. package/dist/{chunk-DE76JROX.js → chunk-MG4ZL5RW.js} +32 -20
  141. package/dist/{chunk-N7IXEHK3.js → chunk-MO6P2JZH.js} +10 -8
  142. package/dist/{chunk-65NYPGRC.js → chunk-MUWDB4R4.js} +1 -1
  143. package/dist/chunk-MZCGSXVR.js +34 -0
  144. package/dist/{chunk-MGYZM27B.js → chunk-N3XIYPU3.js} +51 -23
  145. package/dist/chunk-NDCF4R6S.js +172 -0
  146. package/dist/{chunk-DHPACDG7.js → chunk-NR2UJWOC.js} +10 -8
  147. package/dist/chunk-NXJ5URP6.js +51 -0
  148. package/dist/{chunk-UNM6ZOKP.js → chunk-NY2XSR5K.js} +147 -18
  149. package/dist/{chunk-5GDRCDQB.js → chunk-O45HFTFS.js} +3 -2
  150. package/dist/{chunk-ONLS5QA5.js → chunk-O4GXGGBX.js} +14 -12
  151. package/dist/chunk-O5PXQR67.js +249 -0
  152. package/dist/{chunk-PRPPGYZG.js → chunk-O66DM6DJ.js} +6 -4
  153. package/dist/{chunk-KEM4SDDP.js → chunk-O7FDLW7L.js} +11 -8
  154. package/dist/{chunk-T2UYZSCQ.js → chunk-O7XLYEFA.js} +169 -29
  155. package/dist/{chunk-FITWGOJ2.js → chunk-OHNCWBKF.js} +4 -4
  156. package/dist/{chunk-652QWTLZ.js → chunk-OHVTTOPS.js} +3 -3
  157. package/dist/chunk-OMI6FPPN.js +43 -0
  158. package/dist/chunk-ONUPY3H6.js +53 -0
  159. package/dist/chunk-OTLDSZDV.js +69 -0
  160. package/dist/chunk-OUBGGKDU.js +22 -0
  161. package/dist/chunk-OWNCM4ZN.js +330 -0
  162. package/dist/{chunk-YISXLPHN.js → chunk-P2V7QW4D.js} +235 -35
  163. package/dist/{chunk-CNUKCS2T.js → chunk-P32RUW3B.js} +1632 -93
  164. package/dist/{chunk-BWZ77RQT.js → chunk-PAQ2AS5E.js} +1 -1
  165. package/dist/{chunk-ZMAINWBW.js → chunk-PBPBMP2B.js} +4 -3
  166. package/dist/{chunk-ZVM6AKKM.js → chunk-PJSJPMJI.js} +3 -3
  167. package/dist/{chunk-EFHQ3TWP.js → chunk-PKBHS4AA.js} +233 -56
  168. package/dist/{chunk-GT4I4ZWM.js → chunk-PMUAJGLI.js} +20 -16
  169. package/dist/chunk-PPD7HTDP.js +152 -0
  170. package/dist/chunk-PSOFBQWB.js +42 -0
  171. package/dist/{chunk-S2A7YC3Q.js → chunk-Q2TECDIT.js} +10694 -8896
  172. package/dist/chunk-Q4K5WB2W.js +42 -0
  173. package/dist/{chunk-UGLN7QHV.js → chunk-Q6DFNVRJ.js} +14 -12
  174. package/dist/chunk-QA4O2GGC.js +265 -0
  175. package/dist/chunk-QAXW2N3B.js +1216 -0
  176. package/dist/chunk-QBNWF5ZU.js +19 -0
  177. package/dist/chunk-QFWA2DPH.js +224 -0
  178. package/dist/{chunk-23W7VWKR.js → chunk-QKUJMING.js} +1363 -114
  179. package/dist/{chunk-NXFPS2LR.js → chunk-R44CYFPV.js} +16 -13
  180. package/dist/{chunk-CKSITVS4.js → chunk-R7NYHMUU.js} +2687 -786
  181. package/dist/chunk-REWWDVQB.js +162 -0
  182. package/dist/{chunk-JMSCMUCA.js → chunk-RGERPOIV.js} +145 -21
  183. package/dist/{chunk-6JCPI55B.js → chunk-RLDGWLQ7.js} +49 -7
  184. package/dist/chunk-RUGXBPWI.js +384 -0
  185. package/dist/{chunk-CPA6HPVR.js → chunk-RXPJMEOW.js} +18 -12
  186. package/dist/{chunk-3HI6Z55Y.js → chunk-S4PMJU6H.js} +339 -43
  187. package/dist/chunk-S5LSDXV4.js +40 -0
  188. package/dist/chunk-S7FCV7S5.js +51 -0
  189. package/dist/{chunk-5GGOLAKC.js → chunk-SDKLKB6I.js} +28 -26
  190. package/dist/chunk-SFSPGFCV.js +62 -0
  191. package/dist/{chunk-ZNCEKR4J.js → chunk-SFUP7TTO.js} +12 -8
  192. package/dist/{chunk-64EQYWCA.js → chunk-SLXZOMA6.js} +69 -19
  193. package/dist/chunk-SQV37XO2.js +273 -0
  194. package/dist/{chunk-K65BJIMT.js → chunk-SU74NDMQ.js} +19 -17
  195. package/dist/chunk-SYV5LHML.js +165 -0
  196. package/dist/chunk-T4YPZIQN.js +36 -0
  197. package/dist/chunk-T6DQR5TV.js +323 -0
  198. package/dist/{chunk-JIITQXFB.js → chunk-THDMSZ3R.js} +3075 -802
  199. package/dist/{chunk-JGZCG5VM.js → chunk-TNR75OXT.js} +7 -7
  200. package/dist/{chunk-D3PO4DOZ.js → chunk-TUULXQRW.js} +174 -15
  201. package/dist/chunk-TYQBFTBR.js +516 -0
  202. package/dist/chunk-U76OWS3Q.js +49 -0
  203. package/dist/{chunk-YYQN46FY.js → chunk-UCROWG2D.js} +27 -24
  204. package/dist/{chunk-6M5NK3K3.js → chunk-UF4AZOZB.js} +50 -10
  205. package/dist/chunk-UF4GLFBX.js +345 -0
  206. package/dist/{chunk-OT2O76OS.js → chunk-UGUP3ZM6.js} +240 -39
  207. package/dist/chunk-UNAGJYET.js +45 -0
  208. package/dist/{chunk-5WG5YP4T.js → chunk-UOR2DTHH.js} +16 -14
  209. package/dist/{chunk-S3QNRWYR.js → chunk-UPOY4YSQ.js} +14 -12
  210. package/dist/{chunk-EPTBZ3GP.js → chunk-UPS2DGFC.js} +1 -1
  211. package/dist/{chunk-4AK7IBVP.js → chunk-UUU4CLJG.js} +4 -2
  212. package/dist/{chunk-W3BQAZ3Q.js → chunk-UWMJKX5U.js} +14 -12
  213. package/dist/{chunk-QZSX3F7C.js → chunk-UWOJLHFT.js} +27 -23
  214. package/dist/chunk-VBTP56CE.js +535 -0
  215. package/dist/chunk-VCTWPLAO.js +211 -0
  216. package/dist/chunk-VNU26RCO.js +1215 -0
  217. package/dist/{chunk-CSZ4C5N6.js → chunk-VTZMAJ3W.js} +93 -10
  218. package/dist/{chunk-TXLH6WHQ.js → chunk-VW62D6QE.js} +14 -12
  219. package/dist/{chunk-CAQZUNOH.js → chunk-VY72R2XZ.js} +610 -3
  220. package/dist/{chunk-2DIQQJCB.js → chunk-VZALIKIF.js} +18 -15
  221. package/dist/{chunk-TYAID7B7.js → chunk-W62H2XSO.js} +25 -23
  222. package/dist/{chunk-HVUPKCGJ.js → chunk-W7VWI2YH.js} +9 -6
  223. package/dist/{chunk-TANFV3MZ.js → chunk-WERM6KPJ.js} +7 -4
  224. package/dist/{chunk-BOHJZTEQ.js → chunk-WF7B67YE.js} +208 -87
  225. package/dist/{chunk-7FBGJFEE.js → chunk-WG5U3F5V.js} +14 -12
  226. package/dist/{chunk-4NDAAFQT.js → chunk-WJF2CS6M.js} +8 -5
  227. package/dist/{chunk-XAKWMCXI.js → chunk-WKUJ3KPV.js} +4 -4
  228. package/dist/chunk-WMTRHUI5.js +103 -0
  229. package/dist/chunk-WNKPRSGI.js +36 -0
  230. package/dist/chunk-WXFLMQSR.js +223 -0
  231. package/dist/{chunk-RRTKKSLQ.js → chunk-X3WY6MN3.js} +8 -6
  232. package/dist/{chunk-ZCJDOK2S.js → chunk-X4CXA2IL.js} +47 -23
  233. package/dist/{chunk-UBBC2XSG.js → chunk-X5F3Q5LV.js} +560 -105
  234. package/dist/{chunk-C6EKFH7J.js → chunk-XD75VZIH.js} +9 -7
  235. package/dist/chunk-XERYFDB5.js +297 -0
  236. package/dist/chunk-XI2LCCYQ.js +420 -0
  237. package/dist/chunk-Y2EQNWKV.js +933 -0
  238. package/dist/{chunk-LR3R6Z2A.js → chunk-YHCMZC2J.js} +7 -7
  239. package/dist/chunk-YYY3AZFE.js +122 -0
  240. package/dist/{chunk-GDLULHTC.js → chunk-Z5IA3WDR.js} +19 -31
  241. package/dist/{chunk-T6XQL73F.js → chunk-ZFFYNRIO.js} +63 -23
  242. package/dist/{chunk-EDS2L2LD.js → chunk-ZFL7NY4F.js} +56 -27
  243. package/dist/{chunk-YMZ3TNYB.js → chunk-ZGULQAPZ.js} +7 -4
  244. package/dist/{chunk-4SKZDI62.js → chunk-ZIUFM5HT.js} +3 -1
  245. package/dist/{chunk-DKJA76CJ.js → chunk-ZJFRMSJT.js} +15 -13
  246. package/dist/chunk-ZJXLNDH4.js +239 -0
  247. package/dist/chunk-ZQ3ON2VQ.js +252 -0
  248. package/dist/{chunk-HITVBIJS.js → chunk-ZQ7SKESZ.js} +150 -415
  249. package/dist/{chunk-6QSERWXH.js → chunk-ZQXZ2SES.js} +24 -21
  250. package/dist/chunk-ZX57KSEQ.js +39 -0
  251. package/dist/{chunk-I3E7CX4H.js → chunk-ZZK56BLP.js} +5 -3
  252. package/dist/files.json +664 -368
  253. package/dist/packages/naysayer/constitution/constitution.yaml +19 -0
  254. package/dist/packages/sdk/npm/index.js +6194 -2542
  255. package/dist/packages/ui/dist/assets/{elk-api-tma0MCuh.js → elk-api-CIf-0gz5.js} +2 -2
  256. package/dist/packages/ui/dist/assets/index-CoK_Dl0D.js +47 -0
  257. package/dist/packages/ui/dist/assets/index-Dw20IRhQ.css +1 -0
  258. package/dist/packages/ui/dist/index.html +3 -2
  259. package/package.json +5 -5
  260. package/dist/chunk-2BF2EYJV.js +0 -174
  261. package/dist/chunk-42EVSKME.js +0 -37
  262. package/dist/chunk-4BKGMUIK.js +0 -53
  263. package/dist/chunk-5KLGBXG5.js +0 -48
  264. package/dist/chunk-6BNC6EQ4.js +0 -41
  265. package/dist/chunk-6D2CQ5II.js +0 -41
  266. package/dist/chunk-7AVHLZGD.js +0 -45
  267. package/dist/chunk-D46WUV3T.js +0 -33
  268. package/dist/chunk-E36QOJMD.js +0 -47
  269. package/dist/chunk-FQXYPF7E.js +0 -147
  270. package/dist/chunk-GBRFVT63.js +0 -45
  271. package/dist/chunk-ICY36XCH.js +0 -34
  272. package/dist/chunk-J4K3KVLD.js +0 -131
  273. package/dist/chunk-JZU3H6UQ.js +0 -54
  274. package/dist/chunk-NN2ZQEB4.js +0 -38
  275. package/dist/chunk-PCOCACY3.js +0 -385
  276. package/dist/chunk-RTEYOSJY.js +0 -29
  277. package/dist/chunk-RUVFXE2L.js +0 -666
  278. package/dist/chunk-RVH5SUW5.js +0 -69
  279. package/dist/chunk-VN3DP4LQ.js +0 -43
  280. package/dist/chunk-W5BVQF44.js +0 -36
  281. package/dist/chunk-WVPJSRRJ.js +0 -52
  282. package/dist/chunk-XPTOBS2D.js +0 -48
  283. package/dist/packages/ui/dist/assets/index-B54fm9FQ.js +0 -29
  284. package/dist/packages/ui/dist/assets/index-DD_hufQz.css +0 -1
@@ -8,19 +8,22 @@ import {
8
8
  addStoreReference,
9
9
  readStoreMarker,
10
10
  readStoreVia
11
- } from "./chunk-4AK7IBVP.js";
11
+ } from "./chunk-UUU4CLJG.js";
12
12
  import {
13
13
  AdapterError,
14
14
  mergeBlockers,
15
15
  requireApproval
16
- } from "./chunk-7JUKDRSZ.js";
16
+ } from "./chunk-7IGKHZ4Z.js";
17
17
  import {
18
+ outsideProfileSystems,
18
19
  readCatalog
19
- } from "./chunk-27NKUS3X.js";
20
+ } from "./chunk-J2JAKCIW.js";
20
21
  import {
22
+ baseClonePath,
21
23
  createGit,
22
- readChangeRepos
23
- } from "./chunk-EFHQ3TWP.js";
24
+ readChangeRepos,
25
+ writeChangeRepos
26
+ } from "./chunk-PKBHS4AA.js";
24
27
  import {
25
28
  ChangeArtifact,
26
29
  ChangeLifecycle,
@@ -30,6 +33,8 @@ import {
30
33
  Phase,
31
34
  RecordId,
32
35
  TestType,
36
+ askedGapQuestionId,
37
+ changeBranch,
33
38
  changesDirOf,
34
39
  changesRelOf,
35
40
  external_exports,
@@ -37,18 +42,23 @@ import {
37
42
  fastPathOf,
38
43
  handoffBranch,
39
44
  isAssistantResolution,
45
+ listQuestions,
40
46
  loadOpenSpecSettings,
41
47
  loadOrgPack,
42
48
  openspecDirOf,
43
49
  orgPackDirOf,
50
+ questionsRelOf,
44
51
  readFlow,
45
- require_dist,
52
+ readQuestion,
46
53
  specFlowFiles,
47
54
  toCanonicalYaml
48
- } from "./chunk-S2A7YC3Q.js";
55
+ } from "./chunk-Q2TECDIT.js";
49
56
  import {
50
57
  writeFileAtomic
51
- } from "./chunk-D3PO4DOZ.js";
58
+ } from "./chunk-TUULXQRW.js";
59
+ import {
60
+ require_dist
61
+ } from "./chunk-BQGDB6ZL.js";
52
62
  import {
53
63
  __toESM
54
64
  } from "./chunk-TV45EUGM.js";
@@ -142,8 +152,11 @@ var StageStep = external_exports.strictObject({
142
152
  loop: external_exports.string().min(1),
143
153
  /** The command that runs it; `<change>` stands for the change's id. */
144
154
  command: external_exports.string().min(1),
145
- /** Files it writes, relative to the change folder; a folder ends in "/". They ride with the hand-off. */
146
- writes: external_exports.array(external_exports.string().min(1)).default([]),
155
+ /**
156
+ * Files it writes, relative to the change folder; a folder ends in "/", and a "*" in the last
157
+ * part matches any name in that folder ("tasks/*.prompt.md"). They ride with the hand-off.
158
+ */
159
+ writes: external_exports.array(external_exports.string().min(1).refine((w) => !/(^|[/\\])\.\.([/\\]|$)/.test(w) && !/^([/\\]|[a-zA-Z]:)/.test(w), "a write stays inside the change folder (no .. and no absolute path)").refine((w) => !w.split("/").slice(0, -1).some((part) => part.includes("*")), 'a "*" may only be in the last part of a write (tasks/*.prompt.md, not tasks/*/notes.md)')).default([]),
147
160
  /** The change type's artifacts of the phase that this stage writes: on the stage path, only these are due. */
148
161
  artifacts: external_exports.array(ChangeArtifact).default([])
149
162
  });
@@ -153,7 +166,7 @@ var DEFAULT_STAGES = [
153
166
  phase: "define",
154
167
  loop: "stage/feature.yaml",
155
168
  command: "atticus change feature <change>",
156
- writes: ["feature.yaml", "feature.md", "design/"],
169
+ writes: ["feature.yaml", "feature.md", "proposal.md", "design/"],
157
170
  artifacts: []
158
171
  },
159
172
  {
@@ -161,7 +174,7 @@ var DEFAULT_STAGES = [
161
174
  phase: "design",
162
175
  loop: "stage/architecture.yaml",
163
176
  command: "atticus change architecture <change>",
164
- writes: ["target-flows/"],
177
+ writes: ["target-flows/", "stage/architecture-deploy.yaml", "stage/architecture-reads.yaml"],
165
178
  artifacts: ["design", "data"]
166
179
  },
167
180
  {
@@ -169,10 +182,27 @@ var DEFAULT_STAGES = [
169
182
  phase: "plan",
170
183
  loop: "stage/stories.yaml",
171
184
  command: "atticus plan stories <change>",
172
- writes: ["requests.md", "stage/stories-answer.yaml"],
185
+ writes: [
186
+ "requests.md",
187
+ "proposal.md",
188
+ "needs.yaml",
189
+ "needs.md",
190
+ "specs/",
191
+ "tasks/*.prompt.md",
192
+ "stage/stories-answer.yaml"
193
+ ],
173
194
  artifacts: ["stories", "tasks"]
174
195
  }
175
196
  ];
197
+ var isWritePattern = (w) => w.includes("*");
198
+ function writeMatches(w, path) {
199
+ if (w.endsWith("/"))
200
+ return path.startsWith(w);
201
+ if (!isWritePattern(w))
202
+ return path === w;
203
+ const re = new RegExp(`^${w.split("*").map((x) => x.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join("[^/]*")}$`);
204
+ return re.test(path);
205
+ }
176
206
  var Privacy = external_exports.strictObject({
177
207
  role: RecordId.default("privacy-officer"),
178
208
  classifications: external_exports.array(external_exports.string().min(1)).default(["personal", "pii", "sensitive-personal", "special-category"]),
@@ -530,7 +560,16 @@ var LEADING = [
530
560
  "from",
531
561
  "to"
532
562
  ];
563
+ var isPathLike = (id) => !id || id === "." || id === ".." || /[\\/]/.test(id) || /^[A-Za-z]:/.test(id) || id.includes("\0");
564
+ var isFolderRef = (id) => {
565
+ const parts = id.split("/");
566
+ if (parts.length === 2 && parts[0] === "archive")
567
+ return !isPathLike(parts[1]);
568
+ return !isPathLike(id);
569
+ };
533
570
  function changeFolder(root, id) {
571
+ if (!isFolderRef(id))
572
+ throw new Error(`${id} isn't a change id.`);
534
573
  return join3(changesDirOf(root), id);
535
574
  }
536
575
  function changeFilePath(root, id) {
@@ -553,6 +592,8 @@ function serializeChange(value) {
553
592
  }
554
593
  function changeStore(root) {
555
594
  async function tryRead(id) {
595
+ if (!RecordId.safeParse(id).success)
596
+ return void 0;
556
597
  const path = changeFilePath(root, id);
557
598
  let text;
558
599
  try {
@@ -589,22 +630,33 @@ function changeStore(root) {
589
630
  await writeFileAtomic(path, serializeChange(value));
590
631
  return path;
591
632
  },
633
+ listWithProblems,
592
634
  async list() {
593
- let names;
635
+ return (await listWithProblems()).changes;
636
+ }
637
+ };
638
+ async function listWithProblems() {
639
+ let names;
640
+ try {
641
+ names = (await readdir2(changesDirOf(root), { withFileTypes: true })).filter((d) => d.isDirectory() && d.name !== "archive" && RecordId.safeParse(d.name).success).map((d) => d.name).sort();
642
+ } catch {
643
+ return { changes: [], problems: [] };
644
+ }
645
+ const changes = [];
646
+ const problems = [];
647
+ for (const n of names) {
594
648
  try {
595
- names = (await readdir2(changesDirOf(root), { withFileTypes: true })).filter((d) => d.isDirectory() && d.name !== "archive").map((d) => d.name).sort();
596
- } catch {
597
- return [];
598
- }
599
- const out = [];
600
- for (const n of names) {
601
649
  const c = await tryRead(n);
602
650
  if (c)
603
- out.push(c);
651
+ changes.push(c);
652
+ } catch (e) {
653
+ if (!(e instanceof ChangeFileError))
654
+ throw e;
655
+ problems.push({ file: e.path, message: e.message });
604
656
  }
605
- return out;
606
657
  }
607
- };
658
+ return { changes, problems };
659
+ }
608
660
  }
609
661
 
610
662
  // packages/changes/dist/codeowners.js
@@ -712,6 +764,21 @@ function changeTypeOf(ctx, type) {
712
764
  var import_yaml4 = __toESM(require_dist(), 1);
713
765
  import { readdir as readdir3, readFile as readFile5 } from "node:fs/promises";
714
766
  import { join as join6 } from "node:path";
767
+
768
+ // packages/changes/dist/outside.js
769
+ function outsideSystemsOf(op) {
770
+ return new Map(outsideProfileSystems(op).map((s) => [s.id, s.boundary === "vendor" ? "vendor" : "other-team"]));
771
+ }
772
+ function outsideKindOf(outside, system, catalogBoundary) {
773
+ if (!system)
774
+ return void 0;
775
+ if (catalogBoundary === "vendor")
776
+ return "vendor";
777
+ return outside.get(system);
778
+ }
779
+ var outsideWords = (k) => k === "vendor" ? "a vendor" : "another team's system";
780
+
781
+ // packages/changes/dist/footprint.js
715
782
  function personalLevels(ctx) {
716
783
  const pack = ctx.orgPack.pack?.classification ?? [
717
784
  "public",
@@ -799,6 +866,8 @@ async function changeFootprint(ctx, change) {
799
866
  };
800
867
  const profileSystems = new Map(ctx.orgPack.profiles.flatMap((p) => p.profile?.systems ?? []).map((s) => [s.id, s]));
801
868
  const isSystem = (id) => get("System", id) !== void 0 || profileSystems.has(id);
869
+ const outsideSystems = outsideSystemsOf(ctx.orgPack);
870
+ const outsideKind = (id) => outsideKindOf(outsideSystems, id, get("System", id)?.boundary ?? profileSystems.get(id)?.boundary);
802
871
  const systems = new Set(change.systems);
803
872
  const components = /* @__PURE__ */ new Set();
804
873
  const interfaces = /* @__PURE__ */ new Map();
@@ -846,7 +915,7 @@ async function changeFootprint(ctx, change) {
846
915
  if (table)
847
916
  tables.add(table.includes(".") ? slug(table) : `${store}.${slug(table)}`);
848
917
  }
849
- const staged = await stageFootprint(ctx, dir, { get, isSystem, profileSystems, ref });
918
+ const staged = await stageFootprint(ctx, dir, { get, isSystem, outsideKind, ref });
850
919
  for (const f of staged.from)
851
920
  from.add(f);
852
921
  for (const r of staged.repos)
@@ -905,7 +974,8 @@ async function changeFootprint(ctx, change) {
905
974
  repos.add(r.id);
906
975
  } catch {
907
976
  }
908
- const vendors = [...systems].filter((s) => (get("System", s)?.boundary ?? profileSystems.get(s)?.boundary) === "vendor");
977
+ const vendors = [...systems].filter((s) => outsideKind(s) === "vendor");
978
+ const otherTeams = [...systems].filter((s) => outsideKind(s) === "other-team");
909
979
  const personalSet = new Set(personalLevels(ctx).levels.map((c) => c.toLowerCase()));
910
980
  const personal = [];
911
981
  for (const s of staged.stores) {
@@ -962,6 +1032,7 @@ async function changeFootprint(ctx, change) {
962
1032
  records: sorted(change.touches.records),
963
1033
  repos: sorted(repos),
964
1034
  vendors: vendors.sort(),
1035
+ ...otherTeams.length ? { otherTeams: otherTeams.sort() } : {},
965
1036
  personal,
966
1037
  flows,
967
1038
  entries: sorted(staged.entries),
@@ -995,7 +1066,7 @@ async function stageFootprint(ctx, dir, h) {
995
1066
  const tasksText = await read(join6(dir, "tasks.md"));
996
1067
  if (!files.length && !featureText && !tasksText)
997
1068
  return out;
998
- const { get, isSystem, profileSystems, ref } = h;
1069
+ const { get, isSystem, outsideKind, ref } = h;
999
1070
  const specs = await specIndex(ctx);
1000
1071
  const catalogRepos = /* @__PURE__ */ new Set();
1001
1072
  for (const r of ctx.records)
@@ -1009,9 +1080,9 @@ async function stageFootprint(ctx, dir, h) {
1009
1080
  const outsideOf = async (id) => {
1010
1081
  const c = get("Component", id);
1011
1082
  if (c)
1012
- return get("System", c.system)?.boundary === "vendor";
1083
+ return outsideKind(c.system) !== void 0;
1013
1084
  if (isSystem(id))
1014
- return (get("System", id)?.boundary ?? profileSystems.get(id)?.boundary) === "vendor";
1085
+ return outsideKind(id) !== void 0;
1015
1086
  if (catalogRepos.has(id))
1016
1087
  return false;
1017
1088
  return !await specs.hasComponent(id) && !await specs.hasRepo(id);
@@ -1098,7 +1169,7 @@ async function stageFootprint(ctx, dir, h) {
1098
1169
  const was = op !== "add" && n ? today?.flow.steps[n - 1] : void 0;
1099
1170
  const kind = str(s.kind) ?? was?.kind ?? "step";
1100
1171
  const what = str(s.what) ?? was?.what ?? "";
1101
- const live = op === "add" || op === "change";
1172
+ const live2 = op === "add" || op === "change";
1102
1173
  const store = obj(s.store);
1103
1174
  const table = str(store?.table) ?? was?.store?.table;
1104
1175
  if (table) {
@@ -1111,7 +1182,7 @@ async function stageFootprint(ctx, dir, h) {
1111
1182
  if (typeof v === "string")
1112
1183
  classification[c] = v;
1113
1184
  out.stores.push({ table, columns: cols, classification, kind, cite });
1114
- if (live) {
1185
+ if (live2) {
1115
1186
  const hit = [...wordsOf(table), ...cols.flatMap(wordsOf)].filter((w) => personalWords.has(w));
1116
1187
  if (hit.length)
1117
1188
  out.signals.push({
@@ -1139,7 +1210,7 @@ async function stageFootprint(ctx, dir, h) {
1139
1210
  if (entry)
1140
1211
  o.entries.add(entry);
1141
1212
  outside.set(target, o);
1142
- if (live)
1213
+ if (live2)
1143
1214
  out.signals.push({
1144
1215
  factor: "outside-call",
1145
1216
  detail: `${target}${entry ? ` (${entry}${to?.new === true ? ", new" : ""})` : ""}`,
@@ -1163,7 +1234,7 @@ async function stageFootprint(ctx, dir, h) {
1163
1234
  out.calls.add(iface.id);
1164
1235
  }
1165
1236
  }
1166
- if (!live)
1237
+ if (!live2)
1167
1238
  continue;
1168
1239
  const fields = obj(s.fields) ?? {};
1169
1240
  const names = FIELD_LISTS.flatMap((k) => strs(fields[k]));
@@ -1291,9 +1362,14 @@ function scoreRisk(input, policy) {
1291
1362
  add("systems", `${f.systems.length} systems touched (${f.systems.join(", ")})`, (f.systems.length - 1) * w.extraSystem);
1292
1363
  for (const s of f.dataStores)
1293
1364
  add("data-store", `data store ${s}`, w.dataStore);
1294
- const crossed = f.vendors.filter((v) => !(f.outside ?? []).some((o) => o.id === v));
1295
- if (crossed.length > 0)
1296
- add("vendor", `crosses the vendor boundary (${crossed.join(", ")})`, w.vendor);
1365
+ const notCalled = (v) => !(f.outside ?? []).some((o) => o.id === v);
1366
+ const crossed = f.vendors.filter(notCalled);
1367
+ const teams = (f.otherTeams ?? []).filter(notCalled);
1368
+ if (crossed.length > 0 || teams.length > 0)
1369
+ add("vendor", [
1370
+ ...crossed.length ? [`crosses the vendor boundary (${crossed.join(", ")})`] : [],
1371
+ ...teams.length ? [`calls another team's system (${teams.join(", ")})`] : []
1372
+ ].join("; "), w.vendor);
1297
1373
  for (const i of f.interfaces) {
1298
1374
  if (i.status === "planned-modify")
1299
1375
  add("interface", `interface ${i.id} planned-modify`, w.interfaceModify);
@@ -1414,7 +1490,7 @@ function handoffName(type, phase) {
1414
1490
  case "plan":
1415
1491
  return "Plan ready";
1416
1492
  case "build":
1417
- return "Raise PR";
1493
+ return "Build ready";
1418
1494
  case "qa":
1419
1495
  return "QA sign-off";
1420
1496
  case "release":
@@ -1470,8 +1546,15 @@ async function checkStage(root, change, step) {
1470
1546
  break;
1471
1547
  case "stopped":
1472
1548
  return reason(`The ${step.stage} stage stopped and isn't done: see what stopped it, then run ${run} again.`);
1473
- case "questions":
1474
- return reason(`The ${step.stage} stage waits for a person to answer its questions: answer them (atticus questions), then run ${run} again.`);
1549
+ case "questions": {
1550
+ const asked = Array.isArray(loop?.questions) ? loop.questions.filter((q) => typeof q === "string") : [];
1551
+ if (asked.length) {
1552
+ const open = new Set((await listQuestions(root, { state: "open" }).catch(() => ({ questions: [] }))).questions.map((q) => q.id));
1553
+ if (!asked.some((q) => open.has(q)))
1554
+ return reason(`The ${step.stage} stage's questions are answered: run it again so it reads the answers (${run}).`);
1555
+ }
1556
+ return reason(`The ${step.stage} stage waits for a person to answer its questions: answer them on Questions (or atticus questions), then run the stage again (${run}).`);
1557
+ }
1475
1558
  case "waiting":
1476
1559
  return reason(`The ${step.stage} stage is with your assistant${typeof loop?.run === "string" ? ` (run ${loop.run})` : ""}: finish the hand-off, then run ${run} again.`);
1477
1560
  case "drafting":
@@ -1480,7 +1563,8 @@ async function checkStage(root, change, step) {
1480
1563
  return reason(`${step.loop} doesn't read as a stage's loop state: run ${run} again.`);
1481
1564
  }
1482
1565
  const out = [];
1483
- for (const w of step.writes.filter((x) => !x.endsWith("/") && !x.startsWith("stage/")))
1566
+ const artifactFile = new Set(Object.values(FILE));
1567
+ for (const w of step.writes.filter((x) => !x.endsWith("/") && !x.startsWith("stage/") && !isWritePattern(x) && !artifactFile.has(x)))
1484
1568
  if (!await readFile6(join7(dir, ...w.split("/"))).then(() => true, () => false))
1485
1569
  out.push(...reason(`${w} is missing, though the ${step.stage} stage is done: run ${run} again.`));
1486
1570
  return out;
@@ -1490,7 +1574,10 @@ async function stageFiles(dir, step) {
1490
1574
  for (const w of [...step.writes, step.loop]) {
1491
1575
  if (w.endsWith("/"))
1492
1576
  out.push(...await walk(dir, w.slice(0, -1)));
1493
- else if (await readFile6(join7(dir, ...w.split("/"))).then(() => true, () => false))
1577
+ else if (isWritePattern(w)) {
1578
+ const parent = w.split("/").slice(0, -1).join("/");
1579
+ out.push(...(await walk(dir, parent)).filter((p) => writeMatches(w, p)));
1580
+ } else if (await readFile6(join7(dir, ...w.split("/"))).then(() => true, () => false))
1494
1581
  out.push(w);
1495
1582
  }
1496
1583
  return [...new Set(out)].sort();
@@ -1576,13 +1663,24 @@ async function checkFindings(root, change, gates) {
1576
1663
  continue;
1577
1664
  const round2 = Math.max(...rounds);
1578
1665
  const dir = join7(base, String(round2));
1666
+ const files = (await readdir4(dir)).filter((f) => /\.ya?ml$/.test(f)).sort();
1667
+ const unfinished = {
1668
+ code: "round-unfinished",
1669
+ message: `Critic round ${round2} on the ${gate} gate didn't finish: run atticus critic ${change.id} --gate ${gate} again.`
1670
+ };
1671
+ let hasMeta = false;
1579
1672
  try {
1580
1673
  const meta = (0, import_yaml5.parse)(await readFile6(join7(dir, "meta", "round.yaml"), "utf8"));
1674
+ hasMeta = true;
1675
+ if (meta?.state === "in-progress") {
1676
+ reasons.push(unfinished);
1677
+ continue;
1678
+ }
1581
1679
  for (const l of meta?.lenses ?? [])
1582
1680
  if (l.state === "waiting")
1583
1681
  reasons.push({
1584
1682
  code: "lens-waiting",
1585
- message: `The ${l.lens ?? "AI"} lens on the ${gate} gate (round ${round2}) is waiting for its hand-off answer${l.runId ? ` (atticus handoff done ${l.runId})` : ""}.`
1683
+ message: `The ${l.lens ?? "AI"} lens on the ${gate} gate (round ${round2}) is waiting for its hand-off answer${l.runId ? ` (Done on run ${l.runId} in Model runs, or atticus handoff done ${l.runId})` : ""}.`
1586
1684
  });
1587
1685
  else if (l.state === "failed")
1588
1686
  reasons.push({
@@ -1591,7 +1689,10 @@ async function checkFindings(root, change, gates) {
1591
1689
  });
1592
1690
  } catch {
1593
1691
  }
1594
- const files = (await readdir4(dir)).filter((f) => /\.ya?ml$/.test(f)).sort();
1692
+ if (!hasMeta && files.length === 0) {
1693
+ reasons.push(unfinished);
1694
+ continue;
1695
+ }
1595
1696
  for (const f of files) {
1596
1697
  const rel = `review/${gate}/${round2}/${f}`;
1597
1698
  let data;
@@ -1609,6 +1710,16 @@ async function checkFindings(root, change, gates) {
1609
1710
  continue;
1610
1711
  }
1611
1712
  const x = r.data;
1713
+ if (x.severity === "gap" && x.state === "asked") {
1714
+ const q = await readQuestion(root, askedGapQuestionId(change.id, x)).catch(() => void 0);
1715
+ if (q?.state === "open")
1716
+ reasons.push({
1717
+ code: "open-gap",
1718
+ finding: x.id,
1719
+ message: `Knowledge gap ${x.id} on the ${gate} gate (round ${round2}) was asked of ${q.askedOf ?? x.resolution?.askedOf ?? "its owner"} and waits for the answer (owner question ${q.id}, in Questions, Owner questions): ${x.summary}`
1720
+ });
1721
+ continue;
1722
+ }
1612
1723
  const byAssistant = x.severity === "blocking" && isAssistantResolution(x.resolution);
1613
1724
  if (x.state !== "open" && !byAssistant)
1614
1725
  continue;
@@ -1728,8 +1839,459 @@ function prApprovers(handoff2, approvers) {
1728
1839
  return [...new Set(approvers.map(strip))].filter((p) => p !== raiser).sort();
1729
1840
  }
1730
1841
 
1842
+ // packages/changes/dist/assess.js
1843
+ import { readFile as readFile9 } from "node:fs/promises";
1844
+ import { join as join10 } from "node:path";
1845
+
1846
+ // packages/changes/dist/release-prs.js
1847
+ var import_yaml7 = __toESM(require_dist(), 1);
1848
+ import { readFile as readFile8, writeFile } from "node:fs/promises";
1849
+ import { existsSync } from "node:fs";
1850
+ import { join as join9 } from "node:path";
1851
+ var RELEASE_PR_STATES = [
1852
+ "qa",
1853
+ "release-ready",
1854
+ "released",
1855
+ "post-release-review"
1856
+ ];
1857
+ var nowIso = () => (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
1858
+ var RELEASE_PRS_FILE = "release-prs.yaml";
1859
+ var NothingFile = external_exports.strictObject({
1860
+ schemaVersion: external_exports.literal(1),
1861
+ nothingToRelease: external_exports.array(external_exports.strictObject({
1862
+ repo: external_exports.string().min(1),
1863
+ /** The change branch's commit when it was found to hold nothing beyond the default branch. */
1864
+ commit: external_exports.string().regex(/^[0-9a-f]{7,64}$/).optional(),
1865
+ at: external_exports.string().min(1),
1866
+ by: external_exports.string().min(1)
1867
+ })),
1868
+ /**
1869
+ * Review round 48: the change branch's commit when Sync first read each release PR merged, so
1870
+ * a merged PR stands only while the branch is there (work landed since needs a PR of its own).
1871
+ */
1872
+ merged: external_exports.array(external_exports.strictObject({
1873
+ repo: external_exports.string().min(1),
1874
+ number: external_exports.number().int().positive(),
1875
+ commit: external_exports.string().regex(/^[0-9a-f]{7,64}$/),
1876
+ at: external_exports.string().min(1)
1877
+ })).optional()
1878
+ });
1879
+ async function readReleaseFile(root, id) {
1880
+ const empty = { schemaVersion: 1, nothingToRelease: [] };
1881
+ const text = await readFile8(join9(changeFolder(root, id), RELEASE_PRS_FILE), "utf8").catch(() => void 0);
1882
+ if (text === void 0)
1883
+ return empty;
1884
+ try {
1885
+ const r = NothingFile.safeParse((0, import_yaml7.parse)(text));
1886
+ return r.success ? r.data : empty;
1887
+ } catch {
1888
+ return empty;
1889
+ }
1890
+ }
1891
+ async function readNothing(root, id) {
1892
+ return (await readReleaseFile(root, id)).nothingToRelease;
1893
+ }
1894
+ async function writeReleaseFile(root, id, nothing, merged) {
1895
+ const byRepo = (xs) => [...xs].sort((a, b) => a.repo < b.repo ? -1 : a.repo > b.repo ? 1 : 0);
1896
+ await writeFile(join9(changeFolder(root, id), RELEASE_PRS_FILE), toCanonicalYaml(NothingFile.parse({
1897
+ schemaVersion: 1,
1898
+ nothingToRelease: byRepo(nothing),
1899
+ ...merged.length ? { merged: byRepo(merged) } : {}
1900
+ })), "utf8");
1901
+ }
1902
+ function repoDir(root, r) {
1903
+ const dirs = [join9(root, r.worktree)];
1904
+ try {
1905
+ dirs.push(baseClonePath(root, r.id));
1906
+ } catch {
1907
+ }
1908
+ return dirs.find((d) => existsSync(d));
1909
+ }
1910
+ async function staleReleasePrs(root, id, o = {}) {
1911
+ const out = /* @__PURE__ */ new Set();
1912
+ const cr = await readChangeRepos(root, id).catch(() => void 0);
1913
+ const merged = (await readReleaseFile(root, id)).merged ?? [];
1914
+ if (!cr || !merged.length)
1915
+ return out;
1916
+ const git = o.git ?? createGit({ timeoutMs: 3e4 });
1917
+ const remote = o.remote ?? "origin";
1918
+ for (const r of cr.repos) {
1919
+ if (r.pr?.state !== "merged")
1920
+ continue;
1921
+ const entry = merged.find((x) => x.repo === r.id && x.number === r.pr.number);
1922
+ if (!entry)
1923
+ continue;
1924
+ const dir = repoDir(root, r);
1925
+ if (!dir)
1926
+ continue;
1927
+ if (await movedPast(git, dir, remote, r.branch, entry.commit))
1928
+ out.add(r.id);
1929
+ }
1930
+ return out;
1931
+ }
1932
+ async function movedPast(git, dir, remote, branch, commit) {
1933
+ const ref = `refs/heads/${branch}`;
1934
+ const known = (sha) => git(["cat-file", "-e", `${sha}^{commit}`], dir).then(() => true, () => false);
1935
+ const here = await git(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], dir).then((x) => x.stdout.trim() || void 0, () => void 0);
1936
+ const onHost = await git(["ls-remote", "--heads", remote, ref], dir).then((x) => x.stdout.trim().split(/\s+/)[0] || void 0, () => void 0);
1937
+ if (onHost && !sameSha(onHost, commit) && !await known(onHost))
1938
+ await git(["fetch", "-q", remote, `+${ref}:refs/remotes/${remote}/${branch}`], dir).catch(() => void 0);
1939
+ const ahead = async (sha) => {
1940
+ if (!sha || sameSha(sha, commit))
1941
+ return false;
1942
+ if (!await known(sha) || !await known(commit))
1943
+ return true;
1944
+ return git(["merge-base", "--is-ancestor", sha, commit], dir).then(() => false, () => true);
1945
+ };
1946
+ return await ahead(here) || await ahead(onHost);
1947
+ }
1948
+ async function nothingToRelease(root, id, o = {}) {
1949
+ const entries = await readNothing(root, id);
1950
+ const out = /* @__PURE__ */ new Set();
1951
+ if (!entries.length)
1952
+ return out;
1953
+ const cr = await readChangeRepos(root, id).catch(() => void 0);
1954
+ const git = o.git ?? createGit({ timeoutMs: 3e4 });
1955
+ const remote = o.remote ?? "origin";
1956
+ for (const x of entries) {
1957
+ const r = cr?.repos.find((y) => y.id === x.repo);
1958
+ if (r && await moved(r, x.commit))
1959
+ continue;
1960
+ out.add(x.repo);
1961
+ }
1962
+ return out;
1963
+ async function moved(r, commit) {
1964
+ const dir = repoDir(root, r);
1965
+ if (!dir)
1966
+ return false;
1967
+ const ref = `refs/heads/${r.branch}`;
1968
+ const here = await git(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], dir).then((x) => x.stdout.trim() || void 0, () => void 0);
1969
+ const onHost = await git(["ls-remote", "--heads", remote, ref], dir).then((x) => x.stdout.trim().split(/\s+/)[0] || void 0, () => void 0);
1970
+ const at = (sha) => sha !== void 0 && (commit === void 0 || !sameSha(sha, commit));
1971
+ return at(here) || at(onHost);
1972
+ }
1973
+ }
1974
+ var sameSha = (a, b) => a.startsWith(b) || b.startsWith(a);
1975
+ var firstLine = (e) => (e instanceof Error ? e.message : String(e)).split("\n")[0].replace(/\.$/, "");
1976
+ var prOf = (pr, now) => ({
1977
+ url: pr.url,
1978
+ number: pr.number,
1979
+ state: pr.state,
1980
+ updatedAt: now
1981
+ });
1982
+ var live = (pr) => !!pr && (pr.state === "open" || pr.state === "draft" || pr.state === "merged");
1983
+ function releaseBody(id, title, branch, base) {
1984
+ return [
1985
+ `Release of change ${id} (${title}): merges ${branch}, with the task PRs merged into it, into ${base}.`,
1986
+ "",
1987
+ "The go/no-go hand-off is decided over this PR. A person reviews and merges it on the Git host; Atticus never pushes to or merges into the default branch, and never deploys."
1988
+ ].join("\n");
1989
+ }
1990
+ async function raiseReleasePrs(root, id, o) {
1991
+ requireApproval("changes", o.approval);
1992
+ const change = await changeStore(root).read(id);
1993
+ if (!RELEASE_PR_STATES.includes(change.state))
1994
+ throw new ChangeError("refused", `${id} is ${change.state}; the release PRs are raised once the build is done (in QA or at go/no-go), so go/no-go is decided over the PRs that will ship.`);
1995
+ const cr = await readChangeRepos(root, id);
1996
+ if (!cr?.repos.length)
1997
+ throw new ChangeError("refused", `${id} has no code repos (repos.yaml), so there is no release PR to raise: open its workspace first (atticus workspace open ${id} <repo>).`);
1998
+ const now = o.now ?? nowIso();
1999
+ const git = o.git ?? createGit();
2000
+ const remote = o.remote ?? "origin";
2001
+ const expected = changeBranch(id);
2002
+ const rows = [];
2003
+ let changed2 = false;
2004
+ const repos = cr.repos.map((r) => ({ ...r }));
2005
+ const file = await readReleaseFile(root, id);
2006
+ const before = file.nothingToRelease;
2007
+ const nothing = new Map(before.map((x) => [x.repo, x]));
2008
+ const stale = await staleReleasePrs(root, id, { git, remote });
2009
+ for (const r of repos) {
2010
+ const movedOn = r.pr?.state === "merged" && stale.has(r.id) ? `Release PR #${r.pr.number} merged before ${r.branch} moved on; this one carries what landed since.` : void 0;
2011
+ if (!live(r.pr) || movedOn)
2012
+ nothing.delete(r.id);
2013
+ if (live(r.pr) && !movedOn) {
2014
+ rows.push({
2015
+ repo: r.id,
2016
+ outcome: "kept",
2017
+ pr: r.pr,
2018
+ message: `${r.id}: release PR #${r.pr.number} is already ${r.pr.state} (${r.pr.url}).`
2019
+ });
2020
+ continue;
2021
+ }
2022
+ if (r.branch !== expected) {
2023
+ rows.push({
2024
+ repo: r.id,
2025
+ outcome: "refused",
2026
+ message: `${r.id}: its branch is ${r.branch}, not ${expected}; Atticus raises a release PR only from the change branch.`
2027
+ });
2028
+ continue;
2029
+ }
2030
+ let base;
2031
+ try {
2032
+ const info = await o.gitHost.getRepo(r.id);
2033
+ base = info.defaultBranch;
2034
+ if (info.protectedBranches.includes(r.branch)) {
2035
+ rows.push({
2036
+ repo: r.id,
2037
+ outcome: "refused",
2038
+ message: `${r.id}: ${r.branch} is protected; Atticus never pushes to or raises a PR from a protected branch.`
2039
+ });
2040
+ continue;
2041
+ }
2042
+ if (r.branch === base) {
2043
+ rows.push({
2044
+ repo: r.id,
2045
+ outcome: "refused",
2046
+ message: `${r.id}: ${r.branch} is the default branch; a release PR goes from the change branch into it.`
2047
+ });
2048
+ continue;
2049
+ }
2050
+ } catch (e) {
2051
+ if (!(e instanceof AdapterError))
2052
+ throw e;
2053
+ rows.push({
2054
+ repo: r.id,
2055
+ outcome: "failed",
2056
+ message: `${r.id}: the Git host didn't answer (${firstLine(e)}); raise the release PRs again once it does.`
2057
+ });
2058
+ continue;
2059
+ }
2060
+ const notes = movedOn ? [movedOn] : [];
2061
+ const dir = join9(root, r.worktree);
2062
+ if (existsSync(dir)) {
2063
+ const onHost = await git(["ls-remote", "--heads", remote, `refs/heads/${r.branch}`], dir).then((x) => x.stdout.trim() ? "yes" : "no").catch((e) => {
2064
+ notes.push(`Atticus couldn't reach ${remote} (${firstLine(e)}): push ${r.branch} yourself if the host lacks it.`);
2065
+ return "unknown";
2066
+ });
2067
+ if (onHost === "no") {
2068
+ const from = r.baseSha ?? `${remote}/${base}`;
2069
+ const ahead = await git(["rev-list", "--count", `${from}..refs/heads/${r.branch}`], dir).then((x) => Number(x.stdout.trim()) || 0).catch(() => -1);
2070
+ if (ahead === 0) {
2071
+ const head = await git(["rev-parse", `refs/heads/${r.branch}`], dir).then((x) => x.stdout.trim()).catch(() => "");
2072
+ nothing.set(r.id, {
2073
+ repo: r.id,
2074
+ .../^[0-9a-f]{7,64}$/.test(head) ? { commit: head } : {},
2075
+ at: now,
2076
+ by: o.approval.by
2077
+ });
2078
+ rows.push({
2079
+ repo: r.id,
2080
+ outcome: "nothing",
2081
+ message: `${r.id}: ${r.branch} has no commits beyond ${base} and isn't on the Git host: there is nothing to release from it, so go/no-go doesn't wait for a PR.`
2082
+ });
2083
+ continue;
2084
+ }
2085
+ try {
2086
+ await git(["push", "-q", remote, `refs/heads/${r.branch}:refs/heads/${r.branch}`], dir);
2087
+ notes.push(`Pushed ${r.branch}, which the Git host didn't have.`);
2088
+ } catch (e) {
2089
+ rows.push({
2090
+ repo: r.id,
2091
+ outcome: "failed",
2092
+ message: `${r.id}: ${r.branch} isn't on the Git host and couldn't be pushed (${firstLine(e)}): push it yourself, then raise the release PRs again.`
2093
+ });
2094
+ continue;
2095
+ }
2096
+ }
2097
+ }
2098
+ try {
2099
+ const pr = await o.gitHost.createPullRequest({
2100
+ repo: r.id,
2101
+ head: r.branch,
2102
+ base,
2103
+ title: `${change.title} (release of ${id})`,
2104
+ body: releaseBody(id, change.title, r.branch, base)
2105
+ }, o.approval);
2106
+ r.pr = prOf(pr, now);
2107
+ changed2 = true;
2108
+ rows.push({
2109
+ repo: r.id,
2110
+ outcome: "raised",
2111
+ pr: r.pr,
2112
+ message: [
2113
+ pr.url.startsWith("manual:") ? `${r.id}: the release PR is a step for you (Home \u203A Manual steps): open it on the Git host from ${r.branch} into ${base}, then record its link.` : `${r.id}: release PR #${pr.number} from ${r.branch} into ${base} is open: ${pr.url}`,
2114
+ ...notes
2115
+ ].join(" ")
2116
+ });
2117
+ } catch (e) {
2118
+ if (!(e instanceof AdapterError))
2119
+ throw e;
2120
+ if (e.code === "conflict") {
2121
+ const open = (await o.gitHost.listPullRequests(r.id, { head: r.branch }).catch(() => [])).filter((p) => p.base === base && (p.state === "open" || p.state === "draft")).at(0);
2122
+ if (open) {
2123
+ r.pr = prOf(open, now);
2124
+ changed2 = true;
2125
+ rows.push({
2126
+ repo: r.id,
2127
+ outcome: "kept",
2128
+ pr: r.pr,
2129
+ message: `${r.id}: release PR #${open.number} was already open on the Git host; recorded it (${open.url}).`
2130
+ });
2131
+ continue;
2132
+ }
2133
+ }
2134
+ rows.push({
2135
+ repo: r.id,
2136
+ outcome: "failed",
2137
+ message: `${r.id}: the release PR wasn't raised (${firstLine(e)}); fix that and raise the release PRs again.`
2138
+ });
2139
+ }
2140
+ }
2141
+ if (changed2)
2142
+ await writeChangeRepos(root, { ...cr, repos });
2143
+ const after = [...nothing.values()].sort((a, b) => a.repo < b.repo ? -1 : 1);
2144
+ const mergedBefore = file.merged ?? [];
2145
+ const mergedAfter = mergedBefore.filter((x) => repos.some((r) => r.id === x.repo && r.pr?.number === x.number && r.pr.state === "merged"));
2146
+ if (JSON.stringify(after) !== JSON.stringify(before) || mergedAfter.length !== mergedBefore.length)
2147
+ await writeReleaseFile(root, id, after, mergedAfter);
2148
+ return { change: id, rows };
2149
+ }
2150
+ async function syncReleasePrs(root, id, o) {
2151
+ const cr = await readChangeRepos(root, id);
2152
+ const rows = [];
2153
+ if (!cr)
2154
+ return { change: id, rows };
2155
+ const now = o.now ?? nowIso();
2156
+ let changed2 = false;
2157
+ const repos = cr.repos.map((r) => ({ ...r }));
2158
+ const file = await readReleaseFile(root, id);
2159
+ const merged = [...file.merged ?? []];
2160
+ let mergedChanged = false;
2161
+ for (const r of repos) {
2162
+ if (!r.pr)
2163
+ continue;
2164
+ let pr;
2165
+ try {
2166
+ pr = await o.gitHost.getPullRequest(r.id, r.pr.number);
2167
+ if (pr.head !== r.branch) {
2168
+ const mine = await o.gitHost.listPullRequests(r.id, { head: r.branch });
2169
+ const own = mine.find((x) => x.url === r.pr.url) ?? (mine.length === 1 ? mine[0] : void 0);
2170
+ if (!own) {
2171
+ rows.push({
2172
+ repo: r.id,
2173
+ outcome: "failed",
2174
+ pr: r.pr,
2175
+ message: `${r.id}: PR #${r.pr.number} on the Git host is from ${pr.head}, not ${r.branch}: check the release PR's number on the Git host (on a manual host, on its step), then sync again.`
2176
+ });
2177
+ continue;
2178
+ }
2179
+ pr = own;
2180
+ }
2181
+ } catch (e) {
2182
+ if (!(e instanceof AdapterError))
2183
+ throw e;
2184
+ rows.push({
2185
+ repo: r.id,
2186
+ outcome: "failed",
2187
+ pr: r.pr,
2188
+ message: `${r.id}: release PR #${r.pr.number} couldn't be read (${firstLine(e)}).`
2189
+ });
2190
+ continue;
2191
+ }
2192
+ const build = buildOf(pr);
2193
+ if (pr.state !== r.pr.state || pr.url !== r.pr.url || pr.number !== r.pr.number) {
2194
+ r.pr = prOf(pr, now);
2195
+ changed2 = true;
2196
+ }
2197
+ if (build && build !== r.build.status) {
2198
+ r.build = { ...r.build, status: build, at: now };
2199
+ changed2 = true;
2200
+ }
2201
+ if (pr.state === "merged" && !merged.some((x) => x.repo === r.id && x.number === pr.number)) {
2202
+ const commit = await branchTip(root, r, o.git, o.remote ?? "origin");
2203
+ if (commit) {
2204
+ for (let i = merged.length - 1; i >= 0; i--)
2205
+ if (merged[i].repo === r.id)
2206
+ merged.splice(i, 1);
2207
+ merged.push({ repo: r.id, number: pr.number, commit, at: now });
2208
+ mergedChanged = true;
2209
+ }
2210
+ }
2211
+ rows.push({
2212
+ repo: r.id,
2213
+ outcome: "kept",
2214
+ pr: r.pr,
2215
+ message: `${r.id}: release PR #${r.pr.number} is ${r.pr.state}${build ? `, checks ${build}` : ""} (${r.pr.url}).`
2216
+ });
2217
+ }
2218
+ if (changed2)
2219
+ await writeChangeRepos(root, { ...cr, repos });
2220
+ if (mergedChanged)
2221
+ await writeReleaseFile(root, id, file.nothingToRelease, merged);
2222
+ return { change: id, rows };
2223
+ }
2224
+ async function branchTip(root, r, g, remote) {
2225
+ const dir = repoDir(root, r);
2226
+ if (!dir)
2227
+ return void 0;
2228
+ const git = g ?? createGit({ timeoutMs: 3e4 });
2229
+ const ref = `refs/heads/${r.branch}`;
2230
+ const onHost = await git(["ls-remote", "--heads", remote, ref], dir).then((x) => x.stdout.trim().split(/\s+/)[0] || void 0, () => void 0);
2231
+ if (onHost) {
2232
+ await git(["fetch", "-q", remote, `+${ref}:refs/remotes/${remote}/${r.branch}`], dir).catch(() => void 0);
2233
+ if (/^[0-9a-f]{7,64}$/.test(onHost))
2234
+ return onHost;
2235
+ }
2236
+ const here = await git(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], dir).then((x) => x.stdout.trim(), () => "");
2237
+ return /^[0-9a-f]{7,64}$/.test(here) ? here : void 0;
2238
+ }
2239
+ function buildOf(pr) {
2240
+ if (!pr.checks.length)
2241
+ return void 0;
2242
+ if (pr.checks.some((c) => c.status === "failed"))
2243
+ return "failed";
2244
+ if (pr.checks.some((c) => c.status === "pending"))
2245
+ return "running";
2246
+ return "passed";
2247
+ }
2248
+ async function requireMergedPrs(root) {
2249
+ const text = await readFile8(join9(orgPackDirOf(root), "process", "ci.yaml"), "utf8").catch(() => void 0);
2250
+ if (text === void 0)
2251
+ return true;
2252
+ try {
2253
+ const v = (0, import_yaml7.parse)(text)?.archive?.requireMergedPrs;
2254
+ return v !== false;
2255
+ } catch {
2256
+ return true;
2257
+ }
2258
+ }
2259
+ async function releasePrReasons(root, id, o = {}) {
2260
+ const cr = await readChangeRepos(root, id).catch(() => void 0);
2261
+ if (!cr?.repos.length)
2262
+ return [];
2263
+ const merged = await requireMergedPrs(root);
2264
+ const nothing = await nothingToRelease(root, id, o);
2265
+ const stale = await staleReleasePrs(root, id, o);
2266
+ const out = [];
2267
+ for (const r of cr.repos) {
2268
+ if (!r.pr && nothing.has(r.id))
2269
+ continue;
2270
+ if (r.pr?.state === "merged" && stale.has(r.id)) {
2271
+ out.push({
2272
+ code: "release-pr-stale",
2273
+ message: `${r.id}: ${r.branch} has commits since release PR #${r.pr.number} merged, so they haven't reached its default branch: raise the release PRs again (Release \u203A Raise the release PRs, or atticus change release-prs ${id} --approve).`
2274
+ });
2275
+ continue;
2276
+ }
2277
+ if (!r.pr || r.pr.state === "closed")
2278
+ out.push({
2279
+ code: "release-pr-missing",
2280
+ message: `${r.id}: no release PR from ${r.branch} into its default branch${r.pr ? ` (PR #${r.pr.number} was closed)` : ""}: raise the release PRs (Release \u203A Raise the release PRs, or atticus change release-prs ${id} --approve).`
2281
+ });
2282
+ else if (merged && r.pr.state !== "merged")
2283
+ out.push({
2284
+ code: "release-pr-not-merged",
2285
+ message: `${r.id}: release PR #${r.pr.number} is ${r.pr.state}: a person merges it on the Git host (${r.pr.url}), then Sync records it.`
2286
+ });
2287
+ }
2288
+ return out;
2289
+ }
2290
+
1731
2291
  // packages/changes/dist/assess.js
1732
2292
  var liveHandoff = (c) => c.handoffs.find((h) => h.state === "open" || h.state === "changes-requested");
2293
+ var isManualPrUrl = (url) => /^manual:\/\//i.test(url.trim());
2294
+ var manualHandoffWords = (phase) => `The ${phase} hand-off's PR isn't open yet: your Git host is manual, so opening it is a step waiting on you (atticus todo, or Home \u203A Manual steps in the app). Open it on the Git host as its step says, record its link, then sync; merging it approves the gate.`;
1733
2295
  async function assess(ctx, change) {
1734
2296
  const type = changeTypeOf(ctx, change.type);
1735
2297
  const footprint = await changeFootprint(ctx, change);
@@ -1770,29 +2332,32 @@ async function assess(ctx, change) {
1770
2332
  ...stageReasons,
1771
2333
  ...await checkArtifacts(ctx.root, change, due),
1772
2334
  ...await checkFindings(ctx.root, change, phaseFindingGates(ctx, phase)),
1773
- ...stageReasons.length ? [] : await checkMandatoryRounds(ctx.root, change, phaseGates(ctx, phase).filter((g) => mandatoryGates(ctx).includes(g)))
2335
+ ...stageReasons.length ? [] : await checkMandatoryRounds(ctx.root, change, phaseGates(ctx, phase).filter((g) => mandatoryGates(ctx).includes(g))),
2336
+ // ADR 0184: go/no-go is decided over the release PRs, each repo's change branch into its
2337
+ // default branch, merged (unless the org pack's ci.yaml says archive.requireMergedPrs: false).
2338
+ ...phase === "release" ? await releasePrReasons(ctx.root, change.id) : []
1774
2339
  ];
1775
2340
  const next = change.state === "returned" ? phaseState(phase) : nextState(type, phase);
1776
- const live = change.state === "returned" ? void 0 : liveHandoff(change);
2341
+ const live2 = change.state === "returned" ? void 0 : liveHandoff(change);
1777
2342
  const reasons = [...blockers];
1778
- if (!live) {
2343
+ if (!live2) {
1779
2344
  reasons.push({
1780
2345
  code: "no-handoff",
1781
2346
  message: `No hand-off PR yet: raise "${handoffName(type, phase)}" when the ${phase} work is done.`
1782
2347
  });
1783
- } else if (live.state === "changes-requested") {
2348
+ } else if (live2.state === "changes-requested") {
1784
2349
  reasons.push({
1785
2350
  code: "changes-requested",
1786
- message: `Changes were requested on PR #${live.pr.number}; update the artifacts and hand off again.`
2351
+ message: `Changes were requested on PR #${live2.pr.number}; update the artifacts and hand off again.`
1787
2352
  });
1788
2353
  } else {
1789
- const approved = prApprovers(live, live.approvals.map((a) => a.by));
2354
+ const approved = prApprovers(live2, live2.approvals.map((a) => a.by));
1790
2355
  reasons.push({
1791
2356
  code: "pr-not-merged",
1792
- message: approved.length ? `PR #${live.pr.number} is approved (${approved.join(", ")}) but not merged yet: ${live.pr.url}` : `Waiting for PR #${live.pr.number} to be approved and merged on the Git host: ${live.pr.url}`
2357
+ message: isManualPrUrl(live2.pr.url) ? manualHandoffWords(phase) : approved.length ? `PR #${live2.pr.number} is approved (${approved.join(", ")}) but not merged yet: ${live2.pr.url}` : `Waiting for PR #${live2.pr.number} to be approved and merged on the Git host: ${live2.pr.url}`
1793
2358
  });
1794
2359
  }
1795
- return { ...base, phase, next, ...live ? { handoff: live } : {}, blockers, reasons };
2360
+ return { ...base, phase, next, ...live2 ? { handoff: live2 } : {}, blockers, reasons };
1796
2361
  }
1797
2362
  async function canAdvance(change, root) {
1798
2363
  const ctx = await loadChangeContext(root);
@@ -1809,9 +2374,15 @@ async function canAdvance(change, root) {
1809
2374
  async function changeReport(root, id) {
1810
2375
  const ctx = await loadChangeContext(root);
1811
2376
  const a = await assess(ctx, await changeStore(root).read(id));
1812
- return { ...a, nextStep: nextStep(a), problems: ctx.problems };
2377
+ const tasks = a.phase === "build" ? await taskTicks(root, id) : void 0;
2378
+ return { ...a, nextStep: nextStep(a, tasks), problems: ctx.problems };
2379
+ }
2380
+ async function taskTicks(root, id) {
2381
+ const text = await readFile9(join10(changeFolder(root, id), "tasks.md"), "utf8").catch(() => "");
2382
+ const boxes = [...text.matchAll(/^\s*[-*]\s+\[([ xX])\]/gm)].map((m) => m[1] !== " ");
2383
+ return boxes.length ? { done: boxes.filter(Boolean).length, total: boxes.length } : void 0;
1813
2384
  }
1814
- function nextStep(a) {
2385
+ function nextStep(a, tasks) {
1815
2386
  switch (a.state) {
1816
2387
  case "draft":
1817
2388
  return "Create the change with atticus change new.";
@@ -1825,25 +2396,34 @@ function nextStep(a) {
1825
2396
  return `Revise the work, then hand off ${a.phase} again.`;
1826
2397
  }
1827
2398
  const name = handoffName(a.type, a.phase);
1828
- if (!a.handoff)
2399
+ if (!a.handoff) {
2400
+ if (!a.blockers.length && tasks && tasks.done < tasks.total)
2401
+ return `Finish the ${a.phase} work (${tasks.done} of ${tasks.total} tasks ticked in tasks.md), then raise "${name}".`;
1829
2402
  return a.blockers.length ? `Finish the ${a.phase} work, then raise "${name}".` : `Ready to raise "${name}".`;
2403
+ }
1830
2404
  if (a.handoff.state === "changes-requested")
1831
2405
  return `Update the artifacts and hand off again on PR #${a.handoff.pr.number}.`;
2406
+ if (isManualPrUrl(a.handoff.pr.url))
2407
+ return manualHandoffWords(a.handoff.phase);
1832
2408
  return `Merge PR #${a.handoff.pr.number} once it is approved (${a.handoff.pr.url}); syncing then moves the change on.`;
1833
2409
  }
1834
2410
 
1835
2411
  // packages/changes/dist/new-change.js
1836
- var import_yaml7 = __toESM(require_dist(), 1);
1837
- import { mkdir, readFile as readFile8, writeFile } from "node:fs/promises";
1838
- import { join as join9 } from "node:path";
1839
- var nowIso = () => (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
2412
+ var import_yaml8 = __toESM(require_dist(), 1);
2413
+ import { mkdir, readFile as readFile10, writeFile as writeFile2 } from "node:fs/promises";
2414
+ import { join as join11 } from "node:path";
2415
+ var nowIso2 = () => (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
1840
2416
  var strip2 = (h) => h.startsWith("@") ? h.slice(1) : h;
2417
+ function windowsUnsafeName(id) {
2418
+ return /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\.|$)/i.test(id) || /[. ]$/.test(id);
2419
+ }
1841
2420
  function slugify(title) {
1842
- return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80).replace(/-+$/, "");
2421
+ const id = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80).replace(/-+$/, "");
2422
+ return windowsUnsafeName(id) ? `${id}-change` : id;
1843
2423
  }
1844
2424
  async function exists(path) {
1845
2425
  try {
1846
- await readFile8(path);
2426
+ await readFile10(path);
1847
2427
  return true;
1848
2428
  } catch {
1849
2429
  return false;
@@ -1851,7 +2431,7 @@ async function exists(path) {
1851
2431
  }
1852
2432
  async function defaultSchema(root) {
1853
2433
  try {
1854
- const cfg = (0, import_yaml7.parse)(await readFile8(join9(openspecDirOf(root), "config.yaml"), "utf8"));
2434
+ const cfg = (0, import_yaml8.parse)(await readFile10(join11(openspecDirOf(root), "config.yaml"), "utf8"));
1855
2435
  if (cfg && typeof cfg.schema === "string")
1856
2436
  return cfg.schema;
1857
2437
  } catch {
@@ -1864,26 +2444,28 @@ async function newChange(root, opts) {
1864
2444
  const id = opts.id ?? slugify(opts.title);
1865
2445
  if (!RecordId.safeParse(id).success)
1866
2446
  throw new ChangeError("invalid", `"${id}" can't be a change id; use a lowercase slug such as add-transfer-limit.`);
2447
+ if (windowsUnsafeName(id))
2448
+ throw new ChangeError("invalid", `"${id}" can't be a change id: Windows can't open a folder named that (con, nul, com1 and the like, or a name ending in a dot). Pick another, such as ${id.replace(/[. ]+$/, "").replace(/^([^.]+)/, "$1-change")}.`);
1867
2449
  const template = pickTemplate(ctx.templates, type.id, opts.template);
1868
2450
  if (!template)
1869
2451
  throw new ChangeError("invalid", `Unknown change template ${opts.template}. Use one of: ${ctx.templates.map((t) => t.id).join(", ")}.`);
1870
2452
  const store = changeStore(root);
1871
2453
  if (await store.tryRead(id).catch(() => true))
1872
2454
  throw new ChangeError("conflict", `Change ${id} already exists.`);
1873
- const now = opts.now ?? nowIso();
2455
+ const now = opts.now ?? nowIso2();
1874
2456
  const by = strip2(opts.by);
1875
2457
  const dir = changeFolder(root, id);
1876
- if (opts.openspec && !await exists(join9(dir, ".openspec.yaml"))) {
2458
+ if (opts.openspec && !await exists(join11(dir, ".openspec.yaml"))) {
1877
2459
  await opts.openspec.newChange(id, { description: opts.title });
1878
2460
  } else {
1879
2461
  await mkdir(dir, { recursive: true });
1880
- if (!await exists(join9(dir, ".openspec.yaml")))
1881
- await writeFile(join9(dir, ".openspec.yaml"), `schema: ${await defaultSchema(root)}
2462
+ if (!await exists(join11(dir, ".openspec.yaml")))
2463
+ await writeFile2(join11(dir, ".openspec.yaml"), `schema: ${await defaultSchema(root)}
1882
2464
  created: ${now.slice(0, 10)}
1883
2465
  `, "utf8");
1884
2466
  }
1885
- if (!await exists(join9(dir, "brief.md")))
1886
- await writeFileAtomic(join9(dir, "brief.md"), renderTemplate(template, {
2467
+ if (!await exists(join11(dir, "brief.md")))
2468
+ await writeFileAtomic(join11(dir, "brief.md"), renderTemplate(template, {
1887
2469
  title: opts.title,
1888
2470
  id,
1889
2471
  type: type.id,
@@ -1922,31 +2504,42 @@ created: ${now.slice(0, 10)}
1922
2504
 
1923
2505
  // packages/changes/dist/publish.js
1924
2506
  import { rm } from "node:fs/promises";
1925
- import { join as join10 } from "node:path";
2507
+ import { randomUUID } from "node:crypto";
2508
+ import { join as join12, resolve } from "node:path";
1926
2509
  function gitHandoffPublisher(opts = {}) {
1927
2510
  const remote = opts.remote ?? "origin";
1928
2511
  const env = opts.env ?? process.env;
1929
2512
  return {
1930
- async publish({ root, branch, base, files, message, approval }) {
1931
- requireApproval("git", approval);
2513
+ async publish(input) {
2514
+ requireApproval("git", input.approval);
2515
+ const key = resolve(input.root);
2516
+ const before = queue.get(key) ?? Promise.resolve();
2517
+ const mine = before.catch(() => void 0).then(() => publishOne(input));
2518
+ queue.set(key, mine.catch(() => void 0));
2519
+ return mine;
2520
+ }
2521
+ };
2522
+ async function publishOne({ root, branch, base, files, message }) {
2523
+ {
1932
2524
  const git = createGit({ env });
1933
2525
  const out = async (args) => (await git(args, root)).stdout.trim();
1934
2526
  const tryOut = (args) => out(args).catch(() => void 0);
1935
2527
  const gitDir = await out(["rev-parse", "--absolute-git-dir"]);
1936
2528
  await tryOut(["fetch", "--quiet", remote, base]);
1937
2529
  await tryOut(["fetch", "--quiet", remote, branch]);
1938
- const start = await tryOut([
2530
+ const onRemote = await tryOut([
1939
2531
  "rev-parse",
1940
2532
  "--verify",
1941
2533
  "--quiet",
1942
2534
  `refs/remotes/${remote}/${branch}^{commit}`
1943
- ]) ?? await tryOut([
2535
+ ]);
2536
+ const start = onRemote ?? await tryOut([
1944
2537
  "rev-parse",
1945
2538
  "--verify",
1946
2539
  "--quiet",
1947
2540
  `refs/remotes/${remote}/${base}^{commit}`
1948
2541
  ]) ?? await out(["rev-parse", "--verify", `refs/heads/${base}^{commit}`]);
1949
- const index = join10(gitDir, "atticus-handoff-index");
2542
+ const index = join12(gitDir, `atticus-handoff-index-${process.pid}-${randomUUID()}`);
1950
2543
  const staged = createGit({ env: { ...env, GIT_INDEX_FILE: index } });
1951
2544
  try {
1952
2545
  await staged(["read-tree", start], root);
@@ -1955,24 +2548,42 @@ function gitHandoffPublisher(opts = {}) {
1955
2548
  const tree = (await staged(["write-tree"], root)).stdout.trim();
1956
2549
  const startTree = await out(["rev-parse", `${start}^{tree}`]);
1957
2550
  const commit = tree === startTree ? start : await out(["commit-tree", tree, "-p", start, "-m", message]);
1958
- await out(["update-ref", `refs/heads/${branch}`, commit]);
1959
2551
  await out(["push", "--quiet", remote, `${commit}:refs/heads/${branch}`]);
1960
- return { commit };
2552
+ await keepLocalBranch(out, tryOut, branch, start, commit);
2553
+ return { commit, changed: commit !== onRemote };
1961
2554
  } finally {
1962
2555
  await rm(index, { force: true });
1963
2556
  }
1964
2557
  }
1965
- };
2558
+ }
2559
+ }
2560
+ var queue = /* @__PURE__ */ new Map();
2561
+ async function keepLocalBranch(out, tryOut, branch, start, commit) {
2562
+ const ref = `refs/heads/${branch}`;
2563
+ const local = await tryOut(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]);
2564
+ if (local === commit)
2565
+ return;
2566
+ const list = await tryOut(["worktree", "list", "--porcelain"]) ?? "";
2567
+ if (list.split(/\r?\n/).includes(`branch ${ref}`))
2568
+ return;
2569
+ if (!local) {
2570
+ await tryOut(["update-ref", ref, commit, ""]);
2571
+ return;
2572
+ }
2573
+ const own = await tryOut(["rev-list", "--count", `${start}..${local}`]);
2574
+ if (own === void 0 || own !== "0")
2575
+ return;
2576
+ await out(["update-ref", ref, commit, local]).catch(() => void 0);
1966
2577
  }
1967
2578
 
1968
2579
  // packages/changes/dist/handoff.js
1969
- import { readdir as readdir6, readFile as readFile10 } from "node:fs/promises";
1970
- import { join as join12 } from "node:path";
2580
+ import { readdir as readdir6, readFile as readFile12 } from "node:fs/promises";
2581
+ import { join as join14 } from "node:path";
1971
2582
 
1972
2583
  // packages/changes/dist/drafted.js
1973
- var import_yaml8 = __toESM(require_dist(), 1);
1974
- import { mkdir as mkdir2, readdir as readdir5, readFile as readFile9, rm as rm2 } from "node:fs/promises";
1975
- import { dirname, join as join11, relative, sep } from "node:path";
2584
+ var import_yaml9 = __toESM(require_dist(), 1);
2585
+ import { mkdir as mkdir2, readdir as readdir5, readFile as readFile11, rm as rm2 } from "node:fs/promises";
2586
+ import { dirname, join as join13, relative, sep } from "node:path";
1976
2587
  var CAPTURE_SCHEMA_VERSION = 1;
1977
2588
  var DraftCapture = external_exports.object({
1978
2589
  schemaVersion: external_exports.literal(CAPTURE_SCHEMA_VERSION),
@@ -2026,7 +2637,7 @@ function draftedPath(path, step, artifacts) {
2026
2637
  return false;
2027
2638
  if (step) {
2028
2639
  for (const w of step.writes)
2029
- if (w.endsWith("/") ? path.startsWith(w) : path === w)
2640
+ if (writeMatches(w, path))
2030
2641
  return !machinery(path);
2031
2642
  return step.artifacts.some((a) => artifactMatch(path, a));
2032
2643
  }
@@ -2047,7 +2658,7 @@ var sameText = (text, recorded) => sha256(text) === recorded || sha256(text.repl
2047
2658
  async function walk2(dir, rel = "") {
2048
2659
  let entries;
2049
2660
  try {
2050
- entries = await readdir5(join11(dir, rel), { withFileTypes: true });
2661
+ entries = await readdir5(join13(dir, rel), { withFileTypes: true });
2051
2662
  } catch {
2052
2663
  return [];
2053
2664
  }
@@ -2192,7 +2803,7 @@ async function captureHandoff(input) {
2192
2803
  } else {
2193
2804
  const texts = await Promise.all(recorded.map(async (r) => [
2194
2805
  r,
2195
- await readFile9(join11(folder, ...r.path.split("/")), "utf8").catch(() => void 0)
2806
+ await readFile11(join13(folder, ...r.path.split("/")), "utf8").catch(() => void 0)
2196
2807
  ]));
2197
2808
  if (texts.every(([r, t]) => t !== void 0 && sameText(t, r.sha256))) {
2198
2809
  from = "working-tree";
@@ -2256,10 +2867,10 @@ async function captureHandoff(input) {
2256
2867
  } : {}
2257
2868
  };
2258
2869
  const p = capturePaths(name);
2259
- await rm2(join11(folder, ...p.drafted.split("/")), { recursive: true, force: true });
2260
- await rm2(join11(folder, ...p.merged.split("/")), { recursive: true, force: true });
2870
+ await rm2(join13(folder, ...p.drafted.split("/")), { recursive: true, force: true });
2871
+ await rm2(join13(folder, ...p.merged.split("/")), { recursive: true, force: true });
2261
2872
  const put = async (base, rel, text) => {
2262
- const f = join11(folder, ...base.split("/"), ...rel.split("/"));
2873
+ const f = join13(folder, ...base.split("/"), ...rel.split("/"));
2263
2874
  await mkdir2(dirname(f), { recursive: true });
2264
2875
  await writeFileAtomic(f, text);
2265
2876
  };
@@ -2268,37 +2879,37 @@ async function captureHandoff(input) {
2268
2879
  if (state === "captured")
2269
2880
  for (const [rel, t] of mergedFiles)
2270
2881
  await put(p.merged, rel, t);
2271
- await mkdir2(join11(folder, "stage"), { recursive: true });
2272
- await writeFileAtomic(join11(folder, ...p.capture.split("/")), HEADER + (0, import_yaml8.stringify)(capture, { lineWidth: 0 }));
2882
+ await mkdir2(join13(folder, "stage"), { recursive: true });
2883
+ await writeFileAtomic(join13(folder, ...p.capture.split("/")), HEADER + (0, import_yaml9.stringify)(capture, { lineWidth: 0 }));
2273
2884
  return { capture, file: p.capture };
2274
2885
  }
2275
2886
  async function readFolderFiles(dir) {
2276
2887
  const out = /* @__PURE__ */ new Map();
2277
2888
  for (const f of await walk2(dir))
2278
- out.set(f, (await readFile9(join11(dir, ...f.split("/")), "utf8")).replace(/\r\n/g, "\n"));
2889
+ out.set(f, (await readFile11(join13(dir, ...f.split("/")), "utf8")).replace(/\r\n/g, "\n"));
2279
2890
  return out;
2280
2891
  }
2281
2892
  async function changeFolders(root) {
2282
- const base = join11(root, ...changesRelOf(root).split("/"));
2893
+ const base = join13(root, ...changesRelOf(root).split("/"));
2283
2894
  const out = [];
2284
2895
  const list = async (dir) => (await readdir5(dir, { withFileTypes: true }).catch(() => [])).filter((e) => e.isDirectory()).map((e) => e.name).sort();
2285
2896
  for (const name of await list(base)) {
2286
2897
  if (name === "archive")
2287
- for (const a of await list(join11(base, "archive")))
2288
- out.push(join11(base, "archive", a));
2898
+ for (const a of await list(join13(base, "archive")))
2899
+ out.push(join13(base, "archive", a));
2289
2900
  else
2290
- out.push(join11(base, name));
2901
+ out.push(join13(base, name));
2291
2902
  }
2292
2903
  return out;
2293
2904
  }
2294
2905
  async function readDraftPairs(root) {
2295
2906
  const out = [];
2296
2907
  for (const folder of await changeFolders(root)) {
2297
- const names = (await readdir5(join11(folder, "stage")).catch(() => [])).filter((f) => f.endsWith(".capture.yaml")).sort();
2908
+ const names = (await readdir5(join13(folder, "stage")).catch(() => [])).filter((f) => f.endsWith(".capture.yaml")).sort();
2298
2909
  for (const f of names) {
2299
2910
  let capture;
2300
2911
  try {
2301
- capture = DraftCapture.parse((0, import_yaml8.parse)(await readFile9(join11(folder, "stage", f), "utf8")));
2912
+ capture = DraftCapture.parse((0, import_yaml9.parse)(await readFile11(join13(folder, "stage", f), "utf8")));
2302
2913
  } catch {
2303
2914
  continue;
2304
2915
  }
@@ -2306,8 +2917,8 @@ async function readDraftPairs(root) {
2306
2917
  out.push({
2307
2918
  capture,
2308
2919
  folder: relative(root, folder).split(sep).join("/"),
2309
- drafted: await readFolderFiles(join11(folder, ...p.drafted.split("/"))),
2310
- merged: await readFolderFiles(join11(folder, ...p.merged.split("/")))
2920
+ drafted: await readFolderFiles(join13(folder, ...p.drafted.split("/"))),
2921
+ merged: await readFolderFiles(join13(folder, ...p.merged.split("/")))
2311
2922
  });
2312
2923
  }
2313
2924
  }
@@ -2327,8 +2938,8 @@ async function completeCaptures(root, ctx, o) {
2327
2938
  continue;
2328
2939
  for (const h of change.handoffs.filter((x) => x.state === "merged")) {
2329
2940
  const { name } = await captureStageOf(ctx, change, h.phase);
2330
- const file = join11(changeFolder(root, change.id), ...capturePaths(name).capture.split("/"));
2331
- const prior = await readFile9(file, "utf8").then((t) => DraftCapture.safeParse((0, import_yaml8.parse)(t)), () => void 0);
2941
+ const file = join13(changeFolder(root, change.id), ...capturePaths(name).capture.split("/"));
2942
+ const prior = await readFile11(file, "utf8").then((t) => DraftCapture.safeParse((0, import_yaml9.parse)(t)), () => void 0);
2332
2943
  if (prior?.success && prior.data.pr.number === h.pr.number && prior.data.state !== "waiting-for-merge")
2333
2944
  continue;
2334
2945
  const base = prior?.success ? prior.data.base : o.defaultBase ?? "main";
@@ -2350,7 +2961,7 @@ async function completeCaptures(root, ctx, o) {
2350
2961
  }
2351
2962
 
2352
2963
  // packages/changes/dist/handoff.js
2353
- var nowIso2 = () => (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
2964
+ var nowIso3 = () => (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
2354
2965
  var strip3 = (h) => h.startsWith("@") ? h.slice(1) : h;
2355
2966
  function metarepo(ctx, repo) {
2356
2967
  const fromRuntime = ctx.orgPack.runtime.adapters.gitHost?.settings.metarepo;
@@ -2368,7 +2979,7 @@ async function load(root, id) {
2368
2979
  }
2369
2980
  async function walkReview(dir, gate) {
2370
2981
  const out = [];
2371
- const base = join12(dir, "review", gate);
2982
+ const base = join14(dir, "review", gate);
2372
2983
  let rounds;
2373
2984
  try {
2374
2985
  rounds = (await readdir6(base)).filter((r) => /^\d+$/.test(r)).sort((a, b) => Number(a) - Number(b));
@@ -2377,7 +2988,7 @@ async function walkReview(dir, gate) {
2377
2988
  }
2378
2989
  for (const r of rounds) {
2379
2990
  try {
2380
- for (const f of (await readdir6(join12(base, r))).filter((x) => /\.ya?ml$/.test(x)).sort())
2991
+ for (const f of (await readdir6(join14(base, r))).filter((x) => /\.ya?ml$/.test(x)).sort())
2381
2992
  out.push(`review/${gate}/${r}/${f}`);
2382
2993
  } catch {
2383
2994
  }
@@ -2388,12 +2999,12 @@ async function keptPairFiles(dir) {
2388
2999
  const out = [];
2389
3000
  let entries;
2390
3001
  try {
2391
- entries = await readdir6(join12(dir, "stage"), { withFileTypes: true });
3002
+ entries = await readdir6(join14(dir, "stage"), { withFileTypes: true });
2392
3003
  } catch {
2393
3004
  return out;
2394
3005
  }
2395
3006
  const walk3 = async (rel) => {
2396
- for (const e of await readdir6(join12(dir, ...rel.split("/")), { withFileTypes: true }).catch(() => [])) {
3007
+ for (const e of await readdir6(join14(dir, ...rel.split("/")), { withFileTypes: true }).catch(() => [])) {
2397
3008
  const p = `${rel}/${e.name}`;
2398
3009
  if (e.isDirectory())
2399
3010
  await walk3(p);
@@ -2421,18 +3032,23 @@ async function handoffFiles(ctx, a, phase) {
2421
3032
  for (const x of f)
2422
3033
  files.add(x);
2423
3034
  }
2424
- const stage = await phaseStage(ctx, a.change, phase);
2425
- if (stage)
2426
- for (const f of await stageFiles(dir, stage))
2427
- files.add(f);
3035
+ const at = PHASES.indexOf(phase);
3036
+ for (const step of ctx.policy.stages.filter((s) => PHASES.indexOf(s.phase) <= at)) {
3037
+ if (await phaseStage(ctx, a.change, step.phase))
3038
+ for (const f of await stageFiles(dir, step))
3039
+ files.add(f);
3040
+ }
3041
+ const present = async (f) => readFile12(join14(dir, ...f.split("/"))).then(() => true, () => false);
3042
+ for (const own of ["stage/corrections.yaml", "stage/drafted.yaml"])
3043
+ if (await present(own))
3044
+ files.add(own);
2428
3045
  if (phase === "build") {
2429
- for (const extra of ["tasks.md", "repos.yaml"]) {
2430
- try {
2431
- await readFile10(join12(dir, extra));
3046
+ for (const extra of ["tasks.md", "repos.yaml"])
3047
+ if (await present(extra))
2432
3048
  files.add(extra);
2433
- } catch {
2434
- }
2435
- }
3049
+ for (const t of await readdir6(join14(dir, "tasks"), { withFileTypes: true }).catch(() => []))
3050
+ if (t.isDirectory() && await present(`tasks/${t.name}/report.md`))
3051
+ files.add(`tasks/${t.name}/report.md`);
2436
3052
  }
2437
3053
  for (const gate of phaseGates(ctx, phase))
2438
3054
  for (const f of await walkReview(dir, gate))
@@ -2441,6 +3057,18 @@ async function handoffFiles(ctx, a, phase) {
2441
3057
  files.add(f);
2442
3058
  return { files: [...files].sort(), artifacts };
2443
3059
  }
3060
+ function namesChange(command, id) {
3061
+ const w = (command ?? "").trim().split(/\s+/);
3062
+ if (w[0] !== "atticus")
3063
+ return false;
3064
+ const changeStep = w[1] === "change" || w[1] === "plan" || w[1] === "critic";
3065
+ return changeStep && w.slice(2).includes(id);
3066
+ }
3067
+ async function changeQuestionFiles(root, id) {
3068
+ const { questions } = await listQuestions(root, { state: "answered" });
3069
+ const rel = questionsRelOf(root);
3070
+ return questions.filter((q) => namesChange(q.askedBy.command, id) || namesChange(q.rerun, id)).map((q) => `${rel}/${q.id}.yaml`).sort();
3071
+ }
2444
3072
  function prBody(a, phase, next, paths, reviewers, review) {
2445
3073
  const c = a.change;
2446
3074
  const lines2 = [
@@ -2475,7 +3103,7 @@ async function handoff(root, id, opts) {
2475
3103
  requireApproval("changes", opts.approval);
2476
3104
  const { ctx, change } = await load(root, id);
2477
3105
  const repo = metarepo(ctx, opts.repo);
2478
- const now = opts.now ?? nowIso2();
3106
+ const now = opts.now ?? nowIso3();
2479
3107
  const by = strip3(opts.by);
2480
3108
  const a = await assess(ctx, change);
2481
3109
  const phase = a.phase;
@@ -2487,7 +3115,7 @@ async function handoff(root, id, opts) {
2487
3115
  await requireValid(opts.openspec, id);
2488
3116
  const { files, artifacts } = await handoffFiles(ctx, a, phase);
2489
3117
  const dir = changeFolder(root, id);
2490
- const hashed = await Promise.all(files.map(async (path) => ({ path, sha256: sha256(await readFile10(join12(dir, path))) })));
3118
+ const hashed = await Promise.all(files.map(async (path) => ({ path, sha256: sha256(await readFile12(join14(dir, path))) })));
2491
3119
  const authors = { ...change.authors };
2492
3120
  for (const art of artifacts) {
2493
3121
  const people = (opts.authors?.[art] ?? [by]).map(strip3);
@@ -2495,15 +3123,20 @@ async function handoff(root, id, opts) {
2495
3123
  }
2496
3124
  const branch = handoffBranch(id, phase);
2497
3125
  const rel = changesRelOf(root);
2498
- const metaFiles = [...files.map((f) => `${rel}/${id}/${f}`), `${rel}/${id}/change.yaml`];
3126
+ const questionFiles = await changeQuestionFiles(root, id);
3127
+ const metaFiles = [
3128
+ ...files.map((f) => `${rel}/${id}/${f}`),
3129
+ `${rel}/${id}/change.yaml`,
3130
+ ...questionFiles
3131
+ ];
2499
3132
  const reviewers = [
2500
3133
  ...new Set(files.flatMap((f) => ownersFor(ctx.codeowners, `${rel}/${id}/${f}`)))
2501
3134
  ].filter((p) => p !== by).sort();
2502
- const live = change.state === "returned" ? void 0 : liveHandoff(change);
2503
- if (live && live.phase === phase) {
2504
- const pr2 = await opts.gitHost.getPullRequest(repo, live.pr.number);
3135
+ const live2 = change.state === "returned" ? void 0 : liveHandoff(change);
3136
+ if (live2 && live2.phase === phase) {
3137
+ const pr2 = await handoffPr(opts.gitHost, repo, id, live2);
2505
3138
  if (pr2.state === "open" || pr2.state === "draft") {
2506
- await opts.publisher?.publish({
3139
+ const published = await opts.publisher?.publish({
2507
3140
  root,
2508
3141
  branch,
2509
3142
  base: pr2.base,
@@ -2511,9 +3144,11 @@ async function handoff(root, id, opts) {
2511
3144
  message: `${handoffName(a.type, phase)}: update`,
2512
3145
  approval: opts.approval
2513
3146
  });
2514
- await opts.gitHost.comment(repo, live.pr.number, `${by} updated the hand-off for another review.`, opts.approval);
3147
+ const unchanged = published ? published.changed === false : sameArtifacts(hashed, live2.artifacts);
3148
+ if (!unchanged)
3149
+ await opts.gitHost.comment(repo, pr2.number, `${by} updated the hand-off for another review.`, opts.approval);
2515
3150
  const updated = {
2516
- ...live,
3151
+ ...live2,
2517
3152
  state: "open",
2518
3153
  updatedAt: now,
2519
3154
  reviewers,
@@ -2523,14 +3158,21 @@ async function handoff(root, id, opts) {
2523
3158
  await changeStore(root).write({
2524
3159
  ...change,
2525
3160
  authors,
2526
- handoffs: change.handoffs.map((h2) => h2 === live ? updated : h2)
3161
+ handoffs: change.handoffs.map((h2) => h2 === live2 ? updated : h2)
2527
3162
  });
2528
3163
  return updated;
2529
3164
  }
2530
3165
  if (pr2.state === "merged")
2531
3166
  throw new ChangeError("refused", `PR #${pr2.number} is already merged on the Git host; sync the change instead.`);
2532
- live.state = "closed";
2533
- live.updatedAt = now;
3167
+ live2.state = "closed";
3168
+ live2.updatedAt = now;
3169
+ }
3170
+ if (change.state === "returned") {
3171
+ const open = (await opts.gitHost.listPullRequests(repo, { head: branch })).find((p) => p.state === "open" || p.state === "draft");
3172
+ if (open) {
3173
+ const message = `PR #${open.number} from ${branch} is still open on the Git host (${open.url}): close it there first (the return asked for that), then resubmit the change.`;
3174
+ throw new ChangeError("refused", message, [{ code: "host-refused", message }]);
3175
+ }
2534
3176
  }
2535
3177
  const base = opts.base ?? (await opts.gitHost.getRepo(repo)).defaultBranch;
2536
3178
  const next = a.next;
@@ -2542,13 +3184,23 @@ async function handoff(root, id, opts) {
2542
3184
  message: `${handoffName(a.type, phase)}: ${change.title}`,
2543
3185
  approval: opts.approval
2544
3186
  });
2545
- const pr = await opts.gitHost.createPullRequest({
2546
- repo,
2547
- head: branch,
2548
- base,
2549
- title: `${change.title}: ${handoffName(a.type, phase)}`,
2550
- body: prBody(a, phase, change.state === "returned" ? nextAfterResubmit(a, phase) : next, files.map((f) => `${rel}/${id}/${f}`), reviewers, opts.review)
2551
- }, opts.approval);
3187
+ let pr;
3188
+ try {
3189
+ pr = await opts.gitHost.createPullRequest({
3190
+ repo,
3191
+ head: branch,
3192
+ base,
3193
+ title: `${change.title}: ${handoffName(a.type, phase)}`,
3194
+ body: prBody(a, phase, change.state === "returned" ? nextAfterResubmit(a, phase) : next, [...files.map((f) => `${rel}/${id}/${f}`), ...questionFiles], reviewers, opts.review)
3195
+ }, opts.approval);
3196
+ } catch (e) {
3197
+ if (!(e instanceof AdapterError) || e.code !== "conflict")
3198
+ throw e;
3199
+ const open = (await opts.gitHost.listPullRequests(repo, { head: branch }).catch(() => [])).filter((p) => p.base === base && (p.state === "open" || p.state === "draft")).at(0);
3200
+ if (!open)
3201
+ throw e;
3202
+ pr = open;
3203
+ }
2552
3204
  const h = {
2553
3205
  phase,
2554
3206
  branch,
@@ -2575,28 +3227,44 @@ async function handoff(root, id, opts) {
2575
3227
  });
2576
3228
  return h;
2577
3229
  }
3230
+ function sameArtifacts(a, b) {
3231
+ const key = (xs) => JSON.stringify([...xs].map((x) => `${x.path} ${x.sha256}`).sort());
3232
+ return key(a) === key(b);
3233
+ }
2578
3234
  function nextAfterResubmit(a, phase) {
2579
3235
  const next = a.type.phases[a.type.phases.indexOf(phase) + 1];
2580
3236
  return next ? phaseState(next) : "released";
2581
3237
  }
3238
+ async function handoffPr(gitHost, repo, id, live2) {
3239
+ const branch = handoffBranch(id, live2.phase);
3240
+ const pr = await gitHost.getPullRequest(repo, live2.pr.number);
3241
+ if (pr.head === branch)
3242
+ return pr;
3243
+ const mine = await gitHost.listPullRequests(repo, { head: branch });
3244
+ const own = mine.find((x) => x.url === live2.pr.url) ?? (mine.length === 1 ? mine[0] : void 0);
3245
+ if (own)
3246
+ return own;
3247
+ const message = `PR #${live2.pr.number} on the Git host is from ${pr.head}, not ${branch}, and no PR from ${branch} was found: Atticus uses only the hand-off's own PR. Check the PR's number on the Git host (on a manual host, on its step: atticus todo), then sync again.`;
3248
+ throw new ChangeError("refused", message, [{ code: "host-refused", message }]);
3249
+ }
2582
3250
  async function syncHandoff(root, id, opts) {
2583
3251
  const { ctx, change } = await load(root, id);
2584
3252
  const repo = metarepo(ctx, opts.repo);
2585
- const now = opts.now ?? nowIso2();
2586
- const live = liveHandoff(change);
2587
- if (!live)
3253
+ const now = opts.now ?? nowIso3();
3254
+ const live2 = liveHandoff(change);
3255
+ if (!live2)
2588
3256
  throw new ChangeError("refused", `${id} has no open hand-off PR to sync.`, [
2589
3257
  { code: "no-handoff", message: `${id} has no open hand-off PR.` }
2590
3258
  ]);
2591
3259
  const a = await assess(ctx, change);
2592
- const pr = await opts.gitHost.getPullRequest(repo, live.pr.number);
2593
- const current = pr.url !== live.pr.url || pr.number !== live.pr.number ? { ...live, pr: { number: pr.number, url: pr.url } } : live;
3260
+ const pr = await handoffPr(opts.gitHost, repo, id, live2);
3261
+ const current = pr.url !== live2.pr.url || pr.number !== live2.pr.number ? { ...live2, pr: { number: pr.number, url: pr.url } } : live2;
2594
3262
  const store = changeStore(root);
2595
3263
  const save = async (h2, extra = {}) => {
2596
3264
  const updated = {
2597
3265
  ...change,
2598
3266
  ...extra,
2599
- handoffs: change.handoffs.map((x) => x === live ? h2 : x)
3267
+ handoffs: change.handoffs.map((x) => x === live2 ? h2 : x)
2600
3268
  };
2601
3269
  await store.write(updated);
2602
3270
  return updated;
@@ -2618,7 +3286,7 @@ async function syncHandoff(root, id, opts) {
2618
3286
  latest.set(strip3(r.by), r.state);
2619
3287
  const approvers = [...latest].filter(([, s]) => s === "approved").map(([p]) => p);
2620
3288
  const requested = [...latest].filter(([, s]) => s === "changes-requested").map(([p]) => p).sort();
2621
- const counted = prApprovers(live, approvers);
3289
+ const counted = prApprovers(live2, approvers);
2622
3290
  const approvals = counted.map((by) => ({ by, at: now }));
2623
3291
  const advance = async (by) => {
2624
3292
  const h2 = { ...current, state: "merged", updatedAt: now, approvals };
@@ -2631,7 +3299,7 @@ async function syncHandoff(root, id, opts) {
2631
3299
  at: now,
2632
3300
  by,
2633
3301
  pr: pr.number,
2634
- reason: `${handoffName(a.type, live.phase)} merged`
3302
+ reason: `${handoffName(a.type, live2.phase)} merged`
2635
3303
  }
2636
3304
  ];
2637
3305
  let state = next;
@@ -2666,7 +3334,7 @@ async function syncHandoff(root, id, opts) {
2666
3334
  };
2667
3335
  };
2668
3336
  if (pr.state === "merged")
2669
- return advance(strip3(opts.approval?.by ?? live.by));
3337
+ return advance(strip3(opts.approval?.by ?? live2.by));
2670
3338
  if (requested.length > 0) {
2671
3339
  const h2 = { ...current, state: "changes-requested", updatedAt: now, approvals };
2672
3340
  await save(h2);
@@ -2687,8 +3355,8 @@ async function syncHandoff(root, id, opts) {
2687
3355
  if (counted.length === 0)
2688
3356
  reasons.push(approvers.length > 0 ? {
2689
3357
  code: "self-approval",
2690
- person: strip3(live.by),
2691
- message: `PR #${pr.number} is approved only by ${strip3(live.by)}, who raised it; it needs someone else's approval.`
3358
+ person: strip3(live2.by),
3359
+ message: `PR #${pr.number} is approved only by ${strip3(live2.by)}, who raised it; it needs someone else's approval.`
2692
3360
  } : {
2693
3361
  code: "approval-missing",
2694
3362
  message: `PR #${pr.number} isn't approved yet: ask a reviewer on the Git host (${pr.url}).`
@@ -2727,19 +3395,19 @@ async function syncHandoff(root, id, opts) {
2727
3395
  }
2728
3396
  async function returnToPo(root, id, opts) {
2729
3397
  const { ctx, change } = await load(root, id);
2730
- const live = liveHandoff(change);
2731
- if (!live)
3398
+ const live2 = liveHandoff(change);
3399
+ if (!live2)
2732
3400
  throw new ChangeError("refused", `${id} has no open hand-off PR to return.`, [
2733
3401
  { code: "no-handoff", message: `${id} has no open hand-off PR.` }
2734
3402
  ]);
2735
3403
  requireApproval("changes", opts.approval);
2736
3404
  const repo = metarepo(ctx, opts.repo);
2737
- const now = opts.now ?? nowIso2();
3405
+ const now = opts.now ?? nowIso3();
2738
3406
  const by = strip3(opts.by);
2739
3407
  const host = opts.gitHost;
2740
- await host.comment(repo, live.pr.number, `Returned to the product owner by ${by}: ${opts.reason}`, opts.approval);
2741
- const closedOnHost = host.closePullRequest ? (await host.closePullRequest(repo, live.pr.number, opts.approval)).state === "closed" : false;
2742
- const h = { ...live, state: "closed", updatedAt: now };
3408
+ await host.comment(repo, live2.pr.number, `Returned to the product owner by ${by}: ${opts.reason}`, opts.approval);
3409
+ const closedOnHost = host.closePullRequest ? (await host.closePullRequest(repo, live2.pr.number, opts.approval)).state === "closed" : false;
3410
+ const h = { ...live2, state: "closed", updatedAt: now };
2743
3411
  await changeStore(root).write({
2744
3412
  ...change,
2745
3413
  state: "returned",
@@ -2750,11 +3418,11 @@ async function returnToPo(root, id, opts) {
2750
3418
  to: "returned",
2751
3419
  at: now,
2752
3420
  by,
2753
- pr: live.pr.number,
3421
+ pr: live2.pr.number,
2754
3422
  reason: `Returned to the product owner (${opts.reason})`
2755
3423
  }
2756
3424
  ],
2757
- handoffs: change.handoffs.map((x) => x === live ? h : x)
3425
+ handoffs: change.handoffs.map((x) => x === live2 ? h : x)
2758
3426
  });
2759
3427
  return { state: "returned", closedOnHost, handoff: h };
2760
3428
  }
@@ -2774,7 +3442,7 @@ async function archiveChange(root, id, opts) {
2774
3442
  throw new ChangeError("refused", `${id} can't be archived yet.`, a.reasons);
2775
3443
  if (opts.openspec)
2776
3444
  await requireValid(opts.openspec, id);
2777
- const now = opts.now ?? nowIso2();
3445
+ const now = opts.now ?? nowIso3();
2778
3446
  const updated = {
2779
3447
  ...change,
2780
3448
  state: "archived",
@@ -2790,9 +3458,9 @@ async function archiveChange(root, id, opts) {
2790
3458
  }
2791
3459
 
2792
3460
  // packages/changes/dist/learn.js
2793
- var import_yaml9 = __toESM(require_dist(), 1);
2794
- import { readFile as readFile11 } from "node:fs/promises";
2795
- import { join as join13 } from "node:path";
3461
+ var import_yaml10 = __toESM(require_dist(), 1);
3462
+ import { readFile as readFile13 } from "node:fs/promises";
3463
+ import { join as join15 } from "node:path";
2796
3464
  var LEARN_SCHEMA_VERSION = 1;
2797
3465
  var MAX_CELLS = 4e6;
2798
3466
  var lines = (text) => {
@@ -3139,7 +3807,7 @@ async function pairTouches(root, p) {
3139
3807
  let entry;
3140
3808
  const yaml = (t) => {
3141
3809
  try {
3142
- const v = (0, import_yaml9.parse)(t);
3810
+ const v = (0, import_yaml10.parse)(t);
3143
3811
  return v && typeof v === "object" ? v : void 0;
3144
3812
  } catch {
3145
3813
  return void 0;
@@ -3181,7 +3849,7 @@ async function pairTouches(root, p) {
3181
3849
  flows.push(m[1]);
3182
3850
  }
3183
3851
  }
3184
- const change = yaml(await readFile11(join13(root, ...p.folder.split("/"), "change.yaml"), "utf8").catch(() => ""));
3852
+ const change = yaml(await readFile13(join15(root, ...p.folder.split("/"), "change.yaml"), "utf8").catch(() => ""));
3185
3853
  const touches = change?.touches ?? {};
3186
3854
  for (const r of Array.isArray(touches.repos) ? touches.repos : [])
3187
3855
  if (typeof r === "string")
@@ -3274,8 +3942,8 @@ async function detectConflicts(root) {
3274
3942
  }
3275
3943
 
3276
3944
  // packages/changes/dist/size.js
3277
- import { readFile as readFile12 } from "node:fs/promises";
3278
- import { join as join14 } from "node:path";
3945
+ import { readFile as readFile14 } from "node:fs/promises";
3946
+ import { join as join16 } from "node:path";
3279
3947
  async function changeSize(ctx, change) {
3280
3948
  const type = ctx.orgPack.changeTypes.types.find((t) => t.id === change.type);
3281
3949
  if (!type)
@@ -3284,7 +3952,7 @@ async function changeSize(ctx, change) {
3284
3952
  why: "standard",
3285
3953
  words: `${change.type} isn't a change type in change-types.yaml, so ${change.id} takes the full path.`
3286
3954
  };
3287
- const brief = await readFile12(join14(changeFolder(ctx.root, change.id), "brief.md"), "utf8").catch(() => "");
3955
+ const brief = await readFile14(join16(changeFolder(ctx.root, change.id), "brief.md"), "utf8").catch(() => "");
3288
3956
  const f = fastPathOf(type, brief);
3289
3957
  const gate = f.fast ? fastGateOf(type, ctx.orgPack.gates) : void 0;
3290
3958
  const words = {
@@ -3298,14 +3966,14 @@ async function changeSize(ctx, change) {
3298
3966
  }
3299
3967
 
3300
3968
  // packages/changes/dist/openspec-stores.js
3301
- import { existsSync } from "node:fs";
3302
- import { readFile as readFile13, writeFile as writeFile2 } from "node:fs/promises";
3969
+ import { existsSync as existsSync2 } from "node:fs";
3970
+ import { readFile as readFile15, writeFile as writeFile3 } from "node:fs/promises";
3303
3971
  import { homedir } from "node:os";
3304
- import { basename, isAbsolute, join as join15, relative as relative2, resolve, sep as sep2 } from "node:path";
3972
+ import { basename, isAbsolute, join as join17, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
3305
3973
  function storeDirOf(root, path) {
3306
3974
  if (path === "~" || path.startsWith("~/") || path.startsWith("~\\"))
3307
- return join15(homedir(), path.slice(2));
3308
- return isAbsolute(path) ? resolve(path) : resolve(root, path);
3975
+ return join17(homedir(), path.slice(2));
3976
+ return isAbsolute(path) ? resolve2(path) : resolve2(root, path);
3309
3977
  }
3310
3978
  function packRel(root) {
3311
3979
  return relative2(root, orgPackDirOf(root)).split(sep2).join("/");
@@ -3334,7 +4002,7 @@ async function storeSpecLookup(root, capability) {
3334
4002
  var REMOTE = /^[a-z][a-z0-9+.-]*:\/\//i;
3335
4003
  var SCP = /^[^/\\]+@[^:/\\]+:/;
3336
4004
  async function metarepoOrigin(root, git = createGit()) {
3337
- if (!existsSync(join15(root, ".git")))
4005
+ if (!existsSync2(join17(root, ".git")))
3338
4006
  return void 0;
3339
4007
  try {
3340
4008
  const url = (await git(["remote", "get-url", "origin"], root)).stdout.trim();
@@ -3345,7 +4013,7 @@ async function metarepoOrigin(root, git = createGit()) {
3345
4013
  return void 0;
3346
4014
  }
3347
4015
  }
3348
- async function referenceMetarepoIn(root, repoDir, o = {}) {
4016
+ async function referenceMetarepoIn(root, repoDir2, o = {}) {
3349
4017
  const { settings } = await loadOpenSpecSettings(orgPackDirOf(root));
3350
4018
  if (!settings.referenceInRepos)
3351
4019
  return { action: "off" };
@@ -3357,8 +4025,8 @@ async function referenceMetarepoIn(root, repoDir, o = {}) {
3357
4025
  file,
3358
4026
  why: marker.missing ? `this metarepo isn't an OpenSpec store yet (no ${STORE_MARKER}): run atticus upgrade --openspec-store, commit the file, then hand down again` : `this metarepo's ${STORE_MARKER} can't be read (${marker.notes.join("; ")})`
3359
4027
  };
3360
- const path = join15(repoDir, "openspec", "config.yaml");
3361
- const text = await readFile13(path, "utf8").catch(() => void 0);
4028
+ const path = join17(repoDir2, "openspec", "config.yaml");
4029
+ const text = await readFile15(path, "utf8").catch(() => void 0);
3362
4030
  if (text === void 0)
3363
4031
  return {
3364
4032
  action: "skipped",
@@ -3370,10 +4038,180 @@ async function referenceMetarepoIn(root, repoDir, o = {}) {
3370
4038
  if ("problem" in r)
3371
4039
  return { action: "skipped", file, why: r.problem };
3372
4040
  if (r.action === "added")
3373
- await writeFile2(path, r.text, "utf8");
4041
+ await writeFile3(path, r.text, "utf8");
3374
4042
  return { action: r.action, file };
3375
4043
  }
3376
4044
 
4045
+ // packages/changes/dist/merge-record.js
4046
+ var import_yaml11 = __toESM(require_dist(), 1);
4047
+ import { isDeepStrictEqual } from "node:util";
4048
+ var HANDOFF_RANK = {
4049
+ open: 0,
4050
+ "changes-requested": 1,
4051
+ closed: 2,
4052
+ merged: 3
4053
+ };
4054
+ var Clash = class extends Error {
4055
+ field;
4056
+ constructor(field) {
4057
+ super(field);
4058
+ this.field = field;
4059
+ }
4060
+ };
4061
+ function three(field, base, ours, theirs) {
4062
+ if (isDeepStrictEqual(ours, theirs))
4063
+ return ours;
4064
+ if (base.has) {
4065
+ if (isDeepStrictEqual(ours, base.value))
4066
+ return theirs;
4067
+ if (isDeepStrictEqual(theirs, base.value))
4068
+ return ours;
4069
+ }
4070
+ throw new Clash(field);
4071
+ }
4072
+ function union(ours, theirs, key, pick) {
4073
+ const out = /* @__PURE__ */ new Map();
4074
+ for (const x of ours)
4075
+ if (!out.has(key(x)))
4076
+ out.set(key(x), x);
4077
+ for (const x of theirs) {
4078
+ const k = key(x);
4079
+ const had = out.get(k);
4080
+ out.set(k, had === void 0 ? x : pick ? pick(had, x) : had);
4081
+ }
4082
+ return [...out.values()];
4083
+ }
4084
+ var startsWith = (a, b) => b.length <= a.length && b.every((x, i) => isDeepStrictEqual(x, a[i]));
4085
+ function mergeHistory(ours, theirs) {
4086
+ if (startsWith(ours, theirs))
4087
+ return ours;
4088
+ if (startsWith(theirs, ours))
4089
+ return theirs;
4090
+ const all = union(ours, theirs, (h) => `${h.from}>${h.to}@${h.at}`);
4091
+ const merged = all.map((h, i) => ({ h, i })).sort((a, b) => a.h.at < b.h.at ? -1 : a.h.at > b.h.at ? 1 : a.i - b.i).map((x) => x.h);
4092
+ let at = "draft";
4093
+ for (const h of merged) {
4094
+ if (h.from !== at)
4095
+ throw new Clash("history");
4096
+ at = h.to;
4097
+ }
4098
+ return merged;
4099
+ }
4100
+ function mergeHandoff(a, b) {
4101
+ if (isDeepStrictEqual(a, b))
4102
+ return a;
4103
+ const when = (h) => h.updatedAt ?? h.openedAt;
4104
+ const ra = HANDOFF_RANK[a.state];
4105
+ const rb = HANDOFF_RANK[b.state];
4106
+ const top = rb > ra || rb === ra && when(b) > when(a) ? b : a;
4107
+ return { ...top, reviewers: [.../* @__PURE__ */ new Set([...a.reviewers, ...b.reviewers])] };
4108
+ }
4109
+ function mergeHandoffs(ours, theirs) {
4110
+ const all = union(ours, theirs, (h) => `${h.phase}#${h.pr.number}`, mergeHandoff);
4111
+ return all.map((h, i) => ({ h, i })).sort((a, b) => a.h.openedAt < b.h.openedAt ? -1 : a.h.openedAt > b.h.openedAt ? 1 : a.i - b.i).map((x) => x.h);
4112
+ }
4113
+ function byKey(field, base, ours, theirs) {
4114
+ const out = {};
4115
+ for (const k of [.../* @__PURE__ */ new Set([...Object.keys(ours), ...Object.keys(theirs)])]) {
4116
+ const v = three(`${field}.${k}`, { has: base !== void 0, value: base?.[k] }, ours[k], theirs[k]);
4117
+ if (v !== void 0)
4118
+ out[k] = v;
4119
+ }
4120
+ return out;
4121
+ }
4122
+ function mergeChangeRecords(base, ours, theirs) {
4123
+ const b = base === void 0 ? void 0 : ChangeLifecycle.parse(base);
4124
+ const o = ChangeLifecycle.parse(ours);
4125
+ const t = ChangeLifecycle.parse(theirs);
4126
+ const has = b !== void 0;
4127
+ const at = (k) => ({ has, value: b?.[k] });
4128
+ try {
4129
+ const history = mergeHistory(o.history, t.history);
4130
+ const authors = {};
4131
+ for (const k of [.../* @__PURE__ */ new Set([...Object.keys(o.authors), ...Object.keys(t.authors)])]) {
4132
+ const key = k;
4133
+ authors[key] = [.../* @__PURE__ */ new Set([...o.authors[key] ?? [], ...t.authors[key] ?? []])];
4134
+ }
4135
+ const template = three("template", at("template"), o.template, t.template);
4136
+ const value = {
4137
+ schemaVersion: o.schemaVersion,
4138
+ id: three("id", at("id"), o.id, t.id),
4139
+ title: three("title", at("title"), o.title, t.title),
4140
+ type: three("type", at("type"), o.type, t.type),
4141
+ ...template !== void 0 ? { template } : {},
4142
+ state: history.length ? history.at(-1).to : three("state", at("state"), o.state, t.state),
4143
+ owners: byKey("owners", b?.owners, o.owners, t.owners),
4144
+ systems: three("systems", at("systems"), o.systems, t.systems),
4145
+ touches: byKey("touches", b?.touches, o.touches, t.touches),
4146
+ authors,
4147
+ createdAt: three("createdAt", at("createdAt"), o.createdAt, t.createdAt),
4148
+ createdBy: three("createdBy", at("createdBy"), o.createdBy, t.createdBy),
4149
+ history,
4150
+ handoffs: mergeHandoffs(o.handoffs, t.handoffs)
4151
+ };
4152
+ const r = ChangeLifecycle.safeParse(value);
4153
+ if (!r.success)
4154
+ return { ok: false, field: r.error.issues[0]?.path.map(String).join(".") || "record" };
4155
+ return { ok: true, value: r.data };
4156
+ } catch (e) {
4157
+ if (e instanceof Clash)
4158
+ return { ok: false, field: e.field };
4159
+ throw e;
4160
+ }
4161
+ }
4162
+ function readRecord(text) {
4163
+ try {
4164
+ const r = ChangeLifecycle.safeParse((0, import_yaml11.parse)(text));
4165
+ return r.success ? r.data : void 0;
4166
+ } catch {
4167
+ return void 0;
4168
+ }
4169
+ }
4170
+ function mergeChangeText(baseText, oursText, theirsText) {
4171
+ const ours = readRecord(oursText);
4172
+ const theirs = readRecord(theirsText);
4173
+ const base = baseText.trim() ? readRecord(baseText) : void 0;
4174
+ if (!ours || !theirs || baseText.trim() && !base)
4175
+ return void 0;
4176
+ const r = mergeChangeRecords(base, ours, theirs);
4177
+ return r.ok ? { ok: true, text: serializeChange(r.value) } : r;
4178
+ }
4179
+
4180
+ // packages/changes/dist/owners.js
4181
+ var OWNER_ROLE_KEYS = [
4182
+ "product-owner",
4183
+ "architect",
4184
+ "business-analyst",
4185
+ "team-lead"
4186
+ ];
4187
+ var ChangeOwnersError = class extends Error {
4188
+ constructor(message) {
4189
+ super(message);
4190
+ this.name = "ChangeOwnersError";
4191
+ }
4192
+ };
4193
+ var strip4 = (h) => h.trim().replace(/^@/, "");
4194
+ async function setChangeOwners(root, id, owners) {
4195
+ const store = changeStore(root);
4196
+ const c = await store.read(id);
4197
+ const next = { ...c.owners };
4198
+ for (const [key, people] of Object.entries(owners)) {
4199
+ if (!OWNER_ROLE_KEYS.includes(key))
4200
+ throw new ChangeOwnersError(`The change page names owners for ${OWNER_ROLE_KEYS.join(", ")}, not ${key}: edit change.yaml for other roles.`);
4201
+ const list = [...new Set(people.map(strip4).filter(Boolean))];
4202
+ if (list.length)
4203
+ next[key] = list;
4204
+ else
4205
+ delete next[key];
4206
+ }
4207
+ const sorted2 = Object.fromEntries(Object.entries(next).sort(([a], [b]) => a.localeCompare(b)));
4208
+ const r = ChangeLifecycle.safeParse({ ...c, owners: sorted2 });
4209
+ if (!r.success)
4210
+ throw new ChangeOwnersError(`Those owners can't be written: ${r.error.issues.map((i) => i.message).join("; ")}.`);
4211
+ await store.write(r.data);
4212
+ return r.data;
4213
+ }
4214
+
3377
4215
  export {
3378
4216
  ChangeError,
3379
4217
  readTables,
@@ -3381,6 +4219,8 @@ export {
3381
4219
  isFilled,
3382
4220
  CHANGE_POLICY_FILE,
3383
4221
  StageStep,
4222
+ isWritePattern,
4223
+ writeMatches,
3384
4224
  SIGNAL_FACTORS,
3385
4225
  ChangePolicy,
3386
4226
  DEFAULT_CHANGE_POLICY,
@@ -3405,6 +4245,9 @@ export {
3405
4245
  readCodeowners,
3406
4246
  loadChangeContext,
3407
4247
  changeTypeOf,
4248
+ outsideSystemsOf,
4249
+ outsideKindOf,
4250
+ outsideWords,
3408
4251
  personalLevels,
3409
4252
  wordsOf,
3410
4253
  changeFootprint,
@@ -3435,10 +4278,21 @@ export {
3435
4278
  workProgress,
3436
4279
  privacyImpact,
3437
4280
  prApprovers,
4281
+ RELEASE_PR_STATES,
4282
+ RELEASE_PRS_FILE,
4283
+ staleReleasePrs,
4284
+ nothingToRelease,
4285
+ raiseReleasePrs,
4286
+ syncReleasePrs,
4287
+ requireMergedPrs,
4288
+ releasePrReasons,
3438
4289
  liveHandoff,
4290
+ isManualPrUrl,
4291
+ manualHandoffWords,
3439
4292
  assess,
3440
4293
  canAdvance,
3441
4294
  changeReport,
4295
+ windowsUnsafeName,
3442
4296
  slugify,
3443
4297
  newChange,
3444
4298
  gitHandoffPublisher,
@@ -3450,6 +4304,7 @@ export {
3450
4304
  captureHandoff,
3451
4305
  readDraftPairs,
3452
4306
  completeCaptures,
4307
+ namesChange,
3453
4308
  handoff,
3454
4309
  syncHandoff,
3455
4310
  returnToPo,
@@ -3475,5 +4330,10 @@ export {
3475
4330
  openspecStores,
3476
4331
  storeSpecLookup,
3477
4332
  metarepoOrigin,
3478
- referenceMetarepoIn
4333
+ referenceMetarepoIn,
4334
+ mergeChangeRecords,
4335
+ mergeChangeText,
4336
+ OWNER_ROLE_KEYS,
4337
+ ChangeOwnersError,
4338
+ setChangeOwners
3479
4339
  };