chainlesschain 0.162.162 → 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 (237) hide show
  1. package/package.json +3 -1
  2. package/src/assets/web-panel/assets/{AIOps-ClI2ALgv.js → AIOps-BqRkaHGb.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-BDjRcJaK.js → ActionButton-DWO7OZLC.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-DikSRsaT.js → Analytics-C5oaVezM.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-9mLhMFmz.js → AppLayout-CsUESDoZ.js} +3 -3
  6. package/src/assets/web-panel/assets/{Artifacts-BSr88BrH.js → Artifacts-Dragqu5k.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-CdvyimOF.js → Audit-CQsaHctY.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-D3_hCedh.js → BackgroundAgents-DdG1jNSM.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-Cyvv9RHf.js → Backup-DN6sjK4z.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-BZM14kt8.js → BaseInput-Cxm_ZTCl.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-DrQSokVz.js → Chat-DVXGmA56.js} +6 -6
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-HwJR-C6B.js → ChatBubbleRenderer-BdaCL_iE.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-bFLYeWMV.js → Checkbox-DEc4vD_A.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-7mN7xJXT.js → Codegen-CKiTcv36.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-DAB5ZWm7.js → Col-Br9UUNN0.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-B_1TymaK.js → Community-NeUH_70T.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-Bq1uSWLJ.js → Compact-BqR-z2Df.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-CyiE1jEe.js → Compliance-DcWB_SgZ.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-Cpk0D7vm.js → Cowork-BfhgcudF.js} +2 -2
  20. package/src/assets/web-panel/assets/{Cron-d_Lv3rdB.js → Cron-LaU29LgD.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-DMqhHjMR.js → Crosschain-B2ZB9uwW.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-Br-oPuBw.js → DID-BuHl38A2.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-CfLW_RaP.js → Dashboard-m627yqvZ.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-DZnTOExV.js → Dropdown-C7bxF8F5.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-DHIUHDv1.js → EmailListRenderer-DPLPQzLp.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-Bw3t3myu.js → FamilyGuardDashboard-cOiH6ike.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-D9cpQPAY.js → Federation-Bvkzg88z.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-Jdczr9Mr.js → FormItemContext-BTsiA7h_.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-BT8ZMpC5.js → GenericCardRenderer-BGnVE5qu.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-DjARip2b.js → Git-DS_j6r25.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-BgWbSRcj.js → Governance-CKRG3Rsl.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-CfouJciM.js → Inference-DW8INoNQ.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-50l7mrAr.js → KnowledgeGraph-BhEYeFhC.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-rq50H84B.js → Logs-wqi0uxuR.js} +2 -2
  35. package/src/assets/web-panel/assets/{MarkdownRenderer-DUPIEdQH.js → MarkdownRenderer-DS8dE8ty.js} +1 -1
  36. package/src/assets/web-panel/assets/{Marketplace-BYgPDmHU.js → Marketplace-CMlTuPLT.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-5DVgcFdx.js → McpTools-CSx4m5JJ.js} +3 -3
  38. package/src/assets/web-panel/assets/{Memory-B70LPk7f.js → Memory-vnQCUy-_.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-De8ssA5Y.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-RJsOTsIA.js → Mtc-CzK_FPB1.js} +2 -2
  42. package/src/assets/web-panel/assets/{MtcAudit-Crh-6BEK.js → MtcAudit-Cb7G9oL3.js} +5 -5
  43. package/src/assets/web-panel/assets/{Multisig-wuoR5l3x.js → Multisig-qJxlUyzr.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-U2Mji4fZ.js → NLProgramming-DqaBJ7Te.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-CdTZAyDH.js → Notes-CwhTJkRi.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-C-FucWE7.js → NotificationSettings-YDs4Fal4.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-DCYFTeaI.js → OrderTableRenderer-CvEALpq7.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-an3OF85L.js → Organization-CEmQm9n8.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-C-1vmYFe.js → Overflow-zby_P_LA.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-Cwh47xKg.js → P2P-DsmDNFNT.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-Dm38GT0k.js → PdhVaultBrowser-FWwFlZGF.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-DS2Y8awD.js → Permissions-DuzQ6me5.js} +4 -4
  53. package/src/assets/web-panel/assets/{PersonalDataHub-BQNziA0I.js → PersonalDataHub-DZbeN613.js} +3 -3
  54. package/src/assets/web-panel/assets/{Pipeline-DF85L1ue.js → Pipeline-CoNSVAUS.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-BuLornF-.js → Privacy-OYJEAaMC.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-C3Y31IwX.js → ProjectInit-CWIapV--.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-BCPiI8aD.js → ProjectSettings-CX8xMxZn.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-BZ6s4k6g.js → Projects-BLCN8YyY.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-D3Wp022X.js → Providers-BV_sxp75.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-CUJfQkH4.js → QrScannerModal-BTnRrHRp.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-UAc0EO8l.js → QuickAsk-D1KZg018.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-BaxKupcs.js → Recommend-COm5cuNg.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-C75NNnXZ.js → RemoteSession-0saWEkYI.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-BVXynZAk.js → Reputation-Dr7iKLS2.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-C5MRZ0bi.js → Row-C1vpPBcr.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-DAKkEYka.js → RssFeed-CxlWrtNj.js} +3 -3
  67. package/src/assets/web-panel/assets/{Search-DvWv9YOA.js → Search-BT2V8dPN.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-B5peafW1.js → Security-bm9V3c5k.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-UQmvPEaG.js → Services-CocH1yyX.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-C3bhWpuF.js → Skeleton-DHItlMdd.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-DfkKf9_H.js → Skills-C59sLc4w.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-DqMSjlBM.js → Sla-Crj4BMc8.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-C-WbxWUX.js → SpeechSettings-DX4KFceY.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-CYuHZ4N2.js → SyncSettings-BD_iwQLr.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-CyeAyB9d.js → Tasks-BmCTzYsH.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-BTCeQnpq.js → Templates-BGUaxZYl.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-BifQuA9Y.js → Tenant-BS0tJx2O.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-Bx_9Bocf.js → Terminal--tHSAsld.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-BX0RFCOm.js → TimelineRenderer-D9-MFxS4.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-CGHrCmUS.js → Tokens-CiCB485f.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-DlqaY4SO.js → Trigger-SmCFuW_l.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-DfOmj-ay.js → Trust-DkK3Rqe0.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-B72jH5fE.js → UkeySign-CDM4LGdB.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-BT-63fqv.js → VideoEditing-IDB7DJXt.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-Dsul3sHr.js → Wallet-Bikj5cmt.js} +4 -4
  86. package/src/assets/web-panel/assets/{WebAuthn-Izkwwdgy.js → WebAuthn-C9abWlAw.js} +2 -2
  87. package/src/assets/web-panel/assets/{WorkflowEditor-D3FHSiH1.js → WorkflowEditor-DrbRKVky.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-CmVz5z3U.js → chat-CccCSDQf.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-CuLgyJBp.js → colors-CXObXLhb.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-Ml1YkXpw.js → compact-item-B5kfYAxC.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-BvjJ48bs.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-DVC0157C.js → hasIn-Q6lYteAc.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-C229ABfy.js → index-25Wlqps4.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-Ck_52dz_.js → index-33U29UNV.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-QXzlT9KD.js → index-4oXBDczR.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-4SBta3d7.js → index-BGYXvMc5.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-B0S8NzO6.js → index-BJCCa0s_.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-DJyN28Et.js → index-BJJ5DhKI.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DC_OI9H2.js → index-BLOzViJ6.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-C_XuRgGn.js → index-BRrbUMoG.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-wvb_FtaV.js → index-BUFpT17Q.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-KMqhcF7l.js → index-BkRDiqGc.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-vG4XSJ1d.js → index-BsfvW6my.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DYXHFr4r.js → index-By-kfPED.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-DuGszaWY.js → index-BzFTcQsc.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-CXkJH-lK.js → index-C2FXdboq.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-Dt5KMjwJ.js → index-C5XOlTHN.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-4wcgvPoF.js → index-CLhBDS2w.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-C_H_S-DY.js → index-CMq7UeoT.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BydjdZnl.js → index-CVMTWNKl.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-BjHBpRYZ.js → index-Caghi-yL.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-Bzr0rB5T.js → index-CdB6FuSH.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-BbPtzB8M.js → index-CoK8-kWQ.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-BUt8kTIB.js → index-CsO3GUAY.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-C_vRgTl9.js → index-CwZBa20Z.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-DukDVXCv.js → index-D-kASucw.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-BbepULSc.js → index-D8eBw7vW.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-BfuKP4qe.js → index-D9wgifk4.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-BiC6dQZG.js → index-DAYCTc07.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CmFNvNLZ.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-KjgKdZe-.js → index-DgTLApS9.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-BpFJHiTv.js → index-DgxVJ8pg.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-Cw0Rjy87.js → index-DlJvIqoR.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-CFYVQMvu.js → index-Dw4eHfsG.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-CudmwNBV.js → index-Rh-ytwow.js} +3 -3
  128. package/src/assets/web-panel/assets/{index-DDGaOWBV.js → index-StAEt2pq.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-D3lD5qap.js → index-bzfw951k.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-BzG-x9p4.js → index-fSUMbMvv.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-B_EvG_GW.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-BDgd2YZ8.js → initDefaultProps-BNq7X72N.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-ErtrXdYe.js → motion-DOf9viTc.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-_GPPNCgs.js → move-CRHTcW4x.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-DRLL-8V2.js → mtc-parser-Cyoozgm2.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-p86sl023.js → omit-6-TjFNru.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-BTu7cMLH.js → pickAttrs-DlUmO1zi.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-BEKz6Isr.js → placementArrow-CFOL8mIR.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-Ck42c9MP.js → responsiveObserve-BtaFK5zd.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-1CEvuPsB.js → slide-BIbc2bur.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-BDfdx1GJ.js → statusUtils-BXX9d6-w.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-B4K46m5w.js → styleChecker-BGv10Bpn.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-DoIVpJNq.js → useFlexGapSupport-bKnKT8tw.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-DdAWi-ln.js → useFs-FY4IL4jn.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-iUu6k8dx.js → usePersonalDataHub-Bwl9TEgu.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-DZz4R6Tw.js → vnode-BM3OHxAT.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-DXlf9sA3.js → zoom-D1fbFiHB.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/commands/agenda.js +320 -32
  151. package/src/commands/agent.js +105 -11
  152. package/src/commands/context.js +125 -14
  153. package/src/commands/doctor.js +85 -1
  154. package/src/commands/hook.js +181 -7
  155. package/src/commands/plugin.js +456 -1
  156. package/src/commands/review.js +363 -2
  157. package/src/commands/session.js +191 -0
  158. package/src/commands/team.js +19 -1
  159. package/src/data/changelog.json +10 -4
  160. package/src/gateways/ws/remote-session-protocol.js +39 -0
  161. package/src/gateways/ws/session-protocol.js +55 -2
  162. package/src/gateways/ws/ws-agent-handler.js +50 -0
  163. package/src/harness/jsonl-session-store.js +60 -0
  164. package/src/harness/worktree-isolator.js +140 -5
  165. package/src/lib/agent-schedule-store.js +318 -10
  166. package/src/lib/agent-team/team-worktree.js +37 -2
  167. package/src/lib/agent-tool-admission.js +125 -0
  168. package/src/lib/app-preview.js +360 -0
  169. package/src/lib/async-hook-supervisor.cjs +54 -0
  170. package/src/lib/capability-manifest.js +319 -0
  171. package/src/lib/capability-negotiation.js +23 -12
  172. package/src/lib/channels/channel-manager.js +54 -2
  173. package/src/lib/cli-context-engineering.js +9 -2
  174. package/src/lib/context-breakdown.js +116 -0
  175. package/src/lib/diagnostic-bundle.js +230 -0
  176. package/src/lib/doctor-checkup.js +275 -4
  177. package/src/lib/execution-location.js +286 -0
  178. package/src/lib/headless-manifest.js +13 -40
  179. package/src/lib/hook-event-log.cjs +193 -0
  180. package/src/lib/hook-stats-store.cjs +197 -0
  181. package/src/lib/idempotency.js +109 -0
  182. package/src/lib/interaction-adapter.js +34 -4
  183. package/src/lib/json-schema-output.js +46 -12
  184. package/src/lib/json-schema-validate.js +143 -3
  185. package/src/lib/jsonl-session-store.js +1 -0
  186. package/src/lib/lsp/__tests__/lsp-manager.test.js +58 -0
  187. package/src/lib/lsp/lsp-manager.js +56 -0
  188. package/src/lib/monitor-event.js +290 -0
  189. package/src/lib/operation-fingerprint.js +217 -0
  190. package/src/lib/plugin-runtime/bin.js +37 -0
  191. package/src/lib/plugin-runtime/capabilities.js +19 -0
  192. package/src/lib/plugin-runtime/capability-consent.js +203 -0
  193. package/src/lib/plugin-runtime/hooks.js +37 -0
  194. package/src/lib/plugin-runtime/lsp.js +38 -0
  195. package/src/lib/plugin-runtime/mcp.js +40 -0
  196. package/src/lib/plugin-runtime/monitors.js +37 -0
  197. package/src/lib/plugin-runtime/plugin-options.js +151 -0
  198. package/src/lib/plugin-runtime/policy.js +62 -0
  199. package/src/lib/plugin-runtime/scopes.js +20 -4
  200. package/src/lib/pr-automation-policy.js +224 -0
  201. package/src/lib/project-instructions.js +128 -0
  202. package/src/lib/remote-approval-bridge.js +66 -13
  203. package/src/lib/repl-goal.js +132 -0
  204. package/src/lib/repl-rewind.js +153 -0
  205. package/src/lib/review-comment-anchor.js +241 -0
  206. package/src/lib/review-pipeline.js +46 -7
  207. package/src/lib/runtime-checkup.js +221 -0
  208. package/src/lib/schedule-planner.js +17 -1
  209. package/src/lib/secret-scan.js +409 -0
  210. package/src/lib/session-branch.js +0 -0
  211. package/src/lib/settings-hook-events.cjs +13 -0
  212. package/src/lib/settings-loader.cjs +42 -0
  213. package/src/lib/side-effect-ledger-store.js +88 -0
  214. package/src/lib/side-effect-ledger.js +401 -0
  215. package/src/lib/skill-loader.js +32 -0
  216. package/src/lib/skill-path-scope.js +63 -0
  217. package/src/lib/sub-agent-context.js +110 -9
  218. package/src/lib/sub-agent-registry.js +19 -0
  219. package/src/lib/subagent-inheritance.js +112 -0
  220. package/src/lib/telemetry/span-recorder.js +17 -4
  221. package/src/lib/terminal-context.js +195 -0
  222. package/src/lib/turn-binding-store.js +119 -0
  223. package/src/lib/unattended-action-policy.js +182 -0
  224. package/src/lib/worktree-cleanup-safety.js +138 -0
  225. package/src/lib/worktree-isolator.js +8 -0
  226. package/src/lib/worktree-sparse.js +131 -0
  227. package/src/repl/agent-repl.js +280 -30
  228. package/src/repl/permission-tier.js +28 -0
  229. package/src/runtime/agent-core.js +565 -21
  230. package/src/runtime/coding-agent-contract-shared.cjs +60 -1
  231. package/src/runtime/diagnostics.js +60 -0
  232. package/src/runtime/headless-runner.js +393 -1
  233. package/src/runtime/headless-stream.js +60 -1
  234. package/src/assets/web-panel/assets/MobileProjects-D10VISps.js +0 -1
  235. package/src/assets/web-panel/assets/devWarning-DhiGrbsh.js +0 -1
  236. package/src/assets/web-panel/assets/index-B7a1TrU6.js +0 -1
  237. package/src/assets/web-panel/assets/index-C2B1KRja.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 {
@@ -0,0 +1,132 @@
1
+ /**
2
+ * REPL `/goal <condition>` — a SESSION completion condition for interactive use.
3
+ *
4
+ * Three distinct "goal" concepts, kept separate on purpose:
5
+ * - `cc goal` → long-term, cross-session OKR (goal-store.js).
6
+ * - headless --goal-condition → AUTO re-drives outer turns until met/exhausted.
7
+ * - REPL `/goal <cond>` → THIS: a human is present each turn, so it
8
+ * EVALUATES + REPORTS after every turn whether the condition is met. It does
9
+ * NOT auto-submit follow-ups (that autonomous loop belongs to headless, where
10
+ * no human is watching) — the user decides whether to keep going.
11
+ *
12
+ * This is the pure, clock/dep-injected core over the already-tested
13
+ * goal-condition-engine.js; the agent-repl wiring is thin display + eval glue.
14
+ */
15
+
16
+ import {
17
+ isDeterministicCondition,
18
+ runDeterministicCheck,
19
+ GoalConditionEngine,
20
+ GOAL_DECISION,
21
+ } from "./goal-condition-engine.js";
22
+
23
+ // Interactive sessions are human-paced, so the turn-count budget is generous —
24
+ // it exists only as a backstop (the engine hard-caps at 100 regardless). The
25
+ // meaningful signal in the REPL is met / not-yet-met, not budget exhaustion.
26
+ export const REPL_GOAL_MAX_OUTER_TURNS = 100;
27
+
28
+ /** Parse the `/goal` argument into an intent. Empty → status; a keyword → clear. */
29
+ export function parseGoalCommand(arg) {
30
+ const s = String(arg ?? "").trim();
31
+ if (!s) return { action: "status" };
32
+ if (s === "clear" || s === "off" || s === "stop" || s === "drop")
33
+ return { action: "clear" };
34
+ return { action: "set", spec: s };
35
+ }
36
+
37
+ /**
38
+ * Create the REPL's session goal engine for a spec. Throws (via
39
+ * parseGoalCondition) on a malformed spec so the caller can report it.
40
+ */
41
+ export function createReplGoal(spec, { now = () => 0 } = {}) {
42
+ return new GoalConditionEngine({
43
+ condition: spec,
44
+ budget: { maxOuterTurns: REPL_GOAL_MAX_OUTER_TURNS },
45
+ now,
46
+ });
47
+ }
48
+
49
+ /** Display lines shown when a goal is set. */
50
+ export function renderGoalStart(engine) {
51
+ return [
52
+ `◎ goal set: ${engine.condition.source}`,
53
+ ` evaluated after each turn (≤ ${engine.budget.maxOuterTurns} turns); /goal clear to drop, /goal to check`,
54
+ ];
55
+ }
56
+
57
+ /** Display lines for `/goal` with no argument (status). */
58
+ export function renderGoalStatus(engine) {
59
+ if (!engine)
60
+ return [
61
+ "No session goal. Set one with /goal <condition>",
62
+ " e.g. /goal exit-zero:npm test · /goal file-exists:dist/app.js · /goal contains:ALL GREEN · /goal the tests pass",
63
+ ];
64
+ const s = engine.state;
65
+ return [
66
+ `◎ goal: ${engine.condition.source}`,
67
+ ` progress: outer turn ${s.outerTurns}/${engine.budget.maxOuterTurns}` +
68
+ (isDeterministicCondition(engine.condition)
69
+ ? ` (${engine.condition.kind} check)`
70
+ : " (model-judged)"),
71
+ ];
72
+ }
73
+
74
+ /** Verdict display lines for one evaluation's decision + events. */
75
+ export function renderGoalVerdict(decision, events) {
76
+ const evaluated = (events || []).find((e) => e.type === "goal_evaluated");
77
+ if (decision === GOAL_DECISION.COMPLETE) {
78
+ const done = (events || []).find((e) => e.type === "goal_completed");
79
+ return [
80
+ `✔ goal met: ${done?.reason ?? evaluated?.reason ?? "condition met"}`,
81
+ ];
82
+ }
83
+ if (decision === GOAL_DECISION.EXHAUSTED) {
84
+ const ex = (events || []).find((e) => e.type === "goal_exhausted");
85
+ return [
86
+ `⛔ goal dropped — ${ex?.reason ?? "budget exhausted"}${ex?.limit ? ` (${ex.limit})` : ""}`,
87
+ ];
88
+ }
89
+ return [
90
+ `◎ goal not yet met: ${evaluated?.reason ?? "unmet"} · outer turn ${evaluated?.outerTurns ?? "?"}`,
91
+ ];
92
+ }
93
+
94
+ /**
95
+ * Evaluate the session goal against a completed turn's final text. Deterministic
96
+ * conditions run inline (contains/regex on the text; exit-zero/file-exists via
97
+ * injected `spawnSync`/`existsSync`); a model condition delegates to `judge`.
98
+ *
99
+ * @param {GoalConditionEngine} engine
100
+ * @param {string} finalText the assistant's answer this turn
101
+ * @param {object} deps { cwd, spawnSync, existsSync, judge, tokens, costUsd }
102
+ * judge(condition, {finalText}) => {met, reason, evidence}
103
+ * @returns {Promise<{decision, events, done, met}>}
104
+ */
105
+ export async function evaluateReplGoalTurn(engine, finalText, deps = {}) {
106
+ engine.recordTurnUsage({ tokens: deps.tokens, costUsd: deps.costUsd });
107
+ const cond = engine.condition;
108
+ let evaluation;
109
+ if (isDeterministicCondition(cond)) {
110
+ evaluation = runDeterministicCheck(cond, {
111
+ lastOutput: finalText,
112
+ cwd: deps.cwd,
113
+ spawnSync: deps.spawnSync,
114
+ existsSync: deps.existsSync,
115
+ });
116
+ } else if (typeof deps.judge === "function") {
117
+ evaluation = await deps.judge(cond, { finalText });
118
+ } else {
119
+ evaluation = {
120
+ met: false,
121
+ reason: "no model judge available for this condition",
122
+ evidence: { kind: "model" },
123
+ };
124
+ }
125
+ const { decision, events } = engine.evaluate(evaluation);
126
+ return {
127
+ decision,
128
+ events,
129
+ done: engine.done,
130
+ met: decision === GOAL_DECISION.COMPLETE,
131
+ };
132
+ }
@@ -25,6 +25,18 @@
25
25
  * turn be matched to its pre-mutation snapshot purely from the count.
26
26
  */
27
27
 
28
+ import {
29
+ buildTurnBindingFromMarks,
30
+ resolveRestorePlan,
31
+ RESTORE_SCOPE,
32
+ TURN_COVERAGE,
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 };
39
+
28
40
  export const DEFAULT_LIST_LIMIT = 10;
29
41
  export const PREVIEW_CHARS = 60;
30
42
 
@@ -219,6 +231,147 @@ export function restoreClearedConversation(messages, marks, cleared) {
219
231
  };
220
232
  }
221
233
 
234
+ /**
235
+ * Coverage-aware restore plan for a rewound turn, derived from the REPL's
236
+ * EXISTING implicit state (`listUserTurns()` output + `{atMessageCount,id,tool}`
237
+ * marks) — no new agent-loop events required. Bridges those into the explicit
238
+ * turn-binding table and asks `resolveRestorePlan` for the honest plan, so
239
+ * `/rewind` can warn when a restore can't be fully promised (side-effects, a
240
+ * missing checkpoint, or a conversation/files drift) instead of silently doing a
241
+ * lossy rewind.
242
+ *
243
+ * @param {Array} messages live conversation (for listUserTurns)
244
+ * @param {Array<{atMessageCount:number,id:string,tool?:string}>} marks
245
+ * @param {number} turnIndex message index returned by rewindToTurn()
246
+ * @param {string} [scope] one of RESTORE_SCOPE (default BOTH)
247
+ * @returns {{scope, coverage, conversation, files, warnings:string[]}}
248
+ */
249
+ export function buildRewindPlan(
250
+ messages,
251
+ marks,
252
+ turnIndex,
253
+ scope = RESTORE_SCOPE.BOTH,
254
+ ) {
255
+ const turns = listUserTurns(messages, { limit: 1000 });
256
+ const log = buildTurnBindingFromMarks(turns, marks);
257
+ const turn = log.get(`turn-${Number(turnIndex)}`);
258
+ return resolveRestorePlan(turn, scope);
259
+ }
260
+
261
+ /**
262
+ * Human-readable lines for a rewind plan's coverage + warnings, for `/rewind` to
263
+ * print before it truncates. Returns [] for a fully-restorable turn with no
264
+ * warnings (nothing worth interrupting the user for).
265
+ *
266
+ * @param {{coverage:string, warnings:string[]}} plan from buildRewindPlan()
267
+ * @returns {string[]}
268
+ */
269
+ export function renderRewindWarnings(plan) {
270
+ if (!plan) return [];
271
+ const lines = [];
272
+ if (plan.coverage && plan.coverage !== TURN_COVERAGE.FULL) {
273
+ lines.push(` coverage: ${plan.coverage} (restore is not guaranteed)`);
274
+ }
275
+ for (const w of plan.warnings || []) lines.push(` ⚠ ${w}`);
276
+ return lines;
277
+ }
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
+
222
375
  /** Render the picker list (shared by /rewind and double-Esc). */
223
376
  export function renderTurnList(turns) {
224
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
+ }