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.
- package/package.json +3 -1
- package/src/assets/web-panel/assets/{AIOps-BQ0kgfA1.js → AIOps-BqRkaHGb.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-Dr1MDANe.js → ActionButton-DWO7OZLC.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CZ5vsaXW.js → Analytics-C5oaVezM.js} +2 -2
- package/src/assets/web-panel/assets/{AppLayout-BIBga881.js → AppLayout-CsUESDoZ.js} +3 -3
- package/src/assets/web-panel/assets/{Artifacts-vpXHRU3u.js → Artifacts-Dragqu5k.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-B8exuoub.js → Audit-CQsaHctY.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-923evRRN.js → BackgroundAgents-DdG1jNSM.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-sr_sswlQ.js → Backup-DN6sjK4z.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CnW-4XKT.js → BaseInput-Cxm_ZTCl.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-FZCsTZIL.js → Chat-DVXGmA56.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-DDPd9qT0.js → ChatBubbleRenderer-BdaCL_iE.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-DFrxB59H.js → Checkbox-DEc4vD_A.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-_b6WEel8.js → Codegen-CKiTcv36.js} +1 -1
- package/src/assets/web-panel/assets/{Col-CkbnnZ-K.js → Col-Br9UUNN0.js} +1 -1
- package/src/assets/web-panel/assets/{Community-BxM_82zW.js → Community-NeUH_70T.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DYinP45R.js → Compact-BqR-z2Df.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DyXp3sSP.js → Compliance-DcWB_SgZ.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BEr2DhW7.js → Cowork-BfhgcudF.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-B-Vwo1hN.js → Cron-LaU29LgD.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DbLxKddd.js → Crosschain-B2ZB9uwW.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BsQ_LFw9.js → DID-BuHl38A2.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-lVlXQHu5.js → Dashboard-m627yqvZ.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-Beo_foO7.js → Dropdown-C7bxF8F5.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-DszHIXED.js → EmailListRenderer-DPLPQzLp.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-DcAwoh81.js → FamilyGuardDashboard-cOiH6ike.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-CuJLa2eb.js → Federation-Bvkzg88z.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Cxd1lxhc.js → FormItemContext-BTsiA7h_.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BBvq5Jsw.js → GenericCardRenderer-BGnVE5qu.js} +1 -1
- package/src/assets/web-panel/assets/{Git-WqPKetx6.js → Git-DS_j6r25.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CWYtY3ke.js → Governance-CKRG3Rsl.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BnX1iiNV.js → Inference-DW8INoNQ.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-BJuM8GN5.js → KnowledgeGraph-BhEYeFhC.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-DqLMNntm.js → Logs-wqi0uxuR.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-BqYwEYou.js → MarkdownRenderer-DS8dE8ty.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-_vGfmhNl.js → Marketplace-CMlTuPLT.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BeJDOkDj.js → McpTools-CSx4m5JJ.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-Da0EEZDO.js → Memory-vnQCUy-_.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-DSuzcCCy.js → MobileBridge-BV2NA742.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-Bw0k8e1B.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-BE9YgdA0.js → Mtc-CzK_FPB1.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-fC2BpO5S.js → MtcAudit-Cb7G9oL3.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-D24Xxqw3.js → Multisig-qJxlUyzr.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-Bs3oGvS8.js → NLProgramming-DqaBJ7Te.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-BFjmAA-3.js → Notes-CwhTJkRi.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-BX_9Bhv0.js → NotificationSettings-YDs4Fal4.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-wjkZwJXI.js → OrderTableRenderer-CvEALpq7.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-DBcdsVEP.js → Organization-CEmQm9n8.js} +2 -2
- package/src/assets/web-panel/assets/{Overflow-DDyaF9vC.js → Overflow-zby_P_LA.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BB78L4he.js → P2P-DsmDNFNT.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-DqTiZFum.js → PdhVaultBrowser-FWwFlZGF.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-INbnWv4j.js → Permissions-DuzQ6me5.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BST-VmTq.js → PersonalDataHub-DZbeN613.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-CE7_tS2k.js → Pipeline-CoNSVAUS.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-hK5L8dOB.js → Privacy-OYJEAaMC.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BvkkUoDn.js → ProjectInit-CWIapV--.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-u7_EUtkm.js → ProjectSettings-CX8xMxZn.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-5Fmt_8Nw.js → Projects-BLCN8YyY.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-B9eanarR.js → Providers-BV_sxp75.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-C0dX5N2f.js → QrScannerModal-BTnRrHRp.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-CKTpG-PZ.js → QuickAsk-D1KZg018.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Px_w_XQM.js → Recommend-COm5cuNg.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-Di0fnYkM.js → RemoteSession-0saWEkYI.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-vesnWjXt.js → Reputation-Dr7iKLS2.js} +1 -1
- package/src/assets/web-panel/assets/{Row-exbY6LkT.js → Row-C1vpPBcr.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-B3O3x70t.js → RssFeed-CxlWrtNj.js} +2 -2
- package/src/assets/web-panel/assets/{Search-uRA0DhNz.js → Search-BT2V8dPN.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CViqdH6K.js → Security-bm9V3c5k.js} +4 -4
- package/src/assets/web-panel/assets/{Services-CkXS97WN.js → Services-CocH1yyX.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DxW5WYnE.js → Skeleton-DHItlMdd.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CLxmGFse.js → Skills-C59sLc4w.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-Cr5fziXq.js → Sla-Crj4BMc8.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-H0JSb3cp.js → SpeechSettings-DX4KFceY.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-DMYWMae7.js → SyncSettings-BD_iwQLr.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-CbYyDdF4.js → Tasks-BmCTzYsH.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-Cpc8JjiK.js → Templates-BGUaxZYl.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-CRh77Jda.js → Tenant-BS0tJx2O.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-4D8tNBO5.js → Terminal--tHSAsld.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-D41jzzbG.js → TimelineRenderer-D9-MFxS4.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-C_jWEy6b.js → Tokens-CiCB485f.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-Dk1ZUhsy.js → Trigger-SmCFuW_l.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-CLVy4l7m.js → Trust-DkK3Rqe0.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-C-FeqFDj.js → UkeySign-CDM4LGdB.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-CqVZzyMm.js → VideoEditing-IDB7DJXt.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DX9BW9pu.js → Wallet-Bikj5cmt.js} +2 -2
- package/src/assets/web-panel/assets/{WebAuthn-7hQDpWw_.js → WebAuthn-C9abWlAw.js} +2 -2
- package/src/assets/web-panel/assets/{WorkflowEditor-6XqUAO0j.js → WorkflowEditor-DrbRKVky.js} +1 -1
- package/src/assets/web-panel/assets/{chat-5R_vkaJG.js → chat-CccCSDQf.js} +1 -1
- package/src/assets/web-panel/assets/{colors-huUPZdC-.js → colors-CXObXLhb.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CfJlgisE.js → compact-item-B5kfYAxC.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-jO8heofr.js → createContext-DmiPBfip.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CvKrADNa.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DY8Vxqx7.js → hasIn-Q6lYteAc.js} +1 -1
- package/src/assets/web-panel/assets/{index-BtEXSoPL.js → index-25Wlqps4.js} +1 -1
- package/src/assets/web-panel/assets/{index-BsBtRWbA.js → index-33U29UNV.js} +1 -1
- package/src/assets/web-panel/assets/{index-BQS-uPOK.js → index-4oXBDczR.js} +1 -1
- package/src/assets/web-panel/assets/{index-DXBcvUnf.js → index-BGYXvMc5.js} +1 -1
- package/src/assets/web-panel/assets/{index-C2iVSFpy.js → index-BJCCa0s_.js} +1 -1
- package/src/assets/web-panel/assets/{index-A48PtXw9.js → index-BJJ5DhKI.js} +1 -1
- package/src/assets/web-panel/assets/{index-Db1cLWir.js → index-BLOzViJ6.js} +1 -1
- package/src/assets/web-panel/assets/{index-vXs3B8GJ.js → index-BRrbUMoG.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHE8OIls.js → index-BUFpT17Q.js} +1 -1
- package/src/assets/web-panel/assets/{index-BmqGerpC.js → index-BkRDiqGc.js} +1 -1
- package/src/assets/web-panel/assets/{index-CzKmteNS.js → index-BsfvW6my.js} +1 -1
- package/src/assets/web-panel/assets/{index-BAON7TzU.js → index-By-kfPED.js} +1 -1
- package/src/assets/web-panel/assets/{index-DoI2sZm7.js → index-BzFTcQsc.js} +1 -1
- package/src/assets/web-panel/assets/{index-j3O2EEJe.js → index-C2FXdboq.js} +1 -1
- package/src/assets/web-panel/assets/{index-CRElRhyS.js → index-C5XOlTHN.js} +1 -1
- package/src/assets/web-panel/assets/{index-KRHroryI.js → index-CLhBDS2w.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bxax9vQ6.js → index-CMq7UeoT.js} +1 -1
- package/src/assets/web-panel/assets/{index-BfCx4pFT.js → index-CVMTWNKl.js} +1 -1
- package/src/assets/web-panel/assets/{index-CA6sO5VD.js → index-Caghi-yL.js} +1 -1
- package/src/assets/web-panel/assets/{index-CA90TZxG.js → index-CdB6FuSH.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cbe9G0V4.js → index-CoK8-kWQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-D0pet5Uy.js → index-CsO3GUAY.js} +1 -1
- package/src/assets/web-panel/assets/{index-ChvHubX3.js → index-CwZBa20Z.js} +1 -1
- package/src/assets/web-panel/assets/{index-BRXOP09X.js → index-D-kASucw.js} +1 -1
- package/src/assets/web-panel/assets/{index-B74OXFxC.js → index-D8eBw7vW.js} +1 -1
- package/src/assets/web-panel/assets/{index-D1VLuww0.js → index-D9wgifk4.js} +1 -1
- package/src/assets/web-panel/assets/{index-p85YaaKw.js → index-DAYCTc07.js} +1 -1
- package/src/assets/web-panel/assets/{index-BliaT02i.js → index-DF6Q8l2h.js} +1 -1
- package/src/assets/web-panel/assets/index-DTdFUWc_.js +1 -0
- package/src/assets/web-panel/assets/{index-Cjxbu68P.js → index-DgTLApS9.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cis_Ip7H.js → index-DgxVJ8pg.js} +1 -1
- package/src/assets/web-panel/assets/{index-v3pTaEQm.js → index-DlJvIqoR.js} +1 -1
- package/src/assets/web-panel/assets/{index-BYLBPpt8.js → index-Dw4eHfsG.js} +1 -1
- package/src/assets/web-panel/assets/{index-B300RPnT.js → index-Rh-ytwow.js} +3 -3
- package/src/assets/web-panel/assets/{index-CLhBh-qE.js → index-StAEt2pq.js} +1 -1
- package/src/assets/web-panel/assets/{index-d1xkbtNz.js → index-bzfw951k.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCqxyvqp.js → index-fSUMbMvv.js} +1 -1
- package/src/assets/web-panel/assets/{index-dKx57io7.js → index-tFHy5Wol.js} +1 -1
- package/src/assets/web-panel/assets/index-zQ0M_kz-.js +1 -0
- package/src/assets/web-panel/assets/{initDefaultProps-MCB7-raZ.js → initDefaultProps-BNq7X72N.js} +1 -1
- package/src/assets/web-panel/assets/{motion-UWDF9swV.js → motion-DOf9viTc.js} +1 -1
- package/src/assets/web-panel/assets/{move-CsTmgXZU.js → move-CRHTcW4x.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-fQy6KGLg.js → mtc-parser-Cyoozgm2.js} +1 -1
- package/src/assets/web-panel/assets/{omit-woPnEjd1.js → omit-6-TjFNru.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-Bs2dZ1gf.js → pickAttrs-DlUmO1zi.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-C7EVgPkg.js → placementArrow-CFOL8mIR.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-BH2kYCTN.js → responsiveObserve-BtaFK5zd.js} +1 -1
- package/src/assets/web-panel/assets/{slide-BFBP2V_e.js → slide-BIbc2bur.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-xdDxDYse.js → statusUtils-BXX9d6-w.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-aAWgPPQf.js → styleChecker-BGv10Bpn.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DkGSAWoO.js → useFlexGapSupport-bKnKT8tw.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-BrOih40N.js → useFs-FY4IL4jn.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-eIYrgMqO.js → usePersonalDataHub-Bwl9TEgu.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-B8lutNSE.js → vnode-BM3OHxAT.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CffNre7V.js → zoom-D1fbFiHB.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agenda.js +71 -14
- package/src/commands/doctor.js +85 -1
- package/src/commands/plugin.js +226 -9
- package/src/commands/review.js +17 -1
- package/src/commands/session.js +191 -0
- package/src/commands/team.js +19 -1
- package/src/data/changelog.json +5 -4
- package/src/gateways/ws/session-protocol.js +50 -0
- package/src/gateways/ws/ws-agent-handler.js +50 -0
- package/src/harness/jsonl-session-store.js +60 -0
- package/src/harness/worktree-isolator.js +140 -5
- package/src/lib/agent-schedule-store.js +30 -1
- package/src/lib/agent-team/team-worktree.js +37 -2
- package/src/lib/agent-tool-admission.js +125 -0
- package/src/lib/app-preview.js +360 -0
- package/src/lib/async-hook-supervisor.cjs +54 -0
- package/src/lib/capability-manifest.js +319 -0
- package/src/lib/capability-negotiation.js +23 -12
- package/src/lib/diagnostic-bundle.js +230 -0
- package/src/lib/doctor-checkup.js +223 -4
- package/src/lib/execution-location.js +286 -0
- package/src/lib/headless-manifest.js +13 -40
- package/src/lib/hook-stats-store.cjs +197 -0
- package/src/lib/idempotency.js +109 -0
- package/src/lib/jsonl-session-store.js +1 -0
- package/src/lib/lsp/__tests__/lsp-manager.test.js +58 -0
- package/src/lib/lsp/lsp-manager.js +56 -0
- package/src/lib/monitor-event.js +290 -0
- package/src/lib/operation-fingerprint.js +217 -0
- package/src/lib/plugin-runtime/bin.js +37 -0
- package/src/lib/plugin-runtime/capabilities.js +19 -0
- package/src/lib/plugin-runtime/capability-consent.js +28 -0
- package/src/lib/plugin-runtime/hooks.js +37 -0
- package/src/lib/plugin-runtime/lsp.js +38 -0
- package/src/lib/plugin-runtime/mcp.js +40 -0
- package/src/lib/plugin-runtime/monitors.js +37 -0
- package/src/lib/plugin-runtime/plugin-options.js +151 -0
- package/src/lib/pr-automation-policy.js +224 -0
- package/src/lib/remote-approval-bridge.js +66 -13
- package/src/lib/repl-rewind.js +101 -0
- package/src/lib/review-comment-anchor.js +241 -0
- package/src/lib/review-pipeline.js +46 -7
- package/src/lib/runtime-checkup.js +221 -0
- package/src/lib/secret-scan.js +409 -0
- package/src/lib/session-branch.js +0 -0
- package/src/lib/side-effect-ledger.js +105 -0
- package/src/lib/skill-loader.js +32 -0
- package/src/lib/skill-path-scope.js +63 -0
- package/src/lib/sub-agent-context.js +36 -4
- package/src/lib/sub-agent-registry.js +19 -0
- package/src/lib/telemetry/span-recorder.js +17 -4
- package/src/lib/terminal-context.js +195 -0
- package/src/lib/unattended-action-policy.js +182 -0
- package/src/lib/worktree-cleanup-safety.js +138 -0
- package/src/lib/worktree-isolator.js +8 -0
- package/src/lib/worktree-sparse.js +131 -0
- package/src/repl/agent-repl.js +152 -38
- package/src/runtime/agent-core.js +191 -13
- package/src/runtime/diagnostics.js +60 -0
- package/src/runtime/headless-runner.js +126 -1
- package/src/assets/web-panel/assets/MobileProjects-D0RAk4e8.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DeIrCfH-.js +0 -1
- package/src/assets/web-panel/assets/index-BxUKMRpP.js +0 -1
- 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
|
-
|
|
28
|
-
|
|
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
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
// closed on timeout.
|
|
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
|
-
|
|
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
|
|
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
|
-
},
|
|
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
|
-
|
|
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 {
|
package/src/lib/repl-rewind.js
CHANGED
|
@@ -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
|
-
|
|
188
|
-
|
|
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
|
|
209
|
+
line,
|
|
191
210
|
category: f.category || (f.categories && f.categories[0]) || "correctness",
|
|
192
211
|
severity: normalizeSeverity(f.severity),
|
|
193
|
-
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 = {};
|