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.
- package/package.json +3 -1
- package/src/assets/web-panel/assets/{AIOps-ClI2ALgv.js → AIOps-BqRkaHGb.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BDjRcJaK.js → ActionButton-DWO7OZLC.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DikSRsaT.js → Analytics-C5oaVezM.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-9mLhMFmz.js → AppLayout-CsUESDoZ.js} +3 -3
- package/src/assets/web-panel/assets/{Artifacts-BSr88BrH.js → Artifacts-Dragqu5k.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CdvyimOF.js → Audit-CQsaHctY.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-D3_hCedh.js → BackgroundAgents-DdG1jNSM.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-Cyvv9RHf.js → Backup-DN6sjK4z.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-BZM14kt8.js → BaseInput-Cxm_ZTCl.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DrQSokVz.js → Chat-DVXGmA56.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-HwJR-C6B.js → ChatBubbleRenderer-BdaCL_iE.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-bFLYeWMV.js → Checkbox-DEc4vD_A.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-7mN7xJXT.js → Codegen-CKiTcv36.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DAB5ZWm7.js → Col-Br9UUNN0.js} +1 -1
- package/src/assets/web-panel/assets/{Community-B_1TymaK.js → Community-NeUH_70T.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-Bq1uSWLJ.js → Compact-BqR-z2Df.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CyiE1jEe.js → Compliance-DcWB_SgZ.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-Cpk0D7vm.js → Cowork-BfhgcudF.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-d_Lv3rdB.js → Cron-LaU29LgD.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DMqhHjMR.js → Crosschain-B2ZB9uwW.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Br-oPuBw.js → DID-BuHl38A2.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CfLW_RaP.js → Dashboard-m627yqvZ.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-DZnTOExV.js → Dropdown-C7bxF8F5.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-DHIUHDv1.js → EmailListRenderer-DPLPQzLp.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-Bw3t3myu.js → FamilyGuardDashboard-cOiH6ike.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-D9cpQPAY.js → Federation-Bvkzg88z.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Jdczr9Mr.js → FormItemContext-BTsiA7h_.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BT8ZMpC5.js → GenericCardRenderer-BGnVE5qu.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DjARip2b.js → Git-DS_j6r25.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BgWbSRcj.js → Governance-CKRG3Rsl.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CfouJciM.js → Inference-DW8INoNQ.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-50l7mrAr.js → KnowledgeGraph-BhEYeFhC.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-rq50H84B.js → Logs-wqi0uxuR.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-DUPIEdQH.js → MarkdownRenderer-DS8dE8ty.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-BYgPDmHU.js → Marketplace-CMlTuPLT.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-5DVgcFdx.js → McpTools-CSx4m5JJ.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-B70LPk7f.js → Memory-vnQCUy-_.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-De8ssA5Y.js → MobileBridge-BV2NA742.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-Bw0k8e1B.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-RJsOTsIA.js → Mtc-CzK_FPB1.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-Crh-6BEK.js → MtcAudit-Cb7G9oL3.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-wuoR5l3x.js → Multisig-qJxlUyzr.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-U2Mji4fZ.js → NLProgramming-DqaBJ7Te.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CdTZAyDH.js → Notes-CwhTJkRi.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-C-FucWE7.js → NotificationSettings-YDs4Fal4.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-DCYFTeaI.js → OrderTableRenderer-CvEALpq7.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-an3OF85L.js → Organization-CEmQm9n8.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-C-1vmYFe.js → Overflow-zby_P_LA.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-Cwh47xKg.js → P2P-DsmDNFNT.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-Dm38GT0k.js → PdhVaultBrowser-FWwFlZGF.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-DS2Y8awD.js → Permissions-DuzQ6me5.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BQNziA0I.js → PersonalDataHub-DZbeN613.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-DF85L1ue.js → Pipeline-CoNSVAUS.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BuLornF-.js → Privacy-OYJEAaMC.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-C3Y31IwX.js → ProjectInit-CWIapV--.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BCPiI8aD.js → ProjectSettings-CX8xMxZn.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-BZ6s4k6g.js → Projects-BLCN8YyY.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-D3Wp022X.js → Providers-BV_sxp75.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-CUJfQkH4.js → QrScannerModal-BTnRrHRp.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-UAc0EO8l.js → QuickAsk-D1KZg018.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BaxKupcs.js → Recommend-COm5cuNg.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-C75NNnXZ.js → RemoteSession-0saWEkYI.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-BVXynZAk.js → Reputation-Dr7iKLS2.js} +1 -1
- package/src/assets/web-panel/assets/{Row-C5MRZ0bi.js → Row-C1vpPBcr.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-DAKkEYka.js → RssFeed-CxlWrtNj.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DvWv9YOA.js → Search-BT2V8dPN.js} +1 -1
- package/src/assets/web-panel/assets/{Security-B5peafW1.js → Security-bm9V3c5k.js} +4 -4
- package/src/assets/web-panel/assets/{Services-UQmvPEaG.js → Services-CocH1yyX.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-C3bhWpuF.js → Skeleton-DHItlMdd.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DfkKf9_H.js → Skills-C59sLc4w.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DqMSjlBM.js → Sla-Crj4BMc8.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-C-WbxWUX.js → SpeechSettings-DX4KFceY.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CYuHZ4N2.js → SyncSettings-BD_iwQLr.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-CyeAyB9d.js → Tasks-BmCTzYsH.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-BTCeQnpq.js → Templates-BGUaxZYl.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BifQuA9Y.js → Tenant-BS0tJx2O.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-Bx_9Bocf.js → Terminal--tHSAsld.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-BX0RFCOm.js → TimelineRenderer-D9-MFxS4.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-CGHrCmUS.js → Tokens-CiCB485f.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DlqaY4SO.js → Trigger-SmCFuW_l.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DfOmj-ay.js → Trust-DkK3Rqe0.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-B72jH5fE.js → UkeySign-CDM4LGdB.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BT-63fqv.js → VideoEditing-IDB7DJXt.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-Dsul3sHr.js → Wallet-Bikj5cmt.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-Izkwwdgy.js → WebAuthn-C9abWlAw.js} +2 -2
- package/src/assets/web-panel/assets/{WorkflowEditor-D3FHSiH1.js → WorkflowEditor-DrbRKVky.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CmVz5z3U.js → chat-CccCSDQf.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CuLgyJBp.js → colors-CXObXLhb.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-Ml1YkXpw.js → compact-item-B5kfYAxC.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BvjJ48bs.js → createContext-DmiPBfip.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CvKrADNa.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DVC0157C.js → hasIn-Q6lYteAc.js} +1 -1
- package/src/assets/web-panel/assets/{index-C229ABfy.js → index-25Wlqps4.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ck_52dz_.js → index-33U29UNV.js} +1 -1
- package/src/assets/web-panel/assets/{index-QXzlT9KD.js → index-4oXBDczR.js} +1 -1
- package/src/assets/web-panel/assets/{index-4SBta3d7.js → index-BGYXvMc5.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0S8NzO6.js → index-BJCCa0s_.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJyN28Et.js → index-BJJ5DhKI.js} +1 -1
- package/src/assets/web-panel/assets/{index-DC_OI9H2.js → index-BLOzViJ6.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_XuRgGn.js → index-BRrbUMoG.js} +1 -1
- package/src/assets/web-panel/assets/{index-wvb_FtaV.js → index-BUFpT17Q.js} +1 -1
- package/src/assets/web-panel/assets/{index-KMqhcF7l.js → index-BkRDiqGc.js} +1 -1
- package/src/assets/web-panel/assets/{index-vG4XSJ1d.js → index-BsfvW6my.js} +1 -1
- package/src/assets/web-panel/assets/{index-DYXHFr4r.js → index-By-kfPED.js} +1 -1
- package/src/assets/web-panel/assets/{index-DuGszaWY.js → index-BzFTcQsc.js} +1 -1
- package/src/assets/web-panel/assets/{index-CXkJH-lK.js → index-C2FXdboq.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dt5KMjwJ.js → index-C5XOlTHN.js} +1 -1
- package/src/assets/web-panel/assets/{index-4wcgvPoF.js → index-CLhBDS2w.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_H_S-DY.js → index-CMq7UeoT.js} +1 -1
- package/src/assets/web-panel/assets/{index-BydjdZnl.js → index-CVMTWNKl.js} +1 -1
- package/src/assets/web-panel/assets/{index-BjHBpRYZ.js → index-Caghi-yL.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bzr0rB5T.js → index-CdB6FuSH.js} +1 -1
- package/src/assets/web-panel/assets/{index-BbPtzB8M.js → index-CoK8-kWQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BUt8kTIB.js → index-CsO3GUAY.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_vRgTl9.js → index-CwZBa20Z.js} +1 -1
- package/src/assets/web-panel/assets/{index-DukDVXCv.js → index-D-kASucw.js} +1 -1
- package/src/assets/web-panel/assets/{index-BbepULSc.js → index-D8eBw7vW.js} +1 -1
- package/src/assets/web-panel/assets/{index-BfuKP4qe.js → index-D9wgifk4.js} +1 -1
- package/src/assets/web-panel/assets/{index-BiC6dQZG.js → index-DAYCTc07.js} +1 -1
- package/src/assets/web-panel/assets/{index-CmFNvNLZ.js → index-DF6Q8l2h.js} +1 -1
- package/src/assets/web-panel/assets/index-DTdFUWc_.js +1 -0
- package/src/assets/web-panel/assets/{index-KjgKdZe-.js → index-DgTLApS9.js} +1 -1
- package/src/assets/web-panel/assets/{index-BpFJHiTv.js → index-DgxVJ8pg.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cw0Rjy87.js → index-DlJvIqoR.js} +1 -1
- package/src/assets/web-panel/assets/{index-CFYVQMvu.js → index-Dw4eHfsG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CudmwNBV.js → index-Rh-ytwow.js} +3 -3
- package/src/assets/web-panel/assets/{index-DDGaOWBV.js → index-StAEt2pq.js} +1 -1
- package/src/assets/web-panel/assets/{index-D3lD5qap.js → index-bzfw951k.js} +1 -1
- package/src/assets/web-panel/assets/{index-BzG-x9p4.js → index-fSUMbMvv.js} +1 -1
- package/src/assets/web-panel/assets/{index-B_EvG_GW.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-BDgd2YZ8.js → initDefaultProps-BNq7X72N.js} +1 -1
- package/src/assets/web-panel/assets/{motion-ErtrXdYe.js → motion-DOf9viTc.js} +1 -1
- package/src/assets/web-panel/assets/{move-_GPPNCgs.js → move-CRHTcW4x.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DRLL-8V2.js → mtc-parser-Cyoozgm2.js} +1 -1
- package/src/assets/web-panel/assets/{omit-p86sl023.js → omit-6-TjFNru.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-BTu7cMLH.js → pickAttrs-DlUmO1zi.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BEKz6Isr.js → placementArrow-CFOL8mIR.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-Ck42c9MP.js → responsiveObserve-BtaFK5zd.js} +1 -1
- package/src/assets/web-panel/assets/{slide-1CEvuPsB.js → slide-BIbc2bur.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BDfdx1GJ.js → statusUtils-BXX9d6-w.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-B4K46m5w.js → styleChecker-BGv10Bpn.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DoIVpJNq.js → useFlexGapSupport-bKnKT8tw.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-DdAWi-ln.js → useFs-FY4IL4jn.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-iUu6k8dx.js → usePersonalDataHub-Bwl9TEgu.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DZz4R6Tw.js → vnode-BM3OHxAT.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DXlf9sA3.js → zoom-D1fbFiHB.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agenda.js +320 -32
- package/src/commands/agent.js +105 -11
- package/src/commands/context.js +125 -14
- package/src/commands/doctor.js +85 -1
- package/src/commands/hook.js +181 -7
- package/src/commands/plugin.js +456 -1
- package/src/commands/review.js +363 -2
- package/src/commands/session.js +191 -0
- package/src/commands/team.js +19 -1
- package/src/data/changelog.json +10 -4
- package/src/gateways/ws/remote-session-protocol.js +39 -0
- package/src/gateways/ws/session-protocol.js +55 -2
- 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 +318 -10
- 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/channels/channel-manager.js +54 -2
- package/src/lib/cli-context-engineering.js +9 -2
- package/src/lib/context-breakdown.js +116 -0
- package/src/lib/diagnostic-bundle.js +230 -0
- package/src/lib/doctor-checkup.js +275 -4
- package/src/lib/execution-location.js +286 -0
- package/src/lib/headless-manifest.js +13 -40
- package/src/lib/hook-event-log.cjs +193 -0
- package/src/lib/hook-stats-store.cjs +197 -0
- package/src/lib/idempotency.js +109 -0
- package/src/lib/interaction-adapter.js +34 -4
- package/src/lib/json-schema-output.js +46 -12
- package/src/lib/json-schema-validate.js +143 -3
- 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 +203 -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/plugin-runtime/policy.js +62 -0
- package/src/lib/plugin-runtime/scopes.js +20 -4
- package/src/lib/pr-automation-policy.js +224 -0
- package/src/lib/project-instructions.js +128 -0
- package/src/lib/remote-approval-bridge.js +66 -13
- package/src/lib/repl-goal.js +132 -0
- package/src/lib/repl-rewind.js +153 -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/schedule-planner.js +17 -1
- package/src/lib/secret-scan.js +409 -0
- package/src/lib/session-branch.js +0 -0
- package/src/lib/settings-hook-events.cjs +13 -0
- package/src/lib/settings-loader.cjs +42 -0
- package/src/lib/side-effect-ledger-store.js +88 -0
- package/src/lib/side-effect-ledger.js +401 -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 +110 -9
- package/src/lib/sub-agent-registry.js +19 -0
- package/src/lib/subagent-inheritance.js +112 -0
- package/src/lib/telemetry/span-recorder.js +17 -4
- package/src/lib/terminal-context.js +195 -0
- package/src/lib/turn-binding-store.js +119 -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 +280 -30
- package/src/repl/permission-tier.js +28 -0
- package/src/runtime/agent-core.js +565 -21
- package/src/runtime/coding-agent-contract-shared.cjs +60 -1
- package/src/runtime/diagnostics.js +60 -0
- package/src/runtime/headless-runner.js +393 -1
- package/src/runtime/headless-stream.js +60 -1
- package/src/assets/web-panel/assets/MobileProjects-D10VISps.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DhiGrbsh.js +0 -1
- package/src/assets/web-panel/assets/index-B7a1TrU6.js +0 -1
- 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
|
-
|
|
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 {
|
|
@@ -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
|
+
}
|
package/src/lib/repl-rewind.js
CHANGED
|
@@ -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
|
+
}
|