diffing 0.10.4 → 0.11.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 (178) hide show
  1. package/README.md +77 -32
  2. package/dist/{cli-agent-BBHx1lLJ.mjs → cli-agent-DkwGxuy2.mjs} +43 -12
  3. package/dist/{cli-gh-B8HybRWZ.mjs → cli-gh-D0NfKpOp.mjs} +4 -4
  4. package/dist/cli.mjs +165 -35
  5. package/dist/client/assets/{angular-html-Bn7xcK-X.js → angular-html-Dl53kkiy.js} +1 -1
  6. package/dist/client/assets/{angular-ts-qK80FnOc.js → angular-ts-wNJFFkey.js} +1 -1
  7. package/dist/client/assets/{apl-BhBHq9NH.js → apl-BxtQST-h.js} +1 -1
  8. package/dist/client/assets/{arc-BbRWqKdi.js → arc-CBmZuzPB.js} +1 -1
  9. package/dist/client/assets/{architectureDiagram-3BPJPVTR-DkAZjTe2.js → architectureDiagram-3BPJPVTR-CEci_K9f.js} +1 -1
  10. package/dist/client/assets/{astro--A8NjYkS.js → astro-BpADD5rh.js} +1 -1
  11. package/dist/client/assets/{blade-DUJ_WZzW.js → blade-B4I5SQmP.js} +1 -1
  12. package/dist/client/assets/{blockDiagram-GPEHLZMM-BJnX6alY.js → blockDiagram-GPEHLZMM-DcKhDtnE.js} +1 -1
  13. package/dist/client/assets/{c-Ddzn0JTQ.js → c-C3VtOgg8.js} +1 -1
  14. package/dist/client/assets/{c4Diagram-AAUBKEIU-B_JFq-Tk.js → c4Diagram-AAUBKEIU-CaHozoxF.js} +1 -1
  15. package/dist/client/assets/channel-Ct4-oSQN.js +1 -0
  16. package/dist/client/assets/{chunk-2J33WTMH-CCnCUy9H.js → chunk-2J33WTMH-tvuzKhdK.js} +1 -1
  17. package/dist/client/assets/{chunk-3OPIFGDE-CJvhnVzd.js → chunk-3OPIFGDE-D_nBoD9w.js} +1 -1
  18. package/dist/client/assets/{chunk-4BX2VUAB-7gGFn4DW.js → chunk-4BX2VUAB-yHPdy1G8.js} +1 -1
  19. package/dist/client/assets/{chunk-55IACEB6-CRirC3B0.js → chunk-55IACEB6-yXpL8wjV.js} +1 -1
  20. package/dist/client/assets/{chunk-5ZQYHXKU-DSagYOhc.js → chunk-5ZQYHXKU-DZ5cvCDV.js} +1 -1
  21. package/dist/client/assets/{chunk-727SXJPM-2fqFvPdL.js → chunk-727SXJPM-CJjgOWmY.js} +1 -1
  22. package/dist/client/assets/{chunk-AQP2D5EJ-8l-kDb0Q.js → chunk-AQP2D5EJ-CQYQKXSE.js} +1 -1
  23. package/dist/client/assets/{chunk-BSJP7CBP-BjTEoy7R.js → chunk-BSJP7CBP-CP_f908b.js} +1 -1
  24. package/dist/client/assets/{chunk-CSCIHK7Q-CeHxe0Uf.js → chunk-CSCIHK7Q-tcgA8jR0.js} +1 -1
  25. package/dist/client/assets/{chunk-FMBD7UC4-X-OwPNTB.js → chunk-FMBD7UC4-DlpmhBYg.js} +1 -1
  26. package/dist/client/assets/{chunk-KSCS5N6A-rYccR2Ju.js → chunk-KSCS5N6A-BkfieXpV.js} +1 -1
  27. package/dist/client/assets/{chunk-L5ZTLDWV-8JMoad-S.js → chunk-L5ZTLDWV-BjVXSZnt.js} +1 -1
  28. package/dist/client/assets/{chunk-LZXEDZCA-DOvCLema.js → chunk-LZXEDZCA-CaAWmy4Q.js} +2 -2
  29. package/dist/client/assets/{chunk-ND2GUHAM-CQQJmqC8.js → chunk-ND2GUHAM-kWh41zUn.js} +1 -1
  30. package/dist/client/assets/{chunk-NZK2D7GU-DPwHQE2m.js → chunk-NZK2D7GU-CDrIi44_.js} +1 -1
  31. package/dist/client/assets/{chunk-O5CBEL6O-Bvzv6QAN.js → chunk-O5CBEL6O-CbNGK3Te.js} +1 -1
  32. package/dist/client/assets/chunk-QZHKN3VN-BXhKFVX7.js +1 -0
  33. package/dist/client/assets/chunk-WU5MYG2G-_EwuC51_.js +1 -0
  34. package/dist/client/assets/{chunk-XPW4576I-DY1teoyy.js → chunk-XPW4576I-Cca0fTtG.js} +1 -1
  35. package/dist/client/assets/classDiagram-4FO5ZUOK-C8bdx9R4.js +1 -0
  36. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-B26wrQuH.js +1 -0
  37. package/dist/client/assets/{cobol-EaVCzYAO.js → cobol-j-6eG2UQ.js} +1 -1
  38. package/dist/client/assets/{coffee-CbmPxJhz.js → coffee-CZLH0dtt.js} +1 -1
  39. package/dist/client/assets/{cose-bilkent-S5V4N54A-zMLfkB6c.js → cose-bilkent-S5V4N54A-DlxESVex.js} +1 -1
  40. package/dist/client/assets/{cpp-1v8ov4EG.js → cpp-j6Bp24RI.js} +1 -1
  41. package/dist/client/assets/{crystal-B88NqWAb.js → crystal-DW7wcFUs.js} +1 -1
  42. package/dist/client/assets/{css-QFeW3BCZ.js → css-C43BDU2i.js} +1 -1
  43. package/dist/client/assets/{dagre-BM42HDAG-DxGKZBBq.js → dagre-BM42HDAG-BaemEJFh.js} +1 -1
  44. package/dist/client/assets/{diagram-2AECGRRQ-CiUL7Dov.js → diagram-2AECGRRQ-BaM-bkJ3.js} +1 -1
  45. package/dist/client/assets/{diagram-5GNKFQAL-DCs97wXn.js → diagram-5GNKFQAL-BOH04DJO.js} +1 -1
  46. package/dist/client/assets/{diagram-KO2AKTUF-BsSJWXqr.js → diagram-KO2AKTUF-BP5MyqDZ.js} +1 -1
  47. package/dist/client/assets/{diagram-LMA3HP47-LDuP4WAy.js → diagram-LMA3HP47-Dt4gKMYF.js} +1 -1
  48. package/dist/client/assets/{diagram-OG6HWLK6-CKjIVoHx.js → diagram-OG6HWLK6-DoT1VshN.js} +1 -1
  49. package/dist/client/assets/{dist-BvtPW002.js → dist-DEAxvC4F.js} +1 -1
  50. package/dist/client/assets/{edge-5UREsVXQ.js → edge-D9ySFlnD.js} +1 -1
  51. package/dist/client/assets/{elixir-B_Yali6w.js → elixir-CoJz_LQ5.js} +1 -1
  52. package/dist/client/assets/{elm-STYz8ayI.js → elm-CvedWh_e.js} +1 -1
  53. package/dist/client/assets/{erDiagram-TEJ5UH35-D7iBLlYp.js → erDiagram-TEJ5UH35-CMP78FsQ.js} +1 -1
  54. package/dist/client/assets/{erb-CHRx3Beu.js → erb-lD_yYZsm.js} +1 -1
  55. package/dist/client/assets/{flowDiagram-I6XJVG4X-Dr2J9T5z.js → flowDiagram-I6XJVG4X-Dp0s5BFj.js} +1 -1
  56. package/dist/client/assets/{ganttDiagram-6RSMTGT7-Bb1v2M1H.js → ganttDiagram-6RSMTGT7-GmSG31U-.js} +1 -1
  57. package/dist/client/assets/{git-rebase-CFmRpmLK.js → git-rebase-DwZABzof.js} +1 -1
  58. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DP9xmnTA.js → gitGraphDiagram-PVQCEYII-C1SXShag.js} +1 -1
  59. package/dist/client/assets/{glimmer-js-CdrEdtzi.js → glimmer-js-CWcs3w1Z.js} +1 -1
  60. package/dist/client/assets/{glimmer-ts-CzrwZqCl.js → glimmer-ts-CrzGN4lJ.js} +1 -1
  61. package/dist/client/assets/{glsl-Cb4XetAc.js → glsl-DRCtwX7h.js} +1 -1
  62. package/dist/client/assets/{graphql-mqi9K2_l.js → graphql-rpmrismS.js} +1 -1
  63. package/dist/client/assets/{hack-DaFJ7htO.js → hack-Ba02RnuQ.js} +1 -1
  64. package/dist/client/assets/{haml-Bw7q4nKg.js → haml-BHBACStm.js} +1 -1
  65. package/dist/client/assets/{handlebars-mj81MoXX.js → handlebars-BtlQ1SCa.js} +1 -1
  66. package/dist/client/assets/{html-LHrj0-2t.js → html-BkVU3Dge.js} +1 -1
  67. package/dist/client/assets/{html-derivative-L3_OYrFu.js → html-derivative-DoK92vwi.js} +1 -1
  68. package/dist/client/assets/{http-DE0OFe2c.js → http-CJAwKSPH.js} +1 -1
  69. package/dist/client/assets/{hurl-CNbk7fqf.js → hurl-kaX5jwuN.js} +1 -1
  70. package/dist/client/assets/{index-DHt5Hu--.js → index-BHeNiC07.js} +261 -199
  71. package/dist/client/assets/index-D8hOoJCg.css +1 -0
  72. package/dist/client/assets/{infoDiagram-5YYISTIA-wxqSmRV0.js → infoDiagram-5YYISTIA-B0i3BlNR.js} +1 -1
  73. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CJa0qhXu.js → ishikawaDiagram-YF4QCWOH-BN8sEei4.js} +1 -1
  74. package/dist/client/assets/{java-DAwMoXxA.js → java-BvrigIoq.js} +1 -1
  75. package/dist/client/assets/{javascript-_a2prAz7.js → javascript-DZkPTYAi.js} +1 -1
  76. package/dist/client/assets/{jinja-JKFA3bu8.js → jinja-Ceg7ui1S.js} +1 -1
  77. package/dist/client/assets/{jison-UbTjhW7k.js → jison-iE1GaOyN.js} +1 -1
  78. package/dist/client/assets/{journeyDiagram-JHISSGLW-Bh5L3OBN.js → journeyDiagram-JHISSGLW-CE6O3NhU.js} +1 -1
  79. package/dist/client/assets/{json-D74F0BDU.js → json-C0j8rEcg.js} +1 -1
  80. package/dist/client/assets/{jsx-BsH2-gDN.js → jsx-B7eXljgz.js} +1 -1
  81. package/dist/client/assets/{julia-DyIO_Gy0.js → julia-DMDw0aMP.js} +1 -1
  82. package/dist/client/assets/{just-VQTFAZJr.js → just-BryMKRJ_.js} +1 -1
  83. package/dist/client/assets/{kanban-definition-UN3LZRKU-BB2voi9I.js → kanban-definition-UN3LZRKU-j-aU6Yzp.js} +1 -1
  84. package/dist/client/assets/{latex-D0ONGfIo.js → latex-BNC9-sjr.js} +1 -1
  85. package/dist/client/assets/{line-BkbHeSM4.js → line-CyEqpEDp.js} +1 -1
  86. package/dist/client/assets/{linear-BYUYqOy1.js → linear-CXeaA9km.js} +1 -1
  87. package/dist/client/assets/{liquid-BnYpmfMl.js → liquid-D-fQw-oJ.js} +1 -1
  88. package/dist/client/assets/{lua-WlA-m2lC.js → lua-CD2aJ_rS.js} +1 -1
  89. package/dist/client/assets/{marko-Bd44681W.js → marko-BQ37hu6z.js} +1 -1
  90. package/dist/client/assets/{mdc-DhosSoGv.js → mdc-B5NUi3zZ.js} +1 -1
  91. package/dist/client/assets/{mermaid-parser.core-Dq0EOOjQ.js → mermaid-parser.core-BxHF_XbX.js} +1 -1
  92. package/dist/client/assets/{mermaid.core-qcl7ght-.js → mermaid.core-BUtaf4P1.js} +3 -3
  93. package/dist/client/assets/{mindmap-definition-RKZ34NQL-DrZ9di3F.js → mindmap-definition-RKZ34NQL-D13tPu2o.js} +1 -1
  94. package/dist/client/assets/{nginx-sYwNRk-F.js → nginx-1W1E03Rg.js} +1 -1
  95. package/dist/client/assets/{nim-DrXU2tn9.js → nim-C5A88bFP.js} +1 -1
  96. package/dist/client/assets/{perl-TEELuyd-.js → perl-BYoP0wNt.js} +1 -1
  97. package/dist/client/assets/{php-Dy_Y1r8j.js → php-CFxna09e.js} +1 -1
  98. package/dist/client/assets/{pieDiagram-4H26LBE5-Aev8QdJC.js → pieDiagram-4H26LBE5-CHL7Ml5w.js} +1 -1
  99. package/dist/client/assets/{pug-vancrY0a.js → pug-BR4JgWAk.js} +1 -1
  100. package/dist/client/assets/{qml-BA5z-zbf.js → qml-D1U1EFEe.js} +1 -1
  101. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-DhEDvxx7.js → quadrantDiagram-W4KKPZXB-BnYtv-HY.js} +1 -1
  102. package/dist/client/assets/{r-BR0O4N-s.js → r-ChE6X060.js} +1 -1
  103. package/dist/client/assets/{razor-DXiNCb-5.js → razor-B5iVfQMe.js} +1 -1
  104. package/dist/client/assets/{regexp-BhjJa9MI.js → regexp-Dz2drUIf.js} +1 -1
  105. package/dist/client/assets/{requirementDiagram-4Y6WPE33-DPS5siTy.js → requirementDiagram-4Y6WPE33-D8QEGLKB.js} +1 -1
  106. package/dist/client/assets/{review-export-DABY3y7y.js → review-export-CvFFbCAB.js} +1 -1
  107. package/dist/client/assets/{rst-Bi3zId1d.js → rst-DZM3RGRs.js} +1 -1
  108. package/dist/client/assets/{ruby-BURiJKuD.js → ruby-DkXnm-J8.js} +1 -1
  109. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-CjZF2AQd.js → sankeyDiagram-5OEKKPKP-wVaks6lB.js} +1 -1
  110. package/dist/client/assets/{sas-BO_W1LuL.js → sas-DMbpc71c.js} +1 -1
  111. package/dist/client/assets/{scss-BPUTinXb.js → scss-IwVMMp22.js} +1 -1
  112. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-0rsTmBLB.js → sequenceDiagram-3UESZ5HK-Cg31TB-R.js} +1 -1
  113. package/dist/client/assets/{shellscript-B4rfo4qg.js → shellscript-CIStAKx3.js} +1 -1
  114. package/dist/client/assets/{shellsession-DLX-O8m7.js → shellsession-uLtzHhlo.js} +1 -1
  115. package/dist/client/assets/{soy-BEUiwSVD.js → soy-BkxBYigm.js} +1 -1
  116. package/dist/client/assets/{sql-CwSA8v_H.js → sql-Ba9y9EvJ.js} +1 -1
  117. package/dist/client/assets/{src-Cn22-GjE.js → src-9MKexpFu.js} +1 -1
  118. package/dist/client/assets/{stata-XfdBiAnW.js → stata-CrPCbiIZ.js} +1 -1
  119. package/dist/client/assets/{stateDiagram-AJRCARHV-BVvCsgjA.js → stateDiagram-AJRCARHV-BbvNjVF6.js} +1 -1
  120. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-B5BbZkU5.js +1 -0
  121. package/dist/client/assets/{surrealql-COaehKkS.js → surrealql-Dvip5u_o.js} +1 -1
  122. package/dist/client/assets/{svelte-B0kTIq0J.js → svelte-xBLD60Zd.js} +1 -1
  123. package/dist/client/assets/{templ-DGxWfDf-.js → templ-CWv68xbP.js} +1 -1
  124. package/dist/client/assets/{tex-C9X3gjZ2.js → tex-CHqz-fYs.js} +1 -1
  125. package/dist/client/assets/{timeline-definition-PNZ67QCA-B535P7_e.js → timeline-definition-PNZ67QCA-CWKkJ3QZ.js} +1 -1
  126. package/dist/client/assets/{ts-tags-By9ETsTC.js → ts-tags-DS82EDuE.js} +1 -1
  127. package/dist/client/assets/{tsx-CvVX506n.js → tsx-B2GBjp2F.js} +1 -1
  128. package/dist/client/assets/{twig-CjLWv7eu.js → twig-CC8x9pG0.js} +1 -1
  129. package/dist/client/assets/{typescript-BEq3u3Bc.js → typescript-g7i13lnC.js} +1 -1
  130. package/dist/client/assets/{vennDiagram-CIIHVFJN-CTV9H1N2.js → vennDiagram-CIIHVFJN-OLItKWeg.js} +1 -1
  131. package/dist/client/assets/{vue-html-92z_Aqn0.js → vue-html-D9GQtYub.js} +1 -1
  132. package/dist/client/assets/{vue-DYazforU.js → vue-rFbe7luB.js} +1 -1
  133. package/dist/client/assets/{vue-vine-VY7raYSI.js → vue-vine-mrNtQtY5.js} +1 -1
  134. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-BzJaykc6.js → wardleyDiagram-YWT4CUSO-Dy1yfeb4.js} +1 -1
  135. package/dist/client/assets/{xml-hd7OQ798.js → xml-CHfMjyAM.js} +1 -1
  136. package/dist/client/assets/{xsl-MkuT76g1.js → xsl-BFkNSogJ.js} +1 -1
  137. package/dist/client/assets/{xychartDiagram-2RQKCTM6-D36D8smx.js → xychartDiagram-2RQKCTM6-DfSnfjVL.js} +1 -1
  138. package/dist/client/assets/{yaml-D6zRzOR2.js → yaml-BfSgHc04.js} +1 -1
  139. package/dist/client/index.html +2 -2
  140. package/dist/{completions-Csvfqctx.mjs → completions-BO2DUM-8.mjs} +23 -1
  141. package/dist/{doctor-B3Nrydc0.mjs → doctor-D0Y3kOY7.mjs} +5 -4
  142. package/dist/{find-tui-binary-CRpbYXLC.mjs → find-tui-binary-B71xiwWu.mjs} +33 -15
  143. package/dist/github-CErBub2y.mjs +2 -0
  144. package/dist/{github-Cf5blBty.mjs → github-DBsZ0h_l.mjs} +47 -5
  145. package/dist/github-attachments-plDeB0iE.mjs +351 -0
  146. package/dist/handoff-LAFtmnHf.mjs +11 -0
  147. package/dist/{mcp-DwnYjFS0.mjs → mcp-DtVrMR2S.mjs} +41 -22
  148. package/dist/native/tui-darwin-arm64/diffing-tui +0 -0
  149. package/dist/native/tui-darwin-x64/diffing-tui +0 -0
  150. package/dist/native/tui-linux-arm64-gnu/diffing-tui +0 -0
  151. package/dist/native/tui-linux-arm64-musl/diffing-tui +0 -0
  152. package/dist/native/tui-linux-x64-gnu/diffing-tui +0 -0
  153. package/dist/native/tui-linux-x64-musl/diffing-tui +0 -0
  154. package/dist/native/tui-win32-x64-msvc/diffing-tui.exe +0 -0
  155. package/dist/{review-export-DjxKBeIZ.mjs → review-export-B63eRAVa.mjs} +1 -1
  156. package/dist/{settings-bwe7yQ8U.mjs → search-BMPY_ZW8.mjs} +117 -97
  157. package/dist/search-BO9Im7eu.mjs +2 -0
  158. package/dist/{server-CZeMCHEa.mjs → server-j0PnTVX1.mjs} +174 -75
  159. package/dist/server-lock-CY0_1mcS.mjs +275 -0
  160. package/dist/session-conflict-C6V9ZKo7.mjs +96 -0
  161. package/dist/session-manager-CYNchhVA.mjs +159 -0
  162. package/dist/settings-yK0GYpbn.mjs +60 -0
  163. package/dist/tui-search-bridge-Bfb2ktdA.mjs +133 -0
  164. package/package.json +6 -4
  165. package/dist/client/assets/channel-BcJKoISM.js +0 -1
  166. package/dist/client/assets/chunk-QZHKN3VN-90aaHYyK.js +0 -1
  167. package/dist/client/assets/chunk-WU5MYG2G-CfaWSqjJ.js +0 -1
  168. package/dist/client/assets/classDiagram-4FO5ZUOK-CsDELGUF.js +0 -1
  169. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-R5SygY4h.js +0 -1
  170. package/dist/client/assets/index-B42c0TyV.css +0 -1
  171. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Bo6SeNmU.js +0 -1
  172. package/dist/github-DWgPIcKC.mjs +0 -2
  173. package/dist/server-lock-uqSdDYeR.mjs +0 -153
  174. /package/dist/{apply-suggestion-BAuyCYk6.mjs → apply-suggestion-CHYnw68I.mjs} +0 -0
  175. /package/dist/{comment-format-B5AcGcR-.mjs → comment-format-B85NHZnG.mjs} +0 -0
  176. /package/dist/{diff-fingerprint-CWa-soYD.mjs → diff-fingerprint-BgR0tu1o.mjs} +0 -0
  177. /package/dist/{plan-format-BQyx--mp.mjs → plan-format-BwJh5JSE.mjs} +0 -0
  178. /package/dist/{update-check-fe0tR8tx.mjs → update-check-Copcz9eb.mjs} +0 -0
@@ -0,0 +1,351 @@
1
+ import { d as getProjectStorageDir, w as toSafeRelativePath } from "./git-Czo96ymM.mjs";
2
+ import { _ as isGithubDotCom, g as githubApiBase, m as ghHostnameArgs, o as detectGhCli, s as execWithInput, x as readGithubToken } from "./github-DBsZ0h_l.mjs";
3
+ import { createHash } from "node:crypto";
4
+ import { extname, join } from "node:path";
5
+ import { readFile, stat } from "node:fs/promises";
6
+ //#region src/lib/github-attachments.ts
7
+ /**
8
+ * Publish-time rewrite of local `/api/attachments/…` markdown into
9
+ * repo-ACL-bound GitHub raw blob URLs (hidden orphan ref via Git Data API).
10
+ *
11
+ * Private repos / GHE: URLs inherit repository visibility. Never uses
12
+ * release assets (always public) or cookie-based user-attachments upload.
13
+ */
14
+ const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
15
+ const ALLOWED_EXT = new Set([
16
+ ".png",
17
+ ".jpg",
18
+ ".jpeg",
19
+ ".gif",
20
+ ".webp",
21
+ ".svg"
22
+ ]);
23
+ /** Markdown image/link targets that point at local diffing attachments. */
24
+ const LOCAL_ATTACHMENT_RE = /(?:!?\[[^\]]*\]\()(\/api\/attachments\/([^)\s]+))\)|(?:src=["'])(\/api\/attachments\/([^"'\s]+))["']/gi;
25
+ /** Public web origin for markdown image URLs (not the REST API host). */
26
+ function githubWebOrigin(host) {
27
+ if (isGithubDotCom(host)) return "https://github.com";
28
+ return (host.includes("://") ? host : `https://${host}`).replace(/\/$/, "");
29
+ }
30
+ /** Build a private-repo-safe raw content URL for a blob at a commit. */
31
+ function githubRawBlobUrl(resolved, commitSha, treePath) {
32
+ const origin = githubWebOrigin(resolved.host);
33
+ const segments = treePath.split("/").map((s) => encodeURIComponent(s)).join("/");
34
+ return `${origin}/${resolved.owner}/${resolved.repo}/raw/${commitSha}/${segments}`;
35
+ }
36
+ function attachmentRefName(pullNumber) {
37
+ return `refs/diffing/attachments/pr-${pullNumber}`;
38
+ }
39
+ /**
40
+ * Extract safe local attachment filenames from markdown. Rejects traversal,
41
+ * absolute paths, and empty names.
42
+ */
43
+ function extractLocalAttachmentRefs(markdown) {
44
+ const found = /* @__PURE__ */ new Map();
45
+ LOCAL_ATTACHMENT_RE.lastIndex = 0;
46
+ let match;
47
+ while ((match = LOCAL_ATTACHMENT_RE.exec(markdown)) !== null) {
48
+ const url = match[1] ?? match[3];
49
+ const filename = match[2] ?? match[4];
50
+ if (!url || !filename) continue;
51
+ const safe = sanitizeAttachmentFilename(filename);
52
+ if (!safe) continue;
53
+ found.set(url, {
54
+ url,
55
+ filename: safe
56
+ });
57
+ }
58
+ return [...found.values()];
59
+ }
60
+ /** Returns basename if safe; null if traversal / bad extension / weird chars. */
61
+ function sanitizeAttachmentFilename(filename) {
62
+ let decoded = filename;
63
+ try {
64
+ decoded = decodeURIComponent(filename);
65
+ } catch {}
66
+ if (!decoded || decoded.includes("\0") || decoded.includes("/") || decoded.includes("\\")) return null;
67
+ if (decoded.includes("..") || decoded.startsWith(".") || decoded !== decoded.trim()) return null;
68
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(decoded)) return null;
69
+ const ext = extname(decoded).toLowerCase();
70
+ if (!ALLOWED_EXT.has(ext)) return null;
71
+ return decoded;
72
+ }
73
+ function rewriteAttachmentUrls(markdown, urlMap) {
74
+ if (Object.keys(urlMap).length === 0) return markdown;
75
+ let out = markdown;
76
+ for (const [local, remote] of Object.entries(urlMap)) out = out.split(local).join(remote);
77
+ return out;
78
+ }
79
+ function attachmentsDir(custom) {
80
+ return custom ?? join(getProjectStorageDir(), "attachments");
81
+ }
82
+ async function loadAttachmentFile(dir, filename) {
83
+ const safeRel = toSafeRelativePath(filename, dir);
84
+ if (!safeRel || safeRel.includes("..") || safeRel.includes("/") || safeRel.includes("\\")) return {
85
+ bytes: Buffer.alloc(0),
86
+ error: `Refusing unsafe attachment path: ${filename}`
87
+ };
88
+ const absolute = join(dir, safeRel);
89
+ try {
90
+ const st = await stat(absolute);
91
+ if (!st.isFile()) return {
92
+ bytes: Buffer.alloc(0),
93
+ error: `Attachment is not a file: ${filename}`
94
+ };
95
+ if (st.size > 10485760) return {
96
+ bytes: Buffer.alloc(0),
97
+ error: `Attachment ${filename} exceeds ${MAX_ATTACHMENT_BYTES / (1024 * 1024)} MB limit`
98
+ };
99
+ return { bytes: await readFile(absolute) };
100
+ } catch {
101
+ return {
102
+ bytes: Buffer.alloc(0),
103
+ error: `Missing local attachment: ${filename}`
104
+ };
105
+ }
106
+ }
107
+ async function planAttachmentUploads(markdownBodies, opts) {
108
+ const refs = /* @__PURE__ */ new Map();
109
+ for (const body of markdownBodies) for (const ref of extractLocalAttachmentRefs(body)) refs.set(ref.url, ref);
110
+ if (refs.size === 0) return { plans: [] };
111
+ const dir = attachmentsDir(opts?.attachmentsDir);
112
+ const plans = [];
113
+ const hashToTreePath = /* @__PURE__ */ new Map();
114
+ for (const ref of refs.values()) {
115
+ const loaded = await loadAttachmentFile(dir, ref.filename);
116
+ if (loaded.error) return {
117
+ plans: [],
118
+ error: loaded.error
119
+ };
120
+ const contentHash = createHash("sha256").update(loaded.bytes).digest("hex");
121
+ let treePath = hashToTreePath.get(contentHash);
122
+ if (!treePath) {
123
+ const ext = extname(ref.filename).toLowerCase() || ".bin";
124
+ treePath = `${contentHash.slice(0, 16)}${ext}`;
125
+ hashToTreePath.set(contentHash, treePath);
126
+ }
127
+ plans.push({
128
+ filename: ref.filename,
129
+ localUrl: ref.url,
130
+ bytes: loaded.bytes.length,
131
+ contentHash,
132
+ treePath
133
+ });
134
+ }
135
+ return { plans };
136
+ }
137
+ function contentsWriteHint(message) {
138
+ const lower = message.toLowerCase();
139
+ if (lower.includes("resource not accessible") || lower.includes("not found") || lower.includes("403") || lower.includes("forbidden") || lower.includes("permission") || lower.includes("scopes")) return `${message} — Image upload needs contents:write (re-auth \`gh\` with the repo scope, or use a token with Contents: Write).`;
140
+ return message;
141
+ }
142
+ async function createGhApiCaller(resolved) {
143
+ const gh = await detectGhCli();
144
+ if (gh.available && gh.authenticated) {
145
+ const caller = async (method, path, body) => {
146
+ const args = [
147
+ "api",
148
+ ...ghHostnameArgs(resolved),
149
+ "--method",
150
+ method,
151
+ path,
152
+ "-H",
153
+ "Accept: application/vnd.github+json"
154
+ ];
155
+ try {
156
+ let stdout;
157
+ if (body !== void 0) {
158
+ args.push("--input", "-");
159
+ ({stdout} = await execWithInput("gh", args, JSON.stringify(body)));
160
+ } else ({stdout} = await execWithInput("gh", args, ""));
161
+ return {
162
+ ok: true,
163
+ status: 200,
164
+ json: stdout.trim() ? JSON.parse(stdout) : null,
165
+ text: stdout
166
+ };
167
+ } catch (err) {
168
+ const stdout = String(err?.stdout || "");
169
+ const stderr = String(err?.stderr || err?.message || "");
170
+ let json = null;
171
+ try {
172
+ json = stdout ? JSON.parse(stdout) : null;
173
+ } catch {}
174
+ return {
175
+ ok: false,
176
+ status: typeof err?.code === "number" ? err.code : 500,
177
+ json,
178
+ text: json?.message || stderr || stdout || "gh api failed"
179
+ };
180
+ }
181
+ };
182
+ return {
183
+ caller,
184
+ authSource: "gh"
185
+ };
186
+ }
187
+ const token = readGithubToken();
188
+ if (!token) return { error: "Cannot upload images: no `gh` CLI on $PATH (or not authenticated) and no $GITHUB_TOKEN. Run `gh auth login` or set $GITHUB_TOKEN." };
189
+ const base = githubApiBase(resolved.host);
190
+ const caller = async (method, path, body) => {
191
+ const res = await fetch(`${base}/${path.replace(/^\//, "")}`, {
192
+ method,
193
+ headers: {
194
+ Authorization: `Bearer ${token}`,
195
+ Accept: "application/vnd.github+json",
196
+ "X-GitHub-Api-Version": "2022-11-28",
197
+ "Content-Type": "application/json",
198
+ "User-Agent": "diffing-cli"
199
+ },
200
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
201
+ signal: AbortSignal.timeout(45e3)
202
+ });
203
+ const text = await res.text().catch(() => "");
204
+ let json = null;
205
+ try {
206
+ json = text ? JSON.parse(text) : null;
207
+ } catch {}
208
+ return {
209
+ ok: res.ok,
210
+ status: res.status,
211
+ json,
212
+ text: json?.message || text
213
+ };
214
+ };
215
+ return {
216
+ caller,
217
+ authSource: "token"
218
+ };
219
+ }
220
+ async function readFileBytes(dir, filename) {
221
+ return readFile(join(dir, filename));
222
+ }
223
+ /**
224
+ * Upload unique attachment blobs to `refs/diffing/attachments/pr-<N>` and
225
+ * rewrite every local `/api/attachments/…` URL in the given bodies.
226
+ *
227
+ * On `dryRun: true`, builds placeholder URLs (`…/raw/<pending>/…`) without
228
+ * calling GitHub.
229
+ */
230
+ async function rewriteLocalAttachmentsInBodies(resolved, bodies, opts) {
231
+ const dryRun = opts?.dryRun === true;
232
+ const planned = await planAttachmentUploads(bodies, { attachmentsDir: opts?.attachmentsDir });
233
+ if (planned.error) return {
234
+ bodies,
235
+ urlMap: {},
236
+ uploaded: 0,
237
+ dryRun,
238
+ error: planned.error
239
+ };
240
+ if (planned.plans.length === 0) return {
241
+ bodies,
242
+ urlMap: {},
243
+ uploaded: 0,
244
+ dryRun
245
+ };
246
+ if (dryRun) {
247
+ const urlMap = {};
248
+ for (const p of planned.plans) urlMap[p.localUrl] = githubRawBlobUrl(resolved, "<pending>", p.treePath);
249
+ return {
250
+ bodies: bodies.map((b) => rewriteAttachmentUrls(b, urlMap)),
251
+ urlMap,
252
+ uploaded: 0,
253
+ dryRun: true
254
+ };
255
+ }
256
+ const api = await createGhApiCaller(resolved);
257
+ if ("error" in api) return {
258
+ bodies,
259
+ urlMap: {},
260
+ uploaded: 0,
261
+ dryRun,
262
+ error: api.error
263
+ };
264
+ const { caller } = api;
265
+ const repoPath = `repos/${resolved.owner}/${resolved.repo}`;
266
+ const dir = attachmentsDir(opts?.attachmentsDir);
267
+ const hashToBlobSha = /* @__PURE__ */ new Map();
268
+ const uniqueByHash = /* @__PURE__ */ new Map();
269
+ for (const p of planned.plans) if (!uniqueByHash.has(p.contentHash)) uniqueByHash.set(p.contentHash, p);
270
+ for (const p of uniqueByHash.values()) {
271
+ const bytes = await readFileBytes(dir, p.filename);
272
+ const res = await caller("POST", `${repoPath}/git/blobs`, {
273
+ content: bytes.toString("base64"),
274
+ encoding: "base64"
275
+ });
276
+ if (!res.ok || !res.json?.sha) return {
277
+ bodies,
278
+ urlMap: {},
279
+ uploaded: 0,
280
+ dryRun,
281
+ error: contentsWriteHint(res.text || `Failed to create blob for ${p.filename}`)
282
+ };
283
+ hashToBlobSha.set(p.contentHash, res.json.sha);
284
+ }
285
+ const tree = [...uniqueByHash.values()].map((p) => ({
286
+ path: p.treePath,
287
+ mode: "100644",
288
+ type: "blob",
289
+ sha: hashToBlobSha.get(p.contentHash)
290
+ }));
291
+ const treeRes = await caller("POST", `${repoPath}/git/trees`, { tree });
292
+ if (!treeRes.ok || !treeRes.json?.sha) return {
293
+ bodies,
294
+ urlMap: {},
295
+ uploaded: 0,
296
+ dryRun,
297
+ error: contentsWriteHint(treeRes.text || "Failed to create git tree for attachments")
298
+ };
299
+ const refName = attachmentRefName(resolved.pullNumber);
300
+ const refSuffix = refName.replace(/^refs\//, "");
301
+ const existing = await caller("GET", `${repoPath}/git/ref/${refSuffix}`);
302
+ const parents = existing.ok && existing.json?.object?.sha ? [existing.json.object.sha] : [];
303
+ const commitRes = await caller("POST", `${repoPath}/git/commits`, {
304
+ message: `diffing: attach review images for PR #${resolved.pullNumber}`,
305
+ tree: treeRes.json.sha,
306
+ parents
307
+ });
308
+ if (!commitRes.ok || !commitRes.json?.sha) return {
309
+ bodies,
310
+ urlMap: {},
311
+ uploaded: 0,
312
+ dryRun,
313
+ error: contentsWriteHint(commitRes.text || "Failed to create attachment commit")
314
+ };
315
+ const commitSha = commitRes.json.sha;
316
+ if (parents.length === 0) {
317
+ const createRef = await caller("POST", `${repoPath}/git/refs`, {
318
+ ref: refName,
319
+ sha: commitSha
320
+ });
321
+ if (!createRef.ok) return {
322
+ bodies,
323
+ urlMap: {},
324
+ uploaded: 0,
325
+ dryRun,
326
+ error: contentsWriteHint(createRef.text || `Failed to create ${refName}`)
327
+ };
328
+ } else {
329
+ const updateRef = await caller("PATCH", `${repoPath}/git/refs/${refSuffix}`, {
330
+ sha: commitSha,
331
+ force: false
332
+ });
333
+ if (!updateRef.ok) return {
334
+ bodies,
335
+ urlMap: {},
336
+ uploaded: 0,
337
+ dryRun,
338
+ error: contentsWriteHint(updateRef.text || `Failed to update ${refName}`)
339
+ };
340
+ }
341
+ const urlMap = {};
342
+ for (const p of planned.plans) urlMap[p.localUrl] = githubRawBlobUrl(resolved, commitSha, p.treePath);
343
+ return {
344
+ bodies: bodies.map((b) => rewriteAttachmentUrls(b, urlMap)),
345
+ urlMap,
346
+ uploaded: uniqueByHash.size,
347
+ dryRun: false
348
+ };
349
+ }
350
+ //#endregion
351
+ export { rewriteLocalAttachmentsInBodies };
@@ -0,0 +1,11 @@
1
+ //#region src/lib/handoff.ts
2
+ const PLAN_SUBMIT_NEXT_ACTION = "Share the plan review URL with the human and end your turn (async handoff). Call await_plan_review only when the human is reviewing now or explicitly asked you to wait.";
3
+ const AWAIT_REVIEW_TIMEOUT_NEXT_ACTION = "Wait budget elapsed — expected, not a failure. Park: tell the human the review UI is waiting and end your turn. Call await_review again only if they asked you to keep waiting (at most once more unless they repeat that ask). Do not retry in a silent loop. When they say the review is ready, call await_review once (or list_comments with openOnly) to resume; a prior Send-to-agent handoff is replayed.";
4
+ const AWAIT_PLAN_TIMEOUT_NEXT_ACTION = "Wait budget elapsed — expected, not a failure. Park: tell the human the plan URL is waiting for Submit review and end your turn. Call await_plan_review again only if they asked you to keep waiting (at most once more unless they repeat that ask). Do not retry in a silent loop. When they say a verdict is ready, call await_plan_review once (or get_plan / list_plans) to resume; a prior verdict is replayed.";
5
+ const CLI_AWAIT_REVIEW_TIMEOUT_HINT = "Wait budget elapsed (expected). Prefer parking until the human is ready; re-run `diffing await-review` only if they asked you to keep waiting.";
6
+ const CLI_AWAIT_PLAN_TIMEOUT_HINT = "Wait budget elapsed (expected). Prefer parking until the human is ready; re-run `diffing plan await` only if they asked you to keep waiting.";
7
+ const CLI_PLAN_SUBMIT_PARK_HINT = "Async handoff: share the URL above. Use --wait / `diffing plan await` only when the human is reviewing now or asked you to block.";
8
+ /** Compact MCP / skill preamble for wait tools. */
9
+ const AWAIT_TOOL_DESCRIPTION_SUFFIX = "Timeout means the wait budget elapsed (expected). Park and end the turn unless the human asked you to keep waiting; never silent-loop. Progress notifications are sent between long polls.";
10
+ //#endregion
11
+ export { CLI_AWAIT_REVIEW_TIMEOUT_HINT as a, CLI_AWAIT_PLAN_TIMEOUT_HINT as i, AWAIT_REVIEW_TIMEOUT_NEXT_ACTION as n, CLI_PLAN_SUBMIT_PARK_HINT as o, AWAIT_TOOL_DESCRIPTION_SUFFIX as r, PLAN_SUBMIT_NEXT_ACTION as s, AWAIT_PLAN_TIMEOUT_NEXT_ACTION as t };
@@ -1,7 +1,8 @@
1
- import { i as buildGitDiffArgs, o as parseDiffOptions, t as startServer } from "./server-CZeMCHEa.mjs";
2
- import { t as formatComments } from "./comment-format-B5AcGcR-.mjs";
3
- import { n as formatPlanReview } from "./plan-format-BQyx--mp.mjs";
4
- import { a as removeServerLock, i as readServerLock, n as diffScopeKey, o as removeServerLockIfOwned, r as isLockAlive, s as writeServerLock, t as acquireServerStartupLease } from "./server-lock-uqSdDYeR.mjs";
1
+ import { i as buildGitDiffArgs, s as parseDiffOptions, t as startServer } from "./server-j0PnTVX1.mjs";
2
+ import { t as formatComments } from "./comment-format-B85NHZnG.mjs";
3
+ import { n as formatPlanReview } from "./plan-format-BwJh5JSE.mjs";
4
+ import { c as removeServerLockIfOwned, i as isLockAlive, p as writeServerLock, r as diffScopeKey, s as removeServerLock, t as acquireServerStartupLease, u as resolveActiveServerLock } from "./server-lock-CY0_1mcS.mjs";
5
+ import { n as AWAIT_REVIEW_TIMEOUT_NEXT_ACTION, r as AWAIT_TOOL_DESCRIPTION_SUFFIX, s as PLAN_SUBMIT_NEXT_ACTION, t as AWAIT_PLAN_TIMEOUT_NEXT_ACTION } from "./handoff-LAFtmnHf.mjs";
5
6
  import { execFileSync } from "node:child_process";
6
7
  import { randomUUID } from "node:crypto";
7
8
  import { fileURLToPath } from "node:url";
@@ -337,7 +338,7 @@ function createMcpServer(options) {
337
338
  const repoRoot = resolveMcpRepository(options.repoRoot, true);
338
339
  const startServerFn = options.startServerFn ?? startServer;
339
340
  const now = options.now ?? Date.now;
340
- const readLock = options.readLock ?? readServerLock;
341
+ const readLock = options.readLock ?? resolveActiveServerLock;
341
342
  const writeLock = options.writeLock ?? writeServerLock;
342
343
  const removeLock = options.removeLock ?? removeServerLock;
343
344
  const lockIsAlive = options.lockIsAlive ?? isLockAlive;
@@ -345,14 +346,17 @@ function createMcpServer(options) {
345
346
  const ownerId = options.ownerId ?? randomUUID();
346
347
  const defaultClientDir = existsSync(resolve(moduleDirectory, "client")) ? resolve(moduleDirectory, "client") : resolve(repoRoot, "dist/client");
347
348
  const clientDir = options.clientDir ?? defaultClientDir;
348
- const instructions = `diffing is bound to ${repoRoot} for this connection. First call review_session_status. If no web session is running, call start_review_session; it is safe to retry and never replaces a user session. For code review, prefer diff_summary then paged diff_files/diff_hunks/diff_slice over get_diff; create_comment as needed, then await_review for the human handoff. For a GitHub PR session, call gh_overview and bounded diff tools; use gh_list_threads/reviews for discussion, and mutate GitHub only with explicit user authorization. For plan review, call submit_plan then await_plan_review. A changes-requested verdict means revise and resubmit the same planId; rejected means stop; approved means proceed. Wait tools return released or timeout with a mode; timeout is not failure and the same wait tool may be called again. MCP connects only to the loopback diffing server; explicitly authorized GitHub tools may make the server call GitHub.`;
349
+ const instructions = `diffing is bound to ${repoRoot} for this connection. First call review_session_status. If no web session is running, call start_review_session; it is safe to retry and never replaces a user session. For code review, prefer diff_summary then paged diff_files/diff_hunks/diff_slice over get_diff; create_comment as needed. Handoff: default is async — share the UI URL and end your turn; use await_review only when the human is reviewing now or asked you to wait. For a GitHub PR session, call gh_overview and bounded diff tools; use gh_list_threads/reviews for discussion, and mutate GitHub only with explicit user authorization. For plan review, call submit_plan, share the plan URL, and park unless asked to wait; then await_plan_review when sync waiting. A changes-requested verdict means revise and resubmit the same planId; rejected means stop; approved means proceed. Wait tools return released or timeout with disposition/nextAction; timeout means park (do not silent-loop). MCP connects only to the loopback diffing server; explicitly authorized GitHub tools may make the server call GitHub.`;
349
350
  const server = new McpServer({
350
351
  name: "diffing",
351
352
  version: MCP_VERSION
352
353
  }, { instructions });
353
354
  let reviewCursor = null;
354
355
  let planCursor = null;
356
+ let selectedLock = null;
355
357
  function liveLock() {
358
+ if (selectedLock && lockIsAlive(selectedLock, repoRoot)) return selectedLock;
359
+ selectedLock = null;
356
360
  const lock = readLock(repoRoot);
357
361
  return lock && lockIsAlive(lock, repoRoot) ? lock : null;
358
362
  }
@@ -508,6 +512,7 @@ function createMcpServer(options) {
508
512
  if (!sameScope(existing, requestedScope, diffArgs)) throw new Error(`A live diffing web session already shows a different diff scope. Requested arguments: ${JSON.stringify(diffArgs)}. Use the existing session or end it manually; diffing will not replace it.`);
509
513
  const url = lockUrl(existing);
510
514
  if (!url) throw new Error("The active diffing web session has no safe loopback URL.");
515
+ selectedLock = existing;
511
516
  return {
512
517
  status: "reused",
513
518
  repository: repoRoot,
@@ -541,7 +546,8 @@ function createMcpServer(options) {
541
546
  scope: requestedScope,
542
547
  diffArgs: [...diffArgs],
543
548
  owner: "mcp",
544
- ownerId
549
+ ownerId,
550
+ sessionId: ownerId
545
551
  };
546
552
  try {
547
553
  writeLock(lock);
@@ -551,6 +557,7 @@ function createMcpServer(options) {
551
557
  const detail = error instanceof Error ? error.message : String(error);
552
558
  throw new Error(`The review server bound locally but its discovery lock could not be written: ${detail}. No MCP session was claimed; retry after fixing lock storage. The unadvertised loopback listener will close when this MCP process exits.`);
553
559
  }
560
+ selectedLock = lock;
554
561
  return {
555
562
  status: "started",
556
563
  repository: repoRoot,
@@ -915,10 +922,11 @@ function createMcpServer(options) {
915
922
  });
916
923
  server.registerTool("await_review", {
917
924
  title: "Wait for code review handoff",
918
- description: "Wait for the human to send a code-review round. Returns status=released or status=timeout and always includes mode. Timeout is expected and safe to retry; progress notifications are sent between long polls.",
919
- inputSchema: { timeoutSeconds: z.number().positive().max(3600).optional().describe("Total wait budget in seconds; defaults to 570.") },
925
+ description: "Sync wait for the human to click Send to agent. Prefer async handoff (share the UI URL and end the turn) unless they are reviewing now or asked you to wait. Returns status=released or status=timeout; timeout includes disposition=park. " + AWAIT_TOOL_DESCRIPTION_SUFFIX,
926
+ inputSchema: { timeoutSeconds: z.number().positive().max(3600).optional().describe(`Total wait budget in seconds; defaults to 570.`) },
920
927
  outputSchema: {
921
928
  status: z.enum(["released", "timeout"]),
929
+ disposition: z.enum(["park"]).optional(),
922
930
  mode: z.enum(["standard", "comment-only"]),
923
931
  round: z.number(),
924
932
  openCount: z.number().optional(),
@@ -978,9 +986,10 @@ function createMcpServer(options) {
978
986
  }
979
987
  const structured = {
980
988
  status: "timeout",
989
+ disposition: "park",
981
990
  mode: "standard",
982
991
  round: sinceRound,
983
- nextAction: "No review was sent within the wait budget. Call await_review again to keep waiting."
992
+ nextAction: AWAIT_REVIEW_TIMEOUT_NEXT_ACTION
984
993
  };
985
994
  return textResult(structured.nextAction, structured);
986
995
  });
@@ -1226,7 +1235,7 @@ function createMcpServer(options) {
1226
1235
  });
1227
1236
  server.registerTool("submit_plan", {
1228
1237
  title: "Submit or resubmit a plan",
1229
- description: "Submit Markdown for human plan review. To revise after changes-requested, pass the same planId so a new version is created.",
1238
+ description: "Submit Markdown for human plan review. Default handoff is async: return the URL and park. Call await_plan_review only when the human is reviewing now or asked you to wait. To revise after changes-requested, pass the same planId so a new version is created.",
1230
1239
  inputSchema: {
1231
1240
  title: z.string().optional().describe("Human-readable plan title."),
1232
1241
  body: z.string().min(1).describe("Complete Markdown plan body."),
@@ -1238,7 +1247,8 @@ function createMcpServer(options) {
1238
1247
  status: z.literal("submitted"),
1239
1248
  planId: z.string(),
1240
1249
  version: z.number(),
1241
- url: z.string()
1250
+ url: z.string(),
1251
+ nextAction: z.string()
1242
1252
  },
1243
1253
  annotations: MUTATING
1244
1254
  }, async ({ title, body, source, model, planId }) => {
@@ -1257,19 +1267,21 @@ function createMcpServer(options) {
1257
1267
  })
1258
1268
  });
1259
1269
  const url = `${base}/plan/${plan.id}`;
1260
- return textResult(`Submitted plan ${plan.id} (v${plan.version}) at ${url}. Call await_plan_review.`, {
1270
+ return textResult(`Submitted plan ${plan.id} (v${plan.version}) at ${url}. ${PLAN_SUBMIT_NEXT_ACTION}`, {
1261
1271
  status: "submitted",
1262
1272
  planId: plan.id,
1263
1273
  version: plan.version,
1264
- url
1274
+ url,
1275
+ nextAction: PLAN_SUBMIT_NEXT_ACTION
1265
1276
  });
1266
1277
  });
1267
1278
  server.registerTool("await_plan_review", {
1268
1279
  title: "Wait for plan review verdict",
1269
- description: "Wait for a plan verdict. Returns status=released or status=timeout and always includes mode. Timeout is expected and safe to retry; progress notifications are sent between long polls.",
1270
- inputSchema: { timeoutSeconds: z.number().positive().max(3600).optional().describe("Total wait budget in seconds; defaults to 570.") },
1280
+ description: "Sync wait for a plan verdict (Submit review). Prefer async handoff after submit_plan unless the human is reviewing now or asked you to wait. Returns status=released or status=timeout; timeout includes disposition=park. " + AWAIT_TOOL_DESCRIPTION_SUFFIX,
1281
+ inputSchema: { timeoutSeconds: z.number().positive().max(3600).optional().describe(`Total wait budget in seconds; defaults to 570.`) },
1271
1282
  outputSchema: {
1272
1283
  status: z.enum(["released", "timeout"]),
1284
+ disposition: z.enum(["park"]).optional(),
1273
1285
  mode: z.enum(["standard", "comment-only"]),
1274
1286
  round: z.number(),
1275
1287
  planId: z.string().optional(),
@@ -1335,9 +1347,10 @@ function createMcpServer(options) {
1335
1347
  }
1336
1348
  const structured = {
1337
1349
  status: "timeout",
1350
+ disposition: "park",
1338
1351
  mode: "standard",
1339
1352
  round: sinceRound,
1340
- nextAction: "No plan verdict arrived within the wait budget. Call await_plan_review again to keep waiting."
1353
+ nextAction: AWAIT_PLAN_TIMEOUT_NEXT_ACTION
1341
1354
  };
1342
1355
  return textResult(structured.nextAction, structured);
1343
1356
  });
@@ -1471,18 +1484,24 @@ function createMcpServer(options) {
1471
1484
 
1472
1485
  This MCP connection is immutably bound to \`${repoRoot}\`.
1473
1486
 
1487
+ ## Handoff (sync vs async)
1488
+ - **Async (default):** share the UI/plan URL and end the turn. Resume when the human says the review or verdict is ready.
1489
+ - **Sync:** call \`await_review\` / \`await_plan_review\` only when the human is reviewing now or asked you to wait.
1490
+ - \`status=timeout\` + \`disposition=park\` means the wait budget elapsed — park; do not silent-loop. At most one extra await if they asked you to keep waiting.
1491
+
1474
1492
  ## Code review
1475
1493
  1. Call \`review_session_status\`; call \`start_review_session\` if needed.
1476
1494
  2. Prefer \`diff_summary\` → paged \`diff_files\` → \`diff_hunks\` / bounded \`diff_slice\`; use \`get_diff\` only as an escape hatch.
1477
1495
  3. Use \`create_comment\` for actionable inline findings.
1478
- 4. Use \`await_review\` for a human handoff; a timeout is safe to retry.
1496
+ 4. Prefer async handoff; use \`await_review\` for sync waits. On resume, one \`await_review\` (or \`list_comments\`) replays a prior Send-to-agent.
1479
1497
  5. In \`comment-only\` mode, reply without editing files.
1480
1498
 
1481
1499
  ## Plan review
1482
1500
  1. Start or reuse a review session.
1483
- 2. Call \`submit_plan\`, then \`await_plan_review\`.
1484
- 3. On \`changes-requested\`, revise and resubmit with the same \`planId\`.
1485
- 4. On \`approved\`, proceed; on \`rejected\`, stop.
1501
+ 2. Call \`submit_plan\`, share the URL, and park unless asked to wait.
1502
+ 3. On resume or sync wait, call \`await_plan_review\` (or \`get_plan\` / \`list_plans\`).
1503
+ 4. On \`changes-requested\`, revise and resubmit with the same \`planId\`.
1504
+ 5. On \`approved\`, proceed; on \`rejected\`, stop.
1486
1505
 
1487
1506
  ## GitHub PR
1488
1507
  1. In \`gh-pr\` mode call \`gh_overview\`, then the bounded diff tools.
@@ -1510,7 +1529,7 @@ MCP connects only to the loopback diffing server and never terminates a user-own
1510
1529
  role: "user",
1511
1530
  content: {
1512
1531
  type: "text",
1513
- text: `Use diffing for plan review in ${repoRoot}. Start or reuse a review session, submit this plan, and await the verdict. On changes-requested revise and resubmit the same planId; on rejected stop; on approved proceed.
1532
+ text: `Use diffing for plan review in ${repoRoot}. Start or reuse a review session, submit this plan, share the plan URL, and park (async handoff) unless the human asked you to wait. When they are ready or asked you to block, call await_plan_review. On changes-requested revise and resubmit the same planId; on rejected stop; on approved proceed. Timeout means park — do not silent-loop.
1514
1533
 
1515
1534
  ` + plan
1516
1535
  }
@@ -1,4 +1,4 @@
1
- import { n as reviewDecisionSummary } from "./comment-format-B5AcGcR-.mjs";
1
+ import { n as reviewDecisionSummary } from "./comment-format-B85NHZnG.mjs";
2
2
  //#region src/lib/review-export.ts
3
3
  /**
4
4
  * Export a review as human-readable Markdown (clipboard / download / CLI).