@krelborn/markdown-review 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/README.md +92 -44
  2. package/dist/cli.js +774 -116
  3. package/dist/web/assets/{abnfDiagram-YUKMZFIV-CWLXy0p2.js → abnfDiagram-YUKMZFIV-5p3lK0ml.js} +1 -1
  4. package/dist/web/assets/{angular-html-BqCk3qPO.js → angular-html-DtTOKQVF.js} +1 -1
  5. package/dist/web/assets/{angular-ts-BdHZzt5V.js → angular-ts-DSZF2tmF.js} +1 -1
  6. package/dist/web/assets/{apl-BmM0OXqx.js → apl-BI5mQaXa.js} +1 -1
  7. package/dist/web/assets/{arc-BxwVCTrB.js → arc-BrfkN3mA.js} +1 -1
  8. package/dist/web/assets/{architecture-WOLXFQ4H-B01ykpUW.js → architecture-WOLXFQ4H-CB2TejI2.js} +1 -1
  9. package/dist/web/assets/{architectureDiagram-47P4ROYG-BtFKs1qG.js → architectureDiagram-47P4ROYG-YLfruAdT.js} +1 -1
  10. package/dist/web/assets/{astro-CNT5ujVQ.js → astro-Bx0B2FVT.js} +1 -1
  11. package/dist/web/assets/{blade-DhAlvV-v.js → blade-BWTzueMP.js} +1 -1
  12. package/dist/web/assets/{blockDiagram-E7TT5TSB-gMFCKFl5.js → blockDiagram-E7TT5TSB-DzKCvOwD.js} +1 -1
  13. package/dist/web/assets/{c-y1bwwswS.js → c-DdobnT6K.js} +1 -1
  14. package/dist/web/assets/{c4Diagram-CRA5TL53-BE6g7Cy0.js → c4Diagram-CRA5TL53-p1ftijoG.js} +1 -1
  15. package/dist/web/assets/channel-B7Y79uhb.js +1 -0
  16. package/dist/web/assets/{chapel-O4iWcSUf.js → chapel-D6rLpKo0.js} +1 -1
  17. package/dist/web/assets/{chunk-2BW5OAIV-C7oyR3U5.js → chunk-2BW5OAIV-DT1zLa9q.js} +1 -1
  18. package/dist/web/assets/{chunk-3YJQHVM4-B137wuGG.js → chunk-3YJQHVM4-nLx0WWI4.js} +1 -1
  19. package/dist/web/assets/{chunk-4EA7E6EY-DDeWWnul.js → chunk-4EA7E6EY-C_YyuzUS.js} +1 -1
  20. package/dist/web/assets/{chunk-7TKQ45FW-CF0k8Zaz.js → chunk-7TKQ45FW-BZ15X6vU.js} +1 -1
  21. package/dist/web/assets/{chunk-AW2ZBBNX-LgaoN3H2.js → chunk-AW2ZBBNX-Ck9cAmev.js} +1 -1
  22. package/dist/web/assets/{chunk-DBDB3WZW-Cpb8IX_Y.js → chunk-DBDB3WZW-9vMgzi8H.js} +1 -1
  23. package/dist/web/assets/{chunk-E2ZNV5FY-kQ1CHQVU.js → chunk-E2ZNV5FY-CyMQFbua.js} +1 -1
  24. package/dist/web/assets/{chunk-EU5HNXII-DiRYopKP.js → chunk-EU5HNXII-CWPCfQmx.js} +1 -1
  25. package/dist/web/assets/{chunk-FVRAUYC3-BtTST9-u.js → chunk-FVRAUYC3-jyBnY8AQ.js} +1 -1
  26. package/dist/web/assets/{chunk-HJ2JQQFS-BMWLpO22.js → chunk-HJ2JQQFS-Cj_rp1Qw.js} +1 -1
  27. package/dist/web/assets/{chunk-HTAEGDNF-Ds6Ktllz.js → chunk-HTAEGDNF-pClFnSXR.js} +1 -1
  28. package/dist/web/assets/{chunk-KQW6MTUR-Cs61KkID.js → chunk-KQW6MTUR-C2m9qy9v.js} +1 -1
  29. package/dist/web/assets/{chunk-NTY3LDVX-Cb4zKiDj.js → chunk-NTY3LDVX-CnUW73mn.js} +1 -1
  30. package/dist/web/assets/{chunk-VPRB5NB3-DWCkoa9b.js → chunk-VPRB5NB3-BuB50_Ze.js} +1 -1
  31. package/dist/web/assets/{chunk-XC4XBNZT-wUQj4Qqz.js → chunk-XC4XBNZT-BsgxISHC.js} +1 -1
  32. package/dist/web/assets/{chunk-XXDRQBXY-G5J92hEr.js → chunk-XXDRQBXY-jqsHjX2T.js} +1 -1
  33. package/dist/web/assets/{classDiagram-v2-K4WV4PDN-BCF0112R.js → classDiagram-v2-K4WV4PDN-B0EG_ZZt.js} +1 -1
  34. package/dist/web/assets/{cobol-B9yyG1N7.js → cobol-B_XRwpIg.js} +1 -1
  35. package/dist/web/assets/{coffee-CNQY9j_z.js → coffee-C4eMlpSe.js} +1 -1
  36. package/dist/web/assets/{cose-bilkent-JH36ORCC-xUhaa1rU.js → cose-bilkent-JH36ORCC-CrM0VDy4.js} +1 -1
  37. package/dist/web/assets/{cpp-syZ-JK5F.js → cpp-BE8dVz2Y.js} +1 -1
  38. package/dist/web/assets/{crystal-DNtPOvR4.js → crystal-EaFwVcbb.js} +1 -1
  39. package/dist/web/assets/{css-D5aJMOgO.js → css-oYmBq-By.js} +1 -1
  40. package/dist/web/assets/{cynefin-EF2NZ3EQ-DV3XOKzu.js → cynefin-EF2NZ3EQ-34vs3gIf.js} +1 -1
  41. package/dist/web/assets/{cynefinDiagram-3GCD6N5R-DgURBFKo.js → cynefinDiagram-3GCD6N5R-BknEauUi.js} +1 -1
  42. package/dist/web/assets/{dagre-W4DXFKR2-DF0-xqBq.js → dagre-W4DXFKR2-DZ5C0vE9.js} +1 -1
  43. package/dist/web/assets/{diagram-2UJZ2QOL-BoK-arBq.js → diagram-2UJZ2QOL-7gEGibdm.js} +1 -1
  44. package/dist/web/assets/{diagram-OVF4WLC6-2PnqW9FF.js → diagram-OVF4WLC6-CUXH1cbU.js} +1 -1
  45. package/dist/web/assets/{diagram-PFPMY2P6-D61RpAS8.js → diagram-PFPMY2P6-BOuoN7y0.js} +1 -1
  46. package/dist/web/assets/{diagram-UMYRVEAY-Bn2VK8cA.js → diagram-UMYRVEAY-C3xUjk3B.js} +1 -1
  47. package/dist/web/assets/{diagram-YEKJPTXX-BliOVg-N.js → diagram-YEKJPTXX-CmV-UftY.js} +1 -1
  48. package/dist/web/assets/{diagram-ZIFT7M5P-BUSVx-wF.js → diagram-ZIFT7M5P-DgGdZ1Kl.js} +1 -1
  49. package/dist/web/assets/{dist-B63Xpf6L.js → dist-CN4oxUZG.js} +1 -1
  50. package/dist/web/assets/{ebnfDiagram-VR2GEFS7-BMP6ofKl.js → ebnfDiagram-VR2GEFS7-CgqYKMMK.js} +1 -1
  51. package/dist/web/assets/{edge-nYjgtyX1.js → edge-D6C5szlZ.js} +1 -1
  52. package/dist/web/assets/{elixir-NJaeMcC6.js → elixir-D6PdZkcK.js} +1 -1
  53. package/dist/web/assets/{elk-IJKZMXRS-oJ77omRc.js → elk-IJKZMXRS-CaNtLBYi.js} +1 -1
  54. package/dist/web/assets/{elm-8b_rvSJ8.js → elm-B8heN2jE.js} +1 -1
  55. package/dist/web/assets/{erDiagram-O2IAPWRE-oV_Xnl5q.js → erDiagram-O2IAPWRE-B_Ztqgv4.js} +1 -1
  56. package/dist/web/assets/{erb-BXTZOHPR.js → erb-CvxZym2t.js} +1 -1
  57. package/dist/web/assets/{eventmodeling-K75KTNOO-B4uCS7Wp.js → eventmodeling-K75KTNOO-rIyIwA17.js} +1 -1
  58. package/dist/web/assets/flowDiagram-OXPTDLAJ-BuRr7k3B.js +1 -0
  59. package/dist/web/assets/{ganttDiagram-R7TSEDQI-DCrgwH20.js → ganttDiagram-R7TSEDQI-CZHQ2b2p.js} +1 -1
  60. package/dist/web/assets/{git-rebase-Bemc6KvD.js → git-rebase-BQpmx8Pr.js} +1 -1
  61. package/dist/web/assets/{gitGraph-VSP46ZUC-xNJeVHYy.js → gitGraph-VSP46ZUC-BaI5oVFE.js} +1 -1
  62. package/dist/web/assets/{gitGraphDiagram-XJZIOB7I-BynhGjx_.js → gitGraphDiagram-XJZIOB7I-D019lpA0.js} +1 -1
  63. package/dist/web/assets/{glimmer-js-BbleNzr7.js → glimmer-js-Q0lvksee.js} +1 -1
  64. package/dist/web/assets/{glimmer-ts-yg6t7MXH.js → glimmer-ts-BlwILAHu.js} +1 -1
  65. package/dist/web/assets/{glsl-BaZn8BHQ.js → glsl-C2XhxW29.js} +1 -1
  66. package/dist/web/assets/{graphql-BmqnhkT8.js → graphql-CQpawWlW.js} +1 -1
  67. package/dist/web/assets/{hack-J8sb5uai.js → hack-D8CL9Px1.js} +1 -1
  68. package/dist/web/assets/{haml-FiKLxaeo.js → haml-DZZoeC01.js} +1 -1
  69. package/dist/web/assets/{handlebars-dyXAlfF8.js → handlebars-CnQuZzNH.js} +1 -1
  70. package/dist/web/assets/{html-Bo4E06VK.js → html-CvIoDHKR.js} +1 -1
  71. package/dist/web/assets/{html-derivative-C3RqHwed.js → html-derivative-CGk6QFDb.js} +1 -1
  72. package/dist/web/assets/{http-B8IbeSRs.js → http-Dziu_Xj5.js} +1 -1
  73. package/dist/web/assets/{hurl-CckaloOY.js → hurl-DythJo9n.js} +1 -1
  74. package/dist/web/assets/{index-CGwy-QNW.css → index-CiOT786B.css} +1 -1
  75. package/dist/web/assets/index-Cm7ENbyj.js +256 -0
  76. package/dist/web/assets/{info-OHQRW6UA-D7G_G-1L.js → info-OHQRW6UA-CbxEII--.js} +1 -1
  77. package/dist/web/assets/{infoDiagram-5W2HQ5XZ-fPny1Cf_.js → infoDiagram-5W2HQ5XZ-uncvednK.js} +1 -1
  78. package/dist/web/assets/{ishikawaDiagram-K3B6WC7H-BjipzgOy.js → ishikawaDiagram-K3B6WC7H-DGe07IXX.js} +1 -1
  79. package/dist/web/assets/{java-CUToMvYb.js → java-CZZLsDD_.js} +1 -1
  80. package/dist/web/assets/{javascript-BfEm08nL.js → javascript-BcaihfgA.js} +1 -1
  81. package/dist/web/assets/{jinja-CoCPe0su.js → jinja-v6gp7mmJ.js} +1 -1
  82. package/dist/web/assets/{jison-D8YLHyhZ.js → jison-D2SC7D3U.js} +1 -1
  83. package/dist/web/assets/{journeyDiagram-COXZFDF6-DlfiwZbw.js → journeyDiagram-COXZFDF6-DAIUzadY.js} +1 -1
  84. package/dist/web/assets/{json-B5ImZt8b.js → json-BTX5Muj-.js} +1 -1
  85. package/dist/web/assets/{jsx-BEuHYNcK.js → jsx-sOd5Zb96.js} +1 -1
  86. package/dist/web/assets/{julia-CGDZSSyh.js → julia-BFKK12Cy.js} +1 -1
  87. package/dist/web/assets/{just-k89lTjF_.js → just-5EOII5bf.js} +1 -1
  88. package/dist/web/assets/{kanban-definition-P3RFRI5V-DWtn18JL.js → kanban-definition-P3RFRI5V-CRJ_qKDi.js} +1 -1
  89. package/dist/web/assets/{latex-B2BLRmCh.js → latex-DlpyBAwB.js} +1 -1
  90. package/dist/web/assets/{line-OgG0zNYZ.js → line-DDtZbOq6.js} +1 -1
  91. package/dist/web/assets/{linear-CwKyvaxX.js → linear-DiXfgJNJ.js} +1 -1
  92. package/dist/web/assets/{liquid-DCm9svrE.js → liquid-i0LOb-_p.js} +1 -1
  93. package/dist/web/assets/{lua-D2PZUpll.js → lua-egAglmJR.js} +1 -1
  94. package/dist/web/assets/{marko-BHlW2bwI.js → marko-pexQ6dyw.js} +1 -1
  95. package/dist/web/assets/{mdc-D3qSZtcb.js → mdc-2zx6wVZ1.js} +1 -1
  96. package/dist/web/assets/{mermaid-parser.core-BB3FI1zz.js → mermaid-parser.core-ChhS34Wq.js} +3 -3
  97. package/dist/web/assets/{mermaid.core-COMOGmN5.js → mermaid.core-BLtM-FUH.js} +4 -4
  98. package/dist/web/assets/{mindmap-definition-LZFPQGKD-FVI5kbuu.js → mindmap-definition-LZFPQGKD-qHBfvVvR.js} +1 -1
  99. package/dist/web/assets/{nginx-DE318eQE.js → nginx-BJBqsQfY.js} +1 -1
  100. package/dist/web/assets/{nim-CLxdlaX6.js → nim-BaA50oZ-.js} +1 -1
  101. package/dist/web/assets/{org-BrPnEEho.js → org-B-ByT2Q_.js} +1 -1
  102. package/dist/web/assets/{packet-JDAUHWVQ-Fpkjmnjn.js → packet-JDAUHWVQ-uLD5sbY4.js} +1 -1
  103. package/dist/web/assets/{pegDiagram-BGZESJAR-BKaoWoq7.js → pegDiagram-BGZESJAR-BRNG5qXG.js} +1 -1
  104. package/dist/web/assets/{perl-DHsiEDUm.js → perl-DxoQS1Oz.js} +1 -1
  105. package/dist/web/assets/{php-DQjV-d8l.js → php-DAdurunP.js} +1 -1
  106. package/dist/web/assets/{pie-XZMESJXO-CY04iMLk.js → pie-XZMESJXO-rle9Hab0.js} +1 -1
  107. package/dist/web/assets/{pieDiagram-CAPJLFHJ-BeEngp4p.js → pieDiagram-CAPJLFHJ-CsLaTTZ4.js} +1 -1
  108. package/dist/web/assets/{pug-Dh8JM0fV.js → pug-CGqTuVm8.js} +1 -1
  109. package/dist/web/assets/{qml-B6jp0Ezu.js → qml-qo_XuOpa.js} +1 -1
  110. package/dist/web/assets/{quadrantDiagram-GDMTTRAM-BvJZEJlt.js → quadrantDiagram-GDMTTRAM-CcM5DEH6.js} +1 -1
  111. package/dist/web/assets/{r-6SqhsqXp.js → r-CKiLSGzc.js} +1 -1
  112. package/dist/web/assets/{radar-ABXABTNO-CbPpIJEV.js → radar-ABXABTNO-CC4F4_aS.js} +1 -1
  113. package/dist/web/assets/{railroad-I3PHUGI6-6byRvbF6.js → railroad-I3PHUGI6-EiUjcaSq.js} +1 -1
  114. package/dist/web/assets/{railroad-abnf-LNEFI6M7-Bo-qX1HQ.js → railroad-abnf-LNEFI6M7-BZzYZu7G.js} +1 -1
  115. package/dist/web/assets/{railroad-ebnf-DWYD2UWJ-C4u9WTRa.js → railroad-ebnf-DWYD2UWJ-DGihz-hz.js} +1 -1
  116. package/dist/web/assets/{railroad-peg-JR7BVNR7-EriE5s4B.js → railroad-peg-JR7BVNR7-D9Youaz5.js} +1 -1
  117. package/dist/web/assets/{railroadDiagram-SM67HX2B-DSWmFhHh.js → railroadDiagram-SM67HX2B-ljs2FnSo.js} +1 -1
  118. package/dist/web/assets/{razor-DUvaBJHt.js → razor-DjN9S-r_.js} +1 -1
  119. package/dist/web/assets/{regexp-DQVjJ1C9.js → regexp-yRPnlhcx.js} +1 -1
  120. package/dist/web/assets/{requirementDiagram-X7JNWC4A-DSputoF-.js → requirementDiagram-X7JNWC4A-BJ877Yny.js} +1 -1
  121. package/dist/web/assets/{rst-CimOXJkT.js → rst-NbJF2fMO.js} +1 -1
  122. package/dist/web/assets/{ruby-CkT4UUsu.js → ruby-ZoVZdf-0.js} +1 -1
  123. package/dist/web/assets/{sankeyDiagram-UM26HJRW-CoyT7S1x.js → sankeyDiagram-UM26HJRW-BPwhixv8.js} +1 -1
  124. package/dist/web/assets/{sas-jhGZfw8z.js → sas-CnTL8nFG.js} +1 -1
  125. package/dist/web/assets/{scss-COq5RYby.js → scss-BAnH3m1c.js} +1 -1
  126. package/dist/web/assets/{sequenceDiagram-ZO4K6R2Y-bH6JrL79.js → sequenceDiagram-ZO4K6R2Y-xhx_vaZj.js} +1 -1
  127. package/dist/web/assets/{shellscript-fPSuoebA.js → shellscript-ChY3GVRw.js} +1 -1
  128. package/dist/web/assets/{shellsession-CEOzWFWz.js → shellsession-G4IamFTN.js} +1 -1
  129. package/dist/web/assets/{soy-CoaFeLPp.js → soy-DHZYlzcJ.js} +1 -1
  130. package/dist/web/assets/{sql-CqYtiF66.js → sql-BLAJpvko.js} +1 -1
  131. package/dist/web/assets/{src-Yh046HvT.js → src-CneWU0V7.js} +1 -1
  132. package/dist/web/assets/{stata-4SdpmCwi.js → stata-BF2DeqJI.js} +1 -1
  133. package/dist/web/assets/{stateDiagram-v2-TBUQTH76-Ce-gceoA.js → stateDiagram-v2-TBUQTH76-DYEIdXmD.js} +1 -1
  134. package/dist/web/assets/{surrealql-C2Q1e9ZL.js → surrealql-CjuctJtE.js} +1 -1
  135. package/dist/web/assets/{svelte-CDy8bfpZ.js → svelte-CAAipNMl.js} +1 -1
  136. package/dist/web/assets/{swimlanes-N4OXWK64-XZo3QwsQ.js → swimlanes-N4OXWK64-DaNmUIcz.js} +1 -1
  137. package/dist/web/assets/swimlanesDiagram-K3J5GTZL-Cc_r8_vm.js +8 -0
  138. package/dist/web/assets/{templ-B5OSUAC6.js → templ-B-6t84-k.js} +1 -1
  139. package/dist/web/assets/{tex-0wXDlVyM.js → tex-DsSWxSFa.js} +1 -1
  140. package/dist/web/assets/{timeline-definition-YOQAKGHF-B7Bs6DE5.js → timeline-definition-YOQAKGHF-CmA5uuSJ.js} +1 -1
  141. package/dist/web/assets/{treeView-D4JQ5SDB-DhelC15L.js → treeView-D4JQ5SDB-L8cwVgaN.js} +1 -1
  142. package/dist/web/assets/{treemap-SAJKECNS-CzGjtoOh.js → treemap-SAJKECNS-Cb56YgJH.js} +1 -1
  143. package/dist/web/assets/{ts-tags-tg6YwKan.js → ts-tags-2bkGTXWr.js} +1 -1
  144. package/dist/web/assets/{tsx-CEeuRXvQ.js → tsx-BD6V4u7j.js} +1 -1
  145. package/dist/web/assets/{twig-CWajHX0R.js → twig-BRdwPHsd.js} +1 -1
  146. package/dist/web/assets/{typescript-RFkfgRav.js → typescript-CE43_TcG.js} +1 -1
  147. package/dist/web/assets/{typst-iYtNFWi1.js → typst-Oj4lknbm.js} +1 -1
  148. package/dist/web/assets/{usecaseDiagram-VIAY4XPW-BdnNYShz.js → usecaseDiagram-VIAY4XPW-RzU3uMX7.js} +1 -1
  149. package/dist/web/assets/{vennDiagram-BLWOH2XV-CgGZe_xP.js → vennDiagram-BLWOH2XV-RBYC_TER.js} +1 -1
  150. package/dist/web/assets/{vue-DG85Ls7m.js → vue-DohRCbuk.js} +1 -1
  151. package/dist/web/assets/{vue-html-CHmfKQdN.js → vue-html-CUm-vXDJ.js} +1 -1
  152. package/dist/web/assets/{vue-vine-nZRQ_9Xq.js → vue-vine-CSJNVT6g.js} +1 -1
  153. package/dist/web/assets/{wardley-7MLQ67FV-6V8v9HhV.js → wardley-7MLQ67FV-BSCtc6D6.js} +1 -1
  154. package/dist/web/assets/{wardleyDiagram-YMQ3BMBF-CNjJiy1q.js → wardleyDiagram-YMQ3BMBF-DiJSX_eB.js} +1 -1
  155. package/dist/web/assets/{xml-Cf2rIYfH.js → xml-CRGrymgs.js} +1 -1
  156. package/dist/web/assets/{xsl-BtZjiXjY.js → xsl-5ujj8eCf.js} +1 -1
  157. package/dist/web/assets/{xychartDiagram-TAQBALBS-BRYVq9Me.js → xychartDiagram-TAQBALBS-BmlAXIhs.js} +1 -1
  158. package/dist/web/assets/{yaml-ok1XkEbE.js → yaml-DYq2KMMJ.js} +1 -1
  159. package/dist/web/index.html +2 -2
  160. package/package.json +3 -3
  161. package/dist/web/assets/channel-Bv6xif17.js +0 -1
  162. package/dist/web/assets/flowDiagram-OXPTDLAJ-xE1Htm9m.js +0 -1
  163. package/dist/web/assets/index-CEXlg15b.js +0 -256
  164. package/dist/web/assets/swimlanesDiagram-K3J5GTZL-BuztLICr.js +0 -8
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@ import { setTimeout as setTimeout$1 } from "node:timers/promises";
7
7
  import path from "node:path";
8
8
  import { z } from "zod";
9
9
  import os from "node:os";
10
+ import { parseDocument } from "yaml";
10
11
  import openInBrowser from "open";
11
12
  import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
12
13
  import { createServer } from "node:http";
@@ -43,7 +44,7 @@ function createProcessTerminal() {
43
44
  //#region src/server/runtime/packageVersion.ts
44
45
  var packageVersion = {
45
46
  name: "@krelborn/markdown-review",
46
- version: "0.3.0",
47
+ version: "0.4.0",
47
48
  description: "Review agent-written markdown in the browser and hand the comments back to the agent",
48
49
  homepage: "https://github.com/Krelborn/markdown-review#readme",
49
50
  bugs: { "url": "https://github.com/Krelborn/markdown-review/issues" },
@@ -82,6 +83,7 @@ var packageVersion = {
82
83
  "hono": "^4.13.13",
83
84
  "markdown-it": "^15.0.2",
84
85
  "open": "^11.0.4",
86
+ "yaml": "^2.9.1",
85
87
  "zod": "^4.6.5"
86
88
  },
87
89
  devDependencies: {
@@ -113,8 +115,7 @@ var packageVersion = {
113
115
  "shiki": "^4.5.0",
114
116
  "typescript": "^7.0.2",
115
117
  "vite": "^8.3.3",
116
- "vitest": "^5.0.3",
117
- "yaml": "^2.9.1"
118
+ "vitest": "^5.0.3"
118
119
  },
119
120
  engines: { "node": "^22.22.2 || ^24.15.0 || >=26.0.0" },
120
121
  packageManager: "pnpm@10.18.3"
@@ -292,6 +293,24 @@ var anchorSchema = z.discriminatedUnion("kind", [
292
293
  passageAnchorSchema
293
294
  ]);
294
295
  //#endregion
296
+ //#region src/shared/review/reviewRoundSchema.ts
297
+ var verdictSchema = z.enum([
298
+ "revise",
299
+ "go-ahead",
300
+ "hand-back"
301
+ ]);
302
+ var nextTaskSchema = z.string().trim().min(1).max(200).refine((text) => !/[\n\r\u2028\u2029]/.test(text), "must be one line");
303
+ var reviewRoundSchema = z.strictObject({
304
+ documents: z.array(documentPathSchema),
305
+ lastOpenedAt: z.iso.datetime(),
306
+ nextTask: nextTaskSchema.nullable(),
307
+ startedAt: z.iso.datetime(),
308
+ verdict: z.strictObject({
309
+ at: z.iso.datetime(),
310
+ kind: verdictSchema
311
+ }).nullable()
312
+ });
313
+ //#endregion
295
314
  //#region src/shared/review/threadSchema.ts
296
315
  var messageBodySchema = z.string().refine((body) => body.trim() !== "", "must not be blank");
297
316
  var messageSchema = z.strictObject({
@@ -318,11 +337,6 @@ var threadSchema = z.strictObject({
318
337
  }).refine((thread) => thread.status === "draft" === (thread.messages.length === 0), { message: "must have messages unless it is a draft" }).refine((thread) => thread.status !== "draft" || thread.draft !== void 0, { message: "must hold its comment as a draft while it is a draft" });
319
338
  //#endregion
320
339
  //#region src/shared/api/apiResponseSchemas.ts
321
- var reviewStateSchema = z.strictObject({
322
- approved: z.boolean(),
323
- approvedAt: z.iso.datetime().nullable(),
324
- requestedAt: z.iso.datetime().nullable()
325
- });
326
340
  /**
327
341
  * Not strict, so a newer server that reports more fields is still recognised by its protocol rather than mistaken for
328
342
  * no server
@@ -336,13 +350,13 @@ var healthSchema = z.object({
336
350
  });
337
351
  var threadsSnapshotSchema = z.strictObject({
338
352
  problems: z.array(z.string()),
339
- review: reviewStateSchema,
353
+ round: reviewRoundSchema.nullable(),
340
354
  threads: z.array(threadSchema)
341
355
  });
342
356
  var pollResponseSchema = threadsSnapshotSchema.extend({ timedOut: z.boolean() });
343
357
  var agentOpenResponseSchema = z.strictObject({
344
358
  navigated: z.boolean(),
345
- review: reviewStateSchema,
359
+ round: reviewRoundSchema.nullable(),
346
360
  url: z.string()
347
361
  });
348
362
  var agentThreadResponseSchema = z.strictObject({
@@ -364,6 +378,10 @@ z.strictObject({
364
378
  path: documentPathSchema,
365
379
  source: z.string()
366
380
  });
381
+ z.strictObject({
382
+ availableVersion: z.string().nullable(),
383
+ command: z.string().nullable()
384
+ });
367
385
  var apiErrorSchema = z.strictObject({ error: z.strictObject({
368
386
  message: z.string(),
369
387
  reason: z.string()
@@ -402,14 +420,22 @@ var ServerClient = class {
402
420
  health() {
403
421
  return this.request("GET", "/api/health", healthSchema, { signal: AbortSignal.timeout(healthTimeoutMilliseconds) });
404
422
  }
405
- open(document) {
406
- return this.request("POST", "/api/agent/open", agentOpenResponseSchema, { body: document === null ? {} : { path: document } });
423
+ /**
424
+ * @param document the doc to show, or null for the docs list
425
+ * @param nextTask what the agent will do once the user says go ahead; null keeps a joined round's task and leaves a
426
+ * new round without one
427
+ */
428
+ open(document, nextTask) {
429
+ return this.request("POST", "/api/agent/open", agentOpenResponseSchema, { body: {
430
+ ...document === null ? {} : { path: document },
431
+ ...nextTask === null ? {} : { nextTask }
432
+ } });
407
433
  }
408
434
  inbox(document) {
409
435
  return this.request("GET", `/api/inbox${documentQuery(document)}`, threadsSnapshotSchema);
410
436
  }
411
437
  /**
412
- * Waits for the review to be approved, a thread to need the agent, or the timeout
438
+ * Waits for the user to end the review round, a thread to need the agent, or the timeout
413
439
  */
414
440
  poll(document, timeoutSeconds, signal) {
415
441
  const query = new URLSearchParams({ timeout: String(timeoutSeconds) });
@@ -480,11 +506,11 @@ async function findServer(root) {
480
506
  return { kind: "absent" };
481
507
  }
482
508
  if (health.root !== root) return { kind: "absent" };
483
- if (health.protocol < 1) return {
509
+ if (health.protocol < 2) return {
484
510
  client,
485
511
  kind: "older"
486
512
  };
487
- return health.protocol > 1 ? {
513
+ return health.protocol > 2 ? {
488
514
  health,
489
515
  kind: "newer"
490
516
  } : {
@@ -661,6 +687,18 @@ async function findRoot(directory, outsideGitDirectory = directory) {
661
687
  }
662
688
  }
663
689
  //#endregion
690
+ //#region src/shared/review/roundOutcome.ts
691
+ /**
692
+ * Tells how the user ended a review round
693
+ *
694
+ * @param round the round, or null before any review was opened
695
+ * @returns the verdict that ended the round, or null while it goes on
696
+ */
697
+ function roundOutcome(round) {
698
+ const kind = round?.verdict?.kind;
699
+ return kind === "go-ahead" || kind === "hand-back" ? kind : null;
700
+ }
701
+ //#endregion
664
702
  //#region src/cli/formatIdList.ts
665
703
  /**
666
704
  * Lists thread IDs the way the CLI's sentences name them
@@ -684,7 +722,9 @@ var waitForCommentsStep = `Run \`markdown-review poll\` to wait for the user's c
684
722
  function waitForCommentsInRootStep(root) {
685
723
  return `This doc belongs to the repository at ${root}, so run markdown-review commands from that directory. ` + waitForCommentsStep;
686
724
  }
687
- var approvedStep = "The user approved the review. Carry on with your task; do not run `markdown-review poll` again for this review.";
725
+ var wentAheadStep = "The user said go ahead. Carry on with your task; do not run `markdown-review poll` again for this review.";
726
+ var handedBackStep = "Stop here: do not edit the docs, run `markdown-review poll` or carry on with your task. Tell the user in the chat that you have stopped, and wait for their instructions.";
727
+ var handedBackFinishedStep = "The user handed the review back. Tell them what you changed and wait for their instructions. Only if they ask you to continue the review, run `markdown-review open <path> --next-task \"<what you will do once the user says go ahead>\"`, then `markdown-review poll`.";
688
728
  /**
689
729
  * @param ids the threads that need the agent, at least one
690
730
  */
@@ -692,11 +732,23 @@ function addressThreadsStep(ids) {
692
732
  return `Edit the docs, then run \`markdown-review resolve <id> "<what changed>"\` for ${formatIdList(ids)}, or \`markdown-review reply <id> "<question>"\` if you need input. Then run \`markdown-review poll\` to wait for the next round.`;
693
733
  }
694
734
  /**
695
- * @param ids the threads the user commented on when approving, at least one
735
+ * @param ids the threads the user commented on when saying go ahead, at least one
696
736
  */
697
- function approvedWithCommentsStep(ids) {
737
+ function wentAheadWithCommentsStep(ids) {
698
738
  const resolveCommand = ids.length === 1 ? `\`markdown-review resolve ${ids[0]} "<what changed>"\`` : "`markdown-review resolve <id> \"<what changed>\"` for each";
699
- return `The user approved the review with comments. Address ${formatIdList(ids)} and run ${resolveCommand}, then carry on with your task; do not run \`markdown-review poll\` again for this review.`;
739
+ return `The user said go ahead, with comments. Address ${formatIdList(ids)} and run ${resolveCommand}, then carry on with your task; do not run \`markdown-review poll\` again for this review.`;
740
+ }
741
+ /**
742
+ * @param ids the threads still needing the agent after the user handed the review back, at least one
743
+ */
744
+ function handedBackThreadsLeftStep(ids) {
745
+ return `The user handed the review back, so act on ${formatIdList(ids)} only as they ask in the chat.`;
746
+ }
747
+ /**
748
+ * @param ids the threads the user commented on when handing the review back, at least one
749
+ */
750
+ function handedBackWithCommentsStep(ids) {
751
+ return `Stop here: do not edit the docs, resolve or reply to ${formatIdList(ids)}, run \`markdown-review poll\` or carry on with your task. Tell the user in the chat that you have stopped and what their comments say, and wait for their instructions.`;
700
752
  }
701
753
  //#endregion
702
754
  //#region src/cli/formatAgentAction.ts
@@ -709,10 +761,21 @@ function approvedWithCommentsStep(ids) {
709
761
  */
710
762
  function formatAgentAction(action, { inbox, thread }) {
711
763
  const done = action === "resolved" ? `Resolved #${thread.id}.` : `Replied to #${thread.id}; it waits for the user.`;
764
+ const outcome = roundOutcome(inbox.round);
712
765
  const remaining = inbox.threads.map((remainingThread) => remainingThread.id);
713
- if (remaining.length > 0) return `${done}\n${remaining.length === 1 ? "1 thread still needs you" : `${remaining.length} threads still need you`}: ${formatIdList(remaining)}.\n\nnext_step: Address ${formatIdList(remaining)}, then run \`markdown-review resolve <id> "<what changed>"\`, or \`markdown-review reply <id> "<question>"\` if you need input.
714
- `;
715
- return `${done} No threads need you now.\n\nnext_step: ${inbox.review.approved ? approvedStep : "Run `markdown-review poll` to wait for the user's next round of comments."}\n`;
766
+ if (remaining.length > 0) {
767
+ const count = remaining.length === 1 ? "1 thread still needs you" : `${remaining.length} threads still need you`;
768
+ const step = outcome === "hand-back" ? handedBackThreadsLeftStep(remaining) : `Address ${formatIdList(remaining)}, then run \`markdown-review resolve <id> "<what changed>"\`, or \`markdown-review reply <id> "<question>"\` if you need input.`;
769
+ return `${done}\n${count}: ${formatIdList(remaining)}.\n\nnext_step: ${step}\n`;
770
+ }
771
+ return `${done} No threads need you now.\n\nnext_step: ${finishedStep(outcome)}\n`;
772
+ }
773
+ function finishedStep(outcome) {
774
+ switch (outcome) {
775
+ case "go-ahead": return wentAheadStep;
776
+ case "hand-back": return handedBackFinishedStep;
777
+ case null: return "Run `markdown-review poll` to wait for the user's next round of comments.";
778
+ }
716
779
  }
717
780
  //#endregion
718
781
  //#region src/cli/readMessageBody.ts
@@ -792,16 +855,20 @@ var quoteStartLength = 120;
792
855
  /**
793
856
  * Describes the agent's inbox as compact text for the agent to read
794
857
  *
795
- * @param snapshot the review state and the threads that need the agent
796
- * @returns the summary, each thread grouped by doc with its messages, any store problems, and a final `next_step`
858
+ * @param snapshot the review round and the threads that need the agent
859
+ * @param notices lines to put just before `next_step`, each ending in a newline
860
+ * @returns the summary, which says how the user ended the round if they have, each thread grouped by doc with its
861
+ * messages, any store problems, the notices, and a final `next_step`
797
862
  */
798
- function formatInbox({ problems, review, threads }) {
863
+ function formatInbox({ problems, round, threads }, notices = []) {
864
+ const outcome = roundOutcome(round);
799
865
  const ordered = orderByDocument(threads);
800
866
  const ids = ordered.map((thread) => thread.id);
801
- const sections = [summaryLine(review.approved, ordered)];
867
+ const sections = [summaryLine(outcome, ordered)];
802
868
  if (ordered.length > 0) sections.push(ordered.map(formatThread).join("\n\n"));
803
869
  if (problems.length > 0) sections.push(problems.map((problem) => `warning: ${indentContinuation(problem, " ")}`).join("\n"));
804
- sections.push(`next_step: ${nextStep(review.approved, ids)}`);
870
+ if (notices.length > 0) sections.push(notices.join("").trimEnd());
871
+ sections.push(`next_step: ${nextStep(outcome, ids)}`);
805
872
  return `${sections.join("\n\n")}\n`;
806
873
  }
807
874
  function formatThread(thread) {
@@ -828,20 +895,40 @@ function describeAnchor({ anchor, id }) {
828
895
  }
829
896
  }
830
897
  }
831
- function summaryLine(approved, threads) {
832
- if (threads.length === 0) return approved ? "Review approved. No threads need you." : "No threads need you.";
898
+ function summaryLine(outcome, threads) {
899
+ if (threads.length === 0) return noThreadsSummary(outcome);
900
+ const breakdown = countByGroup(threads);
901
+ const needs = threads.length === 1 ? "1 thread needs you" : `${threads.length} threads need you`;
902
+ switch (outcome) {
903
+ case "go-ahead": return `The user said go ahead, ${needs} (${breakdown})`;
904
+ case "hand-back": return `The user asked to hand back to you, with comments on ${countThreads(threads.length)} (${breakdown})`;
905
+ case null: return `${needs} (${breakdown})`;
906
+ }
907
+ }
908
+ function countByGroup(threads) {
833
909
  const counts = /* @__PURE__ */ new Map();
834
910
  for (const thread of threads) {
835
911
  const group = thread.anchor.kind === "review" ? "review" : thread.anchor.document;
836
912
  counts.set(group, (counts.get(group) ?? 0) + 1);
837
913
  }
838
- const breakdown = [...counts].map(([group, count]) => `${group}: ${count}`).join(", ");
839
- const needs = threads.length === 1 ? "1 thread needs you" : `${threads.length} threads need you`;
840
- return approved ? `Review approved, ${needs} (${breakdown})` : `${needs} (${breakdown})`;
914
+ return [...counts].map(([group, count]) => `${group}: ${count}`).join(", ");
841
915
  }
842
- function nextStep(approved, ids) {
843
- if (ids.length === 0) return approved ? approvedStep : waitForCommentsStep;
844
- return approved ? approvedWithCommentsStep(ids) : addressThreadsStep(ids);
916
+ function countThreads(count) {
917
+ return count === 1 ? "1 thread" : `${count} threads`;
918
+ }
919
+ function noThreadsSummary(outcome) {
920
+ switch (outcome) {
921
+ case "go-ahead": return "The user said go ahead. No threads need you.";
922
+ case "hand-back": return "The user asked to hand back to you. No threads need you.";
923
+ case null: return "No threads need you.";
924
+ }
925
+ }
926
+ function nextStep(outcome, ids) {
927
+ switch (outcome) {
928
+ case "go-ahead": return ids.length === 0 ? wentAheadStep : wentAheadWithCommentsStep(ids);
929
+ case "hand-back": return ids.length === 0 ? handedBackStep : handedBackWithCommentsStep(ids);
930
+ case null: return ids.length === 0 ? waitForCommentsStep : addressThreadsStep(ids);
931
+ }
845
932
  }
846
933
  function orderByDocument(threads) {
847
934
  const groupOf = (thread) => thread.anchor.kind === "review" ? "￿" : thread.anchor.document;
@@ -882,21 +969,319 @@ async function readDocumentOption(root, workingDirectory, value) {
882
969
  return value === void 0 ? null : toRepositoryPath(root, path.resolve(await realpath(workingDirectory), value));
883
970
  }
884
971
  //#endregion
972
+ //#region src/server/update/semanticVersions.ts
973
+ var versionPattern = /^(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<patch>0|[1-9]\d*)(?:-(?<prerelease>[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u;
974
+ /**
975
+ * Orders two semantic versions by semver precedence
976
+ *
977
+ * @param left a version such as "0.4.0" or "0.4.0-beta.1"
978
+ * @param right another version
979
+ * @returns a negative number when `left` comes first, a positive number when `right` does, 0 when they are equal,
980
+ * and null when either is not a semantic version
981
+ */
982
+ function compareVersions(left, right) {
983
+ const parsedLeft = parseVersion(left);
984
+ const parsedRight = parseVersion(right);
985
+ if (parsedLeft === null || parsedRight === null) return null;
986
+ const [leftMajor, leftMinor, leftPatch] = parsedLeft.release;
987
+ const [rightMajor, rightMinor, rightPatch] = parsedRight.release;
988
+ return leftMajor - rightMajor || leftMinor - rightMinor || leftPatch - rightPatch || comparePrereleases(parsedLeft.prerelease, parsedRight.prerelease);
989
+ }
990
+ /**
991
+ * @param version a version such as "0.4.0-beta.1"
992
+ * @returns whether it is a semantic version with a pre-release part
993
+ */
994
+ function isPrerelease(version) {
995
+ return (parseVersion(version)?.prerelease.length ?? 0) > 0;
996
+ }
997
+ function parseVersion(version) {
998
+ const groups = versionPattern.exec(version)?.groups;
999
+ if (groups === void 0) return null;
1000
+ return {
1001
+ prerelease: groups.prerelease?.split(".") ?? [],
1002
+ release: [
1003
+ Number(groups.major),
1004
+ Number(groups.minor),
1005
+ Number(groups.patch)
1006
+ ]
1007
+ };
1008
+ }
1009
+ function comparePrereleases(left, right) {
1010
+ if (left.length === 0 || right.length === 0) return right.length - left.length;
1011
+ for (const [i, leftIdentifier] of left.entries()) {
1012
+ const rightIdentifier = right[i];
1013
+ if (rightIdentifier === void 0) break;
1014
+ const difference = compareIdentifiers(leftIdentifier, rightIdentifier);
1015
+ if (difference !== 0) return difference;
1016
+ }
1017
+ return left.length - right.length;
1018
+ }
1019
+ function compareIdentifiers(left, right) {
1020
+ const leftIsNumeric = /^\d+$/u.test(left);
1021
+ const rightIsNumeric = /^\d+$/u.test(right);
1022
+ if (leftIsNumeric && rightIsNumeric) return Number(left) - Number(right);
1023
+ if (leftIsNumeric !== rightIsNumeric) return leftIsNumeric ? -1 : 1;
1024
+ if (left === right) return 0;
1025
+ return left < right ? -1 : 1;
1026
+ }
1027
+ //#endregion
1028
+ //#region src/server/update/findAvailableUpdate.ts
1029
+ /**
1030
+ * Picks the published version to offer in place of the running one
1031
+ *
1032
+ * @param current the running version
1033
+ * @param distTags the registry's `latest` and `next` tags
1034
+ * @returns the version to offer and its dist-tag, or null when none is newer or a version cannot be read
1035
+ */
1036
+ function findAvailableUpdate(current, distTags) {
1037
+ const distTagNames = isPrerelease(current) ? ["latest", "next"] : ["latest"];
1038
+ let best = null;
1039
+ for (const distTag of distTagNames) {
1040
+ const version = distTags[distTag];
1041
+ if (version !== void 0 && (compareVersions(version, best?.version ?? current) ?? 0) > 0) best = {
1042
+ distTag,
1043
+ version
1044
+ };
1045
+ }
1046
+ return best;
1047
+ }
1048
+ //#endregion
1049
+ //#region src/server/update/installCommands.ts
1050
+ /**
1051
+ * @param cliPath the real path of the running `dist/cli.js`
1052
+ * @returns how that copy of markdown-review was installed
1053
+ */
1054
+ function findInstallKind(cliPath) {
1055
+ return cliPath.split(path.sep).includes("_npx") ? "npx" : "global";
1056
+ }
1057
+ /**
1058
+ * @param versionOrTag the version or dist-tag to run, e.g. "latest" or "0.4.0"
1059
+ * @returns the npx command that runs that markdown-review
1060
+ */
1061
+ function npxCommand(versionOrTag) {
1062
+ return `npx @krelborn/markdown-review@${versionOrTag}`;
1063
+ }
1064
+ /**
1065
+ * @param kind how markdown-review is installed
1066
+ * @param args the command and its arguments, e.g. "install-skill --global"
1067
+ * @param versionOrTag for npx, the version or dist-tag to run; a global install runs whichever is installed
1068
+ * @returns the shell command that runs markdown-review with those arguments
1069
+ */
1070
+ function cliCommand(kind, args, versionOrTag) {
1071
+ return `${kind === "npx" ? npxCommand(versionOrTag) : "markdown-review"} ${args}`;
1072
+ }
1073
+ /**
1074
+ * @param distTag the dist-tag to install, e.g. "latest"
1075
+ * @returns the shell command that updates a global install to that dist-tag's version
1076
+ */
1077
+ function globalInstallCommand(distTag) {
1078
+ return `npm install --global @krelborn/markdown-review@${distTag}`;
1079
+ }
1080
+ /**
1081
+ * @param distTag the dist-tag to install, e.g. "latest"
1082
+ * @param root the repo whose review server is running the old version
1083
+ * @returns a POSIX shell command that updates a global install, then stops that repo's server from wherever it is run
1084
+ */
1085
+ function globalUpdateCommand(distTag, root) {
1086
+ return `${globalInstallCommand(distTag)} && (cd ${quoteForShell(root)} && markdown-review stop)`;
1087
+ }
1088
+ function quoteForShell(text) {
1089
+ return `'${text.replaceAll("'", "'\\''")}'`;
1090
+ }
1091
+ //#endregion
1092
+ //#region src/server/update/updateCheckFileSchema.ts
1093
+ /**
1094
+ * The registry's dist-tags that matter here; any others are dropped
1095
+ */
1096
+ var distTagsSchema = z.object({
1097
+ latest: z.string().optional(),
1098
+ next: z.string().optional()
1099
+ });
1100
+ /**
1101
+ * The user's update check cache: when the registry was last asked, and the dist-tags it last gave
1102
+ */
1103
+ var updateCheckFileSchema = z.strictObject({
1104
+ checkedAt: z.iso.datetime(),
1105
+ distTags: distTagsSchema
1106
+ });
1107
+ //#endregion
1108
+ //#region src/server/update/readUpdateCheckFile.ts
1109
+ /**
1110
+ * Reads the user's update check cache
1111
+ *
1112
+ * @param filePath the cache file
1113
+ * @returns the cache, or null when it is missing, unreadable or invalid
1114
+ */
1115
+ async function readUpdateCheckFile(filePath) {
1116
+ try {
1117
+ const result = await readStoreFile(filePath, updateCheckFileSchema);
1118
+ return result.kind === "valid" ? result.value : null;
1119
+ } catch {
1120
+ return null;
1121
+ }
1122
+ }
1123
+ //#endregion
1124
+ //#region src/server/update/updateCheckSettings.ts
1125
+ var defaultRegistryUrl = "https://registry.npmjs.org";
1126
+ /**
1127
+ * @param env the environment, for `XDG_CACHE_HOME` and `HOME`
1128
+ * @returns the user's update check cache file, shared by every repo
1129
+ */
1130
+ function updateCheckFilePath(env) {
1131
+ const cacheHome = env.XDG_CACHE_HOME;
1132
+ const cacheDirectory = cacheHome !== void 0 && path.isAbsolute(cacheHome) ? cacheHome : path.join(env.HOME ?? os.homedir(), ".cache");
1133
+ return path.join(cacheDirectory, "markdown-review", "update-check.json");
1134
+ }
1135
+ /**
1136
+ * @param env the environment, for `MARKDOWN_REVIEW_REGISTRY_URL`
1137
+ * @returns the npm registry to ask for dist-tags, without a trailing slash
1138
+ */
1139
+ function registryUrl(env) {
1140
+ return (env.MARKDOWN_REVIEW_REGISTRY_URL ?? defaultRegistryUrl).replace(/\/+$/u, "");
1141
+ }
1142
+ /**
1143
+ * @param env the environment
1144
+ * @returns whether the user or CI has opted out of update checks, through `MARKDOWN_REVIEW_NO_UPDATE_CHECK=1`,
1145
+ * `NO_UPDATE_NOTIFIER` or `CI`
1146
+ */
1147
+ function updateChecksDisabled(env) {
1148
+ return env.MARKDOWN_REVIEW_NO_UPDATE_CHECK === "1" || (env.NO_UPDATE_NOTIFIER ?? "") !== "" || (env.CI ?? "") !== "";
1149
+ }
1150
+ //#endregion
1151
+ //#region src/cli/skillInstallPath.ts
1152
+ /**
1153
+ * @param base the repo root, or the home directory for a global install
1154
+ * @returns where `install-skill` writes the skill under that directory
1155
+ */
1156
+ function skillInstallPath(base) {
1157
+ return path.join(base, ".claude", "skills", "markdown-review", "SKILL.md");
1158
+ }
1159
+ //#endregion
1160
+ //#region src/cli/skillStamps.ts
1161
+ var frontmatterPattern = /^---\r?\n(?<frontmatter>[\s\S]*?\r?\n)---\r?\n/u;
1162
+ /**
1163
+ * @param skill the bundled skill
1164
+ * @param version the running markdown-review's version
1165
+ * @returns the skill as `install-skill` writes it, with the version that wrote it in its frontmatter's
1166
+ * `metadata.version`
1167
+ * @throws when the skill has no frontmatter
1168
+ */
1169
+ function stampSkill(skill, version) {
1170
+ const match = frontmatterPattern.exec(skill);
1171
+ const frontmatter = match?.groups?.frontmatter;
1172
+ if (match === null || frontmatter === void 0) throw new TypeError("The skill has no frontmatter to stamp");
1173
+ const document = parseDocument(frontmatter);
1174
+ document.setIn(["metadata", "version"], version);
1175
+ return `---\n${document.toString({ lineWidth: 0 })}---\n${skill.slice(match[0].length)}`;
1176
+ }
1177
+ /**
1178
+ * @param installed an installed copy of the skill
1179
+ * @returns the version that installed it, or null when the copy carries none
1180
+ */
1181
+ function readSkillStamp(installed) {
1182
+ const frontmatter = frontmatterPattern.exec(installed)?.groups?.frontmatter;
1183
+ if (frontmatter === void 0) return null;
1184
+ const version = parseDocument(frontmatter).getIn(["metadata", "version"]);
1185
+ return typeof version === "string" ? version : null;
1186
+ }
1187
+ //#endregion
1188
+ //#region src/cli/findInstalledSkills.ts
1189
+ /**
1190
+ * Finds the copies of the skill installed in the repo and in the home directory
1191
+ *
1192
+ * @param root the repo root
1193
+ * @param home the home directory
1194
+ * @returns each installed copy, the repo's first; a copy that cannot be read counts as not installed
1195
+ */
1196
+ async function findInstalledSkills(root, home) {
1197
+ const locations = [{
1198
+ isGlobal: false,
1199
+ path: skillInstallPath(root)
1200
+ }, {
1201
+ isGlobal: true,
1202
+ path: skillInstallPath(home)
1203
+ }];
1204
+ const installed = [];
1205
+ for (const location of locations) {
1206
+ const contents = await readTextFileOrNull(location.path).catch(() => null);
1207
+ if (contents !== null) installed.push({
1208
+ ...location,
1209
+ version: readSkillStamp(contents)
1210
+ });
1211
+ }
1212
+ return installed;
1213
+ }
1214
+ //#endregion
1215
+ //#region src/cli/formatUpdateNotices.ts
1216
+ /**
1217
+ * Describes a newer markdown-review and outdated copies of the skill for the agent
1218
+ *
1219
+ * @returns the `update_available` and `skill_outdated` lines, each ending in a newline; empty when there is nothing to say
1220
+ */
1221
+ function formatUpdateNotices({ availableUpdate, currentVersion, installKind, skills, wentAhead }) {
1222
+ if (availableUpdate !== null) {
1223
+ const news = `update_available: markdown-review ${availableUpdate.version} is out; this is ${currentVersion}.`;
1224
+ if (!wentAhead) return [`${news} Tell the user, and offer to update once they say go ahead.\n`];
1225
+ return [`${news} Ask the user whether to update. If they agree, ${describeUpdate(installKind, availableUpdate, skills)}\n`];
1226
+ }
1227
+ const when = wentAhead ? "Run" : "Once the user says go ahead, run";
1228
+ return skills.filter((skill) => isOlderThan(skill, currentVersion)).map((skill) => `skill_outdated: The markdown-review skill at ${skill.path} is older than this version (${currentVersion}). ${when} \`${installSkillCommand$1(installKind, currentVersion, skill)}\`. The new skill loads in the next agent session.\n`);
1229
+ }
1230
+ function describeUpdate(installKind, { distTag, version }, skills) {
1231
+ const steps = `run ${[
1232
+ ...installKind === "global" ? [globalInstallCommand(distTag)] : [],
1233
+ cliCommand(installKind, "stop", distTag),
1234
+ ...skills.filter((skill) => isOlderThan(skill, version)).map((skill) => installSkillCommand$1(installKind, distTag, skill))
1235
+ ].map((command) => `\`${command}\``).join(", then ")}`;
1236
+ return installKind === "npx" ? `${steps}, and from now on run markdown-review as \`${npxCommand(distTag)}\`.` : `${steps}.`;
1237
+ }
1238
+ function installSkillCommand$1(installKind, versionOrTag, skill) {
1239
+ return cliCommand(installKind, skill.isGlobal ? "install-skill --global" : "install-skill", versionOrTag);
1240
+ }
1241
+ function isOlderThan(skill, version) {
1242
+ return skill.version === null || (compareVersions(skill.version, version) ?? -1) < 0;
1243
+ }
1244
+ //#endregion
1245
+ //#region src/cli/readUpdateNotices.ts
1246
+ /**
1247
+ * Works out what to tell the agent about a newer markdown-review and outdated copies of the skill, from the update
1248
+ * check cache and the installed skills, without contacting the registry
1249
+ *
1250
+ * @param context the CLI's path and environment
1251
+ * @param root the repo root, where a repo copy of the skill lives
1252
+ * @param wentAhead whether the user said go ahead
1253
+ * @returns the notice lines, each ending in a newline
1254
+ */
1255
+ async function readUpdateNotices({ cliPath, terminal }, root, wentAhead) {
1256
+ const { env } = terminal;
1257
+ const cached = updateChecksDisabled(env) ? null : await readUpdateCheckFile(updateCheckFilePath(env));
1258
+ const realCliPath = await realpath(cliPath).catch(() => cliPath);
1259
+ return formatUpdateNotices({
1260
+ availableUpdate: findAvailableUpdate(packageVersion, cached?.distTags ?? {}),
1261
+ currentVersion: packageVersion,
1262
+ installKind: findInstallKind(realCliPath),
1263
+ skills: await findInstalledSkills(root, env.HOME ?? os.homedir()),
1264
+ wentAhead
1265
+ });
1266
+ }
1267
+ //#endregion
885
1268
  //#region src/cli/commands/inboxCommand.ts
886
- async function inboxCommand(args, { cliPath, terminal }) {
1269
+ async function inboxCommand(args, context) {
1270
+ const { cliPath, terminal } = context;
887
1271
  const { values } = parseArgs({
888
1272
  args,
889
1273
  options: { document: { type: "string" } }
890
1274
  });
891
1275
  const root = await findRoot(terminal.workingDirectory);
892
1276
  const document = await readDocumentOption(root, terminal.workingDirectory, values.document);
893
- const client = await connectToServer(root, cliPath);
894
- terminal.stdout(formatInbox(await client.inbox(document)));
1277
+ const inbox = await (await connectToServer(root, cliPath)).inbox(document);
1278
+ const notices = roundOutcome(inbox.round) === "go-ahead" ? await readUpdateNotices(context, root, true) : [];
1279
+ terminal.stdout(formatInbox(inbox, notices));
895
1280
  return 0;
896
1281
  }
897
1282
  //#endregion
898
1283
  //#region skills/markdown-review/SKILL.md?raw
899
- var SKILL_default = "---\nname: markdown-review\ndescription: Hands markdown docs you wrote, such as plans, specs and design notes, to the user to review in their browser, then reads their comments back so you can edit the docs and answer each one. Use when the user asks you to have a doc reviewed, or when a doc you wrote needs their sign-off before you carry on.\n---\n\n# Markdown Review\n\nThe user reviews your markdown in a browser and comments on passages, blocks, whole docs or the review as a whole. You read the comments with the `markdown-review` command, edit the docs, and answer each comment.\n\nIf the `markdown-review` command is not found, install it with `npm install --global @krelborn/markdown-review`.\n\n## The review loop\n\n1. Run `markdown-review open <doc.md>`. It starts a review round and shows the doc in the user's browser.\n2. Run `markdown-review poll`. It waits until the user submits, then prints each comment: its thread ID, file, line range, the quoted text and the user's words.\n - Give the shell command a timeout of at least 600000 ms. `poll` gives up after 540 seconds by default; run it again if it says there are no comments yet.\n - In Claude Code, run it with `run_in_background` and `--timeout 7080`, and a Bash timeout of 7200000 ms. A submit still ends it at once.\n3. Edit the docs. Then answer every thread:\n - `markdown-review resolve <id> \"<what changed>\"` when you have addressed it.\n - `markdown-review reply <id> \"<question>\"` when you need the user's input.\n - For a long message, put it in a file and pass `--file <path>`, or pass `--file -` and write it to standard input.\n4. Run `markdown-review poll` again for the next round.\n\n`markdown-review inbox` prints what `poll` would, without waiting, for picking comments up later.\n\n## Approval\n\nThe review ends when the user approves it, and `poll` and `inbox` then start with \"Review approved\". Address any comments that came with the approval, then carry on with your task. Do not run `poll` again for that review.\n\n## Good to know\n\n- A quote is the text as the browser shows it, not the markdown source. The line range is exact, so use it to find the passage.\n- If `poll` is stopped before the user submits, nothing is lost: run it again.\n- Every command ends with a `next_step` line that says what to do next. `markdown-review --help` lists the commands.\n- Run `markdown-review stop` to stop the review server when you have finished.\n";
1284
+ var SKILL_default = "---\nname: markdown-review\ndescription: Hands markdown docs you wrote, such as plans, specs and design notes, to the user to review in their browser, then reads their comments back so you can edit the docs and answer each one. Use when the user asks you to have a doc reviewed, or when a doc you wrote needs their sign-off before you carry on.\n---\n\n# Markdown Review\n\nThe user reviews your markdown in a browser and comments on passages, blocks, whole docs or the review as a whole. You read the comments with the `markdown-review` command, edit the docs, and answer each comment.\n\nIf the `markdown-review` command is not found, install it with `npm install --global @krelborn/markdown-review`.\n\n## The review loop\n\n1. Run `markdown-review open <doc.md> --next-task \"<what you will do once the user says go ahead>\"`, for example `--next-task \"implement the plan in src/server\"`. It starts a review round and shows the doc in the user's browser, with your next task under Go ahead.\n2. Run `markdown-review poll`. It waits until the user submits, then prints each comment: its thread ID, file, line range, the quoted text and the user's words.\n - Give the shell command a timeout of at least 600000 ms. `poll` gives up after 540 seconds by default; run it again if it says there are no comments yet.\n - In Claude Code, run it with `run_in_background` and `--timeout 7080`, and a Bash timeout of 7200000 ms. A submit still ends it at once.\n3. Edit the docs. Then answer every thread:\n - `markdown-review resolve <id> \"<what changed>\"` when you have addressed it.\n - `markdown-review reply <id> \"<question>\"` when you need the user's input.\n - For a long message, put it in a file and pass `--file <path>`, or pass `--file -` and write it to standard input.\n4. Run `markdown-review poll` again for the next round.\n\n`markdown-review inbox` prints what `poll` would, without waiting, for picking comments up later.\n\n## How a review ends\n\nThe user ends a review in one of two ways, and `poll` and `inbox` say which on their first line.\n\n- **Go ahead** (\"The user said go ahead\"): address any comments that came with it, then carry on with your task. Do not run `poll` again for that review.\n- **Hand back** (\"The user asked to hand back to you\"): stop. Do not edit the docs, answer the threads, run `poll` or carry on with your task. Tell the user in the chat that you have stopped and what their comments say, and wait for their instructions. If they ask you to continue the review, run `markdown-review open <doc.md> --next-task \"<what you will do once the user says go ahead>\"`, then `markdown-review poll`.\n\n## Updates\n\n- `open` may print `update_available` when a newer markdown-review is out. Tell the user, and carry on with the review.\n- Once the user says go ahead, `poll` and `inbox` print `update_available` again with the commands to run. Ask the user whether to update, and run them only if they agree. If they install packages another way, such as with pnpm or under `sudo`, adapt the install command and keep the rest.\n- `skill_outdated` means an installed copy of this skill is from an older version. Once the user says go ahead, run the command it gives and tell the user.\n\n## Good to know\n\n- A quote is the text as the browser shows it, not the markdown source. The line range is exact, so use it to find the passage.\n- If `poll` is stopped before the user submits, nothing is lost: run it again.\n- Every command ends with a `next_step` line that says what to do next. `markdown-review --help` lists the commands.\n- Run `markdown-review stop` to stop the review server when you have finished.\n";
900
1285
  //#endregion
901
1286
  //#region src/cli/commands/installSkillCommand.ts
902
1287
  /**
@@ -907,37 +1292,47 @@ async function installSkillCommand(args, { terminal }) {
907
1292
  args,
908
1293
  options: { global: { type: "boolean" } }
909
1294
  });
910
- const base = values.global === true ? terminal.env.HOME ?? os.homedir() : await findRoot(terminal.workingDirectory);
911
- const skillPath = path.join(base, ".claude", "skills", "markdown-review", "SKILL.md");
1295
+ const skillPath = skillInstallPath(values.global === true ? terminal.env.HOME ?? os.homedir() : await findRoot(terminal.workingDirectory));
1296
+ const stamped = stampSkill(SKILL_default, packageVersion);
912
1297
  const installed = await readTextFileOrNull(skillPath);
913
1298
  await mkdir(path.dirname(skillPath), { recursive: true });
914
- await writeFile(skillPath, SKILL_default);
915
- const outcome = installed === null ? "Installed" : installed === "---\nname: markdown-review\ndescription: Hands markdown docs you wrote, such as plans, specs and design notes, to the user to review in their browser, then reads their comments back so you can edit the docs and answer each one. Use when the user asks you to have a doc reviewed, or when a doc you wrote needs their sign-off before you carry on.\n---\n\n# Markdown Review\n\nThe user reviews your markdown in a browser and comments on passages, blocks, whole docs or the review as a whole. You read the comments with the `markdown-review` command, edit the docs, and answer each comment.\n\nIf the `markdown-review` command is not found, install it with `npm install --global @krelborn/markdown-review`.\n\n## The review loop\n\n1. Run `markdown-review open <doc.md>`. It starts a review round and shows the doc in the user's browser.\n2. Run `markdown-review poll`. It waits until the user submits, then prints each comment: its thread ID, file, line range, the quoted text and the user's words.\n - Give the shell command a timeout of at least 600000 ms. `poll` gives up after 540 seconds by default; run it again if it says there are no comments yet.\n - In Claude Code, run it with `run_in_background` and `--timeout 7080`, and a Bash timeout of 7200000 ms. A submit still ends it at once.\n3. Edit the docs. Then answer every thread:\n - `markdown-review resolve <id> \"<what changed>\"` when you have addressed it.\n - `markdown-review reply <id> \"<question>\"` when you need the user's input.\n - For a long message, put it in a file and pass `--file <path>`, or pass `--file -` and write it to standard input.\n4. Run `markdown-review poll` again for the next round.\n\n`markdown-review inbox` prints what `poll` would, without waiting, for picking comments up later.\n\n## Approval\n\nThe review ends when the user approves it, and `poll` and `inbox` then start with \"Review approved\". Address any comments that came with the approval, then carry on with your task. Do not run `poll` again for that review.\n\n## Good to know\n\n- A quote is the text as the browser shows it, not the markdown source. The line range is exact, so use it to find the passage.\n- If `poll` is stopped before the user submits, nothing is lost: run it again.\n- Every command ends with a `next_step` line that says what to do next. `markdown-review --help` lists the commands.\n- Run `markdown-review stop` to stop the review server when you have finished.\n" ? "Reinstalled" : "Updated";
1299
+ await writeFile(skillPath, stamped);
1300
+ const outcome = installed === null ? "Installed" : installed === stamped ? "Reinstalled" : "Updated";
916
1301
  terminal.stdout(`${outcome} the markdown-review skill at ${skillPath}\n\nnext_step: The skill loads when an agent session starts. To start a review now, run \`markdown-review open <doc.md>\`.
917
1302
  `);
918
1303
  return 0;
919
1304
  }
920
1305
  //#endregion
921
1306
  //#region src/cli/commands/openCommand.ts
922
- async function openCommand(args, { cliPath, terminal }) {
923
- const { positionals } = parseArgs({
1307
+ async function openCommand(args, context) {
1308
+ const { cliPath, terminal } = context;
1309
+ const { positionals, values } = parseArgs({
924
1310
  allowPositionals: true,
925
1311
  args,
926
- options: {}
1312
+ options: { "next-task": { type: "string" } }
927
1313
  });
928
1314
  if (positionals.length > 1) throw new CliError("open takes at most one path", "Run `markdown-review open [path]`.", 2);
1315
+ const nextTask = readNextTask(values["next-task"]);
929
1316
  const target = positionals[0];
930
1317
  const workingRoot = await findRoot(terminal.workingDirectory);
931
1318
  const { document, root } = target === void 0 ? {
932
1319
  document: null,
933
1320
  root: workingRoot
934
1321
  } : await locate(terminal.workingDirectory, target);
935
- const { navigated, url } = await (await connectToServer(root, cliPath)).open(document);
1322
+ const { navigated, url } = await (await connectToServer(root, cliPath)).open(document, nextTask);
936
1323
  const shown = navigated ? "Shown in the browser tab that was already open." : await openUrl(url, terminal.env);
937
1324
  const nextStep = root === workingRoot ? waitForCommentsStep : waitForCommentsInRootStep(root);
938
- terminal.stdout(`Opened ${document ?? "the docs list"} for review: ${url}\n${shown}\n\nnext_step: ${nextStep}\n`);
1325
+ const notices = await readUpdateNotices(context, root, false);
1326
+ const noticeSection = notices.length > 0 ? `${notices.join("")}\n` : "";
1327
+ terminal.stdout(`Opened ${document ?? "the docs list"} for review: ${url}\n${shown}\n\n${noticeSection}next_step: ${nextStep}\n`);
939
1328
  return 0;
940
1329
  }
1330
+ function readNextTask(value) {
1331
+ if (value === void 0) return null;
1332
+ const result = nextTaskSchema.safeParse(value);
1333
+ if (!result.success) throw new CliError("--next-task must be one line of at most 200 characters", "Run `markdown-review open <doc.md> --next-task \"<what you will do once the user says go ahead>\"`.", 2);
1334
+ return result.data;
1335
+ }
941
1336
  async function locate(workingDirectory, target) {
942
1337
  const absolutePath = path.resolve(workingDirectory, target);
943
1338
  const found = await stat(absolutePath).catch(() => null);
@@ -964,7 +1359,8 @@ var interruptedExitCodes = {
964
1359
  SIGINT: 130,
965
1360
  SIGTERM: 143
966
1361
  };
967
- async function pollCommand(args, { cliPath, terminal }) {
1362
+ async function pollCommand(args, context) {
1363
+ const { cliPath, terminal } = context;
968
1364
  const { values } = parseArgs({
969
1365
  args,
970
1366
  options: {
@@ -987,8 +1383,12 @@ async function pollCommand(args, { cliPath, terminal }) {
987
1383
  try {
988
1384
  terminal.stderr(`Waiting up to ${timeoutSeconds}s for review comments. If this is interrupted, run \`markdown-review poll\` again; nothing is lost.\n`);
989
1385
  const result = await client.poll(document, timeoutSeconds, abort.signal);
990
- if (result.timedOut && result.threads.length === 0 && !result.review.approved) terminal.stdout(`No comments yet.\n\nnext_step: Run \`markdown-review poll\` again to keep waiting. ${pollTimeoutAdvice}\n`);
991
- else terminal.stdout(formatInbox(result));
1386
+ const outcome = roundOutcome(result.round);
1387
+ if (result.timedOut && result.threads.length === 0 && outcome === null) terminal.stdout(`No comments yet.\n\nnext_step: Run \`markdown-review poll\` again to keep waiting. ${pollTimeoutAdvice}\n`);
1388
+ else {
1389
+ const notices = outcome === "go-ahead" ? await readUpdateNotices(context, root, true) : [];
1390
+ terminal.stdout(formatInbox(result, notices));
1391
+ }
992
1392
  return 0;
993
1393
  } catch (error) {
994
1394
  if (interruptedBy !== void 0) {
@@ -1108,10 +1508,13 @@ var StoreError = class extends Error {
1108
1508
  };
1109
1509
  //#endregion
1110
1510
  //#region src/shared/api/apiRequestSchemas.ts
1111
- var agentOpenRequestSchema = z.strictObject({ path: documentPathSchema.optional() });
1511
+ var agentOpenRequestSchema = z.strictObject({
1512
+ nextTask: nextTaskSchema.optional(),
1513
+ path: documentPathSchema.optional()
1514
+ });
1112
1515
  var messageRequestSchema = z.strictObject({ body: messageBodySchema });
1113
1516
  var resolveRequestSchema = z.strictObject({ body: messageBodySchema.nullable() });
1114
- var submitRequestSchema = z.strictObject({ verdict: z.enum(["request-changes", "approve"]) });
1517
+ var submitRequestSchema = z.strictObject({ verdict: verdictSchema });
1115
1518
  //#endregion
1116
1519
  //#region src/server/files/resolveRepositoryPath.ts
1117
1520
  /**
@@ -1241,19 +1644,19 @@ function registerAgentRoutes(app, dependencies) {
1241
1644
  return context.json({ stopping: true });
1242
1645
  });
1243
1646
  app.post("/api/agent/open", async (context) => {
1244
- const { path } = await readJsonBody(context, agentOpenRequestSchema);
1647
+ const { nextTask, path } = await readJsonBody(context, agentOpenRequestSchema);
1245
1648
  if (path !== void 0) {
1246
1649
  await checkDocumentPath(root, path, false);
1247
1650
  recentDocuments.add(path);
1248
1651
  watchDocument(path);
1249
1652
  }
1250
- const review = await store.requestReview();
1653
+ const round = await store.openRound(path ?? null, nextTask ?? null);
1251
1654
  events.publish({ type: "threads-changed" });
1252
1655
  const pagePath = path === void 0 ? "" : `document/${path.split("/").map(encodeURIComponent).join("/")}`;
1253
1656
  const url = `http://127.0.0.1:${port()}/${pagePath}`;
1254
1657
  const response = {
1255
1658
  navigated: tabs.navigateLatest(url),
1256
- review,
1659
+ round,
1257
1660
  url
1258
1661
  };
1259
1662
  return context.json(response);
@@ -1466,7 +1869,7 @@ function registerHealthRoutes(app, { root, version }) {
1466
1869
  const health = {
1467
1870
  name: "markdown-review",
1468
1871
  pid: process.pid,
1469
- protocol: 1,
1872
+ protocol: 2,
1470
1873
  root,
1471
1874
  version
1472
1875
  };
@@ -1476,7 +1879,7 @@ function registerHealthRoutes(app, { root, version }) {
1476
1879
  //#endregion
1477
1880
  //#region src/server/http/waitForInbox.ts
1478
1881
  /**
1479
- * Waits until the review is approved or a thread needs the agent
1882
+ * Waits until the user ends the review round or a thread needs the agent
1480
1883
  *
1481
1884
  * @returns the inbox as soon as it has something for the agent, or as it stands when the time runs out or the
1482
1885
  * request is aborted, with `timedOut` set
@@ -1486,7 +1889,7 @@ async function waitForInbox({ document, events, signal, store, timeoutMillisecon
1486
1889
  for (;;) {
1487
1890
  const nextSubmit = waitForSubmit(events, deadline - Date.now(), signal);
1488
1891
  const inbox = await store.readInbox(document);
1489
- if (inbox.review.approved || inbox.threads.length > 0) {
1892
+ if (roundOutcome(inbox.round) !== null || inbox.threads.length > 0) {
1490
1893
  nextSubmit.cancel();
1491
1894
  return {
1492
1895
  ...inbox,
@@ -1722,6 +2125,18 @@ function withAppearance(html, { mode, palette }) {
1722
2125
  return html.replace("<html", `<html data-sui-mode="${mode}" data-sui-palette="${palette}"`);
1723
2126
  }
1724
2127
  //#endregion
2128
+ //#region src/server/http/updateRoutes.ts
2129
+ function registerUpdateRoutes(app, { installKind, root, updateChecker, version }) {
2130
+ app.get("/api/update", async (context) => {
2131
+ const update = updateChecker === null ? null : findAvailableUpdate(version, await updateChecker.distTags());
2132
+ const response = {
2133
+ availableVersion: update?.version ?? null,
2134
+ command: update !== null && installKind === "global" ? globalUpdateCommand(update.distTag, root) : null
2135
+ };
2136
+ return context.json(response);
2137
+ });
2138
+ }
2139
+ //#endregion
1725
2140
  //#region src/server/http/createApp.ts
1726
2141
  var statusForStoreError = {
1727
2142
  "invalid-file": 500,
@@ -1777,6 +2192,7 @@ function createApp(dependencies) {
1777
2192
  registerBrowserRoutes(app, dependencies);
1778
2193
  registerEventRoutes(app, dependencies);
1779
2194
  registerFileRoutes(app, dependencies);
2195
+ registerUpdateRoutes(app, dependencies);
1780
2196
  registerShellRoutes(app, dependencies);
1781
2197
  return app;
1782
2198
  }
@@ -1796,6 +2212,21 @@ var RecentDocuments = class {
1796
2212
  }
1797
2213
  };
1798
2214
  //#endregion
2215
+ //#region src/shared/review/isDraftInRound.ts
2216
+ /**
2217
+ * Tells whether a submit sends a thread's draft
2218
+ *
2219
+ * @param thread the thread
2220
+ * @param round the current round, or null before any review was opened
2221
+ * @returns false when the thread has no draft; otherwise true when there is no round, or the draft is on the whole
2222
+ * review, on a doc opened this round, or written since the round began
2223
+ */
2224
+ function isDraftInRound(thread, round) {
2225
+ if (thread.draft === void 0) return false;
2226
+ if (round === null || thread.anchor.kind === "review") return true;
2227
+ return round.documents.includes(thread.anchor.document) || Date.parse(thread.draft.at) >= Date.parse(round.startedAt);
2228
+ }
2229
+ //#endregion
1799
2230
  //#region src/shared/markdown/fenceLanguage.ts
1800
2231
  /**
1801
2232
  * Reads the language of a fenced code block from its info string
@@ -2363,12 +2794,53 @@ function reanchorDocumentThreads(threads, source) {
2363
2794
  //#endregion
2364
2795
  //#region src/server/store/emptyReviewFile.ts
2365
2796
  var emptyReviewFile = {
2366
- approvedAt: null,
2367
- requestedAt: null,
2797
+ round: null,
2368
2798
  threads: [],
2369
- version: 1
2799
+ version: 2
2370
2800
  };
2371
2801
  //#endregion
2802
+ //#region src/server/store/joinOrStartRound.ts
2803
+ var roundExpiryMilliseconds = 432e5;
2804
+ /**
2805
+ * Adds a doc to the review round going on, or starts the next round
2806
+ *
2807
+ * @param round the current round, or null before any review was opened
2808
+ * @param path the doc the agent opened, or null for the docs list
2809
+ * @param nextTask what the agent will do once the user says go ahead; null keeps a joined round's task and leaves a
2810
+ * new round without one
2811
+ * @param at the current time, ISO 8601
2812
+ * @returns the round joined when it has not ended and had an open or a submit in the last 12 hours, otherwise a new
2813
+ * round, which starts when the user handed the previous one back if that was in the last 12 hours
2814
+ */
2815
+ function joinOrStartRound(round, path, nextTask, at) {
2816
+ if (round !== null && roundOutcome(round) === null && isRecent(lastActivity(round), at)) return {
2817
+ ...round,
2818
+ documents: path === null || round.documents.includes(path) ? round.documents : [...round.documents, path],
2819
+ lastOpenedAt: at,
2820
+ nextTask: nextTask ?? round.nextTask
2821
+ };
2822
+ return {
2823
+ documents: path === null ? [] : [path],
2824
+ lastOpenedAt: at,
2825
+ nextTask,
2826
+ startedAt: recentHandBack(round, at) ?? at,
2827
+ verdict: null
2828
+ };
2829
+ }
2830
+ function lastActivity({ lastOpenedAt, verdict }) {
2831
+ return verdict !== null && Date.parse(verdict.at) > Date.parse(lastOpenedAt) ? verdict.at : lastOpenedAt;
2832
+ }
2833
+ /**
2834
+ * @returns when the user handed the round back, no later than `at`, or null when they did not or it was 12 hours ago
2835
+ */
2836
+ function recentHandBack(round, at) {
2837
+ if (round?.verdict?.kind !== "hand-back" || !isRecent(round.verdict.at, at)) return null;
2838
+ return Date.parse(round.verdict.at) < Date.parse(at) ? round.verdict.at : at;
2839
+ }
2840
+ function isRecent(earlier, at) {
2841
+ return Date.parse(at) - Date.parse(earlier) < roundExpiryMilliseconds;
2842
+ }
2843
+ //#endregion
2372
2844
  //#region src/server/store/needsAgent.ts
2373
2845
  /**
2374
2846
  * Tells whether a thread is waiting for the agent
@@ -2451,12 +2923,12 @@ var documentThreadsFileSchema = z.strictObject({
2451
2923
  }).refine((file) => file.threads.every((thread) => thread.anchor.kind !== "review" && thread.anchor.document === file.document), { message: "must hold only threads anchored to its own doc" });
2452
2924
  //#endregion
2453
2925
  //#region src/shared/review/reviewFileSchema.ts
2926
+ var reviewThreadsSchema = z.array(threadSchema).refine((threads) => threads.every((thread) => thread.anchor.kind === "review"), { message: "must hold only threads on the whole review" });
2454
2927
  var reviewFileSchema = z.strictObject({
2455
- approvedAt: z.iso.datetime().nullable(),
2456
- requestedAt: z.iso.datetime().nullable(),
2457
- threads: z.array(threadSchema),
2458
- version: z.literal(1)
2459
- }).refine((file) => file.threads.every((thread) => thread.anchor.kind === "review"), { message: "must hold only threads on the whole review" });
2928
+ round: reviewRoundSchema.nullable(),
2929
+ threads: reviewThreadsSchema,
2930
+ version: z.literal(2)
2931
+ });
2460
2932
  //#endregion
2461
2933
  //#region src/server/files/writeTextFileUnlessExists.ts
2462
2934
  /**
@@ -2480,6 +2952,44 @@ async function writeTextFileUnlessExists(filePath, contents) {
2480
2952
  }
2481
2953
  }
2482
2954
  //#endregion
2955
+ //#region src/server/store/storedReviewFileSchema.ts
2956
+ var reviewFileVersion1Schema = z.strictObject({
2957
+ approvedAt: z.iso.datetime().nullable(),
2958
+ requestedAt: z.iso.datetime().nullable(),
2959
+ threads: reviewThreadsSchema,
2960
+ version: z.literal(1)
2961
+ });
2962
+ /**
2963
+ * Reads `review.json` as written by any version, giving the current version
2964
+ */
2965
+ var storedReviewFileSchema = z.discriminatedUnion("version", [reviewFileSchema, reviewFileVersion1Schema]).transform((file) => file.version === 1 ? upgradeVersion1(file) : file);
2966
+ /**
2967
+ * @param file a version 1 file, which recorded when the agent last opened the review and when the user last approved
2968
+ * @returns the same threads, with no round when no review was opened, otherwise a round on no docs that started at the
2969
+ * last open and ended with Go ahead when the approval came after it
2970
+ */
2971
+ function upgradeVersion1({ approvedAt, requestedAt, threads }) {
2972
+ if (requestedAt === null) return {
2973
+ round: null,
2974
+ threads,
2975
+ version: 2
2976
+ };
2977
+ return {
2978
+ round: {
2979
+ documents: [],
2980
+ lastOpenedAt: requestedAt,
2981
+ nextTask: null,
2982
+ startedAt: requestedAt,
2983
+ verdict: approvedAt !== null && Date.parse(approvedAt) > Date.parse(requestedAt) ? {
2984
+ at: approvedAt,
2985
+ kind: "go-ahead"
2986
+ } : null
2987
+ },
2988
+ threads,
2989
+ version: 2
2990
+ };
2991
+ }
2992
+ //#endregion
2483
2993
  //#region src/server/store/StoreFiles.ts
2484
2994
  /**
2485
2995
  * Reads and writes the files of one root's `.markdown-review/` directory and the docs they refer to
@@ -2500,7 +3010,7 @@ var StoreFiles = class {
2500
3010
  * @returns the review file, or an empty one when it does not exist yet
2501
3011
  */
2502
3012
  async readReviewFile() {
2503
- const result = await readStoreFile(reviewFilePath(this.root), reviewFileSchema);
3013
+ const result = await readStoreFile(reviewFilePath(this.root), storedReviewFileSchema);
2504
3014
  return result.kind === "valid" ? {
2505
3015
  kind: "valid",
2506
3016
  value: result.value ?? emptyReviewFile
@@ -2683,21 +3193,6 @@ function withoutDraft(thread) {
2683
3193
  return next;
2684
3194
  }
2685
3195
  //#endregion
2686
- //#region src/server/store/toReviewState.ts
2687
- /**
2688
- * Works out whether the current review round is approved
2689
- *
2690
- * @param reviewFile the review's stored timestamps
2691
- * @returns the timestamps and whether the approval came after the latest request
2692
- */
2693
- function toReviewState({ approvedAt, requestedAt }) {
2694
- return {
2695
- approved: approvedAt !== null && (requestedAt === null || Date.parse(approvedAt) > Date.parse(requestedAt)),
2696
- approvedAt,
2697
- requestedAt
2698
- };
2699
- }
2700
- //#endregion
2701
3196
  //#region src/server/store/ReviewStore.ts
2702
3197
  /**
2703
3198
  * Holds one root's review threads and carries out every change to them, one at a time
@@ -2725,18 +3220,22 @@ var ReviewStore = class {
2725
3220
  });
2726
3221
  }
2727
3222
  /**
2728
- * Starts a review round, so an earlier approval no longer counts
3223
+ * Adds a doc to the review round going on, or starts the next round
2729
3224
  *
2730
- * @returns the review state after the request
3225
+ * @param path the doc the agent opened, or null for the docs list
3226
+ * @param nextTask what the agent will do once the user says go ahead; null keeps a joined round's task and leaves a
3227
+ * new round without one
3228
+ * @returns the round the doc is now in
2731
3229
  */
2732
- requestReview() {
3230
+ openRound(path, nextTask) {
2733
3231
  return this.queue.enqueue(async () => {
2734
- const next = {
2735
- ...requireValid(await this.files.readReviewFile()),
2736
- requestedAt: (/* @__PURE__ */ new Date()).toISOString()
2737
- };
2738
- await this.files.writeReviewFile(next);
2739
- return toReviewState(next);
3232
+ const review = requireValid(await this.files.readReviewFile());
3233
+ const round = joinOrStartRound(review.round, path, nextTask, (/* @__PURE__ */ new Date()).toISOString());
3234
+ await this.files.writeReviewFile({
3235
+ ...review,
3236
+ round
3237
+ });
3238
+ return round;
2740
3239
  });
2741
3240
  }
2742
3241
  /**
@@ -2794,31 +3293,40 @@ var ReviewStore = class {
2794
3293
  return this.changeThread(id, "user", resolveAsUser);
2795
3294
  }
2796
3295
  /**
2797
- * Submits every draft in the repo and records the user's verdict on the review round
3296
+ * Submits the drafts that belong to the review round, leaving the rest as drafts, and records the user's verdict on
3297
+ * the round, starting one when no review was opened
2798
3298
  *
2799
- * @param verdict "approve" ends the round; "request-changes" needs at least one draft
2800
- * @returns the IDs of the threads that received a submitted message, and the review state after the submit
2801
- * @throws StoreError "invalid-state" when requesting changes with no drafts
3299
+ * @param verdict "go-ahead" and "hand-back" end the round; "revise" needs at least one draft to submit
3300
+ * @returns the IDs of the threads that received a submitted message, and the round after the submit
3301
+ * @throws StoreError "invalid-state" when asking for revisions with no drafts to submit
2802
3302
  */
2803
3303
  submit(verdict) {
2804
3304
  return this.queue.enqueue(async () => {
2805
3305
  const at = (/* @__PURE__ */ new Date()).toISOString();
2806
3306
  const review = requireValid(await this.files.readReviewFile());
2807
- const documentFiles = await this.readDocumentFilesWithDrafts();
2808
- const submittedThreadIds = [...review.threads, ...documentFiles.flatMap((file) => file.threads)].filter((thread) => thread.draft !== void 0).map((thread) => thread.id).sort((left, right) => left - right);
2809
- if (verdict === "request-changes" && submittedThreadIds.length === 0) throw new StoreError("invalid-state", "There are no drafts to submit");
3307
+ const isSent = (thread) => isDraftInRound(thread, review.round);
3308
+ const send = (thread) => isSent(thread) ? submitDraft(thread, at) : thread;
3309
+ const documentFiles = await this.readDocumentFilesToSubmit(isSent);
3310
+ const submittedThreadIds = [...review.threads, ...documentFiles.flatMap((file) => file.threads)].filter(isSent).map((thread) => thread.id).sort((left, right) => left - right);
3311
+ if (verdict === "revise" && submittedThreadIds.length === 0) throw new StoreError("invalid-state", "There are no drafts to submit");
2810
3312
  for (const file of documentFiles) await this.files.writeDocumentFile({
2811
3313
  ...file,
2812
- threads: file.threads.map((thread) => submitDraft(thread, at))
3314
+ threads: file.threads.map(send)
2813
3315
  });
2814
- const next = {
2815
- ...review,
2816
- approvedAt: verdict === "approve" ? at : null,
2817
- threads: review.threads.map((thread) => submitDraft(thread, at))
3316
+ const round = {
3317
+ ...review.round ?? joinOrStartRound(null, null, null, at),
3318
+ verdict: {
3319
+ at,
3320
+ kind: verdict
3321
+ }
2818
3322
  };
2819
- await this.files.writeReviewFile(next);
3323
+ await this.files.writeReviewFile({
3324
+ ...review,
3325
+ round,
3326
+ threads: review.threads.map(send)
3327
+ });
2820
3328
  return {
2821
- review: toReviewState(next),
3329
+ round,
2822
3330
  submittedThreadIds
2823
3331
  };
2824
3332
  });
@@ -2842,7 +3350,7 @@ var ReviewStore = class {
2842
3350
  * Reads threads for the browser, first re-anchoring any doc that changed since its threads were last anchored
2843
3351
  *
2844
3352
  * @param document the doc whose threads to read, alongside the review's, or null for every thread
2845
- * @returns the review state, the threads in ID order, and any store files that could not be read
3353
+ * @returns the review round, the threads in ID order, and any store files that could not be read
2846
3354
  * @throws StoreError "invalid-input" when the doc path is not a repo-relative POSIX path
2847
3355
  */
2848
3356
  readThreads(document) {
@@ -2852,7 +3360,7 @@ var ReviewStore = class {
2852
3360
  * Reads what the agent has to act on, first re-anchoring any doc that changed since its threads were last anchored
2853
3361
  *
2854
3362
  * @param document the doc to limit the threads to, or null for every thread including the review's
2855
- * @returns the review state and the threads that need the agent
3363
+ * @returns the review round and the threads that need the agent
2856
3364
  * @throws StoreError "invalid-input" when the doc path is not a repo-relative POSIX path
2857
3365
  */
2858
3366
  readInbox(document) {
@@ -2874,7 +3382,7 @@ var ReviewStore = class {
2874
3382
  for (const name of documents) threads.push(...await this.readDocumentThreadsOrReport(name, problems));
2875
3383
  return {
2876
3384
  problems,
2877
- review: toReviewState(review),
3385
+ round: review.round,
2878
3386
  threads: threads.sort((left, right) => left.id - right.id)
2879
3387
  };
2880
3388
  }
@@ -2959,12 +3467,12 @@ var ReviewStore = class {
2959
3467
  problems
2960
3468
  };
2961
3469
  }
2962
- async readDocumentFilesWithDrafts() {
3470
+ async readDocumentFilesToSubmit(isSent) {
2963
3471
  const files = [];
2964
3472
  for (const document of await this.files.listDocuments()) {
2965
3473
  const result = await this.files.readDocumentFile(document);
2966
3474
  if (result.kind === "invalid") this.logger.warn(`Drafts in ${document} were not submitted. ${result.problem}`);
2967
- else if (result.value?.threads.some((thread) => thread.draft !== void 0)) files.push((await this.readCurrentDocumentFile(document)).file);
3475
+ else if (result.value?.threads.some(isSent)) files.push((await this.readCurrentDocumentFile(document)).file);
2968
3476
  }
2969
3477
  return files;
2970
3478
  }
@@ -3029,6 +3537,139 @@ function requireValid(result) {
3029
3537
  return result.value;
3030
3538
  }
3031
3539
  //#endregion
3540
+ //#region src/server/update/RegistryRequestError.ts
3541
+ /**
3542
+ * A dist-tags request the npm registry answered with an error status
3543
+ */
3544
+ var RegistryRequestError = class extends Error {
3545
+ status;
3546
+ constructor(url, status) {
3547
+ super(`${url} answered ${status}`);
3548
+ this.name = "RegistryRequestError";
3549
+ this.status = status;
3550
+ }
3551
+ };
3552
+ //#endregion
3553
+ //#region src/server/update/fetchDistTags.ts
3554
+ var packageName = "@krelborn/markdown-review";
3555
+ /**
3556
+ * Asks the registry for the package's dist-tags
3557
+ *
3558
+ * @param registry the registry's URL, without a trailing slash
3559
+ * @param fetchUrl the fetch to send the request with
3560
+ * @param timeoutMilliseconds how long to wait for the whole response
3561
+ * @returns the `latest` and `next` tags, when the registry has them
3562
+ * @throws when the request fails, times out, or the response is not a dist-tags object
3563
+ */
3564
+ async function fetchDistTags(registry, fetchUrl, timeoutMilliseconds) {
3565
+ const url = `${registry}/-/package/${encodeURIComponent(packageName)}/dist-tags`;
3566
+ const response = await fetchUrl(url, { signal: AbortSignal.timeout(timeoutMilliseconds) });
3567
+ if (!response.ok) throw new RegistryRequestError(url, response.status);
3568
+ return distTagsSchema.parse(await response.json());
3569
+ }
3570
+ //#endregion
3571
+ //#region src/server/update/UpdateChecker.ts
3572
+ var dayMilliseconds = 864e5;
3573
+ var minuteMilliseconds = 6e4;
3574
+ /**
3575
+ * Asks the npm registry for markdown-review's dist-tags about once a day for the user, keeping the answer in a cache
3576
+ * file that every repo's server and the CLI read
3577
+ */
3578
+ var UpdateChecker = class {
3579
+ cachePath;
3580
+ checking;
3581
+ fetch;
3582
+ isStarted = false;
3583
+ logger;
3584
+ now;
3585
+ recorded = null;
3586
+ registry;
3587
+ timer;
3588
+ timeoutMilliseconds;
3589
+ constructor({ cachePath, fetch: fetchUrl = fetch, logger, now = () => /* @__PURE__ */ new Date(), registry, timeoutMilliseconds = 3e3 }) {
3590
+ this.cachePath = cachePath;
3591
+ this.fetch = fetchUrl;
3592
+ this.logger = logger;
3593
+ this.now = now;
3594
+ this.registry = registry;
3595
+ this.timeoutMilliseconds = timeoutMilliseconds;
3596
+ }
3597
+ /**
3598
+ * Asks the registry unless the cache is less than a day old; never rejects
3599
+ *
3600
+ * @returns once the cache is up to date, or the check has failed
3601
+ */
3602
+ check() {
3603
+ this.checking ??= this.refresh().finally(() => {
3604
+ this.checking = void 0;
3605
+ });
3606
+ return this.checking;
3607
+ }
3608
+ /**
3609
+ * @returns the dist-tags last recorded, after any check in progress ends; empty when none are known
3610
+ */
3611
+ async distTags() {
3612
+ await this.checking;
3613
+ return (await this.readLatest())?.distTags ?? {};
3614
+ }
3615
+ /**
3616
+ * Checks now, then whenever the cache turns a day old while the process runs
3617
+ */
3618
+ start() {
3619
+ this.isStarted = true;
3620
+ this.checkAndSchedule();
3621
+ }
3622
+ stop() {
3623
+ this.isStarted = false;
3624
+ clearTimeout(this.timer);
3625
+ }
3626
+ async checkAndSchedule() {
3627
+ await this.check();
3628
+ if (!this.isStarted) return;
3629
+ const cached = await this.readLatest();
3630
+ const untilStale = cached === null ? dayMilliseconds : Date.parse(cached.checkedAt) + dayMilliseconds - this.now().getTime();
3631
+ const delay = Math.min(Math.max(untilStale, minuteMilliseconds), dayMilliseconds);
3632
+ clearTimeout(this.timer);
3633
+ this.timer = setTimeout(() => void this.checkAndSchedule(), delay);
3634
+ this.timer.unref();
3635
+ }
3636
+ async refresh() {
3637
+ const cached = await this.readLatest();
3638
+ const now = this.now();
3639
+ if (isFresh(cached, now)) return;
3640
+ let distTags;
3641
+ try {
3642
+ distTags = await fetchDistTags(this.registry, this.fetch, this.timeoutMilliseconds);
3643
+ } catch (error) {
3644
+ this.logger.warn("Could not check the npm registry for a newer markdown-review", error);
3645
+ const latest = await this.readLatest();
3646
+ if (isFresh(latest, now)) return;
3647
+ distTags = latest?.distTags ?? {};
3648
+ }
3649
+ const file = {
3650
+ checkedAt: now.toISOString(),
3651
+ distTags
3652
+ };
3653
+ this.recorded = file;
3654
+ try {
3655
+ await writeJsonAtomically(this.cachePath, file);
3656
+ } catch (error) {
3657
+ this.logger.warn(`Could not write ${this.cachePath}`, error);
3658
+ }
3659
+ }
3660
+ /**
3661
+ * The cache file, or this checker's own last check when that is newer, as it is when the file could not be written
3662
+ */
3663
+ async readLatest() {
3664
+ const cached = await readUpdateCheckFile(this.cachePath);
3665
+ if (cached === null || this.recorded !== null && Date.parse(this.recorded.checkedAt) > Date.parse(cached.checkedAt)) return this.recorded;
3666
+ return cached;
3667
+ }
3668
+ };
3669
+ function isFresh(file, now) {
3670
+ return file !== null && now.getTime() - Date.parse(file.checkedAt) < dayMilliseconds;
3671
+ }
3672
+ //#endregion
3032
3673
  //#region src/server/runtime/ActivityMonitor.ts
3033
3674
  /**
3034
3675
  * Tracks connected tabs and waiting polls, reports whether the agent is waiting, and notices when the server is idle
@@ -3164,7 +3805,7 @@ function preferredPort(root) {
3164
3805
  *
3165
3806
  * @returns the running server; it removes `server.json` when it stops
3166
3807
  */
3167
- async function runServer({ idleMilliseconds = 18e5, logger, pollKeepAliveMilliseconds = 15e3, root, webDirectory }) {
3808
+ async function runServer({ idleMilliseconds = 18e5, logger, pollKeepAliveMilliseconds = 15e3, root, updateChecker = createUpdateChecker(logger), webDirectory }) {
3168
3809
  const store = new ReviewStore(root, logger);
3169
3810
  await store.initialize();
3170
3811
  const events = new ServerEvents();
@@ -3199,6 +3840,7 @@ async function runServer({ idleMilliseconds = 18e5, logger, pollKeepAliveMillise
3199
3840
  const app = createApp({
3200
3841
  activity,
3201
3842
  events,
3843
+ installKind: findInstallKind(process.argv[1] ?? ""),
3202
3844
  logger,
3203
3845
  pollKeepAliveMilliseconds,
3204
3846
  port: () => port,
@@ -3208,16 +3850,18 @@ async function runServer({ idleMilliseconds = 18e5, logger, pollKeepAliveMillise
3208
3850
  store,
3209
3851
  tabs: new BrowserTabs(),
3210
3852
  token,
3853
+ updateChecker,
3211
3854
  version: packageVersion,
3212
3855
  watchDocument: (document) => watcher.watch(document),
3213
3856
  webDirectory
3214
3857
  });
3858
+ updateChecker?.start();
3215
3859
  const server = createServer(getRequestListener(app.fetch));
3216
3860
  port = await listen(server, preferredPort(root));
3217
3861
  const serverFile = {
3218
3862
  pid: process.pid,
3219
3863
  port,
3220
- protocol: 1,
3864
+ protocol: 2,
3221
3865
  root,
3222
3866
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3223
3867
  token,
@@ -3228,6 +3872,7 @@ async function runServer({ idleMilliseconds = 18e5, logger, pollKeepAliveMillise
3228
3872
  logger.info(`Serving ${root} on http://127.0.0.1:${port}`);
3229
3873
  async function stop() {
3230
3874
  activity.stop();
3875
+ updateChecker?.stop();
3231
3876
  await watcher.close();
3232
3877
  await closeServer(server);
3233
3878
  await removeServerFile(root);
@@ -3240,6 +3885,14 @@ async function runServer({ idleMilliseconds = 18e5, logger, pollKeepAliveMillise
3240
3885
  port
3241
3886
  };
3242
3887
  }
3888
+ function createUpdateChecker(logger) {
3889
+ if (updateChecksDisabled(process.env)) return null;
3890
+ return new UpdateChecker({
3891
+ cachePath: updateCheckFilePath(process.env),
3892
+ logger,
3893
+ registry: registryUrl(process.env)
3894
+ });
3895
+ }
3243
3896
  async function listen(server, port) {
3244
3897
  try {
3245
3898
  return await listenOn(server, port);
@@ -3333,16 +3986,21 @@ async function stopCommand(args, { terminal }) {
3333
3986
  //#endregion
3334
3987
  //#region src/cli/helpText.ts
3335
3988
  var commandHelp = {
3336
- inbox: ["markdown-review inbox [--document <path>]", " Print whether the review is approved and the threads that need you, then return at once."].join("\n"),
3989
+ inbox: ["markdown-review inbox [--document <path>]", " Print how the user ended the review, if they have, and the threads that need you, then return at once."].join("\n"),
3337
3990
  "install-skill": [
3338
3991
  "markdown-review install-skill [--global]",
3339
3992
  " Install the skill that teaches agents this workflow in this repository's .claude/skills, or with --global in",
3340
3993
  " ~/.claude/skills. Claude Code and OpenCode both read skills from there."
3341
3994
  ].join("\n"),
3342
- open: ["markdown-review open [path]", " Start a review round and show the doc (or the docs list) in the user's browser. Prints the URL."].join("\n"),
3995
+ open: [
3996
+ "markdown-review open [path] [--next-task <text>]",
3997
+ " Start a review round, or add the doc to the one in progress, and show the doc (or the docs list) in the",
3998
+ " user's browser. --next-task says what you will do once the user says go ahead; the user sees it.",
3999
+ " Prints the URL."
4000
+ ].join("\n"),
3343
4001
  poll: [
3344
4002
  "markdown-review poll [--document <path>] [--timeout <seconds>]",
3345
- " Wait until the user submits or approves, then print what needs you. The timeout defaults to 540 seconds.",
4003
+ " Wait until the user submits, then print what needs you. The timeout defaults to 540 seconds.",
3346
4004
  " Give the shell command a timeout of at least 600000 ms, or in Claude Code run it with run_in_background",
3347
4005
  " and --timeout 7080."
3348
4006
  ].join("\n"),