diffing 0.10.3 → 0.10.5

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 (160) hide show
  1. package/dist/{cli-agent-RJ5s4n77.mjs → cli-agent-BJiATn11.mjs} +10 -8
  2. package/dist/{cli-gh-B8HybRWZ.mjs → cli-gh-_bkDCxhh.mjs} +1 -1
  3. package/dist/cli.mjs +214 -13
  4. package/dist/client/assets/{angular-html-BFXVRkhg.js → angular-html-CBcwKYDC.js} +1 -1
  5. package/dist/client/assets/{angular-ts-bUwtWqya.js → angular-ts-DRBYUaJA.js} +1 -1
  6. package/dist/client/assets/{apl-bnOQInIz.js → apl-BM0X8jeL.js} +1 -1
  7. package/dist/client/assets/{arc-CRYHn0oW.js → arc-DTkmmKl3.js} +1 -1
  8. package/dist/client/assets/{architectureDiagram-3BPJPVTR-BLK33h0-.js → architectureDiagram-3BPJPVTR-Cw75ay7q.js} +1 -1
  9. package/dist/client/assets/{astro-BpPieWCw.js → astro-DKTNkoRZ.js} +1 -1
  10. package/dist/client/assets/{blade-B804X9BP.js → blade-DaM7a9Zp.js} +1 -1
  11. package/dist/client/assets/{blockDiagram-GPEHLZMM-VZc4C656.js → blockDiagram-GPEHLZMM-DAzINMr_.js} +1 -1
  12. package/dist/client/assets/{c-Cyh0ngfN.js → c-B3C0Or4o.js} +1 -1
  13. package/dist/client/assets/{c4Diagram-AAUBKEIU-9t9UJlU3.js → c4Diagram-AAUBKEIU-C7SsS28g.js} +1 -1
  14. package/dist/client/assets/channel-lsVHTuYR.js +1 -0
  15. package/dist/client/assets/{chunk-2J33WTMH-LudE8z-b.js → chunk-2J33WTMH-DHOUGWx8.js} +1 -1
  16. package/dist/client/assets/{chunk-3OPIFGDE-BxaQxo50.js → chunk-3OPIFGDE-D8d2scLX.js} +1 -1
  17. package/dist/client/assets/{chunk-4BX2VUAB-BfHcXCJ6.js → chunk-4BX2VUAB-BSC-E5yg.js} +1 -1
  18. package/dist/client/assets/{chunk-55IACEB6-o0ADQlnq.js → chunk-55IACEB6-27pC5pKn.js} +1 -1
  19. package/dist/client/assets/{chunk-5ZQYHXKU-E1qBkOlk.js → chunk-5ZQYHXKU-BM60x82y.js} +1 -1
  20. package/dist/client/assets/{chunk-727SXJPM-BitjzJ_b.js → chunk-727SXJPM-CHCF96vE.js} +1 -1
  21. package/dist/client/assets/{chunk-AQP2D5EJ-B5pw4PfB.js → chunk-AQP2D5EJ-BoVsvmTa.js} +1 -1
  22. package/dist/client/assets/{chunk-BSJP7CBP-CoL841vB.js → chunk-BSJP7CBP-Iip4uSio.js} +1 -1
  23. package/dist/client/assets/{chunk-CSCIHK7Q-Dw4jsICo.js → chunk-CSCIHK7Q-DuIWPyay.js} +1 -1
  24. package/dist/client/assets/{chunk-FMBD7UC4-hpHFrNoE.js → chunk-FMBD7UC4-Du6rtd2k.js} +1 -1
  25. package/dist/client/assets/{chunk-KSCS5N6A-D67-Zs6D.js → chunk-KSCS5N6A-BBq105vI.js} +1 -1
  26. package/dist/client/assets/{chunk-L5ZTLDWV-Brr6wA9I.js → chunk-L5ZTLDWV-0DmB5Qp8.js} +1 -1
  27. package/dist/client/assets/{chunk-LZXEDZCA-e3dTHwrC.js → chunk-LZXEDZCA-CoRsjUfr.js} +2 -2
  28. package/dist/client/assets/{chunk-ND2GUHAM-Cj2MlAOf.js → chunk-ND2GUHAM-tX8cKEls.js} +1 -1
  29. package/dist/client/assets/{chunk-NZK2D7GU-CEyXOVMI.js → chunk-NZK2D7GU-BoJGgPic.js} +1 -1
  30. package/dist/client/assets/{chunk-O5CBEL6O-CymJeKCI.js → chunk-O5CBEL6O-mlwCWs97.js} +1 -1
  31. package/dist/client/assets/chunk-QZHKN3VN-AHUiTM0z.js +1 -0
  32. package/dist/client/assets/chunk-WU5MYG2G-D41j4yxb.js +1 -0
  33. package/dist/client/assets/{chunk-XPW4576I-DfoZU8hD.js → chunk-XPW4576I-B5efww8M.js} +1 -1
  34. package/dist/client/assets/classDiagram-4FO5ZUOK-D1Eo3kly.js +1 -0
  35. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-bAMsbsfB.js +1 -0
  36. package/dist/client/assets/{cobol-D0BZ7BzH.js → cobol-De9QR_Td.js} +1 -1
  37. package/dist/client/assets/{coffee-BAPNIyTD.js → coffee-Ct_GvuHP.js} +1 -1
  38. package/dist/client/assets/{cose-bilkent-S5V4N54A-wCBDLLgv.js → cose-bilkent-S5V4N54A-ATaJZGF7.js} +1 -1
  39. package/dist/client/assets/{cpp-D54qUqoX.js → cpp-Dw52w9cj.js} +1 -1
  40. package/dist/client/assets/{crystal-BvYuHO8u.js → crystal-Dx8ADdsd.js} +1 -1
  41. package/dist/client/assets/{css-BwYPZ1qE.js → css-CZ13XvDU.js} +1 -1
  42. package/dist/client/assets/{dagre-BM42HDAG-DSO2rfSO.js → dagre-BM42HDAG-aDltuYut.js} +1 -1
  43. package/dist/client/assets/{diagram-2AECGRRQ-OO89Fatz.js → diagram-2AECGRRQ-DGWnHVMJ.js} +1 -1
  44. package/dist/client/assets/{diagram-5GNKFQAL-CARudkst.js → diagram-5GNKFQAL-Dd7eZBKe.js} +1 -1
  45. package/dist/client/assets/{diagram-KO2AKTUF-MMqM8Xip.js → diagram-KO2AKTUF-DWThXREf.js} +1 -1
  46. package/dist/client/assets/{diagram-LMA3HP47-CA7_Ug5b.js → diagram-LMA3HP47-CXR4Bjtc.js} +1 -1
  47. package/dist/client/assets/{diagram-OG6HWLK6-BAb7zakI.js → diagram-OG6HWLK6-D4Cbf0nW.js} +1 -1
  48. package/dist/client/assets/{dist-CP7b6OkU.js → dist-CUDhrHNw.js} +1 -1
  49. package/dist/client/assets/{edge-BbeCKm9i.js → edge-CQCjdJKj.js} +1 -1
  50. package/dist/client/assets/{elixir-C2M5DZQF.js → elixir-bkjwPazA.js} +1 -1
  51. package/dist/client/assets/{elm-CwqP8u0T.js → elm-CCweX5Cq.js} +1 -1
  52. package/dist/client/assets/{erDiagram-TEJ5UH35-Jy0Sqs_1.js → erDiagram-TEJ5UH35-BmAyt7SU.js} +1 -1
  53. package/dist/client/assets/{erb-C3C66A5u.js → erb-D-Z-KBLp.js} +1 -1
  54. package/dist/client/assets/{flowDiagram-I6XJVG4X-D9L8qr8V.js → flowDiagram-I6XJVG4X-C6nIFUVc.js} +1 -1
  55. package/dist/client/assets/{ganttDiagram-6RSMTGT7-CGaqGarh.js → ganttDiagram-6RSMTGT7-DMlxThBf.js} +1 -1
  56. package/dist/client/assets/{git-rebase-BQxiUc18.js → git-rebase-BdoFdenB.js} +1 -1
  57. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-RAteUysV.js → gitGraphDiagram-PVQCEYII-JaIFkAIT.js} +1 -1
  58. package/dist/client/assets/{glimmer-js-CSiwvg27.js → glimmer-js-CZQrBJKq.js} +1 -1
  59. package/dist/client/assets/{glimmer-ts-Dpvu35Rk.js → glimmer-ts-CBYW46D4.js} +1 -1
  60. package/dist/client/assets/{glsl-DusmoVD0.js → glsl-wxixKkY_.js} +1 -1
  61. package/dist/client/assets/{graphql-B4CCj0BK.js → graphql-CqZ0Lo5a.js} +1 -1
  62. package/dist/client/assets/{hack-BNgw8w6u.js → hack-rqWymQRV.js} +1 -1
  63. package/dist/client/assets/{haml-BhHHy_kW.js → haml-DFw_IoGM.js} +1 -1
  64. package/dist/client/assets/{handlebars-BBUsDvYG.js → handlebars-3VmTVMYt.js} +1 -1
  65. package/dist/client/assets/{html-DMohn6Yq.js → html-C0Li76jB.js} +1 -1
  66. package/dist/client/assets/{html-derivative-BY6zEZAg.js → html-derivative-CO-RQW8y.js} +1 -1
  67. package/dist/client/assets/{http-BHAVGGfJ.js → http-sPrZOeBY.js} +1 -1
  68. package/dist/client/assets/{hurl-B2q8TNaP.js → hurl-DUoPe4Lc.js} +1 -1
  69. package/dist/client/assets/{index-CguUPVUR.css → index-B42c0TyV.css} +1 -1
  70. package/dist/client/assets/{index-CxN77Qol.js → index-BN_-di_r.js} +31 -31
  71. package/dist/client/assets/{infoDiagram-5YYISTIA-DCyKTiIZ.js → infoDiagram-5YYISTIA-B6AZ5SCV.js} +1 -1
  72. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-BMTEXPAX.js → ishikawaDiagram-YF4QCWOH-CytMrpo8.js} +1 -1
  73. package/dist/client/assets/{java-DHj6gaNP.js → java-Cj0G6QRg.js} +1 -1
  74. package/dist/client/assets/{javascript-CbF7u-DE.js → javascript-d5SUuyWD.js} +1 -1
  75. package/dist/client/assets/{jinja-MOxap5pr.js → jinja-BTw9k0vO.js} +1 -1
  76. package/dist/client/assets/{jison-XUjMP-LK.js → jison-9xSVvR1q.js} +1 -1
  77. package/dist/client/assets/{journeyDiagram-JHISSGLW-Ckd41c1S.js → journeyDiagram-JHISSGLW-Bd4jZUYI.js} +1 -1
  78. package/dist/client/assets/{json-7vp-fS_h.js → json-DjAt53ka.js} +1 -1
  79. package/dist/client/assets/{jsx-BgiCPhz9.js → jsx-BX5KmQ7d.js} +1 -1
  80. package/dist/client/assets/{julia-DADvZSF3.js → julia-CLLx1dHC.js} +1 -1
  81. package/dist/client/assets/{just-BS3xPvUT.js → just-C0v80bKe.js} +1 -1
  82. package/dist/client/assets/{kanban-definition-UN3LZRKU-BlLY2sqY.js → kanban-definition-UN3LZRKU-Ds4pXHJa.js} +1 -1
  83. package/dist/client/assets/{latex-C_QvolBN.js → latex-DY-oQfwO.js} +1 -1
  84. package/dist/client/assets/{line-Bkj6DIfl.js → line-BbSLfOl6.js} +1 -1
  85. package/dist/client/assets/{linear-Cm6J5YrP.js → linear-BMIZJAAR.js} +1 -1
  86. package/dist/client/assets/{liquid-CL0UH-ot.js → liquid-DDHDgbZx.js} +1 -1
  87. package/dist/client/assets/{lua-DlAyxrVn.js → lua-CXymerKS.js} +1 -1
  88. package/dist/client/assets/{marko-6e-PRCyy.js → marko-DoPFsptl.js} +1 -1
  89. package/dist/client/assets/{mdc-BbLfT_IA.js → mdc-BxSmTphd.js} +1 -1
  90. package/dist/client/assets/{mermaid-parser.core-DwyEEn2f.js → mermaid-parser.core-CcYSvN4q.js} +1 -1
  91. package/dist/client/assets/{mermaid.core-K3JZIfb0.js → mermaid.core-COTNtqgQ.js} +3 -3
  92. package/dist/client/assets/{mindmap-definition-RKZ34NQL-B2pkZuEx.js → mindmap-definition-RKZ34NQL-BTxraRzn.js} +1 -1
  93. package/dist/client/assets/{nginx-CoBWheEf.js → nginx-Bma0jNOT.js} +1 -1
  94. package/dist/client/assets/{nim-2_Wea-mV.js → nim-DMBpjygU.js} +1 -1
  95. package/dist/client/assets/{perl-SkTVK0CC.js → perl-CrnrV-AJ.js} +1 -1
  96. package/dist/client/assets/{php-C2cF2oJ6.js → php-CDYxzX6t.js} +1 -1
  97. package/dist/client/assets/{pieDiagram-4H26LBE5-BQeH8sHJ.js → pieDiagram-4H26LBE5-DglRmzer.js} +1 -1
  98. package/dist/client/assets/{pug-C01v9yeE.js → pug-DNewy3GU.js} +1 -1
  99. package/dist/client/assets/{qml-CCP9ULJs.js → qml-COjcnZ-W.js} +1 -1
  100. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-D9CC7zgZ.js → quadrantDiagram-W4KKPZXB-BjSmyES7.js} +1 -1
  101. package/dist/client/assets/{r-DDJJP6PS.js → r-CrCaMxLS.js} +1 -1
  102. package/dist/client/assets/{razor-DKBWfXRk.js → razor-jeuOZIar.js} +1 -1
  103. package/dist/client/assets/{regexp-DOe8oQhc.js → regexp-DSHVUX08.js} +1 -1
  104. package/dist/client/assets/{requirementDiagram-4Y6WPE33-DlYO_tzx.js → requirementDiagram-4Y6WPE33-B4f430jS.js} +1 -1
  105. package/dist/client/assets/{review-export-CgROw3iz.js → review-export-Cen1WEcX.js} +1 -1
  106. package/dist/client/assets/{rst-KHKSB4M-.js → rst-CIDgytzc.js} +1 -1
  107. package/dist/client/assets/{ruby-3gO1sivg.js → ruby-DtuseoNq.js} +1 -1
  108. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DvgoVn_C.js → sankeyDiagram-5OEKKPKP-TQPPlxc6.js} +1 -1
  109. package/dist/client/assets/{sas-D1u2TkIr.js → sas-C72Theia.js} +1 -1
  110. package/dist/client/assets/{scss-C__x0Kwu.js → scss-ClECtwRk.js} +1 -1
  111. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-Dk46IxzZ.js → sequenceDiagram-3UESZ5HK-D1tx07Wg.js} +1 -1
  112. package/dist/client/assets/{shellscript-YrQZ5Lwf.js → shellscript-BCwBsi2k.js} +1 -1
  113. package/dist/client/assets/{shellsession-CoF5Zc6A.js → shellsession-BrOddEl5.js} +1 -1
  114. package/dist/client/assets/{soy-BKqc8OYl.js → soy-Dcs0GWFW.js} +1 -1
  115. package/dist/client/assets/{sql-D3MVYjA_.js → sql-BAjA7Gv7.js} +1 -1
  116. package/dist/client/assets/{src-UnI8qjTK.js → src-CYYhkqTK.js} +1 -1
  117. package/dist/client/assets/{stata-BqvSK3te.js → stata-19ngQtzR.js} +1 -1
  118. package/dist/client/assets/{stateDiagram-AJRCARHV-BuhZPSPC.js → stateDiagram-AJRCARHV-BZRCEzb5.js} +1 -1
  119. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Da-mGMA2.js +1 -0
  120. package/dist/client/assets/{surrealql-BhWLIqpd.js → surrealql-Ce2TBRjE.js} +1 -1
  121. package/dist/client/assets/{svelte-DNNWxYsj.js → svelte-DQ_1eGZz.js} +1 -1
  122. package/dist/client/assets/{templ-DdMgSlsy.js → templ-Cw2DDxWF.js} +1 -1
  123. package/dist/client/assets/{tex-B4JFnTVn.js → tex-myDSO2eP.js} +1 -1
  124. package/dist/client/assets/{timeline-definition-PNZ67QCA-C1xzVz0K.js → timeline-definition-PNZ67QCA-CP0OZhBT.js} +1 -1
  125. package/dist/client/assets/{ts-tags-BQI5G_ZS.js → ts-tags-rh39ZwVs.js} +1 -1
  126. package/dist/client/assets/{tsx-C7qfZi8B.js → tsx-B0-26jrU.js} +1 -1
  127. package/dist/client/assets/{twig-RWfC7gXX.js → twig-CgasHzPb.js} +1 -1
  128. package/dist/client/assets/{typescript-aLdMd7mo.js → typescript-Dq3T0c-m.js} +1 -1
  129. package/dist/client/assets/{vennDiagram-CIIHVFJN-CBkCNH_s.js → vennDiagram-CIIHVFJN-DHaKHj0j.js} +1 -1
  130. package/dist/client/assets/{vue-BHCbeDIL.js → vue-CzFVh92-.js} +1 -1
  131. package/dist/client/assets/{vue-html-DXTiTIOp.js → vue-html-Bv2uFkTH.js} +1 -1
  132. package/dist/client/assets/{vue-vine-CuhsWKNe.js → vue-vine-DzieBk9Y.js} +1 -1
  133. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-aFrlILUG.js → wardleyDiagram-YWT4CUSO-Dky7jxlZ.js} +1 -1
  134. package/dist/client/assets/{xml-Dnahor-u.js → xml-wcdHRHN8.js} +1 -1
  135. package/dist/client/assets/{xsl-DB6zLGtG.js → xsl-CXrwsLG9.js} +1 -1
  136. package/dist/client/assets/{xychartDiagram-2RQKCTM6-CpPDrWcg.js → xychartDiagram-2RQKCTM6-BQ2hG60h.js} +1 -1
  137. package/dist/client/assets/{yaml-VItgjNOZ.js → yaml-lO6lK6Xe.js} +1 -1
  138. package/dist/client/index.html +3 -3
  139. package/dist/{completions-Csvfqctx.mjs → completions-3UF8uzhx.mjs} +2 -0
  140. package/dist/{doctor-DGZoOg8f.mjs → doctor-BGYZNIYP.mjs} +3 -3
  141. package/dist/{github-Cf5blBty.mjs → github-CEjtGd41.mjs} +47 -5
  142. package/dist/github-DKT0lJXv.mjs +2 -0
  143. package/dist/github-attachments-Cf17YWp4.mjs +351 -0
  144. package/dist/handoff-DPoIyeWn.mjs +11 -0
  145. package/dist/{mcp-DOqu6S64.mjs → mcp-Dq7J5QOP.mjs} +31 -18
  146. package/dist/{server-Bjzyb6NG.mjs → server-C9i4V36o.mjs} +96 -16
  147. package/dist/{server-lock-uqSdDYeR.mjs → server-lock-Dd_ss_Il.mjs} +1 -1
  148. package/dist/{settings-BVnzGFL7.mjs → settings-bwe7yQ8U.mjs} +1 -1
  149. package/package.json +1 -1
  150. package/dist/client/assets/channel-CdyG8Wtt.js +0 -1
  151. package/dist/client/assets/chunk-QZHKN3VN-DycW3jgO.js +0 -1
  152. package/dist/client/assets/chunk-WU5MYG2G-BXFsAzYU.js +0 -1
  153. package/dist/client/assets/classDiagram-4FO5ZUOK-BxVI8nIr.js +0 -1
  154. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-DZHTBTt9.js +0 -1
  155. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CyVz0INc.js +0 -1
  156. package/dist/github-DWgPIcKC.mjs +0 -2
  157. /package/dist/{apply-suggestion-BAuyCYk6.mjs → apply-suggestion-DNNsC5gx.mjs} +0 -0
  158. /package/dist/{diff-fingerprint-CWa-soYD.mjs → diff-fingerprint-C8YO3TTE.mjs} +0 -0
  159. /package/dist/{review-export-DjxKBeIZ.mjs → review-export-BBDWvftT.mjs} +0 -0
  160. /package/dist/{update-check-fe0tR8tx.mjs → update-check-DUuHmPeZ.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-CEjtGd41.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-Bjzyb6NG.mjs";
1
+ import { i as buildGitDiffArgs, o as parseDiffOptions, t as startServer } from "./server-C9i4V36o.mjs";
2
2
  import { t as formatComments } from "./comment-format-B5AcGcR-.mjs";
3
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";
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-Dd_ss_Il.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-DPoIyeWn.mjs";
5
6
  import { execFileSync } from "node:child_process";
6
7
  import { randomUUID } from "node:crypto";
7
8
  import { fileURLToPath } from "node:url";
@@ -345,7 +346,7 @@ 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
@@ -915,10 +916,11 @@ function createMcpServer(options) {
915
916
  });
916
917
  server.registerTool("await_review", {
917
918
  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.") },
919
+ 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,
920
+ inputSchema: { timeoutSeconds: z.number().positive().max(3600).optional().describe(`Total wait budget in seconds; defaults to 570.`) },
920
921
  outputSchema: {
921
922
  status: z.enum(["released", "timeout"]),
923
+ disposition: z.enum(["park"]).optional(),
922
924
  mode: z.enum(["standard", "comment-only"]),
923
925
  round: z.number(),
924
926
  openCount: z.number().optional(),
@@ -978,9 +980,10 @@ function createMcpServer(options) {
978
980
  }
979
981
  const structured = {
980
982
  status: "timeout",
983
+ disposition: "park",
981
984
  mode: "standard",
982
985
  round: sinceRound,
983
- nextAction: "No review was sent within the wait budget. Call await_review again to keep waiting."
986
+ nextAction: AWAIT_REVIEW_TIMEOUT_NEXT_ACTION
984
987
  };
985
988
  return textResult(structured.nextAction, structured);
986
989
  });
@@ -1226,7 +1229,7 @@ function createMcpServer(options) {
1226
1229
  });
1227
1230
  server.registerTool("submit_plan", {
1228
1231
  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.",
1232
+ 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
1233
  inputSchema: {
1231
1234
  title: z.string().optional().describe("Human-readable plan title."),
1232
1235
  body: z.string().min(1).describe("Complete Markdown plan body."),
@@ -1238,7 +1241,8 @@ function createMcpServer(options) {
1238
1241
  status: z.literal("submitted"),
1239
1242
  planId: z.string(),
1240
1243
  version: z.number(),
1241
- url: z.string()
1244
+ url: z.string(),
1245
+ nextAction: z.string()
1242
1246
  },
1243
1247
  annotations: MUTATING
1244
1248
  }, async ({ title, body, source, model, planId }) => {
@@ -1257,19 +1261,21 @@ function createMcpServer(options) {
1257
1261
  })
1258
1262
  });
1259
1263
  const url = `${base}/plan/${plan.id}`;
1260
- return textResult(`Submitted plan ${plan.id} (v${plan.version}) at ${url}. Call await_plan_review.`, {
1264
+ return textResult(`Submitted plan ${plan.id} (v${plan.version}) at ${url}. ${PLAN_SUBMIT_NEXT_ACTION}`, {
1261
1265
  status: "submitted",
1262
1266
  planId: plan.id,
1263
1267
  version: plan.version,
1264
- url
1268
+ url,
1269
+ nextAction: PLAN_SUBMIT_NEXT_ACTION
1265
1270
  });
1266
1271
  });
1267
1272
  server.registerTool("await_plan_review", {
1268
1273
  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.") },
1274
+ 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,
1275
+ inputSchema: { timeoutSeconds: z.number().positive().max(3600).optional().describe(`Total wait budget in seconds; defaults to 570.`) },
1271
1276
  outputSchema: {
1272
1277
  status: z.enum(["released", "timeout"]),
1278
+ disposition: z.enum(["park"]).optional(),
1273
1279
  mode: z.enum(["standard", "comment-only"]),
1274
1280
  round: z.number(),
1275
1281
  planId: z.string().optional(),
@@ -1335,9 +1341,10 @@ function createMcpServer(options) {
1335
1341
  }
1336
1342
  const structured = {
1337
1343
  status: "timeout",
1344
+ disposition: "park",
1338
1345
  mode: "standard",
1339
1346
  round: sinceRound,
1340
- nextAction: "No plan verdict arrived within the wait budget. Call await_plan_review again to keep waiting."
1347
+ nextAction: AWAIT_PLAN_TIMEOUT_NEXT_ACTION
1341
1348
  };
1342
1349
  return textResult(structured.nextAction, structured);
1343
1350
  });
@@ -1471,18 +1478,24 @@ function createMcpServer(options) {
1471
1478
 
1472
1479
  This MCP connection is immutably bound to \`${repoRoot}\`.
1473
1480
 
1481
+ ## Handoff (sync vs async)
1482
+ - **Async (default):** share the UI/plan URL and end the turn. Resume when the human says the review or verdict is ready.
1483
+ - **Sync:** call \`await_review\` / \`await_plan_review\` only when the human is reviewing now or asked you to wait.
1484
+ - \`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.
1485
+
1474
1486
  ## Code review
1475
1487
  1. Call \`review_session_status\`; call \`start_review_session\` if needed.
1476
1488
  2. Prefer \`diff_summary\` → paged \`diff_files\` → \`diff_hunks\` / bounded \`diff_slice\`; use \`get_diff\` only as an escape hatch.
1477
1489
  3. Use \`create_comment\` for actionable inline findings.
1478
- 4. Use \`await_review\` for a human handoff; a timeout is safe to retry.
1490
+ 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
1491
  5. In \`comment-only\` mode, reply without editing files.
1480
1492
 
1481
1493
  ## Plan review
1482
1494
  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.
1495
+ 2. Call \`submit_plan\`, share the URL, and park unless asked to wait.
1496
+ 3. On resume or sync wait, call \`await_plan_review\` (or \`get_plan\` / \`list_plans\`).
1497
+ 4. On \`changes-requested\`, revise and resubmit with the same \`planId\`.
1498
+ 5. On \`approved\`, proceed; on \`rejected\`, stop.
1486
1499
 
1487
1500
  ## GitHub PR
1488
1501
  1. In \`gh-pr\` mode call \`gh_overview\`, then the bounded diff tools.
@@ -1510,7 +1523,7 @@ MCP connects only to the loopback diffing server and never terminates a user-own
1510
1523
  role: "user",
1511
1524
  content: {
1512
1525
  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.
1526
+ 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
1527
 
1515
1528
  ` + plan
1516
1529
  }