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
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persist the explicit turn→checkpoint binding table ([[turn-binding.js]]) as a
|
|
3
|
+
* tamper-evident chained event in the session transcript — the first half of the
|
|
4
|
+
* P1 gap "把绑定作为新链式事件落 jsonl-session-store.js(继承哈希链防篡改,替代
|
|
5
|
+
* 易失的 `_checkpointMarks`)".
|
|
6
|
+
*
|
|
7
|
+
* The REPL's in-process `_checkpointMarks` array is lost the moment the session
|
|
8
|
+
* ends or the process exits, so a resumed session can't offer a coverage-aware
|
|
9
|
+
* rewind for anything from before the restart. Snapshotting the whole
|
|
10
|
+
* `TurnBindingLog` into an append-only `turn_checkpoint_binding` event makes the
|
|
11
|
+
* binding survive resume AND inherit the transcript's hash chain (tamper
|
|
12
|
+
* evidence), exactly like every other session event.
|
|
13
|
+
*
|
|
14
|
+
* All I/O is delegated to the canonical session store via `_deps` so this module
|
|
15
|
+
* stays pure-testable (inject an in-memory append/read pair — no fs, no home dir).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
appendEvent as storeAppendEvent,
|
|
20
|
+
readEvents as storeReadEvents,
|
|
21
|
+
} from "../harness/jsonl-session-store.js";
|
|
22
|
+
import {
|
|
23
|
+
TurnBindingLog,
|
|
24
|
+
resolveRestorePlan,
|
|
25
|
+
selectTurnRange,
|
|
26
|
+
RESTORE_SCOPE,
|
|
27
|
+
} from "./turn-binding.js";
|
|
28
|
+
|
|
29
|
+
/** Event type carrying a full TurnBindingLog snapshot. */
|
|
30
|
+
export const TURN_BINDING_EVENT = "turn_checkpoint_binding";
|
|
31
|
+
|
|
32
|
+
// Injected so tests can drive persistence without touching the real sessions dir.
|
|
33
|
+
export const _deps = {
|
|
34
|
+
appendEvent: storeAppendEvent,
|
|
35
|
+
readEvents: storeReadEvents,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Append the current binding table as a new chained event. Each call writes the
|
|
40
|
+
* FULL table (append-only snapshot), so the newest event is authoritative — a
|
|
41
|
+
* later `loadTurnBindingLog` never has to merge partial deltas across a chain
|
|
42
|
+
* that a compaction could have truncated.
|
|
43
|
+
*
|
|
44
|
+
* Best-effort: a persistence failure must not crash the turn it is recording, so
|
|
45
|
+
* the caller gets `false` instead of a throw.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} sessionId
|
|
48
|
+
* @param {TurnBindingLog|object} log a TurnBindingLog or its toJSON() shape
|
|
49
|
+
* @returns {boolean} true when the snapshot was written
|
|
50
|
+
*/
|
|
51
|
+
export function persistTurnBinding(sessionId, log) {
|
|
52
|
+
if (!sessionId || !log) return false;
|
|
53
|
+
const snapshot = log instanceof TurnBindingLog ? log.toJSON() : log;
|
|
54
|
+
if (!snapshot || !Array.isArray(snapshot.turns)) return false;
|
|
55
|
+
try {
|
|
56
|
+
_deps.appendEvent(sessionId, TURN_BINDING_EVENT, snapshot);
|
|
57
|
+
return true;
|
|
58
|
+
} catch {
|
|
59
|
+
// Persistence is advisory — never let it break the agent turn.
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Rebuild the TurnBindingLog from the LATEST persisted snapshot in a session
|
|
66
|
+
* transcript. Returns an empty log when the session has no binding event (or on
|
|
67
|
+
* any read error) so callers can treat "never persisted" and "unreadable" alike.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} sessionId
|
|
70
|
+
* @returns {TurnBindingLog}
|
|
71
|
+
*/
|
|
72
|
+
export function loadTurnBindingLog(sessionId) {
|
|
73
|
+
let events = [];
|
|
74
|
+
try {
|
|
75
|
+
events = _deps.readEvents(sessionId) || [];
|
|
76
|
+
} catch {
|
|
77
|
+
return new TurnBindingLog();
|
|
78
|
+
}
|
|
79
|
+
// Newest snapshot wins — scan backwards for the first well-formed one.
|
|
80
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
81
|
+
const e = events[i];
|
|
82
|
+
if (
|
|
83
|
+
e &&
|
|
84
|
+
e.type === TURN_BINDING_EVENT &&
|
|
85
|
+
e.data &&
|
|
86
|
+
Array.isArray(e.data.turns)
|
|
87
|
+
) {
|
|
88
|
+
return TurnBindingLog.fromJSON(e.data);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return new TurnBindingLog();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Compose load + plan: resolve the honest restore plan for one turn of a
|
|
96
|
+
* PERSISTED session, so a resumed `/rewind` can show coverage warnings for a
|
|
97
|
+
* turn taken before the restart. Delegates the over-promise guards to
|
|
98
|
+
* `resolveRestorePlan` (unknown turn → a "nothing to restore" plan).
|
|
99
|
+
*
|
|
100
|
+
* @param {string} sessionId
|
|
101
|
+
* @param {string} turnId
|
|
102
|
+
* @param {string} [scope] one of RESTORE_SCOPE
|
|
103
|
+
*/
|
|
104
|
+
export function resolveRestorePlanFromSession(
|
|
105
|
+
sessionId,
|
|
106
|
+
turnId,
|
|
107
|
+
scope = RESTORE_SCOPE.BOTH,
|
|
108
|
+
) {
|
|
109
|
+
const log = loadTurnBindingLog(sessionId);
|
|
110
|
+
return resolveRestorePlan(log.get(turnId), scope);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Compose load + range select for "Summarize from here / up to here" over a
|
|
115
|
+
* persisted session's turns.
|
|
116
|
+
*/
|
|
117
|
+
export function selectTurnRangeFromSession(sessionId, range = {}) {
|
|
118
|
+
return selectTurnRange(loadTurnBindingLog(sessionId), range);
|
|
119
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unattended-action policy — the fail-closed guard behind P1-8's
|
|
3
|
+
* "默认禁止无人值守发布、合并或修改共享基础设施" of
|
|
4
|
+
* CLAUDE_CODE_IDE_INCREMENTAL_GAP_ANALYSIS_2026-07-13.md.
|
|
5
|
+
*
|
|
6
|
+
* A scheduled or event-driven session runs with NO human watching, and P1-8 is
|
|
7
|
+
* explicit that external trigger content is untrusted by default. [[cost-budget.js]]
|
|
8
|
+
* already caps USD spend and [[schedule-planner.js]] decides WHEN a task fires,
|
|
9
|
+
* but nothing decides WHAT a fired task may irreversibly do. Publishing a
|
|
10
|
+
* package, merging a PR, deploying, or mutating shared infrastructure while
|
|
11
|
+
* unattended is exactly the class of action that must never happen ambiently.
|
|
12
|
+
*
|
|
13
|
+
* This module is that pure decision. Given an action's risk class and the run's
|
|
14
|
+
* context (attended? trigger trusted? explicitly allowlisted? within budget?) it
|
|
15
|
+
* returns allow/deny with a named reason. Everything defaults to DENY: an
|
|
16
|
+
* unrecognized action on an unattended run is refused, not assumed safe.
|
|
17
|
+
*
|
|
18
|
+
* PURE: no fs / clock / RNG / process.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Canonical action classes, ordered low → high consequence. */
|
|
22
|
+
export const ACTION_CLASS = Object.freeze({
|
|
23
|
+
READ: "read",
|
|
24
|
+
LOCAL_WRITE: "local_write", // edit files in the worktree
|
|
25
|
+
COMMIT: "commit", // local commit (reversible, not shared)
|
|
26
|
+
PUSH: "push", // push a branch to a remote
|
|
27
|
+
PUBLISH: "publish", // npm publish / release — irreversible & public
|
|
28
|
+
MERGE: "merge", // merge a PR into a shared branch
|
|
29
|
+
DEPLOY: "deploy", // deploy / roll out
|
|
30
|
+
INFRA_MUTATION: "infra_mutation", // modify shared infrastructure
|
|
31
|
+
EXTERNAL_MESSAGE: "external_message", // send mail / chat / webhook outward
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const ACTION_ALIASES = new Map([
|
|
35
|
+
["read", ACTION_CLASS.READ],
|
|
36
|
+
["local_write", ACTION_CLASS.LOCAL_WRITE],
|
|
37
|
+
["write", ACTION_CLASS.LOCAL_WRITE],
|
|
38
|
+
["edit", ACTION_CLASS.LOCAL_WRITE],
|
|
39
|
+
["commit", ACTION_CLASS.COMMIT],
|
|
40
|
+
["push", ACTION_CLASS.PUSH],
|
|
41
|
+
["publish", ACTION_CLASS.PUBLISH],
|
|
42
|
+
["release", ACTION_CLASS.PUBLISH],
|
|
43
|
+
["merge", ACTION_CLASS.MERGE],
|
|
44
|
+
["deploy", ACTION_CLASS.DEPLOY],
|
|
45
|
+
["rollout", ACTION_CLASS.DEPLOY],
|
|
46
|
+
["infra_mutation", ACTION_CLASS.INFRA_MUTATION],
|
|
47
|
+
["infra", ACTION_CLASS.INFRA_MUTATION],
|
|
48
|
+
["external_message", ACTION_CLASS.EXTERNAL_MESSAGE],
|
|
49
|
+
["message", ACTION_CLASS.EXTERNAL_MESSAGE],
|
|
50
|
+
["notify", ACTION_CLASS.EXTERNAL_MESSAGE],
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
/** Actions that are safe to run unattended (local, reversible, not shared). */
|
|
54
|
+
const LOW_RISK = new Set([
|
|
55
|
+
ACTION_CLASS.READ,
|
|
56
|
+
ACTION_CLASS.LOCAL_WRITE,
|
|
57
|
+
ACTION_CLASS.COMMIT,
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Actions that touch a shared / public / irreversible surface and therefore
|
|
62
|
+
* require attendance (or an explicit allowlist entry) when unattended.
|
|
63
|
+
*/
|
|
64
|
+
const HIGH_RISK = new Set([
|
|
65
|
+
ACTION_CLASS.PUBLISH,
|
|
66
|
+
ACTION_CLASS.MERGE,
|
|
67
|
+
ACTION_CLASS.DEPLOY,
|
|
68
|
+
ACTION_CLASS.INFRA_MUTATION,
|
|
69
|
+
ACTION_CLASS.EXTERNAL_MESSAGE,
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
/** Normalize an action label to a canonical class, or null when unrecognized. */
|
|
73
|
+
export function normalizeActionClass(value) {
|
|
74
|
+
if (typeof value !== "string") return null;
|
|
75
|
+
return ACTION_ALIASES.get(value.trim().toLowerCase()) || null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Agent tools whose effect maps to a high-risk action class that can be cleanly
|
|
80
|
+
* blocked at the tool layer. `git push` / npm-publish-via-shell are deliberately
|
|
81
|
+
* NOT here — they ride the `git` / `run_shell` tools (which also do read-only
|
|
82
|
+
* work) and stay governed by the shell policy, not a blanket tool removal.
|
|
83
|
+
*/
|
|
84
|
+
export const TOOL_ACTION_CLASS = Object.freeze({
|
|
85
|
+
publish_artifact: ACTION_CLASS.PUBLISH,
|
|
86
|
+
notify: ACTION_CLASS.EXTERNAL_MESSAGE,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The agent tools an UNATTENDED run must be denied — the enforcement projection
|
|
91
|
+
* of `evaluateUnattendedAction` onto the tool layer (P1-8). A scheduled
|
|
92
|
+
* `cc agent` run passes the result as `--disallowed-tools`, so the model can
|
|
93
|
+
* never even call a denied high-risk tool. An attended run — or one whose class
|
|
94
|
+
* is on the allowlist — yields no restriction. PURE; result is sorted so the
|
|
95
|
+
* spawned argv is stable.
|
|
96
|
+
*
|
|
97
|
+
* @param {{attended?:boolean, allowlist?:string[], trigger?:object, budgetExhausted?:boolean}} p
|
|
98
|
+
* @returns {string[]} tool names to disallow
|
|
99
|
+
*/
|
|
100
|
+
export function unattendedDisallowedTools(p = {}) {
|
|
101
|
+
const out = [];
|
|
102
|
+
for (const [tool, actionClass] of Object.entries(TOOL_ACTION_CLASS)) {
|
|
103
|
+
const verdict = evaluateUnattendedAction({
|
|
104
|
+
actionClass,
|
|
105
|
+
attended: p.attended === true,
|
|
106
|
+
allowlist: p.allowlist,
|
|
107
|
+
trigger: p.trigger,
|
|
108
|
+
budgetExhausted: p.budgetExhausted,
|
|
109
|
+
});
|
|
110
|
+
if (!verdict.allow) out.push(tool);
|
|
111
|
+
}
|
|
112
|
+
return out.sort();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Coarse risk tier for an action: "low" | "high" | "conditional" | "unknown". */
|
|
116
|
+
export function classifyActionRisk(actionClass) {
|
|
117
|
+
const a = normalizeActionClass(actionClass);
|
|
118
|
+
if (a == null) return "unknown";
|
|
119
|
+
if (LOW_RISK.has(a)) return "low";
|
|
120
|
+
if (HIGH_RISK.has(a)) return "high";
|
|
121
|
+
return "conditional"; // push — depends on branch protection
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Decide whether a fired task may perform one action. Fail-closed.
|
|
126
|
+
*
|
|
127
|
+
* @param {object} params
|
|
128
|
+
* @param {string} params.actionClass action label (normalized)
|
|
129
|
+
* @param {boolean} params.attended a human is watching this run
|
|
130
|
+
* @param {object} [params.trigger] {trusted?:boolean} — event source trust
|
|
131
|
+
* @param {string[]} [params.allowlist] action classes pre-approved for unattended
|
|
132
|
+
* @param {boolean} [params.protectedBranch] the push/merge targets a protected branch
|
|
133
|
+
* @param {boolean} [params.budgetExhausted] the run's budget is spent
|
|
134
|
+
* @returns {{allow:boolean, reason:string}}
|
|
135
|
+
*/
|
|
136
|
+
export function evaluateUnattendedAction(params = {}) {
|
|
137
|
+
const action = normalizeActionClass(params.actionClass);
|
|
138
|
+
|
|
139
|
+
// A hard budget cap blocks everything, attended or not.
|
|
140
|
+
if (params.budgetExhausted === true) {
|
|
141
|
+
return { allow: false, reason: "budget-exhausted" };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Unrecognized action: a human may vet it, but an unattended run must not.
|
|
145
|
+
if (action == null) {
|
|
146
|
+
return params.attended === true
|
|
147
|
+
? { allow: true, reason: "attended" }
|
|
148
|
+
: { allow: false, reason: "unknown-action-unattended" };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// A watching human can authorize anything within budget.
|
|
152
|
+
if (params.attended === true) return { allow: true, reason: "attended" };
|
|
153
|
+
|
|
154
|
+
// ── Unattended from here on ──
|
|
155
|
+
const allowlist = new Set(
|
|
156
|
+
(Array.isArray(params.allowlist) ? params.allowlist : [])
|
|
157
|
+
.map((a) => normalizeActionClass(a))
|
|
158
|
+
.filter(Boolean),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// A push to a PROTECTED branch is effectively a merge into shared state.
|
|
162
|
+
const effective =
|
|
163
|
+
action === ACTION_CLASS.PUSH && params.protectedBranch === true
|
|
164
|
+
? ACTION_CLASS.MERGE
|
|
165
|
+
: action;
|
|
166
|
+
|
|
167
|
+
if (LOW_RISK.has(effective)) return { allow: true, reason: "low-risk" };
|
|
168
|
+
|
|
169
|
+
// A plain push to an unprotected branch is allowed unattended.
|
|
170
|
+
if (effective === ACTION_CLASS.PUSH)
|
|
171
|
+
return { allow: true, reason: "push-unprotected" };
|
|
172
|
+
|
|
173
|
+
// High-risk unattended: external trigger content is untrusted by default, and
|
|
174
|
+
// even a trusted trigger must be on the explicit allowlist.
|
|
175
|
+
if (params.trigger && params.trigger.trusted === false) {
|
|
176
|
+
return { allow: false, reason: "untrusted-trigger" };
|
|
177
|
+
}
|
|
178
|
+
if (!allowlist.has(effective)) {
|
|
179
|
+
return { allow: false, reason: "requires-attendance" };
|
|
180
|
+
}
|
|
181
|
+
return { allow: true, reason: "allowlisted" };
|
|
182
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worktree cleanup-safety gate — the fail-closed pre-removal check from P1-5
|
|
3
|
+
* (统一后台 Agent / Worktree 工作台) of
|
|
4
|
+
* CLAUDE_CODE_IDE_INCREMENTAL_GAP_ANALYSIS_2026-07-13.md:
|
|
5
|
+
*
|
|
6
|
+
* "清理前检查未提交修改、未追踪文件、未 Push Commit 和关联 PR。"
|
|
7
|
+
*
|
|
8
|
+
* [[agent-worktree.js]] `finishAgentWorktree()` already refuses to remove a
|
|
9
|
+
* worktree with uncommitted changes or new commits, but it collapses "dirty"
|
|
10
|
+
* into one flag and does NOT distinguish untracked files, does NOT know whether
|
|
11
|
+
* commits were pushed, and has no notion of a linked PR — so a background agent
|
|
12
|
+
* that committed + pushed but left an OPEN PR, or left brand-new untracked
|
|
13
|
+
* scratch files, could still be reaped by a broader sweep.
|
|
14
|
+
*
|
|
15
|
+
* This module is the PURE decision behind any worktree reaper: given a plain
|
|
16
|
+
* state descriptor (whatever the caller gathered from `git status --porcelain`,
|
|
17
|
+
* `git rev-list @{u}..HEAD`, the PR store, …) it returns whether the worktree is
|
|
18
|
+
* safe to remove, and — like [[pr-automation-policy.js]] — it EXHAUSTIVELY
|
|
19
|
+
* collects every blocker so the caller can show the user all of them at once.
|
|
20
|
+
*
|
|
21
|
+
* Everything defaults to KEEP: an unreadable or under-described worktree is
|
|
22
|
+
* never removed, because we must never destroy work we cannot verify is safe to
|
|
23
|
+
* lose (the same invariant `finishAgentWorktree` guards with its try/catch).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** Named blockers — one per reason a worktree must be kept, not reaped. */
|
|
27
|
+
export const CLEANUP_BLOCKER = Object.freeze({
|
|
28
|
+
UNVERIFIABLE: "unverifiable", // git state could not be read → fail-closed keep
|
|
29
|
+
UNCOMMITTED: "uncommitted-changes", // tracked files modified but not committed
|
|
30
|
+
UNTRACKED: "untracked-files", // new files git isn't tracking yet
|
|
31
|
+
UNPUSHED: "unpushed-commits", // commits beyond base not on any remote
|
|
32
|
+
LINKED_PR: "linked-pr", // an open PR references this branch
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const BLOCKER_REASON = Object.freeze({
|
|
36
|
+
[CLEANUP_BLOCKER.UNVERIFIABLE]:
|
|
37
|
+
"git state could not be read — refusing to remove work that cannot be verified safe to lose",
|
|
38
|
+
[CLEANUP_BLOCKER.UNCOMMITTED]: "tracked files have uncommitted modifications",
|
|
39
|
+
[CLEANUP_BLOCKER.UNTRACKED]: "untracked files would be lost on removal",
|
|
40
|
+
[CLEANUP_BLOCKER.UNPUSHED]:
|
|
41
|
+
"commits beyond the base are not pushed to any remote",
|
|
42
|
+
[CLEANUP_BLOCKER.LINKED_PR]:
|
|
43
|
+
"an open pull request still references this branch",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function toCount(v) {
|
|
47
|
+
const n = Number(v);
|
|
48
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function prList(v) {
|
|
52
|
+
if (!Array.isArray(v)) return [];
|
|
53
|
+
return v.filter((p) => p != null);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Decide whether commits on this worktree are unpushed. A caller that already
|
|
58
|
+
* computed push-state passes an explicit `unpushed` boolean, which wins. Else it
|
|
59
|
+
* is derived: a worktree whose HEAD equals its base has no new commits;
|
|
60
|
+
* otherwise, with a known upstream, unpushed iff `aheadCount > 0`; WITHOUT a
|
|
61
|
+
* known upstream, any divergence from base is treated as unpushed (there is
|
|
62
|
+
* nowhere it could have been pushed to) — the fail-closed reading.
|
|
63
|
+
*/
|
|
64
|
+
function commitsAreUnpushed(state) {
|
|
65
|
+
if (state.unpushed === true) return true;
|
|
66
|
+
if (state.unpushed === false) return false;
|
|
67
|
+
const { baseSha, headSha, hasUpstream, aheadCount } = state;
|
|
68
|
+
const hasNewCommits =
|
|
69
|
+
headSha != null && baseSha != null && String(headSha) !== String(baseSha);
|
|
70
|
+
if (!hasNewCommits) {
|
|
71
|
+
// Base/head unknown but the caller told us we're ahead → still unpushed.
|
|
72
|
+
return toCount(aheadCount) > 0;
|
|
73
|
+
}
|
|
74
|
+
if (hasUpstream === true) return toCount(aheadCount) > 0;
|
|
75
|
+
return true; // new commits + no upstream → nowhere pushed
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Evaluate a worktree's cleanup safety. EXHAUSTIVE + fail-closed.
|
|
80
|
+
*
|
|
81
|
+
* @param {object} state
|
|
82
|
+
* @param {boolean} [state.readable] false / omitted when git status could not be read
|
|
83
|
+
* @param {boolean} [state.uncommitted] tracked modifications present
|
|
84
|
+
* @param {boolean} [state.untracked] untracked files present
|
|
85
|
+
* @param {string} [state.baseSha] worktree base commit
|
|
86
|
+
* @param {string} [state.headSha] worktree HEAD commit
|
|
87
|
+
* @param {boolean} [state.hasUpstream] branch has a configured upstream
|
|
88
|
+
* @param {number} [state.aheadCount] commits ahead of upstream
|
|
89
|
+
* @param {boolean} [state.unpushed] explicit push-state (wins over derivation)
|
|
90
|
+
* @param {Array} [state.linkedPrs] open PRs referencing this branch
|
|
91
|
+
* @returns {{safeToRemove:boolean, blockers:string[], reasons:object}}
|
|
92
|
+
*/
|
|
93
|
+
export function evaluateWorktreeCleanup(state = {}) {
|
|
94
|
+
const blockers = [];
|
|
95
|
+
|
|
96
|
+
// Unverifiable state is the hard fail-closed gate: if we could not read the
|
|
97
|
+
// worktree, we cannot prove anything else, so keep and report only this.
|
|
98
|
+
if (state.readable === false) {
|
|
99
|
+
blockers.push(CLEANUP_BLOCKER.UNVERIFIABLE);
|
|
100
|
+
return finalize(blockers);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (state.uncommitted === true) blockers.push(CLEANUP_BLOCKER.UNCOMMITTED);
|
|
104
|
+
if (state.untracked === true) blockers.push(CLEANUP_BLOCKER.UNTRACKED);
|
|
105
|
+
if (commitsAreUnpushed(state)) blockers.push(CLEANUP_BLOCKER.UNPUSHED);
|
|
106
|
+
if (prList(state.linkedPrs).length > 0)
|
|
107
|
+
blockers.push(CLEANUP_BLOCKER.LINKED_PR);
|
|
108
|
+
|
|
109
|
+
return finalize(blockers);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function finalize(blockers) {
|
|
113
|
+
const reasons = {};
|
|
114
|
+
for (const b of blockers) reasons[b] = BLOCKER_REASON[b];
|
|
115
|
+
return {
|
|
116
|
+
safeToRemove: blockers.length === 0,
|
|
117
|
+
blockers,
|
|
118
|
+
reasons,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* A human-readable one-line summary of a cleanup decision — never echoes file
|
|
124
|
+
* contents or PR bodies, only counts and named blockers.
|
|
125
|
+
*/
|
|
126
|
+
export function summarizeWorktreeCleanup(state = {}) {
|
|
127
|
+
const { safeToRemove, blockers } = evaluateWorktreeCleanup(state);
|
|
128
|
+
if (safeToRemove)
|
|
129
|
+
return "safe to remove — no uncommitted, untracked, unpushed or PR-linked work";
|
|
130
|
+
const parts = blockers.map((b) => {
|
|
131
|
+
if (b === CLEANUP_BLOCKER.LINKED_PR) {
|
|
132
|
+
const n = prList(state.linkedPrs).length;
|
|
133
|
+
return `${n} open PR${n === 1 ? "" : "s"}`;
|
|
134
|
+
}
|
|
135
|
+
return b;
|
|
136
|
+
});
|
|
137
|
+
return `keep — ${parts.join(", ")}`;
|
|
138
|
+
}
|
|
@@ -5,9 +5,17 @@ export {
|
|
|
5
5
|
pruneWorktrees,
|
|
6
6
|
isolateTask,
|
|
7
7
|
cleanupAgentWorktrees,
|
|
8
|
+
assessAgentWorktreeCleanup,
|
|
8
9
|
diffWorktree,
|
|
9
10
|
previewWorktreeMerge,
|
|
10
11
|
applyWorktreeAutomationCandidate,
|
|
11
12
|
mergeWorktree,
|
|
12
13
|
worktreeLog,
|
|
13
14
|
} from "../harness/worktree-isolator.js";
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
normalizeSparsePaths,
|
|
18
|
+
planSymlinkDirectories,
|
|
19
|
+
isContainedPath,
|
|
20
|
+
isSafeRelPath,
|
|
21
|
+
} from "./worktree-sparse.js";
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worktree sparse-checkout + dependency-symlink planning (pure core).
|
|
3
|
+
*
|
|
4
|
+
* P1 "large monorepo" pillar: only check out the packages a task needs
|
|
5
|
+
* (`worktree.sparsePaths`) and reuse explicitly-approved dependency
|
|
6
|
+
* directories from the main checkout via junction/symlink
|
|
7
|
+
* (`worktree.symlinkDirectories`), with junction/symlink escape defense.
|
|
8
|
+
*
|
|
9
|
+
* Pure + deterministic: no fs, no git, no clock. The isolator does the
|
|
10
|
+
* actual sparse-checkout and link creation from the plans produced here.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { resolve, join } from "node:path";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Safe repo-relative path predicate — mirrors the reject rules of
|
|
17
|
+
* `git-integration.assertSafeGitPath` but as a boolean, so callers can
|
|
18
|
+
* filter/normalize instead of throwing. Rejects absolute paths, `..`
|
|
19
|
+
* traversal, flag injection, empty strings, and shell metacharacters.
|
|
20
|
+
*/
|
|
21
|
+
export function isSafeRelPath(p) {
|
|
22
|
+
if (typeof p !== "string") return false;
|
|
23
|
+
const s = p.trim();
|
|
24
|
+
if (s.length === 0) return false;
|
|
25
|
+
return (
|
|
26
|
+
/^[A-Za-z0-9._/+@=, -]+$/.test(s) && // safe chars only
|
|
27
|
+
!s.startsWith("-") && // no flag injection
|
|
28
|
+
!s.startsWith("/") && // no absolute (posix)
|
|
29
|
+
!/^[A-Za-z]:/.test(s) && // no absolute (windows drive)
|
|
30
|
+
!s.includes("..") // no traversal
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Normalize a raw relative path to git's forward-slash form. Converts
|
|
36
|
+
* backslash separators (Windows-style `packages\cli`) and trims trailing
|
|
37
|
+
* slashes, but keeps any LEADING slash so absolute paths are still rejected
|
|
38
|
+
* by the safety check (never silently reinterpreted as repo-relative).
|
|
39
|
+
*/
|
|
40
|
+
function toGitRel(p) {
|
|
41
|
+
return String(p).trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Normalize a raw path (backslash → slash, trim slashes) THEN validate — so
|
|
46
|
+
* Windows-style config (`packages\cli`) is accepted, not silently dropped.
|
|
47
|
+
* Returns the safe git-relative form, or null when unsafe/empty.
|
|
48
|
+
*/
|
|
49
|
+
function safeGitRel(raw) {
|
|
50
|
+
if (typeof raw !== "string") return null;
|
|
51
|
+
const rel = toGitRel(raw);
|
|
52
|
+
if (rel.length === 0) return null;
|
|
53
|
+
return isSafeRelPath(rel) ? rel : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Validate + normalize sparse-checkout include paths.
|
|
58
|
+
*
|
|
59
|
+
* @param {string[]|string|null|undefined} paths
|
|
60
|
+
* @returns {string[]|null} sorted, de-duped, forward-slash repo-relative
|
|
61
|
+
* directories, or null when nothing valid is requested (caller keeps a
|
|
62
|
+
* full checkout — byte-identical to the no-sparse path).
|
|
63
|
+
*/
|
|
64
|
+
export function normalizeSparsePaths(paths) {
|
|
65
|
+
if (paths == null) return null;
|
|
66
|
+
const list = Array.isArray(paths) ? paths : [paths];
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
for (const raw of list) {
|
|
69
|
+
const rel = safeGitRel(raw);
|
|
70
|
+
if (rel) seen.add(rel);
|
|
71
|
+
}
|
|
72
|
+
if (seen.size === 0) return null;
|
|
73
|
+
return [...seen].sort();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** True when `child` resolves to `parent` itself or a descendant of it. */
|
|
77
|
+
export function isContainedPath(child, parent) {
|
|
78
|
+
const p = resolve(parent);
|
|
79
|
+
const c = resolve(child);
|
|
80
|
+
if (c === p) return true;
|
|
81
|
+
// Append a separator so `/repo-evil` is not treated as inside `/repo`.
|
|
82
|
+
const sep = p.endsWith("/") || p.endsWith("\\") ? "" : "/";
|
|
83
|
+
const cNorm = c.replace(/\\/g, "/");
|
|
84
|
+
const pNorm = (p + sep).replace(/\\/g, "/");
|
|
85
|
+
return cNorm.startsWith(pNorm);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Plan dependency-directory links from the main checkout into a worktree.
|
|
90
|
+
*
|
|
91
|
+
* Each entry must be an explicitly-approved, safe repo-relative directory
|
|
92
|
+
* (e.g. `node_modules`, `packages/cli/node_modules`). The plan asserts both
|
|
93
|
+
* lexical safety (no `..`, no absolute, no flag) AND resolved containment:
|
|
94
|
+
* the source stays inside `repoDir` and the destination stays inside
|
|
95
|
+
* `worktreePath`. This defeats junction/symlink escape where a crafted name
|
|
96
|
+
* would place the link (or its target) outside the intended roots.
|
|
97
|
+
*
|
|
98
|
+
* @throws {Error} on any unsafe/escaping entry — fail-closed, never a
|
|
99
|
+
* silent skip, so a bad config cannot quietly link outside the sandbox.
|
|
100
|
+
* @returns {{name:string, source:string, dest:string}[]}
|
|
101
|
+
*/
|
|
102
|
+
export function planSymlinkDirectories(dirs, { repoDir, worktreePath } = {}) {
|
|
103
|
+
if (dirs == null) return [];
|
|
104
|
+
if (!repoDir || !worktreePath) {
|
|
105
|
+
throw new Error("planSymlinkDirectories requires repoDir and worktreePath");
|
|
106
|
+
}
|
|
107
|
+
const list = Array.isArray(dirs) ? dirs : [dirs];
|
|
108
|
+
const plan = [];
|
|
109
|
+
const seen = new Set();
|
|
110
|
+
for (const raw of list) {
|
|
111
|
+
const rel = safeGitRel(raw);
|
|
112
|
+
if (!rel) {
|
|
113
|
+
throw new Error(`Unsafe symlink directory: ${JSON.stringify(raw)}`);
|
|
114
|
+
}
|
|
115
|
+
if (seen.has(rel)) continue;
|
|
116
|
+
seen.add(rel);
|
|
117
|
+
|
|
118
|
+
const source = resolve(join(repoDir, rel));
|
|
119
|
+
const dest = resolve(join(worktreePath, rel));
|
|
120
|
+
if (!isContainedPath(source, repoDir)) {
|
|
121
|
+
throw new Error(`Symlink source escapes repo: ${JSON.stringify(raw)}`);
|
|
122
|
+
}
|
|
123
|
+
if (!isContainedPath(dest, worktreePath)) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Symlink target escapes worktree: ${JSON.stringify(raw)}`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
plan.push({ name: rel, source, dest });
|
|
129
|
+
}
|
|
130
|
+
return plan;
|
|
131
|
+
}
|