chainlesschain 0.162.163 → 0.162.164

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 (213) hide show
  1. package/package.json +3 -1
  2. package/src/assets/web-panel/assets/{AIOps-BQ0kgfA1.js → AIOps-BqRkaHGb.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-Dr1MDANe.js → ActionButton-DWO7OZLC.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-CZ5vsaXW.js → Analytics-C5oaVezM.js} +2 -2
  5. package/src/assets/web-panel/assets/{AppLayout-BIBga881.js → AppLayout-CsUESDoZ.js} +3 -3
  6. package/src/assets/web-panel/assets/{Artifacts-vpXHRU3u.js → Artifacts-Dragqu5k.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-B8exuoub.js → Audit-CQsaHctY.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-923evRRN.js → BackgroundAgents-DdG1jNSM.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-sr_sswlQ.js → Backup-DN6sjK4z.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-CnW-4XKT.js → BaseInput-Cxm_ZTCl.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-FZCsTZIL.js → Chat-DVXGmA56.js} +5 -5
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-DDPd9qT0.js → ChatBubbleRenderer-BdaCL_iE.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-DFrxB59H.js → Checkbox-DEc4vD_A.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-_b6WEel8.js → Codegen-CKiTcv36.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-CkbnnZ-K.js → Col-Br9UUNN0.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-BxM_82zW.js → Community-NeUH_70T.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-DYinP45R.js → Compact-BqR-z2Df.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-DyXp3sSP.js → Compliance-DcWB_SgZ.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-BEr2DhW7.js → Cowork-BfhgcudF.js} +2 -2
  20. package/src/assets/web-panel/assets/{Cron-B-Vwo1hN.js → Cron-LaU29LgD.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-DbLxKddd.js → Crosschain-B2ZB9uwW.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-BsQ_LFw9.js → DID-BuHl38A2.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-lVlXQHu5.js → Dashboard-m627yqvZ.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-Beo_foO7.js → Dropdown-C7bxF8F5.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-DszHIXED.js → EmailListRenderer-DPLPQzLp.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-DcAwoh81.js → FamilyGuardDashboard-cOiH6ike.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-CuJLa2eb.js → Federation-Bvkzg88z.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-Cxd1lxhc.js → FormItemContext-BTsiA7h_.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-BBvq5Jsw.js → GenericCardRenderer-BGnVE5qu.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-WqPKetx6.js → Git-DS_j6r25.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-CWYtY3ke.js → Governance-CKRG3Rsl.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-BnX1iiNV.js → Inference-DW8INoNQ.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-BJuM8GN5.js → KnowledgeGraph-BhEYeFhC.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-DqLMNntm.js → Logs-wqi0uxuR.js} +2 -2
  35. package/src/assets/web-panel/assets/{MarkdownRenderer-BqYwEYou.js → MarkdownRenderer-DS8dE8ty.js} +1 -1
  36. package/src/assets/web-panel/assets/{Marketplace-_vGfmhNl.js → Marketplace-CMlTuPLT.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-BeJDOkDj.js → McpTools-CSx4m5JJ.js} +3 -3
  38. package/src/assets/web-panel/assets/{Memory-Da0EEZDO.js → Memory-vnQCUy-_.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-DSuzcCCy.js → MobileBridge-BV2NA742.js} +1 -1
  40. package/src/assets/web-panel/assets/MobileProjects-Bw0k8e1B.js +1 -0
  41. package/src/assets/web-panel/assets/{Mtc-BE9YgdA0.js → Mtc-CzK_FPB1.js} +2 -2
  42. package/src/assets/web-panel/assets/{MtcAudit-fC2BpO5S.js → MtcAudit-Cb7G9oL3.js} +2 -2
  43. package/src/assets/web-panel/assets/{Multisig-D24Xxqw3.js → Multisig-qJxlUyzr.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-Bs3oGvS8.js → NLProgramming-DqaBJ7Te.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-BFjmAA-3.js → Notes-CwhTJkRi.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-BX_9Bhv0.js → NotificationSettings-YDs4Fal4.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-wjkZwJXI.js → OrderTableRenderer-CvEALpq7.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-DBcdsVEP.js → Organization-CEmQm9n8.js} +2 -2
  49. package/src/assets/web-panel/assets/{Overflow-DDyaF9vC.js → Overflow-zby_P_LA.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-BB78L4he.js → P2P-DsmDNFNT.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-DqTiZFum.js → PdhVaultBrowser-FWwFlZGF.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-INbnWv4j.js → Permissions-DuzQ6me5.js} +4 -4
  53. package/src/assets/web-panel/assets/{PersonalDataHub-BST-VmTq.js → PersonalDataHub-DZbeN613.js} +2 -2
  54. package/src/assets/web-panel/assets/{Pipeline-CE7_tS2k.js → Pipeline-CoNSVAUS.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-hK5L8dOB.js → Privacy-OYJEAaMC.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-BvkkUoDn.js → ProjectInit-CWIapV--.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-u7_EUtkm.js → ProjectSettings-CX8xMxZn.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-5Fmt_8Nw.js → Projects-BLCN8YyY.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-B9eanarR.js → Providers-BV_sxp75.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-C0dX5N2f.js → QrScannerModal-BTnRrHRp.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-CKTpG-PZ.js → QuickAsk-D1KZg018.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-Px_w_XQM.js → Recommend-COm5cuNg.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-Di0fnYkM.js → RemoteSession-0saWEkYI.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-vesnWjXt.js → Reputation-Dr7iKLS2.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-exbY6LkT.js → Row-C1vpPBcr.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-B3O3x70t.js → RssFeed-CxlWrtNj.js} +2 -2
  67. package/src/assets/web-panel/assets/{Search-uRA0DhNz.js → Search-BT2V8dPN.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-CViqdH6K.js → Security-bm9V3c5k.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-CkXS97WN.js → Services-CocH1yyX.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-DxW5WYnE.js → Skeleton-DHItlMdd.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-CLxmGFse.js → Skills-C59sLc4w.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-Cr5fziXq.js → Sla-Crj4BMc8.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-H0JSb3cp.js → SpeechSettings-DX4KFceY.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-DMYWMae7.js → SyncSettings-BD_iwQLr.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-CbYyDdF4.js → Tasks-BmCTzYsH.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-Cpc8JjiK.js → Templates-BGUaxZYl.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-CRh77Jda.js → Tenant-BS0tJx2O.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-4D8tNBO5.js → Terminal--tHSAsld.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-D41jzzbG.js → TimelineRenderer-D9-MFxS4.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-C_jWEy6b.js → Tokens-CiCB485f.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-Dk1ZUhsy.js → Trigger-SmCFuW_l.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-CLVy4l7m.js → Trust-DkK3Rqe0.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-C-FeqFDj.js → UkeySign-CDM4LGdB.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-CqVZzyMm.js → VideoEditing-IDB7DJXt.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-DX9BW9pu.js → Wallet-Bikj5cmt.js} +2 -2
  86. package/src/assets/web-panel/assets/{WebAuthn-7hQDpWw_.js → WebAuthn-C9abWlAw.js} +2 -2
  87. package/src/assets/web-panel/assets/{WorkflowEditor-6XqUAO0j.js → WorkflowEditor-DrbRKVky.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-5R_vkaJG.js → chat-CccCSDQf.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-huUPZdC-.js → colors-CXObXLhb.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-CfJlgisE.js → compact-item-B5kfYAxC.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-jO8heofr.js → createContext-DmiPBfip.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-CvKrADNa.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-DY8Vxqx7.js → hasIn-Q6lYteAc.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-BtEXSoPL.js → index-25Wlqps4.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-BsBtRWbA.js → index-33U29UNV.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BQS-uPOK.js → index-4oXBDczR.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-DXBcvUnf.js → index-BGYXvMc5.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-C2iVSFpy.js → index-BJCCa0s_.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-A48PtXw9.js → index-BJJ5DhKI.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-Db1cLWir.js → index-BLOzViJ6.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-vXs3B8GJ.js → index-BRrbUMoG.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-BHE8OIls.js → index-BUFpT17Q.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-BmqGerpC.js → index-BkRDiqGc.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CzKmteNS.js → index-BsfvW6my.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-BAON7TzU.js → index-By-kfPED.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-DoI2sZm7.js → index-BzFTcQsc.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-j3O2EEJe.js → index-C2FXdboq.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-CRElRhyS.js → index-C5XOlTHN.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-KRHroryI.js → index-CLhBDS2w.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-Bxax9vQ6.js → index-CMq7UeoT.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BfCx4pFT.js → index-CVMTWNKl.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-CA6sO5VD.js → index-Caghi-yL.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CA90TZxG.js → index-CdB6FuSH.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-Cbe9G0V4.js → index-CoK8-kWQ.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-D0pet5Uy.js → index-CsO3GUAY.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-ChvHubX3.js → index-CwZBa20Z.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BRXOP09X.js → index-D-kASucw.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-B74OXFxC.js → index-D8eBw7vW.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-D1VLuww0.js → index-D9wgifk4.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-p85YaaKw.js → index-DAYCTc07.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-BliaT02i.js → index-DF6Q8l2h.js} +1 -1
  122. package/src/assets/web-panel/assets/index-DTdFUWc_.js +1 -0
  123. package/src/assets/web-panel/assets/{index-Cjxbu68P.js → index-DgTLApS9.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-Cis_Ip7H.js → index-DgxVJ8pg.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-v3pTaEQm.js → index-DlJvIqoR.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-BYLBPpt8.js → index-Dw4eHfsG.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-B300RPnT.js → index-Rh-ytwow.js} +3 -3
  128. package/src/assets/web-panel/assets/{index-CLhBh-qE.js → index-StAEt2pq.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-d1xkbtNz.js → index-bzfw951k.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-BCqxyvqp.js → index-fSUMbMvv.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-dKx57io7.js → index-tFHy5Wol.js} +1 -1
  132. package/src/assets/web-panel/assets/index-zQ0M_kz-.js +1 -0
  133. package/src/assets/web-panel/assets/{initDefaultProps-MCB7-raZ.js → initDefaultProps-BNq7X72N.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-UWDF9swV.js → motion-DOf9viTc.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-CsTmgXZU.js → move-CRHTcW4x.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-fQy6KGLg.js → mtc-parser-Cyoozgm2.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-woPnEjd1.js → omit-6-TjFNru.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-Bs2dZ1gf.js → pickAttrs-DlUmO1zi.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-C7EVgPkg.js → placementArrow-CFOL8mIR.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-BH2kYCTN.js → responsiveObserve-BtaFK5zd.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-BFBP2V_e.js → slide-BIbc2bur.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-xdDxDYse.js → statusUtils-BXX9d6-w.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-aAWgPPQf.js → styleChecker-BGv10Bpn.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-DkGSAWoO.js → useFlexGapSupport-bKnKT8tw.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-BrOih40N.js → useFs-FY4IL4jn.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-eIYrgMqO.js → usePersonalDataHub-Bwl9TEgu.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-B8lutNSE.js → vnode-BM3OHxAT.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-CffNre7V.js → zoom-D1fbFiHB.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/commands/agenda.js +71 -14
  151. package/src/commands/doctor.js +85 -1
  152. package/src/commands/plugin.js +226 -9
  153. package/src/commands/review.js +17 -1
  154. package/src/commands/session.js +191 -0
  155. package/src/commands/team.js +19 -1
  156. package/src/data/changelog.json +5 -4
  157. package/src/gateways/ws/session-protocol.js +50 -0
  158. package/src/gateways/ws/ws-agent-handler.js +50 -0
  159. package/src/harness/jsonl-session-store.js +60 -0
  160. package/src/harness/worktree-isolator.js +140 -5
  161. package/src/lib/agent-schedule-store.js +30 -1
  162. package/src/lib/agent-team/team-worktree.js +37 -2
  163. package/src/lib/agent-tool-admission.js +125 -0
  164. package/src/lib/app-preview.js +360 -0
  165. package/src/lib/async-hook-supervisor.cjs +54 -0
  166. package/src/lib/capability-manifest.js +319 -0
  167. package/src/lib/capability-negotiation.js +23 -12
  168. package/src/lib/diagnostic-bundle.js +230 -0
  169. package/src/lib/doctor-checkup.js +223 -4
  170. package/src/lib/execution-location.js +286 -0
  171. package/src/lib/headless-manifest.js +13 -40
  172. package/src/lib/hook-stats-store.cjs +197 -0
  173. package/src/lib/idempotency.js +109 -0
  174. package/src/lib/jsonl-session-store.js +1 -0
  175. package/src/lib/lsp/__tests__/lsp-manager.test.js +58 -0
  176. package/src/lib/lsp/lsp-manager.js +56 -0
  177. package/src/lib/monitor-event.js +290 -0
  178. package/src/lib/operation-fingerprint.js +217 -0
  179. package/src/lib/plugin-runtime/bin.js +37 -0
  180. package/src/lib/plugin-runtime/capabilities.js +19 -0
  181. package/src/lib/plugin-runtime/capability-consent.js +28 -0
  182. package/src/lib/plugin-runtime/hooks.js +37 -0
  183. package/src/lib/plugin-runtime/lsp.js +38 -0
  184. package/src/lib/plugin-runtime/mcp.js +40 -0
  185. package/src/lib/plugin-runtime/monitors.js +37 -0
  186. package/src/lib/plugin-runtime/plugin-options.js +151 -0
  187. package/src/lib/pr-automation-policy.js +224 -0
  188. package/src/lib/remote-approval-bridge.js +66 -13
  189. package/src/lib/repl-rewind.js +101 -0
  190. package/src/lib/review-comment-anchor.js +241 -0
  191. package/src/lib/review-pipeline.js +46 -7
  192. package/src/lib/runtime-checkup.js +221 -0
  193. package/src/lib/secret-scan.js +409 -0
  194. package/src/lib/session-branch.js +0 -0
  195. package/src/lib/side-effect-ledger.js +105 -0
  196. package/src/lib/skill-loader.js +32 -0
  197. package/src/lib/skill-path-scope.js +63 -0
  198. package/src/lib/sub-agent-context.js +36 -4
  199. package/src/lib/sub-agent-registry.js +19 -0
  200. package/src/lib/telemetry/span-recorder.js +17 -4
  201. package/src/lib/terminal-context.js +195 -0
  202. package/src/lib/unattended-action-policy.js +182 -0
  203. package/src/lib/worktree-cleanup-safety.js +138 -0
  204. package/src/lib/worktree-isolator.js +8 -0
  205. package/src/lib/worktree-sparse.js +131 -0
  206. package/src/repl/agent-repl.js +152 -38
  207. package/src/runtime/agent-core.js +191 -13
  208. package/src/runtime/diagnostics.js +60 -0
  209. package/src/runtime/headless-runner.js +126 -1
  210. package/src/assets/web-panel/assets/MobileProjects-D0RAk4e8.js +0 -1
  211. package/src/assets/web-panel/assets/devWarning-DeIrCfH-.js +0 -1
  212. package/src/assets/web-panel/assets/index-BxUKMRpP.js +0 -1
  213. package/src/assets/web-panel/assets/index-DscU9iPI.js +0 -1
@@ -24,8 +24,8 @@
24
24
  import { randomBytes } from "crypto";
25
25
  import { WsRpcClient } from "./ws-rpc-client.js";
26
26
  import {
27
- approvalFingerprintOk,
28
- operationFingerprint,
27
+ fingerprintsMatch,
28
+ OperationApprovalRegistry,
29
29
  } from "./operation-fingerprint.js";
30
30
  import {
31
31
  buildDirectPairingUri,
@@ -60,9 +60,15 @@ export class RemoteApprovalBridge {
60
60
  this.client = null;
61
61
  this.remoteSessionId = null;
62
62
  this.pairing = null;
63
- this._pending = new Map(); // requestId → { resolve, timer }
63
+ this._pending = new Map(); // requestId → { resolve, timer, fingerprint }
64
64
  this._counter = 0;
65
65
  this._closed = false;
66
+ // §8.2 cross-device approval registry: full-tuple operation fingerprints,
67
+ // single-winner across concurrent cards for one logical operation, at-most-
68
+ // once resolution, and validity-window enforcement — all fail-closed.
69
+ this._registry = new OperationApprovalRegistry({
70
+ clock: () => this._now(),
71
+ });
66
72
  }
67
73
 
68
74
  /** Connect, register the client-hosted remote session, start listening. */
@@ -117,15 +123,24 @@ export class RemoteApprovalBridge {
117
123
  const requestId = message.event.requestId || message.event.approvalId;
118
124
  const pending = requestId ? this._pending.get(requestId) : null;
119
125
  if (!pending) return;
120
- // Confused-deputy guard: a resolve that carries an operation fingerprint
121
- // must match the operation this ask published. A mismatch (stale card /
122
- // replayed / swapped body) is rejected — the ask stays pending and fails
123
- // closed on timeout. Absent fingerprint = legacy device, accepted.
126
+ // Confused-deputy guard: a resolve that echoes a fingerprint must match the
127
+ // operation this ask published. A mismatch (stale card / replayed / swapped
128
+ // body / different session·workspace·env·policy) is rejected — the ask stays
129
+ // pending and fails closed on timeout. An absent fingerprint is a legacy
130
+ // device: resolved via this ask's own fingerprint (still window-guarded).
124
131
  if (
125
- !approvalFingerprintOk(pending.fingerprint, message.event.fingerprint)
132
+ message.event.fingerprint != null &&
133
+ !fingerprintsMatch(pending.fingerprint, message.event.fingerprint)
126
134
  ) {
127
135
  return;
128
136
  }
137
+ // Fail-closed §8.2 enforcement: single-winner (superseded card), at-most-once
138
+ // (duplicate), and validity window (not-yet-valid / expired). A rejected
139
+ // verdict leaves the ask pending so it still fails closed on timeout.
140
+ const verdict = this._registry.resolve(pending.fingerprint, {
141
+ now: this._now(),
142
+ });
143
+ if (!verdict.ok) return;
129
144
  this._pending.delete(requestId);
130
145
  clearTimeout(pending.timer);
131
146
  const answer = message.event.answer ?? message.event.approved;
@@ -159,11 +174,35 @@ export class RemoteApprovalBridge {
159
174
  tool,
160
175
  action = null,
161
176
  detail = null,
177
+ workspace = null,
178
+ session = null,
179
+ targetEnv = null,
180
+ policyVersion = null,
162
181
  timeoutMs,
163
182
  onRequestId = null,
164
183
  } = {}) {
165
184
  const requestId = `ra-${process.pid}-${++this._counter}-${randomBytes(4).toString("hex")}`;
166
- const fingerprint = operationFingerprint({ tool, action, detail });
185
+ const askedAt = this._now();
186
+ const effectiveTimeout = timeoutMs || this.decisionTimeoutMs;
187
+ // §8.2 full-tuple descriptor: the fingerprint binds tool + params + target
188
+ // env + workspace + session + policy version + validity window, so an
189
+ // approval never carries over to a different operation OR a changed context.
190
+ // The validity window rides the ask lifetime — a resolve after it expires is
191
+ // rejected `expired`.
192
+ const desc = {
193
+ toolName: tool,
194
+ params: detail,
195
+ workspace,
196
+ session: session || this.agentSessionId,
197
+ targetEnv,
198
+ policyVersion,
199
+ notBefore: askedAt,
200
+ notAfter:
201
+ Number.isFinite(askedAt) && Number.isFinite(effectiveTimeout)
202
+ ? askedAt + effectiveTimeout
203
+ : null,
204
+ };
205
+ const card = this._registry.issue(desc);
167
206
  if (onRequestId) {
168
207
  try {
169
208
  onRequestId(requestId);
@@ -181,11 +220,11 @@ export class RemoteApprovalBridge {
181
220
  via: "timeout",
182
221
  });
183
222
  resolve({ approved: false, via: "timeout", from: null });
184
- }, timeoutMs || this.decisionTimeoutMs);
223
+ }, effectiveTimeout);
185
224
  if (typeof timer.unref === "function") timer.unref();
186
225
  this._pending.set(requestId, {
187
226
  timer,
188
- fingerprint,
227
+ fingerprint: card.fingerprint,
189
228
  resolve: (decision) => {
190
229
  this._publish({
191
230
  type: "permission.resolved",
@@ -202,8 +241,14 @@ export class RemoteApprovalBridge {
202
241
  tool: tool || null,
203
242
  action,
204
243
  detail,
205
- fingerprint,
206
- askedAt: this._now(),
244
+ // Full fingerprint (protocol) + short id / secret-free summary (what the
245
+ // operator eyeballs on the device to confirm it is the SAME card).
246
+ fingerprint: card.fingerprint,
247
+ shortId: card.shortId,
248
+ summary: card.summary,
249
+ notBefore: desc.notBefore,
250
+ notAfter: desc.notAfter,
251
+ askedAt,
207
252
  });
208
253
  });
209
254
  }
@@ -214,6 +259,9 @@ export class RemoteApprovalBridge {
214
259
  if (!pending) return false;
215
260
  this._pending.delete(requestId);
216
261
  clearTimeout(pending.timer);
262
+ // Consume the registry card so a late remote resolve for the SAME operation
263
+ // is a `duplicate`, never a second settle.
264
+ this._registry.resolve(pending.fingerprint, { now: this._now() });
217
265
  pending.resolve({ approved, via: "local", from: null });
218
266
  return true;
219
267
  }
@@ -248,6 +296,11 @@ export class RemoteApprovalBridge {
248
296
  : ctx?.args
249
297
  ? JSON.stringify(ctx.args).slice(0, 2000)
250
298
  : null,
299
+ // §8.2 context that binds the fingerprint (session defaults to this
300
+ // bridge's agent session inside requestDecision).
301
+ workspace: ctx?.cwd || ctx?.workspace || null,
302
+ targetEnv: ctx?.targetEnv || null,
303
+ policyVersion: ctx?.policyVersion || null,
251
304
  };
252
305
  if (onAsk) {
253
306
  try {
@@ -31,6 +31,11 @@ import {
31
31
  RESTORE_SCOPE,
32
32
  TURN_COVERAGE,
33
33
  } from "./turn-binding.js";
34
+ import { planSessionBranch } from "./session-branch.js";
35
+
36
+ // Re-exported so the REPL handler can reference the scope constants from the
37
+ // same module it imports the rewind helpers from.
38
+ export { RESTORE_SCOPE };
34
39
 
35
40
  export const DEFAULT_LIST_LIMIT = 10;
36
41
  export const PREVIEW_CHARS = 60;
@@ -271,6 +276,102 @@ export function renderRewindWarnings(plan) {
271
276
  return lines;
272
277
  }
273
278
 
279
+ /**
280
+ * Coverage-aware BRANCH plan for a chosen turn ("从这里分支" — P0-3's fourth
281
+ * restore action). Bridges the REPL's implicit state (`listUserTurns()` output +
282
+ * checkpoint marks) into the explicit turn-binding table exactly like
283
+ * buildRewindPlan(), then asks the pure [[session-branch.js]] planner for a
284
+ * deterministic branch plan (new session id, worktree-for-writes decision,
285
+ * honest coverage warnings). Unlike a rewind, a branch never truncates the live
286
+ * conversation — the origin session is preserved.
287
+ *
288
+ * @param {Array} messages live conversation (for listUserTurns)
289
+ * @param {Array<{atMessageCount:number,id:string,tool?:string}>} marks
290
+ * @param {number} turnIndex message index of the branch-point user turn
291
+ * @param {{parentSessionId?:string, seq?:number, writeIntent?:boolean}} [opts]
292
+ * @returns {object} the plan from planSessionBranch()
293
+ */
294
+ export function buildBranchPlan(
295
+ messages,
296
+ marks,
297
+ turnIndex,
298
+ { parentSessionId = null, seq = 0, writeIntent = false } = {},
299
+ ) {
300
+ const turns = listUserTurns(messages, { limit: 1000 });
301
+ const log = buildTurnBindingFromMarks(turns, marks);
302
+ const turn = log.get(`turn-${Number(turnIndex)}`);
303
+ return planSessionBranch({ parentSessionId, turn, seq, writeIntent });
304
+ }
305
+
306
+ /**
307
+ * Human-readable coverage + warning lines for a branch plan, printed before the
308
+ * branch is written. Returns [] for a full-coverage branch with no warnings.
309
+ *
310
+ * @param {{coverage:string, warnings:string[]}} plan from buildBranchPlan()
311
+ * @returns {string[]}
312
+ */
313
+ export function renderBranchPlan(plan) {
314
+ if (!plan) return [];
315
+ const lines = [];
316
+ if (plan.coverage && plan.coverage !== TURN_COVERAGE.FULL) {
317
+ lines.push(
318
+ ` coverage: ${plan.coverage} (the branch inherits the working tree as-is)`,
319
+ );
320
+ }
321
+ for (const w of plan.warnings || []) lines.push(` ⚠ ${w}`);
322
+ return lines;
323
+ }
324
+
325
+ /**
326
+ * Parse a `/rewind` argument line into a command + optional restore scope.
327
+ * Claude-Code parity: a rewind can restore the conversation only, the files
328
+ * only, or both. The turn number and an optional `--conversation` / `--files` /
329
+ * `--both` flag may appear in any order. `--branch` (optionally `--write`) turns
330
+ * the pick into a branch-from-here (P0-3) instead of a rewind.
331
+ *
332
+ * /rewind → { command: "list", scope: BOTH }
333
+ * /rewind clear → { command: "clear", scope: BOTH }
334
+ * /rewind 3 → { command: "turn", n: 3, scope: BOTH }
335
+ * /rewind 3 --conversation → { command: "turn", n: 3, scope: CONVERSATION }
336
+ * /rewind --files 3 → { command: "turn", n: 3, scope: FILES }
337
+ * /rewind 3 --branch → { command: "branch", n: 3, writeIntent: false }
338
+ * /rewind 3 --branch --write → { command: "branch", n: 3, writeIntent: true }
339
+ *
340
+ * An unknown flag is ignored (kept forgiving); a non-numeric, flag-less arg with
341
+ * no turn number falls back to `list` so a typo can't silently rewind — and a
342
+ * `--branch` with no turn number lists too rather than branch off nothing.
343
+ *
344
+ * @param {string} arg the text after `/rewind`
345
+ * @returns {{command:"list"|"clear"|"turn"|"branch", n?:number, scope:string, writeIntent?:boolean}}
346
+ */
347
+ export function parseRewindArg(arg) {
348
+ const raw = String(arg == null ? "" : arg).trim();
349
+ if (raw === "clear" || raw === "undo-clear") {
350
+ return { command: "clear", scope: RESTORE_SCOPE.BOTH };
351
+ }
352
+ const tokens = raw.split(/\s+/).filter(Boolean);
353
+ let n = null;
354
+ let scope = RESTORE_SCOPE.BOTH;
355
+ let branch = false;
356
+ let writeIntent = false;
357
+ for (const t of tokens) {
358
+ if (t === "--files" || t === "--files-only") scope = RESTORE_SCOPE.FILES;
359
+ else if (
360
+ t === "--conversation" ||
361
+ t === "--conv" ||
362
+ t === "--conversation-only"
363
+ )
364
+ scope = RESTORE_SCOPE.CONVERSATION;
365
+ else if (t === "--both") scope = RESTORE_SCOPE.BOTH;
366
+ else if (t === "--branch" || t === "--fork") branch = true;
367
+ else if (t === "--write" || t === "--write-intent") writeIntent = true;
368
+ else if (n === null && /^\d+$/.test(t)) n = Number(t);
369
+ }
370
+ if (branch && n !== null) return { command: "branch", n, scope, writeIntent };
371
+ if (n === null) return { command: "list", scope };
372
+ return { command: "turn", n, scope };
373
+ }
374
+
274
375
  /** Render the picker list (shared by /rewind and double-Esc). */
275
376
  export function renderTurnList(turns) {
276
377
  if (!turns.length) return " (no user turns yet)";
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Review-comment anchoring + staleness — the "行评论 / comment-to-fix" half of
3
+ * P1-1 (原生复杂 Diff 与评论驱动修复) of
4
+ * CLAUDE_CODE_IDE_INCREMENTAL_GAP_ANALYSIS_2026-07-13.md:
5
+ *
6
+ * "行评论传递 file + base hash + line/hunk anchor + comment。
7
+ * Agent 修复后评论标记为 resolved/outdated,不错误复用旧行号。"
8
+ *
9
+ * [[review-pipeline.js]] aggregates FINDINGS (dedup / verdicts / rank), but a
10
+ * finding's `line` is a raw number against one snapshot. Once the agent edits
11
+ * the file, that number is a lie: line 42 now points at unrelated code. Claude
12
+ * Code's diff review keeps a comment anchored to the CODE it addressed, not to a
13
+ * coordinate, and after a re-diff it relocates the comment or marks it stale —
14
+ * it never silently reuses the old line number against new content.
15
+ *
16
+ * This is the PURE anchoring core. An anchor captures the file, a content hash,
17
+ * the 1-based line and the anchored line's text plus a small context window. On
18
+ * a new file version it recomputes the comment's fate:
19
+ *
20
+ * current — file unchanged (hash match): line preserved.
21
+ * moved — the anchored line relocated UNIQUELY: line updated.
22
+ * outdated — the anchored line is gone (edited/deleted): line = null. The fix
23
+ * most likely addressed it; the old number must NOT be reused.
24
+ * ambiguous — several equally-plausible locations: line = null, needs a human.
25
+ *
26
+ * The hard invariant: whenever we cannot uniquely place the comment in the new
27
+ * content, `line` is null. No stale coordinate ever survives a re-anchor.
28
+ *
29
+ * PURE: no fs / clock / RNG. Content hashing uses node:crypto (deterministic).
30
+ */
31
+
32
+ import { createHash } from "node:crypto";
33
+
34
+ /** Terminal + transient states a comment can be in after re-anchoring. */
35
+ export const ANCHOR_STATUS = Object.freeze({
36
+ CURRENT: "current", // file unchanged — line preserved
37
+ MOVED: "moved", // relocated uniquely — line updated
38
+ OUTDATED: "outdated", // anchored code gone — line null, do not reuse
39
+ AMBIGUOUS: "ambiguous", // multiple candidates — line null, needs human
40
+ RESOLVED: "resolved", // caller-set: the comment's issue was fixed
41
+ });
42
+
43
+ /** Deterministic content hash used as the anchor's base-version fingerprint. */
44
+ export function hashContent(content) {
45
+ return createHash("sha256")
46
+ .update(String(content ?? ""))
47
+ .digest("hex");
48
+ }
49
+
50
+ function splitLines(content) {
51
+ return String(content ?? "").split("\n");
52
+ }
53
+
54
+ /** Normalize a line for matching — trailing CR + surrounding whitespace only,
55
+ * preserving interior text so distinct statements stay distinct. */
56
+ function norm(line) {
57
+ return String(line ?? "")
58
+ .replace(/\r$/, "")
59
+ .trim();
60
+ }
61
+
62
+ /**
63
+ * Build a review-comment anchor from the file content it was written against.
64
+ *
65
+ * @param {object} params
66
+ * @param {string} params.file repo-relative path
67
+ * @param {string} params.content the base file content the comment addresses
68
+ * @param {number} params.line 1-based line the comment anchors to
69
+ * @param {string} params.comment the review text
70
+ * @param {string} [params.id] stable comment id (caller-owned)
71
+ * @param {number} [params.contextRadius=2] context lines captured each side
72
+ * @returns {object} an anchor record (serializable)
73
+ */
74
+ export function makeCommentAnchor({
75
+ file,
76
+ content,
77
+ line,
78
+ comment,
79
+ id = null,
80
+ contextRadius = 2,
81
+ } = {}) {
82
+ const lines = splitLines(content);
83
+ const idx = Number(line) - 1; // to 0-based
84
+ const inRange = Number.isInteger(idx) && idx >= 0 && idx < lines.length;
85
+ const radius = Math.max(0, Math.floor(Number(contextRadius) || 0));
86
+ return {
87
+ id: id == null ? null : String(id),
88
+ file: String(file ?? ""),
89
+ baseHash: hashContent(content),
90
+ line: inRange ? idx + 1 : null,
91
+ comment: String(comment ?? ""),
92
+ anchorLine: inRange ? String(lines[idx]) : "",
93
+ contextBefore: inRange ? lines.slice(Math.max(0, idx - radius), idx) : [],
94
+ contextAfter: inRange ? lines.slice(idx + 1, idx + 1 + radius) : [],
95
+ status: ANCHOR_STATUS.CURRENT,
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Score how well a candidate index's neighbourhood matches the anchor context.
101
+ * Context is only a TIE-BREAKER among equal text matches, so a 0-radius anchor
102
+ * (bare line match) still relocates when the match is unique.
103
+ */
104
+ function contextScore(anchor, lines, i) {
105
+ let score = 0;
106
+ const before = anchor.contextBefore || [];
107
+ for (let k = 0; k < before.length; k++) {
108
+ const li = i - before.length + k;
109
+ if (li >= 0 && norm(lines[li]) === norm(before[k])) score++;
110
+ }
111
+ const after = anchor.contextAfter || [];
112
+ for (let k = 0; k < after.length; k++) {
113
+ const li = i + 1 + k;
114
+ if (li < lines.length && norm(lines[li]) === norm(after[k])) score++;
115
+ }
116
+ return score;
117
+ }
118
+
119
+ /**
120
+ * Recompute a comment's fate against a new version of its file.
121
+ *
122
+ * @param {object} anchor a record from makeCommentAnchor()
123
+ * @param {string} newContent the file's new content
124
+ * @returns {object} the anchor with updated {status, line, previousLine}
125
+ */
126
+ export function reanchorComment(anchor, newContent) {
127
+ if (!anchor || typeof anchor !== "object") return anchor;
128
+ const previousLine = anchor.line;
129
+ const newHash = hashContent(newContent);
130
+
131
+ // Unchanged file → the coordinate is still true.
132
+ if (newHash === anchor.baseHash) {
133
+ return {
134
+ ...anchor,
135
+ status: ANCHOR_STATUS.CURRENT,
136
+ line: previousLine,
137
+ previousLine,
138
+ };
139
+ }
140
+
141
+ const target = norm(anchor.anchorLine);
142
+ // An anchor with no captured text can never be relocated — it's stale.
143
+ if (target === "") {
144
+ return {
145
+ ...anchor,
146
+ status: ANCHOR_STATUS.OUTDATED,
147
+ line: null,
148
+ previousLine,
149
+ };
150
+ }
151
+
152
+ const lines = splitLines(newContent);
153
+ const candidates = [];
154
+ for (let i = 0; i < lines.length; i++) {
155
+ if (norm(lines[i]) === target) candidates.push(i);
156
+ }
157
+
158
+ if (candidates.length === 0) {
159
+ // The exact anchored code no longer exists — the fix changed/removed it.
160
+ return {
161
+ ...anchor,
162
+ status: ANCHOR_STATUS.OUTDATED,
163
+ line: null,
164
+ previousLine,
165
+ };
166
+ }
167
+
168
+ // Disambiguate multiple text matches by surrounding context.
169
+ let best = -1;
170
+ const winners = [];
171
+ for (const i of candidates) {
172
+ const s = contextScore(anchor, lines, i);
173
+ if (s > best) {
174
+ best = s;
175
+ winners.length = 0;
176
+ winners.push(i);
177
+ } else if (s === best) {
178
+ winners.push(i);
179
+ }
180
+ }
181
+
182
+ if (winners.length !== 1) {
183
+ // Equally-plausible locations — refuse to guess; never reuse the old line.
184
+ return {
185
+ ...anchor,
186
+ status: ANCHOR_STATUS.AMBIGUOUS,
187
+ line: null,
188
+ previousLine,
189
+ };
190
+ }
191
+
192
+ const newLine = winners[0] + 1;
193
+ const status =
194
+ newLine === previousLine ? ANCHOR_STATUS.CURRENT : ANCHOR_STATUS.MOVED;
195
+ return { ...anchor, status, line: newLine, previousLine };
196
+ }
197
+
198
+ /**
199
+ * Explicitly mark a comment resolved (the agent's fix addressed its issue). A
200
+ * caller-driven terminal state distinct from the auto-detected OUTDATED, so a
201
+ * reviewer can tell "you fixed this" from "the code you commented on vanished".
202
+ */
203
+ export function markResolved(anchor, resolvedReason = "") {
204
+ return {
205
+ ...anchor,
206
+ status: ANCHOR_STATUS.RESOLVED,
207
+ line: null,
208
+ resolvedReason: String(resolvedReason || ""),
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Re-anchor a list of comments against a new file version and bucket them.
214
+ * `resolvedIds` (optional) forces those comment ids to RESOLVED before/around
215
+ * the automatic re-anchor.
216
+ *
217
+ * @param {Array} anchors
218
+ * @param {string} newContent
219
+ * @param {{resolvedIds?:Iterable<string>}} [opts]
220
+ * @returns {{comments:Array, current:string[], moved:string[],
221
+ * outdated:string[], ambiguous:string[], resolved:string[]}}
222
+ */
223
+ export function reconcileComments(anchors, newContent, { resolvedIds } = {}) {
224
+ const resolved = new Set([...(resolvedIds || [])].map((x) => String(x)));
225
+ const comments = (Array.isArray(anchors) ? anchors : []).map((a) => {
226
+ if (a && a.id != null && resolved.has(String(a.id))) {
227
+ return markResolved(a, "marked resolved by caller");
228
+ }
229
+ return reanchorComment(a, newContent);
230
+ });
231
+ const bucket = (status) =>
232
+ comments.filter((c) => c && c.status === status).map((c) => c.id);
233
+ return {
234
+ comments,
235
+ current: bucket(ANCHOR_STATUS.CURRENT),
236
+ moved: bucket(ANCHOR_STATUS.MOVED),
237
+ outdated: bucket(ANCHOR_STATUS.OUTDATED),
238
+ ambiguous: bucket(ANCHOR_STATUS.AMBIGUOUS),
239
+ resolved: bucket(ANCHOR_STATUS.RESOLVED),
240
+ };
241
+ }
@@ -12,8 +12,16 @@
12
12
  * verifier verdicts, confidence-filter, rank, and render the structured report.
13
13
  * The fan-out of finder/verifier AGENTS is orchestration the command layer adds;
14
14
  * everything here is deterministic and testable without an LLM.
15
+ *
16
+ * P1-1 wiring (行评论锚定): when the caller supplies the reviewed file contents,
17
+ * each finding's output carries a re-anchorable comment ANCHOR (file + base hash
18
+ * + line + anchored line text + context) via [[review-comment-anchor.js]] instead
19
+ * of only a bare `line` coordinate — so a downstream IDE Diff Review can relocate
20
+ * or stale the comment after the agent edits the file, never reusing a dead line.
15
21
  */
16
22
 
23
+ import { makeCommentAnchor } from "./review-comment-anchor.js";
24
+
17
25
  /** The finder dimensions a full review fans out across. */
18
26
  export const REVIEW_DIMENSIONS = Object.freeze([
19
27
  "correctness",
@@ -184,16 +192,44 @@ export function rankFindings(findings) {
184
192
  );
185
193
  }
186
194
 
187
- function toOutputFinding(f) {
188
- return {
195
+ /** Look a path's content up from a Map or plain object, or return undefined. */
196
+ function lookupContent(fileContents, filePath) {
197
+ if (!fileContents) return undefined;
198
+ if (typeof fileContents.get === "function") return fileContents.get(filePath);
199
+ return fileContents[filePath];
200
+ }
201
+
202
+ function toOutputFinding(f, fileContents) {
203
+ const line = Number.isFinite(Number(f.line))
204
+ ? Math.floor(Number(f.line))
205
+ : null;
206
+ const failureScenario = String(f.failureScenario || f.failure_scenario || "");
207
+ const out = {
189
208
  path: String(f.path || ""),
190
- line: Number.isFinite(Number(f.line)) ? Math.floor(Number(f.line)) : null,
209
+ line,
191
210
  category: f.category || (f.categories && f.categories[0]) || "correctness",
192
211
  severity: normalizeSeverity(f.severity),
193
- failure_scenario: String(f.failureScenario || f.failure_scenario || ""),
212
+ failure_scenario: failureScenario,
194
213
  evidence: String(f.evidence || ""),
195
214
  confidence: clampConfidence(f.confidence),
196
215
  };
216
+ // P1-1: anchor the comment to the CODE it addresses, not to a coordinate, so
217
+ // an IDE diff review can relocate/stale it after edits (never reuse a dead
218
+ // line). Only when the caller supplied this file's content AND we have a
219
+ // valid 1-based line to anchor.
220
+ if (line != null && line >= 1) {
221
+ const content = lookupContent(fileContents, out.path);
222
+ if (typeof content === "string") {
223
+ out.anchor = makeCommentAnchor({
224
+ file: out.path,
225
+ content,
226
+ line,
227
+ comment: failureScenario || out.evidence,
228
+ id: `${out.path}:${line}:${out.category}`,
229
+ });
230
+ }
231
+ }
232
+ return out;
197
233
  }
198
234
 
199
235
  /**
@@ -202,17 +238,20 @@ function toOutputFinding(f) {
202
238
  * evidence output) plus severity/category rollups.
203
239
  *
204
240
  * @param {Array} rawFindings findings from all finders
205
- * @param {object} [opts] { verdicts, minConfidence }
241
+ * @param {object} [opts] { verdicts, minConfidence, fileContents }
242
+ * fileContents — optional Map/object of `path → current file content`. When a
243
+ * finding's file is present, its output gains a re-anchorable `anchor` (P1-1);
244
+ * omit it and the output is byte-identical to before.
206
245
  */
207
246
  export function buildReviewReport(
208
247
  rawFindings,
209
- { verdicts = {}, minConfidence = 0 } = {},
248
+ { verdicts = {}, minConfidence = 0, fileContents = null } = {},
210
249
  ) {
211
250
  const deduped = dedupeFindings(rawFindings);
212
251
  const verified = applyVerdicts(deduped, verdicts);
213
252
  const filtered = filterByConfidence(verified, { minConfidence });
214
253
  const ranked = rankFindings(filtered);
215
- const findings = ranked.map(toOutputFinding);
254
+ const findings = ranked.map((f) => toOutputFinding(f, fileContents));
216
255
 
217
256
  const bySeverity = { Critical: 0, High: 0, Medium: 0, Low: 0, Note: 0 };
218
257
  const byCategory = {};