chainlesschain 0.162.165 → 0.162.167
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-DgGZr3PR.js → AIOps-BZJrchIA.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BAOgBIc_.js → ActionButton-Bhlrylt7.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-Ds4GAvNF.js → Analytics-pHtXVQYs.js} +2 -2
- package/src/assets/web-panel/assets/{AppLayout-CoJvCkoF.js → AppLayout-ORpuri4R.js} +4 -4
- package/src/assets/web-panel/assets/{Artifacts-6EgUaE1T.js → Artifacts-eC77yr1_.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-BYie_i74.js → Audit-B2dZIiqz.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-D5gxOhjd.js → BackgroundAgents-COZiTiwP.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-CIsVK2pT.js → Backup-vY_Srl5d.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-ClorP55M.js → BaseInput-Dili5BdM.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CnhTPGGA.js → Chat-EzKBf0XL.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-Bl1VXgwp.js → ChatBubbleRenderer-D1CtRAyy.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-BRavl7TP.js → Checkbox-Beh6BmuW.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BXg_ErNT.js → Codegen-C3fA6kBU.js} +1 -1
- package/src/assets/web-panel/assets/{Col-CfDPzFPq.js → Col-CefMqONH.js} +1 -1
- package/src/assets/web-panel/assets/{Community-Cgo0httP.js → Community-CBAxFJtB.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DU5JcEQM.js → Compact--flHc-sM.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DCH_TVBl.js → Compliance-CPeNLvA6.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-IGb5O2IH.js → Cowork-Bv5yv4BU.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-CPp_Xp9F.js → Cron-C22yMEas.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-Cwrqklzw.js → Crosschain-Dsb2bcpu.js} +1 -1
- package/src/assets/web-panel/assets/{DID-D55sBQwM.js → DID-DAxei4w0.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CedXhGmP.js → Dashboard-DEUwuVZ8.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-Dm6_v3V4.js → Dropdown-BVp4si2j.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-CWjnuE3G.js → EmailListRenderer-CyFkybOG.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-DubTh7y-.js → FamilyGuardDashboard-BWmmsbVx.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DlSM4uJv.js → Federation-Cxw27A5d.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BYhg0vtG.js → FormItemContext-Q9XthzL1.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-Bkh7qjfK.js → GenericCardRenderer-DS5DIdsW.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DSLWPKvn.js → Git-BOliHfxP.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DIj9O8BB.js → Governance-CD4oxa-W.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-Dhep3e5Y.js → Inference-CMlcivo8.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-Cj8vzxB_.js → KnowledgeGraph-BwhERcC4.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-Ba4SsPja.js → Logs-BjVcZR5P.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-BfRhXdyG.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-CGs9IIbf.js → Marketplace-DQLU3E2w.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BTPAoX4K.js → McpTools-CeK0XkpB.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-BJ6ISf2c.js → Memory-DLFkgwaz.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-Ca6n3Dcn.js → MobileBridge-Bh6ymXyj.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-lvilHuLt.js → MobileProjects-D950tUIe.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-CMo0acBV.js → Mtc-Bx5x9_2m.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-Hz5wBvFX.js → MtcAudit-a1-Fexsq.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-BK2aKWE6.js → Multisig-eaw0X8X1.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CExpnYMI.js → NLProgramming-BaNsPV4k.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-B7IFbuBo.js → Notes-C7pevN2X.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-VrGchtb0.js → NotificationSettings-HDqeoP0r.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-CyrNTxkU.js → OrderTableRenderer-DvzVl5T3.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-I0C17GBP.js → Organization-D_xHDlGW.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-CXcWvERe.js → Overflow-B5sRnueL.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-FFSCXrZq.js → P2P-C-IRjOPW.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-1PB-p8Zk.js → PdhVaultBrowser-BqgUPlcU.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-C1YHejsb.js → Permissions-Bm_T1uWw.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-CEMPpgf8.js → PersonalDataHub-DujDllye.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-FoIScdLr.js → Pipeline-DyNPOLIG.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-DDfbEzEj.js → Privacy-CYKTW3iK.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-Dso4kB0O.js → ProjectInit-CGf03uZ0.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-WvDoydDc.js → ProjectSettings-DIec4bTI.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-NJp0kzkd.js → Projects-BY43-bmi.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-BuJXnOLI.js → Providers-RrnUmgPk.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-Bb3lMvGG.js → QrScannerModal-BBWfqd4y.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-Ddxra3fK.js → QuickAsk-BsICSrFp.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BW-dR9qw.js → Recommend-B8y0b4ys.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-Hnm7-Qkm.js → RemoteSession-0QgdB1Oe.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-DKz6meHE.js → Reputation-DBJGM0YO.js} +1 -1
- package/src/assets/web-panel/assets/{Row-BuAsoNTS.js → Row-DtWQ5otv.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-_do9DMIH.js → RssFeed-2zBxHd5H.js} +3 -3
- package/src/assets/web-panel/assets/{Search-CMlcw7ND.js → Search-BofliSkZ.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DhAkfSpM.js → Security-ockRBBEL.js} +3 -3
- package/src/assets/web-panel/assets/{Services-5zKLhy46.js → Services-QyvG7Vd0.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DCMZ3wfv.js → Skeleton-DwbOndX3.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DxQgYCC_.js → Skills-cHWa63sZ.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-CJmE4cSN.js → Sla-BHnTEydQ.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-BgpRTPQp.js → SpeechSettings-DE2hrrLb.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-DkogtYbB.js → SyncSettings-CZ9vnbJH.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-DWgoAHvM.js +1 -0
- package/src/assets/web-panel/assets/{Templates-DAZIu5nN.js → Templates-DiR3ad4X.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BRJMI-su.js → Tenant-NqcW3frV.js} +1 -1
- package/src/assets/web-panel/assets/Terminal-iYeqRQZ0.js +3 -0
- package/src/assets/web-panel/assets/{TimelineRenderer-D4z9aSGD.js → TimelineRenderer-C5GOoxFz.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-zgYA4UOI.js → Tokens-82dz0XdU.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DMOgHX4m.js → Trigger-B3JCNrOi.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-CEtoNwcT.js → Trust-BehFYsN_.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DFx2pQfO.js → UkeySign-BfOxJjVf.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DLssy-7B.js → VideoEditing-DdITCCPz.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DQHc6gTl.js → Wallet-B0EhkCmD.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DgKcODkn.js → WebAuthn-DJ4i1f3-.js} +2 -2
- package/src/assets/web-panel/assets/{WorkflowEditor-cPEWfMOf.js → WorkflowEditor-Dr9XTqIT.js} +1 -1
- package/src/assets/web-panel/assets/{chat-Coj7rSO1.js → chat-Dc2nFyOj.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DiyGfr-6.js → colors-Drw9djBR.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DB63aMXh.js → compact-item-BdLXbU52.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CENpsYHg.js → createContext-BpQMZiWY.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CnfpDzMr.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-B9NOmk0K.js → hasIn-CbYwawHf.js} +1 -1
- package/src/assets/web-panel/assets/{index-BvQq6gX2.js → index-B1ANzVc6.js} +1 -1
- package/src/assets/web-panel/assets/{index-mTK_ZHhE.js → index-B3uRI8_y.js} +1 -1
- package/src/assets/web-panel/assets/{index-D_rZCxxI.js → index-B5KboakZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DAMKhjtT.js → index-B9_u4Qhe.js} +1 -1
- package/src/assets/web-panel/assets/{index-7IMjHwGU.js → index-BAq-4KqY.js} +1 -1
- package/src/assets/web-panel/assets/{index-DqLC0iE4.js → index-BBueKSTR.js} +1 -1
- package/src/assets/web-panel/assets/{index-Kp2AETEw.js → index-BIcEW07n.js} +1 -1
- package/src/assets/web-panel/assets/{index-KDCuTzdx.js → index-BWUoc9-s.js} +1 -1
- package/src/assets/web-panel/assets/{index-nio98wGC.js → index-BZxzdAI0.js} +1 -1
- package/src/assets/web-panel/assets/{index-BxvNtndl.js → index-BiGDrEt4.js} +1 -1
- package/src/assets/web-panel/assets/{index-B45osqnZ.js → index-Blv7c80V.js} +1 -1
- package/src/assets/web-panel/assets/{index-BpmTl8QC.js → index-BmQlot_u.js} +1 -1
- package/src/assets/web-panel/assets/{index-DANoIqXP.js → index-Bo_KwJTF.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dg0v-CXZ.js → index-Bu8Vy-mj.js} +1 -1
- package/src/assets/web-panel/assets/{index-U_snupDl.js → index-C1eo1Qkd.js} +1 -1
- package/src/assets/web-panel/assets/{index-ClEy1v1o.js → index-C3Alz2QD.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bvj07Ab1.js → index-CBwQwtsE.js} +1 -1
- package/src/assets/web-panel/assets/{index-CKNt2vfs.js → index-CJQiSQw7.js} +1 -1
- package/src/assets/web-panel/assets/{index-CXRknr9W.js → index-CKmz3Is-.js} +1 -1
- package/src/assets/web-panel/assets/{index-CRhEJ8JO.js → index-COoIIMbO.js} +1 -1
- package/src/assets/web-panel/assets/index-CYTEB0tb.js +1 -0
- package/src/assets/web-panel/assets/index-CgsEp5bm.js +1 -0
- package/src/assets/web-panel/assets/{index-CxVfbuj9.js → index-CpmlTfpv.js} +1 -1
- package/src/assets/web-panel/assets/{index-CmNCDN5t.js → index-D7Ipfs4F.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0C6zl0y.js → index-DHMeEu-P.js} +1 -1
- package/src/assets/web-panel/assets/{index-DsPDEHOg.js → index-DKQlF7Pw.js} +1 -1
- package/src/assets/web-panel/assets/{index-Yb9EfUnY.js → index-DO7LD_1t.js} +1 -1
- package/src/assets/web-panel/assets/{index-CvTJ-x6b.js → index-DSmZjrFy.js} +1 -1
- package/src/assets/web-panel/assets/{index-C9d8hdgY.js → index-DTeqWdAS.js} +1 -1
- package/src/assets/web-panel/assets/{index-Y_UKQpsV.js → index-DWUN8c01.js} +1 -1
- package/src/assets/web-panel/assets/{index-t6hXF2Cw.js → index-D_lkTnZ0.js} +1 -1
- package/src/assets/web-panel/assets/{index-VDFbduzG.js → index-DyU21xVi.js} +1 -1
- package/src/assets/web-panel/assets/{index-76GMjcbq.js → index-GG_MXjSL.js} +3 -3
- package/src/assets/web-panel/assets/{index-BramLq29.js → index-IG3__pGj.js} +1 -1
- package/src/assets/web-panel/assets/{index-BXzMKVLp.js → index-UX_Mfyfv.js} +1 -1
- package/src/assets/web-panel/assets/{index-CwkK9Kl2.js → index-VIyNlXPX.js} +1 -1
- package/src/assets/web-panel/assets/{index-CAXouogg.js → index-XYe6kBmi.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTuX_MU0.js → index-_vVucYqC.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkoqtJzV.js → index-aHrUfrv1.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-GTwJVIOz.js → initDefaultProps-DybC85E4.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BJQLsPx8.js → motion-BNNuyDg7.js} +1 -1
- package/src/assets/web-panel/assets/{move-DVQM0bdG.js → move-Bm8gSjEN.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-CMXI1I2y.js → mtc-parser-CuHQvzzr.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CAc8MDw9.js → omit-lv4NO7Ng.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-BgqzoyP7.js → pickAttrs-BoSMfjHq.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-B9ebCdRy.js → placementArrow-BOMDSvgz.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-Dy8D0D3n.js → responsiveObserve-C064Ubmu.js} +1 -1
- package/src/assets/web-panel/assets/{slide-Bcrc8J39.js → slide-CeqhbKoG.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-CDW6FOlc.js → statusUtils-CunFoYww.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-CWhJrDON.js → styleChecker-Cxz3wPL7.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-C82c33XI.js → useFlexGapSupport-DJsIaU2S.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-A6m--mih.js → useFs-DnAygceJ.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-m-1laJY5.js → usePersonalDataHub-CrEUY7ta.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-CL9oKKRa.js → vnode-Cq3rN2eT.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DrOyaof3.js → zoom-TMzcOx__.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +75 -0
- package/src/commands/session.js +132 -0
- package/src/data/changelog.json +12 -2
- package/src/harness/jsonl-session-store.js +9 -0
- package/src/harness/session-mirror.js +212 -6
- package/src/lib/async-hook-queue.cjs +288 -0
- package/src/lib/async-hook-supervisor.cjs +52 -0
- package/src/lib/background-session-transport.js +24 -2
- package/src/lib/doctor-checkup.js +184 -4
- package/src/lib/governance-coverage.js +179 -0
- package/src/lib/install-command-policy.js +333 -0
- package/src/lib/jsonl-session-store.js +1 -0
- package/src/lib/lsp/__tests__/lsp-server-registry.test.js +27 -0
- package/src/lib/lsp/lsp-server-registry.js +14 -0
- package/src/lib/packer/smoke-runner.js +85 -15
- package/src/lib/protocol-replay.js +153 -0
- package/src/lib/runtime-checkup.js +33 -0
- package/src/lib/settings-hook-events.cjs +157 -0
- package/src/lib/settings-hooks.cjs +4 -0
- package/src/lib/shell-approval.js +47 -1
- package/src/repl/add-dir.js +59 -1
- package/src/repl/agent-repl.js +85 -3
- package/src/runtime/agent-core.js +35 -0
- package/src/runtime/headless-runner.js +69 -1
- package/src/runtime/headless-stream.js +22 -0
- package/src/runtime/system-prompt.js +39 -12
- package/src/assets/web-panel/assets/MarkdownRenderer-DUjyuVB3.js +0 -1
- package/src/assets/web-panel/assets/Tasks-D5OrwAl9.js +0 -1
- package/src/assets/web-panel/assets/Terminal-CMXLqQKF.js +0 -3
- package/src/assets/web-panel/assets/devWarning-XvRaIiyt.js +0 -1
- package/src/assets/web-panel/assets/index-CbO1zSXV.js +0 -1
- package/src/assets/web-panel/assets/index-aCu2gbUb.js +0 -1
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline protocol replay — the last open pure item of P1-9 ("离线协议回放") in
|
|
3
|
+
* docs/CLAUDE_CODE_IDE_INCREMENTAL_GAP_ANALYSIS_2026-07-13.md.
|
|
4
|
+
*
|
|
5
|
+
* [[event-seq-replay.js]] replays for TRANSPORT reliability (re-send a gap over a
|
|
6
|
+
* lossy socket). This module replays for PROTOCOL COMPAT: take a recorded
|
|
7
|
+
* stream-json session and a negotiation context (server offer × client offer) and
|
|
8
|
+
* answer two offline questions no live peer is needed for —
|
|
9
|
+
*
|
|
10
|
+
* 1. REPLAY: what would these frames look like on the wire under THIS
|
|
11
|
+
* negotiation? (fields whose feature did not survive negotiation are
|
|
12
|
+
* stripped, exactly as the live emitter's field-gate would strip them.)
|
|
13
|
+
* 2. AUDIT: does a recorded session OBEY a claimed negotiation, i.e. does any
|
|
14
|
+
* frame carry a negotiable wire field the negotiation gated OFF? A vN-only
|
|
15
|
+
* client must never receive a vN+1 field; a recording that violates that is
|
|
16
|
+
* a compat bug this catches deterministically, offline, in CI.
|
|
17
|
+
*
|
|
18
|
+
* The set of gateable wire fields ({seq, trace_id, tool_use_id}) is projected
|
|
19
|
+
* from the ONE canonical [[capability-manifest.js]] via `toFieldGate()` — it can
|
|
20
|
+
* NOT drift from the negotiated feature list. All non-negotiable frame fields
|
|
21
|
+
* (type, content, tool name, …) always pass through untouched.
|
|
22
|
+
*
|
|
23
|
+
* PURE + deterministic: no fs / process / clock / RNG. The `digest` lets a golden
|
|
24
|
+
* recording be pinned so any change to negotiation/gating that alters wire output
|
|
25
|
+
* fails a byte/hash compare (same anti-drift philosophy as the capability doc).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { createHash } from "node:crypto";
|
|
29
|
+
import {
|
|
30
|
+
negotiateProtocol,
|
|
31
|
+
applyNegotiationToGate,
|
|
32
|
+
} from "./capability-negotiation.js";
|
|
33
|
+
import { toFieldGate } from "./capability-manifest.js";
|
|
34
|
+
|
|
35
|
+
/** Negotiable wire field names ({seq, trace_id, tool_use_id}), single-sourced. */
|
|
36
|
+
export const GATEABLE_FIELDS = Object.freeze(
|
|
37
|
+
[...new Set(Object.values(toFieldGate()))].sort(),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Run negotiation and fold it into a per-field gate the replay/audit read.
|
|
42
|
+
* On an incompatible (ok:false) negotiation the gate stays empty → every gateable
|
|
43
|
+
* field is treated as OFF (fail-closed safe baseline).
|
|
44
|
+
*
|
|
45
|
+
* @param {object} params
|
|
46
|
+
* @param {object} params.server server offer {protocolVersion,minProtocolVersion,features}
|
|
47
|
+
* @param {object|null} [params.client] client offer, or null/undefined = legacy peer
|
|
48
|
+
* @returns {{negotiation:object, gate:object, enabledFields:string[], gatedFields:string[]}}
|
|
49
|
+
*/
|
|
50
|
+
export function gateFromNegotiation({ server = {}, client = null } = {}) {
|
|
51
|
+
const negotiation = negotiateProtocol(server, client ?? null);
|
|
52
|
+
const gate = applyNegotiationToGate(negotiation, {});
|
|
53
|
+
const enabledFields = GATEABLE_FIELDS.filter((f) => gate[f] === true);
|
|
54
|
+
const gatedFields = GATEABLE_FIELDS.filter((f) => gate[f] !== true);
|
|
55
|
+
return { negotiation, gate, enabledFields, gatedFields };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Strip from a single frame every gateable wire field the gate has OFF, exactly
|
|
60
|
+
* as the live emitter would; all other fields pass through in original order.
|
|
61
|
+
* Returns a shallow copy — the input frame is never mutated.
|
|
62
|
+
*
|
|
63
|
+
* @param {object} frame
|
|
64
|
+
* @param {object} gate {seq?:boolean, trace_id?:boolean, tool_use_id?:boolean}
|
|
65
|
+
* @returns {object}
|
|
66
|
+
*/
|
|
67
|
+
export function replayFrame(frame, gate = {}) {
|
|
68
|
+
if (!frame || typeof frame !== "object") return frame;
|
|
69
|
+
const out = {};
|
|
70
|
+
for (const [k, v] of Object.entries(frame)) {
|
|
71
|
+
if (GATEABLE_FIELDS.includes(k) && gate[k] !== true) continue;
|
|
72
|
+
out[k] = v;
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Replay a recorded session under a negotiation → the frames as they'd appear on
|
|
79
|
+
* the wire (gated-off fields stripped) plus a deterministic digest.
|
|
80
|
+
*
|
|
81
|
+
* @param {object[]} frames recorded stream-json frames
|
|
82
|
+
* @param {{server:object, client?:object|null}} ctx negotiation context
|
|
83
|
+
* @returns {{negotiation:object, gate:object, enabledFields:string[],
|
|
84
|
+
* gatedFields:string[], frames:object[], digest:string}}
|
|
85
|
+
*/
|
|
86
|
+
export function replaySession(frames, ctx = {}) {
|
|
87
|
+
const { negotiation, gate, enabledFields, gatedFields } =
|
|
88
|
+
gateFromNegotiation(ctx);
|
|
89
|
+
const emitted = (Array.isArray(frames) ? frames : []).map((f) =>
|
|
90
|
+
replayFrame(f, gate),
|
|
91
|
+
);
|
|
92
|
+
return {
|
|
93
|
+
negotiation,
|
|
94
|
+
gate,
|
|
95
|
+
enabledFields,
|
|
96
|
+
gatedFields,
|
|
97
|
+
frames: emitted,
|
|
98
|
+
digest: sessionDigest(emitted),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Audit a recorded session against a claimed negotiation WITHOUT stripping:
|
|
104
|
+
* report every frame that carries a gateable wire field the negotiation gated
|
|
105
|
+
* OFF (a forward-compat violation — a field a peer at the agreed version must not
|
|
106
|
+
* have received). Fail-closed: `ok` is true only when there are zero violations.
|
|
107
|
+
*
|
|
108
|
+
* @param {object[]} frames
|
|
109
|
+
* @param {{server:object, client?:object|null}} ctx
|
|
110
|
+
* @returns {{ok:boolean, violations:Array<{index:number, field:string, type:(string|null)}>, gate:object, gatedFields:string[]}}
|
|
111
|
+
*/
|
|
112
|
+
export function auditRecordedSession(frames, ctx = {}) {
|
|
113
|
+
const { gate, gatedFields } = gateFromNegotiation(ctx);
|
|
114
|
+
const violations = [];
|
|
115
|
+
(Array.isArray(frames) ? frames : []).forEach((frame, index) => {
|
|
116
|
+
if (!frame || typeof frame !== "object") return;
|
|
117
|
+
for (const field of GATEABLE_FIELDS) {
|
|
118
|
+
if (
|
|
119
|
+
gate[field] !== true &&
|
|
120
|
+
Object.prototype.hasOwnProperty.call(frame, field)
|
|
121
|
+
) {
|
|
122
|
+
violations.push({
|
|
123
|
+
index,
|
|
124
|
+
field,
|
|
125
|
+
type: typeof frame.type === "string" ? frame.type : null,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
return { ok: violations.length === 0, violations, gate, gatedFields };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Recursively key-sort a value so JSON.stringify is order-independent. */
|
|
134
|
+
function canonical(value) {
|
|
135
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
136
|
+
if (value && typeof value === "object") {
|
|
137
|
+
const out = {};
|
|
138
|
+
for (const k of Object.keys(value).sort()) out[k] = canonical(value[k]);
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Deterministic sha256 over a frame sequence (key-order independent) so a golden
|
|
146
|
+
* recording can be pinned and diffed. Pure.
|
|
147
|
+
* @param {object[]} frames
|
|
148
|
+
* @returns {string} 64-hex digest
|
|
149
|
+
*/
|
|
150
|
+
export function sessionDigest(frames) {
|
|
151
|
+
const body = (Array.isArray(frames) ? frames : []).map(canonical);
|
|
152
|
+
return createHash("sha256").update(JSON.stringify(body)).digest("hex");
|
|
153
|
+
}
|
|
@@ -437,6 +437,39 @@ export function checkDuplicateSkills(entries = []) {
|
|
|
437
437
|
return out;
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
+
/**
|
|
441
|
+
* LSP READINESS: languages the project actually contains but for which no
|
|
442
|
+
* language server is installed — the silent reason `code_intelligence` /
|
|
443
|
+
* `cc code-intel` quietly degrades to plain text search. Unlike
|
|
444
|
+
* `checkPluginsAndLsp` (live server health) this is a STATIC install-gap check a
|
|
445
|
+
* separate `cc doctor` process can compute without any server running: it
|
|
446
|
+
* correlates source files present with `resolveServer` availability. Only
|
|
447
|
+
* present-but-missing languages warn; installed languages produce nothing (no
|
|
448
|
+
* noise). Each `languages` entry: `{ languageId, fileCount, available, serverId,
|
|
449
|
+
* exampleBin }`.
|
|
450
|
+
*/
|
|
451
|
+
export function checkLspReadiness(languages = []) {
|
|
452
|
+
const out = [];
|
|
453
|
+
for (const l of languages) {
|
|
454
|
+
if (!l || !l.languageId) continue;
|
|
455
|
+
const fileCount = Number(l.fileCount) || 0;
|
|
456
|
+
if (fileCount <= 0 || l.available) continue;
|
|
457
|
+
const server = l.serverId || `a ${l.languageId} language server`;
|
|
458
|
+
const install = l.exampleBin ? ` (e.g. install ${l.exampleBin})` : "";
|
|
459
|
+
out.push(
|
|
460
|
+
finding(
|
|
461
|
+
"lsp-server-missing",
|
|
462
|
+
"lsp",
|
|
463
|
+
"warn",
|
|
464
|
+
`${fileCount} ${l.languageId} file(s) present but no language server installed${install}`,
|
|
465
|
+
`install ${server} for real code-intelligence, or ignore to keep text-search fallback`,
|
|
466
|
+
{ ref: l.languageId, fileCount, serverId: l.serverId || null },
|
|
467
|
+
),
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
return out;
|
|
471
|
+
}
|
|
472
|
+
|
|
440
473
|
/** Plugins / LSP servers reported unhealthy or dead. */
|
|
441
474
|
export function checkPluginsAndLsp(plugins = []) {
|
|
442
475
|
const out = [];
|
|
@@ -178,6 +178,159 @@ function runSessionStartHooks(settingsHooks, { source, cwd, sessionId } = {}) {
|
|
|
178
178
|
return { additionalContext: aggregateContext(outcome.results) };
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
/**
|
|
182
|
+
* CwdChanged settings hooks (observe + context injection). Fired when the
|
|
183
|
+
* working directory changes mid-session (`/cd`) — a distinct lifecycle event
|
|
184
|
+
* (Claude-Code-parity extension) automation can react to (re-audit trust of the
|
|
185
|
+
* new dir, reload a per-dir tool config, …). Observe-only: a cwd change never
|
|
186
|
+
* gates flow, so any `block` decision is ignored and only emitted context is
|
|
187
|
+
* returned. No-op (byte-unchanged) without a registered CwdChanged hook.
|
|
188
|
+
* @returns {{ additionalContext:string|null }}
|
|
189
|
+
*/
|
|
190
|
+
function runCwdChangedHooks(
|
|
191
|
+
settingsHooks,
|
|
192
|
+
{ oldCwd, newCwd, cwd, sessionId } = {},
|
|
193
|
+
) {
|
|
194
|
+
if (!settingsHooks) return { additionalContext: null };
|
|
195
|
+
const matched = collectHooks(settingsHooks, "CwdChanged", newCwd || "");
|
|
196
|
+
const { sync } = partitionAsyncHooks(matched);
|
|
197
|
+
if (sync.length === 0) return { additionalContext: null };
|
|
198
|
+
const payload = withDeliveryId(
|
|
199
|
+
"CwdChanged",
|
|
200
|
+
{
|
|
201
|
+
hook_event_name: "CwdChanged",
|
|
202
|
+
old_cwd: oldCwd || null,
|
|
203
|
+
cwd: newCwd || cwd || null,
|
|
204
|
+
session_id: sessionId || null,
|
|
205
|
+
},
|
|
206
|
+
{ sessionId },
|
|
207
|
+
);
|
|
208
|
+
const outcome = runHooks(sync, payload, {
|
|
209
|
+
cwd: newCwd || cwd,
|
|
210
|
+
event: "CwdChanged",
|
|
211
|
+
});
|
|
212
|
+
return { additionalContext: aggregateContext(outcome.results) };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* WorktreeCreate settings hooks (observe + context injection). Fired right after
|
|
217
|
+
* `cc agent --worktree` carves a fresh isolated git worktree — a hook can react
|
|
218
|
+
* (register the worktree with an external tracker, seed per-branch tooling,
|
|
219
|
+
* warm a build cache). The matcher target is the branch name, so a regex matcher
|
|
220
|
+
* can scope to a branch pattern. Observe-only: worktree setup already succeeded,
|
|
221
|
+
* so a `block` decision is ignored and only emitted context is returned. No-op
|
|
222
|
+
* (byte-unchanged) without a registered WorktreeCreate hook.
|
|
223
|
+
* @returns {{ additionalContext:string|null }}
|
|
224
|
+
*/
|
|
225
|
+
function runWorktreeCreateHooks(
|
|
226
|
+
settingsHooks,
|
|
227
|
+
{ worktreePath, branch, baseSha, cwd, sessionId } = {},
|
|
228
|
+
) {
|
|
229
|
+
if (!settingsHooks) return { additionalContext: null };
|
|
230
|
+
const matched = collectHooks(settingsHooks, "WorktreeCreate", branch || "");
|
|
231
|
+
const { sync } = partitionAsyncHooks(matched);
|
|
232
|
+
if (sync.length === 0) return { additionalContext: null };
|
|
233
|
+
const payload = withDeliveryId(
|
|
234
|
+
"WorktreeCreate",
|
|
235
|
+
{
|
|
236
|
+
hook_event_name: "WorktreeCreate",
|
|
237
|
+
worktree_path: worktreePath || null,
|
|
238
|
+
branch: branch || null,
|
|
239
|
+
base_sha: baseSha || null,
|
|
240
|
+
cwd: cwd || worktreePath || null,
|
|
241
|
+
session_id: sessionId || null,
|
|
242
|
+
},
|
|
243
|
+
{ sessionId },
|
|
244
|
+
);
|
|
245
|
+
const outcome = runHooks(sync, payload, {
|
|
246
|
+
cwd: cwd || worktreePath,
|
|
247
|
+
event: "WorktreeCreate",
|
|
248
|
+
});
|
|
249
|
+
return { additionalContext: aggregateContext(outcome.results) };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* WorktreeRemove settings hooks (observe + context injection). Fired when a
|
|
254
|
+
* `cc agent --worktree` session finishes and its worktree is torn down (or when
|
|
255
|
+
* a `keep` decision leaves it in place — `removed` distinguishes the two) — a
|
|
256
|
+
* hook can react (unregister the worktree, prune a per-branch cache, notify CI).
|
|
257
|
+
* Matcher target is the branch name. Observe-only: teardown already happened, so
|
|
258
|
+
* a `block` decision is ignored and only emitted context is returned. No-op
|
|
259
|
+
* (byte-unchanged) without a registered WorktreeRemove hook.
|
|
260
|
+
* @returns {{ additionalContext:string|null }}
|
|
261
|
+
*/
|
|
262
|
+
function runWorktreeRemoveHooks(
|
|
263
|
+
settingsHooks,
|
|
264
|
+
{ worktreePath, branch, removed, reason, cwd, sessionId } = {},
|
|
265
|
+
) {
|
|
266
|
+
if (!settingsHooks) return { additionalContext: null };
|
|
267
|
+
const matched = collectHooks(settingsHooks, "WorktreeRemove", branch || "");
|
|
268
|
+
const { sync } = partitionAsyncHooks(matched);
|
|
269
|
+
if (sync.length === 0) return { additionalContext: null };
|
|
270
|
+
const payload = withDeliveryId(
|
|
271
|
+
"WorktreeRemove",
|
|
272
|
+
{
|
|
273
|
+
hook_event_name: "WorktreeRemove",
|
|
274
|
+
worktree_path: worktreePath || null,
|
|
275
|
+
branch: branch || null,
|
|
276
|
+
removed: removed === true,
|
|
277
|
+
reason: reason || null,
|
|
278
|
+
cwd: cwd || worktreePath || null,
|
|
279
|
+
session_id: sessionId || null,
|
|
280
|
+
},
|
|
281
|
+
{ sessionId },
|
|
282
|
+
);
|
|
283
|
+
const outcome = runHooks(sync, payload, {
|
|
284
|
+
cwd: cwd || worktreePath,
|
|
285
|
+
event: "WorktreeRemove",
|
|
286
|
+
});
|
|
287
|
+
return { additionalContext: aggregateContext(outcome.results) };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* InstructionsLoaded settings hooks (observe + context injection). Fired once at
|
|
292
|
+
* session start after the project-instruction block (cc.md / CLAUDE.md / AGENTS.md
|
|
293
|
+
* hierarchy + `.claude/rules/*`) is composed into the system prompt — a hook can
|
|
294
|
+
* react to WHICH instruction files are authoritative this session (audit them,
|
|
295
|
+
* warn on an unexpected local override, load a companion tool config). The
|
|
296
|
+
* caller passes the EXACT loaded set; this producer trims each entry to
|
|
297
|
+
* `{path, scope, truncated}` (never the file CONTENT — a hook that wants content
|
|
298
|
+
* reads the path). Observe-only: instructions are already loaded, so a `block`
|
|
299
|
+
* decision is ignored and only emitted context is returned. No-op (byte-unchanged)
|
|
300
|
+
* without a registered InstructionsLoaded hook.
|
|
301
|
+
* @returns {{ additionalContext:string|null }}
|
|
302
|
+
*/
|
|
303
|
+
function runInstructionsLoadedHooks(
|
|
304
|
+
settingsHooks,
|
|
305
|
+
{ files, cwd, sessionId } = {},
|
|
306
|
+
) {
|
|
307
|
+
if (!settingsHooks) return { additionalContext: null };
|
|
308
|
+
const matched = collectHooks(settingsHooks, "InstructionsLoaded", "");
|
|
309
|
+
const { sync } = partitionAsyncHooks(matched);
|
|
310
|
+
if (sync.length === 0) return { additionalContext: null };
|
|
311
|
+
const list = Array.isArray(files) ? files : [];
|
|
312
|
+
const payload = withDeliveryId(
|
|
313
|
+
"InstructionsLoaded",
|
|
314
|
+
{
|
|
315
|
+
hook_event_name: "InstructionsLoaded",
|
|
316
|
+
files: list.map((f) => ({
|
|
317
|
+
path: f?.path || null,
|
|
318
|
+
scope: f?.scope || null,
|
|
319
|
+
truncated: f?.truncated === true,
|
|
320
|
+
})),
|
|
321
|
+
count: list.length,
|
|
322
|
+
cwd: cwd || null,
|
|
323
|
+
session_id: sessionId || null,
|
|
324
|
+
},
|
|
325
|
+
{ sessionId },
|
|
326
|
+
);
|
|
327
|
+
const outcome = runHooks(sync, payload, {
|
|
328
|
+
cwd,
|
|
329
|
+
event: "InstructionsLoaded",
|
|
330
|
+
});
|
|
331
|
+
return { additionalContext: aggregateContext(outcome.results) };
|
|
332
|
+
}
|
|
333
|
+
|
|
181
334
|
/**
|
|
182
335
|
* Generic observe-only fire (SessionEnd / Stop / PreCompact). Returns the raw
|
|
183
336
|
* runHooks outcome so callers can read a block reason; never gates flow here.
|
|
@@ -206,6 +359,10 @@ function runObserveHooks(
|
|
|
206
359
|
module.exports = {
|
|
207
360
|
runUserPromptSubmitHooks,
|
|
208
361
|
runSessionStartHooks,
|
|
362
|
+
runCwdChangedHooks,
|
|
363
|
+
runWorktreeCreateHooks,
|
|
364
|
+
runWorktreeRemoveHooks,
|
|
365
|
+
runInstructionsLoadedHooks,
|
|
209
366
|
runObserveHooks,
|
|
210
367
|
aggregateContext,
|
|
211
368
|
partitionAsyncHooks,
|
|
@@ -24,6 +24,13 @@ import {
|
|
|
24
24
|
APPROVAL_RISK as RISK,
|
|
25
25
|
APPROVAL_DECISION as DECISION,
|
|
26
26
|
} from "@chainlesschain/session-core";
|
|
27
|
+
import {
|
|
28
|
+
classifyCodeAcquisition,
|
|
29
|
+
hasGlobalInstall,
|
|
30
|
+
applyRiskFloor,
|
|
31
|
+
recordInstallCommandAudit,
|
|
32
|
+
resolveInstallPolicy,
|
|
33
|
+
} from "./install-command-policy.js";
|
|
27
34
|
|
|
28
35
|
export const SHELL_TO_RISK = {
|
|
29
36
|
allow: RISK.LOW,
|
|
@@ -37,13 +44,49 @@ export async function evaluateShellCommandWithApproval({
|
|
|
37
44
|
sessionId = null,
|
|
38
45
|
approvalGate = null,
|
|
39
46
|
shellPolicyOptions = {},
|
|
47
|
+
installPolicy = null,
|
|
40
48
|
} = {}) {
|
|
41
49
|
const shellPolicy = sharedShellPolicy.evaluateShellCommandPolicy(
|
|
42
50
|
command,
|
|
43
51
|
shellPolicyOptions,
|
|
44
52
|
);
|
|
45
53
|
|
|
46
|
-
|
|
54
|
+
let riskLevel = SHELL_TO_RISK[shellPolicy.decision] || RISK.MEDIUM;
|
|
55
|
+
|
|
56
|
+
// Unified install-command classification (OPT-IN via installPolicy). A package
|
|
57
|
+
// install (npm/pip/winget/brew/…) fetches and runs third-party code, so it is
|
|
58
|
+
// a distinct, auditable permission type regardless of which tool does it. When
|
|
59
|
+
// a riskFloor is configured it RAISES (never lowers) the risk before gating;
|
|
60
|
+
// audit records the attempt. Absent/disabled → nothing runs, byte-unchanged.
|
|
61
|
+
// Explicit policy wins; otherwise self-activate from env (CC_INSTALL_AUDIT /
|
|
62
|
+
// CC_INSTALL_RISK_FLOOR) so the real run_shell path picks it up with no extra
|
|
63
|
+
// wiring. Both unset → disabled → the block below is skipped, byte-unchanged.
|
|
64
|
+
const effectiveInstallPolicy =
|
|
65
|
+
installPolicy || resolveInstallPolicy({ env: process.env });
|
|
66
|
+
let install = null;
|
|
67
|
+
if (effectiveInstallPolicy && effectiveInstallPolicy.enabled) {
|
|
68
|
+
const cls = classifyCodeAcquisition(command);
|
|
69
|
+
if (cls.flagged) {
|
|
70
|
+
install = cls;
|
|
71
|
+
if (effectiveInstallPolicy.riskFloor) {
|
|
72
|
+
riskLevel = applyRiskFloor(riskLevel, effectiveInstallPolicy.riskFloor);
|
|
73
|
+
}
|
|
74
|
+
if (effectiveInstallPolicy.audit) {
|
|
75
|
+
recordInstallCommandAudit(
|
|
76
|
+
{
|
|
77
|
+
command,
|
|
78
|
+
shellDecision: shellPolicy.decision,
|
|
79
|
+
riskLevel,
|
|
80
|
+
installs: cls.installs,
|
|
81
|
+
remoteExec: cls.remoteExec,
|
|
82
|
+
global: hasGlobalInstall(cls),
|
|
83
|
+
sessionId,
|
|
84
|
+
},
|
|
85
|
+
effectiveInstallPolicy.auditOpts || {},
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
47
90
|
|
|
48
91
|
// Hard-blocked rules bypass the gate entirely — the gate tier cannot
|
|
49
92
|
// up-authorize them.
|
|
@@ -56,6 +99,7 @@ export async function evaluateShellCommandWithApproval({
|
|
|
56
99
|
shellPolicy,
|
|
57
100
|
riskLevel,
|
|
58
101
|
policy: null,
|
|
102
|
+
install,
|
|
59
103
|
};
|
|
60
104
|
}
|
|
61
105
|
|
|
@@ -69,6 +113,7 @@ export async function evaluateShellCommandWithApproval({
|
|
|
69
113
|
shellPolicy,
|
|
70
114
|
riskLevel,
|
|
71
115
|
policy: null,
|
|
116
|
+
install,
|
|
72
117
|
};
|
|
73
118
|
}
|
|
74
119
|
|
|
@@ -87,6 +132,7 @@ export async function evaluateShellCommandWithApproval({
|
|
|
87
132
|
shellPolicy,
|
|
88
133
|
riskLevel,
|
|
89
134
|
policy: gateResult.policy,
|
|
135
|
+
install,
|
|
90
136
|
// Explainability passthrough (autoMode.decisions wrapper attaches these):
|
|
91
137
|
// which gate rule fired and why — surfaced in the permission chain.
|
|
92
138
|
gateReason: gateResult.reason ?? null,
|
package/src/repl/add-dir.js
CHANGED
|
@@ -61,10 +61,68 @@ export function resolveAddDir(input, { cwd, existing = [], deps } = {}) {
|
|
|
61
61
|
*/
|
|
62
62
|
export function formatAddDirRoots(cwd, extraRoots) {
|
|
63
63
|
const extras = Array.isArray(extraRoots) ? extraRoots : [];
|
|
64
|
-
const lines = [
|
|
64
|
+
const lines = [
|
|
65
|
+
`Working roots (${1 + extras.length}):`,
|
|
66
|
+
` ${cwd} (primary)`,
|
|
67
|
+
];
|
|
65
68
|
for (const r of extras) lines.push(` ${r}`);
|
|
66
69
|
if (extras.length === 0) {
|
|
67
70
|
lines.push(" (no extra roots — add one with /add-dir <dir>)");
|
|
68
71
|
}
|
|
69
72
|
return lines.join("\n");
|
|
70
73
|
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The full workspace root list a session advertises to MCP servers (`roots/list`):
|
|
77
|
+
* the primary cwd first, then each extra `--add-dir` / `/add-dir` root, all
|
|
78
|
+
* resolved absolute and de-duplicated (the primary is dropped from the extras if
|
|
79
|
+
* a caller re-added it). Pure. Mirrors what the system prompt advertises so an
|
|
80
|
+
* MCP server sees the same roots the model does.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} cwd primary working directory
|
|
83
|
+
* @param {string[]} extraRoots additional roots
|
|
84
|
+
* @returns {string[]} deduped absolute dirs, primary first
|
|
85
|
+
*/
|
|
86
|
+
export function workspaceRootDirs(cwd, extraRoots) {
|
|
87
|
+
const primary = nodePath.resolve(cwd);
|
|
88
|
+
const out = [primary];
|
|
89
|
+
const seen = new Set([primary]);
|
|
90
|
+
for (const r of Array.isArray(extraRoots) ? extraRoots : []) {
|
|
91
|
+
if (!r) continue;
|
|
92
|
+
const abs = nodePath.resolve(cwd, String(r));
|
|
93
|
+
if (seen.has(abs)) continue;
|
|
94
|
+
seen.add(abs);
|
|
95
|
+
out.push(abs);
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Best-effort broadcast of a changed workspace-root list to every connected MCP
|
|
102
|
+
* client (via `client.setRoots(dirs)`, which advertises the new list AND fires
|
|
103
|
+
* `notifications/roots/list_changed` only when it actually changed). Skips
|
|
104
|
+
* nullish/duplicate clients and any without `setRoots`; a failing client never
|
|
105
|
+
* disturbs the others or the caller. Returns the number of clients notified.
|
|
106
|
+
* Claude-Code 2.1.203 roots-change parity for the `/add-dir` (and startup
|
|
107
|
+
* `--add-dir`) vector, mirroring what `/cd` already does via
|
|
108
|
+
* `notifyRootsListChanged`.
|
|
109
|
+
*
|
|
110
|
+
* @param {Array<object|null|undefined>} clients MCP clients (may repeat/null)
|
|
111
|
+
* @param {string[]} dirs absolute root dirs
|
|
112
|
+
* @returns {number}
|
|
113
|
+
*/
|
|
114
|
+
export function notifyMcpRootsChanged(clients, dirs) {
|
|
115
|
+
let notified = 0;
|
|
116
|
+
const seen = new Set();
|
|
117
|
+
for (const c of Array.isArray(clients) ? clients : []) {
|
|
118
|
+
if (!c || seen.has(c) || typeof c.setRoots !== "function") continue;
|
|
119
|
+
seen.add(c);
|
|
120
|
+
try {
|
|
121
|
+
c.setRoots(dirs);
|
|
122
|
+
notified += 1;
|
|
123
|
+
} catch {
|
|
124
|
+
/* best-effort — one server's failure must not block the rest */
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return notified;
|
|
128
|
+
}
|
package/src/repl/agent-repl.js
CHANGED
|
@@ -1045,6 +1045,7 @@ export async function startAgentRepl(options = {}) {
|
|
|
1045
1045
|
// `undefined` so the runtime default-on path — and CC_PROJECT_MEMORY=0 — keep
|
|
1046
1046
|
// their existing behaviour byte-identically.
|
|
1047
1047
|
const _leanNoProjectMemory = options.projectMemory === false;
|
|
1048
|
+
let _loadedReplInstructions = null;
|
|
1048
1049
|
let _replBaseSystem = composeSystemPrompt(
|
|
1049
1050
|
buildSystemPrompt(process.cwd(), {
|
|
1050
1051
|
additionalDirectories,
|
|
@@ -1054,6 +1055,9 @@ export async function startAgentRepl(options = {}) {
|
|
|
1054
1055
|
systemPrompt: options.systemPrompt,
|
|
1055
1056
|
appendSystemPrompt: options.appendSystemPrompt,
|
|
1056
1057
|
projectMemory: _leanNoProjectMemory ? false : undefined,
|
|
1058
|
+
onInstructionsLoaded: (loaded) => {
|
|
1059
|
+
_loadedReplInstructions = loaded;
|
|
1060
|
+
},
|
|
1057
1061
|
},
|
|
1058
1062
|
);
|
|
1059
1063
|
let _activeOutputStyle = null; // { name, body }
|
|
@@ -1088,6 +1092,25 @@ export async function startAgentRepl(options = {}) {
|
|
|
1088
1092
|
// best-effort — no output style
|
|
1089
1093
|
}
|
|
1090
1094
|
|
|
1095
|
+
// settings.json InstructionsLoaded hooks (observe-only): fire once with the
|
|
1096
|
+
// exact loaded instruction file set so a hook can audit which cc.md/CLAUDE.md/
|
|
1097
|
+
// AGENTS.md/rules are authoritative this session. Best-effort; no-op when
|
|
1098
|
+
// project memory is off or no hook is registered.
|
|
1099
|
+
if (_settingsHooks && _loadedReplInstructions) {
|
|
1100
|
+
try {
|
|
1101
|
+
const { runInstructionsLoadedHooks } =
|
|
1102
|
+
await import("../lib/settings-hook-events.cjs");
|
|
1103
|
+
const ctx = runInstructionsLoadedHooks(_settingsHooks, {
|
|
1104
|
+
files: _loadedReplInstructions.files,
|
|
1105
|
+
cwd: process.cwd(),
|
|
1106
|
+
sessionId,
|
|
1107
|
+
}).additionalContext;
|
|
1108
|
+
if (ctx) messages.push({ role: "system", content: ctx });
|
|
1109
|
+
} catch (_err) {
|
|
1110
|
+
// best-effort
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1091
1114
|
// settings.json SessionStart hooks → inject session context (observe-only).
|
|
1092
1115
|
if (_settingsHooks) {
|
|
1093
1116
|
try {
|
|
@@ -1267,6 +1290,23 @@ export async function startAgentRepl(options = {}) {
|
|
|
1267
1290
|
}
|
|
1268
1291
|
}
|
|
1269
1292
|
|
|
1293
|
+
// Seed connected MCP servers with the startup workspace roots when the session
|
|
1294
|
+
// began with extra `--add-dir` roots — otherwise `roots/list` would only ever
|
|
1295
|
+
// return the cwd until the first mid-session /add-dir. No-op (byte-unchanged)
|
|
1296
|
+
// when there are no extra roots. Best-effort.
|
|
1297
|
+
if (additionalDirectories.length > 0) {
|
|
1298
|
+
try {
|
|
1299
|
+
const { workspaceRootDirs, notifyMcpRootsChanged } =
|
|
1300
|
+
await import("./add-dir.js");
|
|
1301
|
+
notifyMcpRootsChanged(
|
|
1302
|
+
[_adhocMcp?.mcpClient, _bundleMcpClient],
|
|
1303
|
+
workspaceRootDirs(process.cwd(), additionalDirectories),
|
|
1304
|
+
);
|
|
1305
|
+
} catch {
|
|
1306
|
+
/* best-effort — root advertisement must not block session startup */
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1270
1310
|
// Apply bundle approval policy to this session (after both gate and sessionId are ready)
|
|
1271
1311
|
if (_bundleResolved?.approvalPolicy?.default && _approvalGate && sessionId) {
|
|
1272
1312
|
try {
|
|
@@ -2382,8 +2422,12 @@ export async function startAgentRepl(options = {}) {
|
|
|
2382
2422
|
if (trimmed === "/add-dir" || trimmed.startsWith("/add-dir ")) {
|
|
2383
2423
|
const arg = trimmed.slice("/add-dir".length).trim();
|
|
2384
2424
|
try {
|
|
2385
|
-
const {
|
|
2386
|
-
|
|
2425
|
+
const {
|
|
2426
|
+
resolveAddDir,
|
|
2427
|
+
formatAddDirRoots,
|
|
2428
|
+
workspaceRootDirs,
|
|
2429
|
+
notifyMcpRootsChanged,
|
|
2430
|
+
} = await import("./add-dir.js");
|
|
2387
2431
|
if (!arg) {
|
|
2388
2432
|
logger.log(
|
|
2389
2433
|
"\n" +
|
|
@@ -2417,6 +2461,14 @@ export async function startAgentRepl(options = {}) {
|
|
|
2417
2461
|
messages[0].content = _activeOutputStyle
|
|
2418
2462
|
? `${_replBaseSystem}\n\n${_activeOutputStyle.body}`
|
|
2419
2463
|
: _replBaseSystem;
|
|
2464
|
+
// The workspace root LIST changed — advertise it to connected MCP
|
|
2465
|
+
// servers and fire notifications/roots/list_changed so they re-query
|
|
2466
|
+
// roots/list (Claude-Code 2.1.203 parity, same as /cd but with an
|
|
2467
|
+
// explicit new root, not just a cwd-derived change).
|
|
2468
|
+
notifyMcpRootsChanged(
|
|
2469
|
+
[_adhocMcp?.mcpClient, _bundleMcpClient],
|
|
2470
|
+
workspaceRootDirs(process.cwd(), additionalDirectories),
|
|
2471
|
+
);
|
|
2420
2472
|
logger.log(chalk.green(`Added working root: ${res.dir}`));
|
|
2421
2473
|
}
|
|
2422
2474
|
}
|
|
@@ -3002,9 +3054,11 @@ export async function startAgentRepl(options = {}) {
|
|
|
3002
3054
|
logger.info(`cwd: ${process.cwd()}`);
|
|
3003
3055
|
} else {
|
|
3004
3056
|
try {
|
|
3057
|
+
const oldCwd = process.cwd();
|
|
3005
3058
|
const expanded = target.replace(/^~(?=$|[\\/])/, os.homedir());
|
|
3006
3059
|
process.chdir(path.resolve(process.cwd(), expanded));
|
|
3007
|
-
|
|
3060
|
+
const newCwd = process.cwd();
|
|
3061
|
+
logger.log(chalk.green(`cwd → ${newCwd}`));
|
|
3008
3062
|
// MCP roots derive from cwd — tell connected servers to re-query
|
|
3009
3063
|
// roots/list (Claude-Code 2.1.203 change-notification parity).
|
|
3010
3064
|
for (const c of [_adhocMcp?.mcpClient, _bundleMcpClient]) {
|
|
@@ -3016,6 +3070,34 @@ export async function startAgentRepl(options = {}) {
|
|
|
3016
3070
|
}
|
|
3017
3071
|
}
|
|
3018
3072
|
}
|
|
3073
|
+
// CwdChanged settings hooks (lifecycle-event parity): the working dir
|
|
3074
|
+
// just changed, so fire CwdChanged with old→new cwd — a hook can
|
|
3075
|
+
// observe/react (re-audit the new dir's trust, reload per-dir config).
|
|
3076
|
+
// Observe-only (a cwd change never gates flow); best-effort; no-op
|
|
3077
|
+
// (byte-unchanged) without a registered CwdChanged hook.
|
|
3078
|
+
if (_settingsHooks && oldCwd !== newCwd) {
|
|
3079
|
+
try {
|
|
3080
|
+
const { runCwdChangedHooks, dispatchAsyncHooks } =
|
|
3081
|
+
await import("../lib/settings-hook-events.cjs");
|
|
3082
|
+
const payload = { oldCwd, newCwd, sessionId };
|
|
3083
|
+
runCwdChangedHooks(_settingsHooks, payload);
|
|
3084
|
+
if (!_asyncHookSupervisor) {
|
|
3085
|
+
const { AsyncHookSupervisor } =
|
|
3086
|
+
await import("../lib/async-hook-supervisor.cjs");
|
|
3087
|
+
_asyncHookSupervisor = new AsyncHookSupervisor({
|
|
3088
|
+
persistStats: true,
|
|
3089
|
+
});
|
|
3090
|
+
}
|
|
3091
|
+
dispatchAsyncHooks(
|
|
3092
|
+
_settingsHooks,
|
|
3093
|
+
"CwdChanged",
|
|
3094
|
+
{ old_cwd: oldCwd, cwd: newCwd, session_id: sessionId || null },
|
|
3095
|
+
{ cwd: newCwd, supervisor: _asyncHookSupervisor },
|
|
3096
|
+
);
|
|
3097
|
+
} catch {
|
|
3098
|
+
/* CwdChanged firing is best-effort */
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3019
3101
|
} catch (err) {
|
|
3020
3102
|
logger.error(`/cd failed: ${err.message}`);
|
|
3021
3103
|
}
|