chainlesschain 0.162.160 → 0.162.162
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 +1 -1
- package/src/assets/web-panel/assets/{AIOps-CkRHjW8e.js → AIOps-ClI2ALgv.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BP3MeNi2.js → ActionButton-BDjRcJaK.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CjbjQsOr.js → Analytics-DikSRsaT.js} +2 -2
- package/src/assets/web-panel/assets/{AppLayout-i0adH1M8.js → AppLayout-9mLhMFmz.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-CCCG2GOU.js → Artifacts-BSr88BrH.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-DrsgvwHC.js → Audit-CdvyimOF.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-DkXNkbZM.js → BackgroundAgents-D3_hCedh.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DarTWCex.js → Backup-Cyvv9RHf.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-DaDzhcto.js → BaseInput-BZM14kt8.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-C4EzGw6V.js → Chat-DrQSokVz.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-lkavIWMS.js → ChatBubbleRenderer-HwJR-C6B.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-gddNgaQi.js → Checkbox-bFLYeWMV.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BKldFWf9.js → Codegen-7mN7xJXT.js} +1 -1
- package/src/assets/web-panel/assets/{Col-ChR-brDs.js → Col-DAB5ZWm7.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DlHtZX86.js → Community-B_1TymaK.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BAhfcBpC.js → Compact-Bq1uSWLJ.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-gIrKkm6m.js → Compliance-CyiE1jEe.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BYnzLBO3.js → Cowork-Cpk0D7vm.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-ByUDIljI.js → Cron-d_Lv3rdB.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DOdNbHFp.js → Crosschain-DMqhHjMR.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BJHaNWD2.js → DID-Br-oPuBw.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-Drm_tClz.js → Dashboard-CfLW_RaP.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CA1WLzGN.js → Dropdown-DZnTOExV.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-Di1SKjNO.js → EmailListRenderer-DHIUHDv1.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-44Ve7ZFe.js → FamilyGuardDashboard-Bw3t3myu.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-CubPwv_N.js → Federation-D9cpQPAY.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-CsUFe5bU.js → FormItemContext-Jdczr9Mr.js} +1 -1
- package/src/assets/web-panel/assets/GenericCardRenderer-BT8ZMpC5.js +1 -0
- package/src/assets/web-panel/assets/{Git-D-Qx9dhO.js → Git-DjARip2b.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-_YMYaU4n.js → Governance-BgWbSRcj.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-J7Yzh-K_.js → Inference-CfouJciM.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-D37iI_Mo.js → KnowledgeGraph-50l7mrAr.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-oMTSYfGY.js → Logs-rq50H84B.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-BqaPmC0W.js → MarkdownRenderer-DUPIEdQH.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-DKUqYDaF.js → Marketplace-BYgPDmHU.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BEv3Gx61.js → McpTools-5DVgcFdx.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-CtA8XD2y.js → Memory-B70LPk7f.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-ug7c1mMj.js → MobileBridge-De8ssA5Y.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-D10VISps.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-WJaMs4i2.js → Mtc-RJsOTsIA.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-DXnK_-ru.js → MtcAudit-Crh-6BEK.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-C4Ev7726.js → Multisig-wuoR5l3x.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-BMFb9_zk.js → NLProgramming-U2Mji4fZ.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-Bl93nvBX.js → Notes-CdTZAyDH.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-BN2W5cl6.js → NotificationSettings-C-FucWE7.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-Dxc9oN4o.js → OrderTableRenderer-DCYFTeaI.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-CAs48637.js → Organization-an3OF85L.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-Bhq27Zoc.js → Overflow-C-1vmYFe.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-D61-2542.js → P2P-Cwh47xKg.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-C1j4iUxz.js → PdhVaultBrowser-Dm38GT0k.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-rJ2-fScM.js → Permissions-DS2Y8awD.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-DFIlTok6.js → PersonalDataHub-BQNziA0I.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-BjIgEWoZ.js → Pipeline-DF85L1ue.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BQoH5wcX.js → Privacy-BuLornF-.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CYR_-_Ub.js → ProjectInit-C3Y31IwX.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-nkatCq-9.js → ProjectSettings-BCPiI8aD.js} +2 -2
- package/src/assets/web-panel/assets/Projects-BZ6s4k6g.js +1 -0
- package/src/assets/web-panel/assets/{Providers-BnKM9fsF.js → Providers-D3Wp022X.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-Cfe82dbz.js → QrScannerModal-CUJfQkH4.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-r1RbPufV.js → QuickAsk-UAc0EO8l.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CjCbM2WE.js → Recommend-BaxKupcs.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-BuMGPH6V.js → RemoteSession-C75NNnXZ.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-Bmj3NY1F.js → Reputation-BVXynZAk.js} +1 -1
- package/src/assets/web-panel/assets/{Row-BIsNcE_1.js → Row-C5MRZ0bi.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BObmojD2.js → RssFeed-DAKkEYka.js} +3 -3
- package/src/assets/web-panel/assets/{Search-BRmbkQNS.js → Search-DvWv9YOA.js} +1 -1
- package/src/assets/web-panel/assets/{Security-_rF3DRpY.js → Security-B5peafW1.js} +4 -4
- package/src/assets/web-panel/assets/{Services-D0If9ZV4.js → Services-UQmvPEaG.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-o7zIksUg.js → Skeleton-C3bhWpuF.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-oUR7zi9u.js → Skills-DfkKf9_H.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-fqKyGi03.js → Sla-DqMSjlBM.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-B1wZ_gxx.js → SpeechSettings-C-WbxWUX.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-B3qJWYXq.js → SyncSettings-CYuHZ4N2.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-BH5_dUfr.js → Tasks-CyeAyB9d.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-CCIVpjRP.js → Templates-BTCeQnpq.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-2GZtTGnB.js → Tenant-BifQuA9Y.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-BlMkfs_u.js → Terminal-Bx_9Bocf.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-U6hi5OMc.js → TimelineRenderer-BX0RFCOm.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-C-_BerGJ.js → Tokens-CGHrCmUS.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DaKzBgN3.js → Trigger-DlqaY4SO.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-CLvcsco3.js → Trust-DfOmj-ay.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-BFn_Y1zZ.js → UkeySign-B72jH5fE.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-B0V18dLT.js → VideoEditing-BT-63fqv.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BbmP7Imu.js → Wallet-Dsul3sHr.js} +3 -3
- package/src/assets/web-panel/assets/{WebAuthn-Dewj_0kt.js → WebAuthn-Izkwwdgy.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-DOxz3Lwa.js → WorkflowEditor-D3FHSiH1.js} +1 -1
- package/src/assets/web-panel/assets/{chat-B1OIpZnh.js → chat-CmVz5z3U.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CSGNVdri.js → colors-CuLgyJBp.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-8S0vcg0u.js → compact-item-Ml1YkXpw.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-vZyc27TN.js → createContext-BvjJ48bs.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DhiGrbsh.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DWw3-UhA.js → hasIn-DVC0157C.js} +1 -1
- package/src/assets/web-panel/assets/{index-DLAvUtOH.js → index-4SBta3d7.js} +1 -1
- package/src/assets/web-panel/assets/{index-BZnauCqN.js → index-4wcgvPoF.js} +1 -1
- package/src/assets/web-panel/assets/{index-t-q3rkVm.js → index-B0S8NzO6.js} +1 -1
- package/src/assets/web-panel/assets/index-B7a1TrU6.js +1 -0
- package/src/assets/web-panel/assets/{index-BwKrTVV0.js → index-BUt8kTIB.js} +1 -1
- package/src/assets/web-panel/assets/{index-BQssf0EQ.js → index-B_EvG_GW.js} +1 -1
- package/src/assets/web-panel/assets/{index-xx5M5Rbj.js → index-BbPtzB8M.js} +1 -1
- package/src/assets/web-panel/assets/{index-DKwl1AkQ.js → index-BbepULSc.js} +1 -1
- package/src/assets/web-panel/assets/{index-B4pP2ycr.js → index-BfuKP4qe.js} +1 -1
- package/src/assets/web-panel/assets/{index-D1i_8oop.js → index-BiC6dQZG.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dc40pX3F.js → index-BjHBpRYZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DeCS12Mh.js → index-BpFJHiTv.js} +1 -1
- package/src/assets/web-panel/assets/{index-BjC-TrOb.js → index-BydjdZnl.js} +1 -1
- package/src/assets/web-panel/assets/{index-DcfLXAW7.js → index-BzG-x9p4.js} +1 -1
- package/src/assets/web-panel/assets/{index-CfEvacz7.js → index-Bzr0rB5T.js} +1 -1
- package/src/assets/web-panel/assets/{index-BoIPHP4R.js → index-C229ABfy.js} +1 -1
- package/src/assets/web-panel/assets/index-C2B1KRja.js +1 -0
- package/src/assets/web-panel/assets/{index-BdyokEau.js → index-CFYVQMvu.js} +1 -1
- package/src/assets/web-panel/assets/{index-CLPr2vC_.js → index-CXkJH-lK.js} +1 -1
- package/src/assets/web-panel/assets/{index-DtHWHJnj.js → index-C_H_S-DY.js} +1 -1
- package/src/assets/web-panel/assets/{index-Q6oyLnKC.js → index-C_XuRgGn.js} +1 -1
- package/src/assets/web-panel/assets/{index-BeBv896m.js → index-C_vRgTl9.js} +1 -1
- package/src/assets/web-panel/assets/{index-DLgkAhEE.js → index-Ck_52dz_.js} +1 -1
- package/src/assets/web-panel/assets/{index-tr1x96W9.js → index-CmFNvNLZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CB2MOS_8.js → index-CudmwNBV.js} +3 -3
- package/src/assets/web-panel/assets/{index-CFQWZvak.js → index-Cw0Rjy87.js} +1 -1
- package/src/assets/web-panel/assets/{index-BuB5P0TR.js → index-D3lD5qap.js} +1 -1
- package/src/assets/web-panel/assets/{index-D8qXVfoV.js → index-DC_OI9H2.js} +1 -1
- package/src/assets/web-panel/assets/{index-C6r27A-o.js → index-DDGaOWBV.js} +1 -1
- package/src/assets/web-panel/assets/{index-CbZqw7pg.js → index-DJyN28Et.js} +1 -1
- package/src/assets/web-panel/assets/{index-CJgKP2cm.js → index-DYXHFr4r.js} +1 -1
- package/src/assets/web-panel/assets/{index-CI3slBKI.js → index-Dt5KMjwJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CwXBblm-.js → index-DuGszaWY.js} +1 -1
- package/src/assets/web-panel/assets/{index-BKEmQQvR.js → index-DukDVXCv.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dg0Izipr.js → index-KMqhcF7l.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkWd6YL7.js → index-KjgKdZe-.js} +1 -1
- package/src/assets/web-panel/assets/{index-DPgY5GLy.js → index-QXzlT9KD.js} +1 -1
- package/src/assets/web-panel/assets/{index-BDHyovnK.js → index-vG4XSJ1d.js} +1 -1
- package/src/assets/web-panel/assets/{index-dxocDVD_.js → index-wvb_FtaV.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-Di5YFId0.js → initDefaultProps-BDgd2YZ8.js} +1 -1
- package/src/assets/web-panel/assets/{motion-BVSN0aeL.js → motion-ErtrXdYe.js} +1 -1
- package/src/assets/web-panel/assets/{move-DHNiaP2l.js → move-_GPPNCgs.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-xJPf3gQD.js → mtc-parser-DRLL-8V2.js} +1 -1
- package/src/assets/web-panel/assets/{omit-C7qzUdkN.js → omit-p86sl023.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-pSpjg3Le.js → pickAttrs-BTu7cMLH.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-cs_jL_UC.js → placementArrow-BEKz6Isr.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DkS_Zs4r.js → responsiveObserve-Ck42c9MP.js} +1 -1
- package/src/assets/web-panel/assets/{slide-B5O1Kpxo.js → slide-1CEvuPsB.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BX02GnZp.js → statusUtils-BDfdx1GJ.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-EmBYRftd.js → styleChecker-B4K46m5w.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-ZGzN1cU8.js → useFlexGapSupport-DoIVpJNq.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CfP7M1m7.js → useFs-DdAWi-ln.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-CbIPHgNh.js → usePersonalDataHub-iUu6k8dx.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-7xh0If_O.js → vnode-DZz4R6Tw.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DmitvTR2.js → zoom-DXlf9sA3.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agenda.js +15 -1
- package/src/commands/agent.js +7 -0
- package/src/commands/background-session.js +16 -2
- package/src/commands/review.js +6 -1
- package/src/data/changelog.json +12 -2
- package/src/gateways/ws/background-agent-protocol.js +88 -14
- package/src/lib/agent-authority.js +251 -0
- package/src/lib/agents.js +19 -0
- package/src/lib/background-agent-phase.js +100 -0
- package/src/lib/background-agent-supervisor.js +322 -15
- package/src/lib/backpressure-policy.js +105 -0
- package/src/lib/capability-negotiation.js +212 -0
- package/src/lib/credential-guard.js +11 -0
- package/src/lib/credential-proxy.js +182 -0
- package/src/lib/docs-drift.js +166 -0
- package/src/lib/doctor-checkup.js +76 -0
- package/src/lib/event-seq-replay.js +167 -0
- package/src/lib/goal-condition-engine.js +369 -0
- package/src/lib/headless-manifest.js +13 -0
- package/src/lib/hook-event-bus.cjs +0 -0
- package/src/lib/ide-context.js +53 -14
- package/src/lib/json-schema-output.js +30 -1
- package/src/lib/json-schema-validate.js +691 -0
- package/src/lib/lsp/__tests__/diagnostics-scheduler.test.js +93 -0
- package/src/lib/lsp/diagnostics-scheduler.js +127 -0
- package/src/lib/operation-fingerprint.js +95 -0
- package/src/lib/plugin-runtime/capabilities.js +371 -0
- package/src/lib/plugin-runtime/manifest.js +30 -0
- package/src/lib/project-instructions.js +98 -1
- package/src/lib/remote-approval-bridge.js +16 -0
- package/src/lib/remote-path-mapping.js +229 -0
- package/src/lib/review-pipeline.js +227 -0
- package/src/lib/runtime-checkup.js +318 -0
- package/src/lib/schedule-planner.js +136 -0
- package/src/lib/session-lifecycle.js +305 -0
- package/src/lib/settings-hook-events.cjs +39 -13
- package/src/lib/subagent-contract.js +383 -0
- package/src/lib/telemetry-ids.js +190 -0
- package/src/lib/turn-binding.js +397 -0
- package/src/repl/bg-dashboard.js +13 -12
- package/src/runtime/agent-core.js +24 -10
- package/src/runtime/headless-stream.js +195 -11
- package/src/runtime/system-prompt.js +3 -0
- package/src/workers/background-agent-worker.js +37 -2
- package/src/assets/web-panel/assets/GenericCardRenderer-D7xRjPjZ.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-BpX5g9It.js +0 -1
- package/src/assets/web-panel/assets/Projects-SS9uZKCl.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DYEZcY55.js +0 -1
- package/src/assets/web-panel/assets/index-DmVFfcNR.js +0 -1
- package/src/assets/web-panel/assets/index-x7YyhMUa.js +0 -1
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bidirectional capability negotiation with N / N-1 downgrade for Agent
|
|
3
|
+
* Protocol (agent-sdk docs/PROTOCOL.md §1.3).
|
|
4
|
+
*
|
|
5
|
+
* The CLI advertises what it can speak one-directionally (buildAgentCapabilities
|
|
6
|
+
* → `cc agent --capabilities`, and the `system/init` line's protocol_version).
|
|
7
|
+
* That is not negotiation: a client (VS Code / JetBrains panel) had no way to
|
|
8
|
+
* announce what IT understands, and the CLI had no rule to pick a common level
|
|
9
|
+
* or step DOWN when the two disagree. When the protocol later grows a v2 line
|
|
10
|
+
* shape, a v1-only client would silently mis-parse it.
|
|
11
|
+
*
|
|
12
|
+
* This module is the missing algorithm. A client MAY send a `hello` as its
|
|
13
|
+
* first stream-json input line:
|
|
14
|
+
* {"type":"hello","protocol_version":2,"min_protocol_version":1,
|
|
15
|
+
* "features":["event_seq","trace_id"]}
|
|
16
|
+
* The CLI negotiates it against its own offer and echoes the agreed level as a
|
|
17
|
+
* `system/negotiated` line, then honors it for the rest of the run.
|
|
18
|
+
*
|
|
19
|
+
* Rules:
|
|
20
|
+
* - agreedVersion = min(server.max, client.max) — never above what either
|
|
21
|
+
* can parse.
|
|
22
|
+
* - Incompatible when agreedVersion < max(server.min, client.min): the two
|
|
23
|
+
* version ranges don't overlap. ok:false — the caller keeps a safe baseline
|
|
24
|
+
* or errors out; it never speaks a version the peer can't read.
|
|
25
|
+
* - effective features = the server-offered features the client also accepts
|
|
26
|
+
* (intersection), minus any whose minimum protocol version is above the
|
|
27
|
+
* agreed version (an N-only field is dropped for an N-1 session).
|
|
28
|
+
* - No client offer at all → legacy client that predates negotiation. The
|
|
29
|
+
* additive fields are defined "consumers MUST tolerate absence AND
|
|
30
|
+
* presence" (docs/PROTOCOL.md §1.2.1), so we keep FULL behavior unchanged
|
|
31
|
+
* (byte-for-byte) and mark clientAware:false. Negotiation only ever
|
|
32
|
+
* RESTRICTS when a client explicitly narrows the set.
|
|
33
|
+
*
|
|
34
|
+
* Pure: no fs / process / clock. The same algorithm is mirrored in the Java
|
|
35
|
+
* twin (jetbrains-plugin CapabilityNegotiation.java); both read the shared
|
|
36
|
+
* fixture __tests__/fixtures/capability-negotiation-cases.json.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/** The current protocol version the CLI speaks (mirror of PROTOCOL_VERSION). */
|
|
40
|
+
export const PROTOCOL_VERSION = 1;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The oldest protocol version the CLI can still speak end-to-end (the N-1 in
|
|
44
|
+
* "N / N-1"). At v1 there is no older line shape, so min === current; once v2
|
|
45
|
+
* ships this drops to 1 so a v1-only client negotiates a v1 session.
|
|
46
|
+
*/
|
|
47
|
+
export const PROTOCOL_MIN_VERSION = 1;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The wire-protocol features subject to negotiation — the additive per-line
|
|
51
|
+
* fields a client may or may not understand. Runtime capabilities (bare,
|
|
52
|
+
* worktree, mcp, …) are NOT negotiated: they change what the CLI can do, not
|
|
53
|
+
* the shape of a line the client must parse.
|
|
54
|
+
*/
|
|
55
|
+
export const PROTOCOL_FEATURES = ["event_seq", "tool_use_id", "trace_id"];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Minimum protocol version a feature requires. Empty today (every current
|
|
59
|
+
* feature is a v1 additive field). When a v2 field lands, add it here so an
|
|
60
|
+
* N-1 (v1) session drops it automatically.
|
|
61
|
+
* @type {Record<string, number>}
|
|
62
|
+
*/
|
|
63
|
+
export const FEATURE_MIN_VERSION = {};
|
|
64
|
+
|
|
65
|
+
function intOr(value, fallback) {
|
|
66
|
+
const n = Number(value);
|
|
67
|
+
return Number.isInteger(n) && n > 0 ? n : fallback;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Coerce a features declaration into a sorted, de-duplicated array of keys.
|
|
72
|
+
* Accepts an array of strings, or an object whose truthy keys are the features
|
|
73
|
+
* (the nested manifest shape). Anything else → [].
|
|
74
|
+
* @returns {string[]}
|
|
75
|
+
*/
|
|
76
|
+
export function normalizeFeatureList(input) {
|
|
77
|
+
let keys;
|
|
78
|
+
if (Array.isArray(input)) {
|
|
79
|
+
keys = input.filter((k) => typeof k === "string" && k);
|
|
80
|
+
} else if (input && typeof input === "object") {
|
|
81
|
+
keys = Object.keys(input).filter((k) => input[k]);
|
|
82
|
+
} else {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
return [...new Set(keys)].sort();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Extract the server's negotiation offer from a `cc agent --capabilities`
|
|
90
|
+
* manifest (buildAgentCapabilities output). Only the negotiable wire features
|
|
91
|
+
* the manifest advertises truthy are offered.
|
|
92
|
+
* @param {object} manifest
|
|
93
|
+
* @returns {{protocolVersion:number, minProtocolVersion:number, features:string[]}}
|
|
94
|
+
*/
|
|
95
|
+
export function buildServerOffer(manifest = {}) {
|
|
96
|
+
const protocolVersion = intOr(manifest.protocol_version, PROTOCOL_VERSION);
|
|
97
|
+
const minProtocolVersion = intOr(
|
|
98
|
+
manifest.min_protocol_version,
|
|
99
|
+
Math.min(PROTOCOL_MIN_VERSION, protocolVersion),
|
|
100
|
+
);
|
|
101
|
+
const advertised = normalizeFeatureList(manifest.features);
|
|
102
|
+
const features = PROTOCOL_FEATURES.filter((f) => advertised.includes(f));
|
|
103
|
+
return { protocolVersion, minProtocolVersion, features };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Negotiate the effective protocol version + feature set between a server offer
|
|
108
|
+
* and a client offer.
|
|
109
|
+
*
|
|
110
|
+
* @param {{protocolVersion:number, minProtocolVersion?:number, features:string[]}} server
|
|
111
|
+
* @param {null|{protocolVersion?:number, minProtocolVersion?:number, features?:string[]}} clientOffer
|
|
112
|
+
* @param {object} [opts]
|
|
113
|
+
* @param {Record<string,number>} [opts.featureMinVersion=FEATURE_MIN_VERSION]
|
|
114
|
+
* @returns {{
|
|
115
|
+
* ok:boolean, agreedVersion:(number|null), features:string[],
|
|
116
|
+
* downgraded:boolean, disabledFeatures:string[], clientAware:boolean,
|
|
117
|
+
* reason:(string|null)
|
|
118
|
+
* }}
|
|
119
|
+
*/
|
|
120
|
+
export function negotiateProtocol(server = {}, clientOffer = null, opts = {}) {
|
|
121
|
+
const featureMinVersion = opts.featureMinVersion || FEATURE_MIN_VERSION;
|
|
122
|
+
const serverMax = intOr(server.protocolVersion, PROTOCOL_VERSION);
|
|
123
|
+
const serverMin = intOr(server.minProtocolVersion, serverMax);
|
|
124
|
+
const serverFeatures = normalizeFeatureList(server.features);
|
|
125
|
+
|
|
126
|
+
const versionOk = (f, v) => (featureMinVersion[f] || 0) <= v;
|
|
127
|
+
|
|
128
|
+
// No client offer → legacy peer. Keep full behavior (only version-gate the
|
|
129
|
+
// server's own features against its own max), byte-for-byte unchanged.
|
|
130
|
+
if (clientOffer == null) {
|
|
131
|
+
return {
|
|
132
|
+
ok: true,
|
|
133
|
+
agreedVersion: serverMax,
|
|
134
|
+
features: serverFeatures.filter((f) => versionOk(f, serverMax)),
|
|
135
|
+
downgraded: false,
|
|
136
|
+
disabledFeatures: [],
|
|
137
|
+
clientAware: false,
|
|
138
|
+
reason: null,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const clientMax = intOr(clientOffer.protocolVersion, serverMax);
|
|
143
|
+
const clientMin = intOr(clientOffer.minProtocolVersion, clientMax);
|
|
144
|
+
const agreedVersion = Math.min(serverMax, clientMax);
|
|
145
|
+
const floor = Math.max(serverMin, clientMin);
|
|
146
|
+
|
|
147
|
+
if (agreedVersion < floor) {
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
agreedVersion: null,
|
|
151
|
+
features: [],
|
|
152
|
+
downgraded: true,
|
|
153
|
+
disabledFeatures: [...serverFeatures].sort(),
|
|
154
|
+
clientAware: true,
|
|
155
|
+
reason:
|
|
156
|
+
`no common protocol version (server ${serverMin}-${serverMax}, ` +
|
|
157
|
+
`client ${clientMin}-${clientMax})`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// A client that omits `features` accepts whatever the agreed version offers;
|
|
162
|
+
// a client that sends the array narrows to the intersection.
|
|
163
|
+
const clientFeatures =
|
|
164
|
+
clientOffer.features === undefined
|
|
165
|
+
? null
|
|
166
|
+
: new Set(normalizeFeatureList(clientOffer.features));
|
|
167
|
+
|
|
168
|
+
const enabled = [];
|
|
169
|
+
const disabled = [];
|
|
170
|
+
for (const f of serverFeatures) {
|
|
171
|
+
const okVersion = versionOk(f, agreedVersion);
|
|
172
|
+
const okClient = clientFeatures == null || clientFeatures.has(f);
|
|
173
|
+
if (okVersion && okClient) enabled.push(f);
|
|
174
|
+
else disabled.push(f);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
ok: true,
|
|
179
|
+
agreedVersion,
|
|
180
|
+
features: enabled.sort(),
|
|
181
|
+
downgraded: agreedVersion < serverMax || disabled.length > 0,
|
|
182
|
+
disabledFeatures: disabled.sort(),
|
|
183
|
+
clientAware: true,
|
|
184
|
+
reason: null,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Feature key → the stream line field it gates. */
|
|
189
|
+
const FEATURE_TO_FIELD = {
|
|
190
|
+
event_seq: "seq",
|
|
191
|
+
trace_id: "trace_id",
|
|
192
|
+
tool_use_id: "tool_use_id",
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Fold a negotiation result into a live field-gate the emitter reads per line:
|
|
197
|
+
* a field stays stamped only if its feature survived negotiation. On an
|
|
198
|
+
* incompatible (ok:false) result nothing is changed (the caller keeps its safe
|
|
199
|
+
* baseline). Mutates `gate` in place and returns it.
|
|
200
|
+
*
|
|
201
|
+
* @param {{features:string[], ok:boolean}} result
|
|
202
|
+
* @param {{seq?:boolean, trace_id?:boolean, tool_use_id?:boolean}} gate
|
|
203
|
+
*/
|
|
204
|
+
export function applyNegotiationToGate(result, gate = {}) {
|
|
205
|
+
if (!result || result.ok === false) return gate;
|
|
206
|
+
const enabled = new Set(result.features || []);
|
|
207
|
+
for (const feature of PROTOCOL_FEATURES) {
|
|
208
|
+
const field = FEATURE_TO_FIELD[feature];
|
|
209
|
+
if (field) gate[field] = enabled.has(feature);
|
|
210
|
+
}
|
|
211
|
+
return gate;
|
|
212
|
+
}
|
|
@@ -213,6 +213,17 @@ const PRINT_COMMANDS = new Set([
|
|
|
213
213
|
const SECRET_VAR_RE =
|
|
214
214
|
/(?:^|_)(KEY|KEYS|TOKEN|SECRET|SECRETS|PASSWORD|PASSWD|PASSPHRASE|CREDENTIAL|CREDENTIALS|PRIVATE|APIKEY|ACCESSKEY)(?:_|$)/i;
|
|
215
215
|
|
|
216
|
+
/**
|
|
217
|
+
* True when an environment-variable NAME looks like it holds a secret
|
|
218
|
+
* (ANTHROPIC_API_KEY, GITHUB_TOKEN, DB_PASSWORD, …). Shared single source of
|
|
219
|
+
* truth so the credential READ guard and the credential PROXY
|
|
220
|
+
* ([[credential-proxy.js]] — which keeps secrets out of subprocess envs)
|
|
221
|
+
* classify identically. `MONKEY`/`KEYBOARD`/`TOKENIZER` do NOT match.
|
|
222
|
+
*/
|
|
223
|
+
export function isSecretEnvName(name) {
|
|
224
|
+
return typeof name === "string" && SECRET_VAR_RE.test(name);
|
|
225
|
+
}
|
|
226
|
+
|
|
216
227
|
// Ways a secret var is referenced inside a shell segment.
|
|
217
228
|
const SECRET_REF_PATTERNS = [
|
|
218
229
|
/\$env:([A-Za-z_][A-Za-z0-9_]*)/gi, // PowerShell $env:NAME
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential proxy — keep the agent's real, long-lived credentials out of the
|
|
3
|
+
* environment that run_shell / run_code / hook / plugin subprocesses inherit
|
|
4
|
+
* (P0 §"跨平台沙箱与凭据代理").
|
|
5
|
+
*
|
|
6
|
+
* By default a spawned command inherits the WHOLE parent environment — including
|
|
7
|
+
* the agent's ANTHROPIC_API_KEY, cloud keys and tokens. A compromised or
|
|
8
|
+
* careless command can then echo, log, or exfiltrate them. This module replaces
|
|
9
|
+
* credential-named vars with an opaque SENTINEL before the child starts and
|
|
10
|
+
* keeps the real values in a parent-held vault, to be injected only for an
|
|
11
|
+
* approved host (via the egress proxy) — never handed to the child wholesale.
|
|
12
|
+
* The audit log only ever sees redacted values, never a restored secret.
|
|
13
|
+
*
|
|
14
|
+
* Pairs with credential-guard.js: that guard stops the AGENT reading secrets
|
|
15
|
+
* into model context; this stops the SUBPROCESS inheriting them. Both classify
|
|
16
|
+
* secret var names through the same `isSecretEnvName` so they never drift.
|
|
17
|
+
*
|
|
18
|
+
* Opt-in for now (`CC_CREDENTIAL_PROXY=1` or `config.credentialProxy.enabled`)
|
|
19
|
+
* so existing workflows that legitimately read a token from env keep working;
|
|
20
|
+
* default-on is the eventual goal once per-host injection is wired everywhere.
|
|
21
|
+
* Pure + dependency-light so every spawn seam can share it.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { isSecretEnvName } from "./credential-guard.js";
|
|
25
|
+
|
|
26
|
+
/** Opaque replacement a masked credential var carries into the child env. */
|
|
27
|
+
export const CREDENTIAL_SENTINEL_PREFIX = "cc-cred-redacted:";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Well-known credential vars whose NAME does not obviously match the generic
|
|
31
|
+
* KEY/TOKEN/SECRET/PASSWORD pattern. Most real ones already match; this is a
|
|
32
|
+
* small safety net, extendable per-project via `deny`.
|
|
33
|
+
*/
|
|
34
|
+
const EXTRA_CREDENTIAL_NAMES = new Set([
|
|
35
|
+
"AWS_SESSION_TOKEN", // matches TOKEN, kept explicit
|
|
36
|
+
"GOOGLE_APPLICATION_CREDENTIALS", // path to a key file
|
|
37
|
+
"CLOUDSDK_AUTH_ACCESS_TOKEN",
|
|
38
|
+
"DIGITALOCEAN_ACCESS_TOKEN",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
function toSet(value) {
|
|
42
|
+
if (value instanceof Set) return value;
|
|
43
|
+
return new Set(Array.isArray(value) ? value : value ? [value] : []);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Classify an env var NAME as credential-bearing. `opts.allow` forces
|
|
48
|
+
* pass-through (never masked); `opts.deny` forces masking; otherwise the shared
|
|
49
|
+
* secret-name classifier + the curated extra set decide.
|
|
50
|
+
*/
|
|
51
|
+
export function isCredentialEnvName(name, opts = {}) {
|
|
52
|
+
if (typeof name !== "string" || !name) return false;
|
|
53
|
+
if (toSet(opts.allow).has(name)) return false;
|
|
54
|
+
if (toSet(opts.deny).has(name)) return true;
|
|
55
|
+
return isSecretEnvName(name) || EXTRA_CREDENTIAL_NAMES.has(name);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The sentinel a child sees in place of a credential's real value. */
|
|
59
|
+
export function makeSentinel(name) {
|
|
60
|
+
return CREDENTIAL_SENTINEL_PREFIX + String(name ?? "");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** True when a value is a proxy sentinel (carries no secret). */
|
|
64
|
+
export function isSentinel(value) {
|
|
65
|
+
return (
|
|
66
|
+
typeof value === "string" && value.startsWith(CREDENTIAL_SENTINEL_PREFIX)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Mask credential-named vars in `env`. Returns a NEW object (never mutates the
|
|
72
|
+
* input):
|
|
73
|
+
* - `env` — credential vars replaced with a sentinel (mode "mask", the
|
|
74
|
+
* default) or removed entirely (mode "deny"); everything else
|
|
75
|
+
* copied through verbatim.
|
|
76
|
+
* - `masked` — sorted list of the var names that were masked/removed.
|
|
77
|
+
* - `vault` — Map<name, realValue> the PARENT keeps for approved injection;
|
|
78
|
+
* the child never receives it.
|
|
79
|
+
* Values that are already sentinels, null, or undefined are left as-is (a
|
|
80
|
+
* sentinel is never double-masked; an absent var is not invented).
|
|
81
|
+
*/
|
|
82
|
+
export function maskCredentialEnv(env = {}, opts = {}) {
|
|
83
|
+
const mode = opts.mode === "deny" ? "deny" : "mask";
|
|
84
|
+
const out = {};
|
|
85
|
+
const masked = [];
|
|
86
|
+
const vault = new Map();
|
|
87
|
+
for (const [name, value] of Object.entries(env || {})) {
|
|
88
|
+
if (
|
|
89
|
+
value != null &&
|
|
90
|
+
!isSentinel(value) &&
|
|
91
|
+
isCredentialEnvName(name, opts)
|
|
92
|
+
) {
|
|
93
|
+
masked.push(name);
|
|
94
|
+
vault.set(name, String(value));
|
|
95
|
+
if (mode === "mask") out[name] = makeSentinel(name);
|
|
96
|
+
// mode "deny": drop the var so the child does not even see a sentinel
|
|
97
|
+
} else {
|
|
98
|
+
out[name] = value;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
masked.sort();
|
|
102
|
+
return { env: out, masked, vault };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Replace a secret value with a fixed marker — never the real value. */
|
|
106
|
+
export function redactSecretValue(value) {
|
|
107
|
+
if (value == null) return value;
|
|
108
|
+
return String(value) ? "***" : "";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* A log-safe projection of an env for the audit trail: credential values →
|
|
113
|
+
* "***", sentinels kept verbatim (they hold no secret), everything else
|
|
114
|
+
* untouched. NEVER emits a restored/real credential value.
|
|
115
|
+
*/
|
|
116
|
+
export function redactEnvForAudit(env = {}, opts = {}) {
|
|
117
|
+
const out = {};
|
|
118
|
+
for (const [name, value] of Object.entries(env || {})) {
|
|
119
|
+
if (isSentinel(value)) out[name] = value;
|
|
120
|
+
else if (isCredentialEnvName(name, opts))
|
|
121
|
+
out[name] = redactSecretValue(value);
|
|
122
|
+
else out[name] = value;
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Resolve the real value for a masked credential — but ONLY when the target
|
|
129
|
+
* host is on the approved list. The whole point of the proxy is that the child
|
|
130
|
+
* never gets the raw secret; the parent injects it just-in-time for an approved
|
|
131
|
+
* destination (e.g. the egress proxy adding Authorization for an allowed API
|
|
132
|
+
* host). Returns null (fail closed) for an unknown var, an empty host, or a
|
|
133
|
+
* non-approved host.
|
|
134
|
+
*/
|
|
135
|
+
export function resolveApprovedInjection(vault, name, opts = {}) {
|
|
136
|
+
if (!(vault instanceof Map) || !vault.has(name)) return null;
|
|
137
|
+
const host = String(opts.host || "").toLowerCase();
|
|
138
|
+
if (!host) return null;
|
|
139
|
+
const approved = (opts.approvedHosts || []).map((h) =>
|
|
140
|
+
String(h || "").toLowerCase(),
|
|
141
|
+
);
|
|
142
|
+
return approved.includes(host) ? vault.get(name) : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Resolve whether the credential proxy is enabled. Env var wins so a spawn
|
|
147
|
+
* seam without config access can still honor it; otherwise the config flag.
|
|
148
|
+
*/
|
|
149
|
+
export function credentialProxyEnabled(config = {}, env = process.env) {
|
|
150
|
+
const raw = env && env.CC_CREDENTIAL_PROXY;
|
|
151
|
+
if (raw != null) {
|
|
152
|
+
const v = String(raw).toLowerCase();
|
|
153
|
+
return v === "1" || v === "true" || v === "on" || v === "yes";
|
|
154
|
+
}
|
|
155
|
+
return config?.credentialProxy?.enabled === true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Apply the credential proxy to a child env when enabled; otherwise return the
|
|
160
|
+
* env UNCHANGED (same reference — the default path stays byte-identical). The
|
|
161
|
+
* returned `vault` lets the caller inject approved creds later.
|
|
162
|
+
*
|
|
163
|
+
* @returns {{env:object, masked:string[], vault:Map, enabled:boolean}}
|
|
164
|
+
*/
|
|
165
|
+
export function applyCredentialProxy(env, options = {}) {
|
|
166
|
+
const procEnv = options.env || process.env;
|
|
167
|
+
const config = options.config || {};
|
|
168
|
+
if (!credentialProxyEnabled(config, procEnv)) {
|
|
169
|
+
return { env, masked: [], vault: new Map(), enabled: false };
|
|
170
|
+
}
|
|
171
|
+
const allow = new Set([
|
|
172
|
+
...toSet(options.allow),
|
|
173
|
+
...(config?.credentialProxy?.allow || []),
|
|
174
|
+
]);
|
|
175
|
+
const deny = new Set([
|
|
176
|
+
...toSet(options.deny),
|
|
177
|
+
...(config?.credentialProxy?.deny || []),
|
|
178
|
+
]);
|
|
179
|
+
const mode = options.mode || config?.credentialProxy?.mode || "mask";
|
|
180
|
+
const result = maskCredentialEnv(env, { allow, deny, mode });
|
|
181
|
+
return { ...result, enabled: true };
|
|
182
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-generated CLI reference + doc-drift detection (P2 "文档").
|
|
3
|
+
*
|
|
4
|
+
* The gap: commands, agent tools, flags, exit codes, and protocol fields are
|
|
5
|
+
* documented by hand and silently drift from the code. `command-manifest.json`
|
|
6
|
+
* (regenerated + CI-guarded) and the `AGENT_TOOLS` registry are the authoritative
|
|
7
|
+
* data sources; nothing turns them into a reference doc or checks a hand-written
|
|
8
|
+
* doc against them.
|
|
9
|
+
*
|
|
10
|
+
* This is the PURE core: build a canonical reference from the manifest + tool
|
|
11
|
+
* list, render it, and diff a documentation surface against the authoritative
|
|
12
|
+
* sets to catch drift in BOTH directions (documented-but-gone / present-but-
|
|
13
|
+
* undocumented). No I/O — the caller reads the manifest, tool names, and doc text.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Normalize the command-manifest.json commands into a stable reference list. */
|
|
17
|
+
export function extractCommands(manifest = {}) {
|
|
18
|
+
const commands = Array.isArray(manifest.commands) ? manifest.commands : [];
|
|
19
|
+
return commands
|
|
20
|
+
.map((c) => ({
|
|
21
|
+
name: String(c?.name || "").trim(),
|
|
22
|
+
aliases: Array.isArray(c?.aliases)
|
|
23
|
+
? c.aliases.map((a) => String(a).trim()).filter(Boolean)
|
|
24
|
+
: [],
|
|
25
|
+
summary: String(c?.summary || "").trim(),
|
|
26
|
+
}))
|
|
27
|
+
.filter((c) => c.name)
|
|
28
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Normalize the AGENT_TOOLS registry into a sorted, de-duped name list. Accepts
|
|
33
|
+
* plain strings, `{ name }` contracts, and OpenAI-style `{ function: { name } }`
|
|
34
|
+
* function-tool definitions.
|
|
35
|
+
*/
|
|
36
|
+
export function extractToolNames(tools = []) {
|
|
37
|
+
const names = (Array.isArray(tools) ? tools : [])
|
|
38
|
+
.map((t) => {
|
|
39
|
+
if (typeof t === "string") return t.trim();
|
|
40
|
+
const n = t?.name ?? t?.function?.name;
|
|
41
|
+
return String(n || "").trim();
|
|
42
|
+
})
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
return [...new Set(names)].sort();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Every command token a user can invoke: primary names + aliases. */
|
|
48
|
+
export function commandTokens(commands) {
|
|
49
|
+
const set = new Set();
|
|
50
|
+
for (const c of commands) {
|
|
51
|
+
set.add(c.name);
|
|
52
|
+
for (const a of c.aliases) set.add(a);
|
|
53
|
+
}
|
|
54
|
+
return set;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build the canonical reference object from the authoritative sources. This is
|
|
59
|
+
* the data a generator serializes; `renderReferenceMarkdown` renders it.
|
|
60
|
+
*/
|
|
61
|
+
export function buildCliReference({ manifest = {}, tools = [] } = {}) {
|
|
62
|
+
const commands = extractCommands(manifest);
|
|
63
|
+
const toolNames = extractToolNames(tools);
|
|
64
|
+
return {
|
|
65
|
+
commandCount: commands.length,
|
|
66
|
+
toolCount: toolNames.length,
|
|
67
|
+
commands,
|
|
68
|
+
tools: toolNames,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Deterministic markdown render of the reference (stable output for diffing). */
|
|
73
|
+
export function renderReferenceMarkdown(reference) {
|
|
74
|
+
const lines = [];
|
|
75
|
+
lines.push("# CLI Reference (generated)");
|
|
76
|
+
lines.push("");
|
|
77
|
+
lines.push(
|
|
78
|
+
"<!-- Generated from command-manifest.json + AGENT_TOOLS. Do not edit by hand. -->",
|
|
79
|
+
);
|
|
80
|
+
lines.push("");
|
|
81
|
+
lines.push(`## Commands (${reference.commandCount})`);
|
|
82
|
+
lines.push("");
|
|
83
|
+
for (const c of reference.commands) {
|
|
84
|
+
const alias = c.aliases.length ? ` (aliases: ${c.aliases.join(", ")})` : "";
|
|
85
|
+
lines.push(`- \`${c.name}\`${alias}${c.summary ? ` — ${c.summary}` : ""}`);
|
|
86
|
+
}
|
|
87
|
+
lines.push("");
|
|
88
|
+
lines.push(`## Agent tools (${reference.toolCount})`);
|
|
89
|
+
lines.push("");
|
|
90
|
+
for (const name of reference.tools) lines.push(`- \`${name}\``);
|
|
91
|
+
lines.push("");
|
|
92
|
+
return lines.join("\n");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Which authoritative command/tool tokens appear as literal `cc <token>` /
|
|
97
|
+
* backticked `<token>` mentions in the doc text. We look for word-boundary
|
|
98
|
+
* mentions; a token is "documented" if it appears anywhere in the doc.
|
|
99
|
+
*/
|
|
100
|
+
function isMentioned(doc, token) {
|
|
101
|
+
if (!token) return false;
|
|
102
|
+
const esc = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
103
|
+
return new RegExp(`(^|[^\\w-])${esc}([^\\w-]|$)`).test(doc);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Diff a documentation surface against the authoritative sets. Reports drift both
|
|
108
|
+
* ways so neither a NEW undocumented command nor a REMOVED-but-still-documented
|
|
109
|
+
* one slips through.
|
|
110
|
+
*
|
|
111
|
+
* @param {object} args
|
|
112
|
+
* manifest — command-manifest.json object
|
|
113
|
+
* tools — AGENT_TOOLS array
|
|
114
|
+
* doc — the documentation text to check
|
|
115
|
+
* knownTokens — optional extra tokens to treat as authoritative (e.g. flags,
|
|
116
|
+
* exit codes, protocol fields) to catch their drift too
|
|
117
|
+
* @returns {{ undocumentedCommands, undocumentedTools, staleCommandMentions,
|
|
118
|
+
* ok, counts }}
|
|
119
|
+
*/
|
|
120
|
+
export function detectDocDrift({
|
|
121
|
+
manifest = {},
|
|
122
|
+
tools = [],
|
|
123
|
+
doc = "",
|
|
124
|
+
knownTokens = [],
|
|
125
|
+
} = {}) {
|
|
126
|
+
const commands = extractCommands(manifest);
|
|
127
|
+
const toolNames = extractToolNames(tools);
|
|
128
|
+
const text = String(doc || "");
|
|
129
|
+
|
|
130
|
+
const undocumentedCommands = commands
|
|
131
|
+
.filter((c) => !isMentioned(text, c.name))
|
|
132
|
+
.map((c) => c.name);
|
|
133
|
+
const undocumentedTools = toolNames.filter((n) => !isMentioned(text, n));
|
|
134
|
+
|
|
135
|
+
// Tokens the doc claims as commands (in a `cc <token>` construct) that are not
|
|
136
|
+
// in the manifest → stale references to removed/renamed commands.
|
|
137
|
+
const known = commandTokens(commands);
|
|
138
|
+
for (const t of knownTokens) known.add(String(t));
|
|
139
|
+
const staleCommandMentions = [];
|
|
140
|
+
const seen = new Set();
|
|
141
|
+
const ccRe = /\bcc\s+([a-z][a-z0-9-]*)\b/g;
|
|
142
|
+
let m;
|
|
143
|
+
while ((m = ccRe.exec(text)) !== null) {
|
|
144
|
+
const tok = m[1];
|
|
145
|
+
if (!known.has(tok) && !seen.has(tok)) {
|
|
146
|
+
seen.add(tok);
|
|
147
|
+
staleCommandMentions.push(tok);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const counts = {
|
|
152
|
+
undocumentedCommands: undocumentedCommands.length,
|
|
153
|
+
undocumentedTools: undocumentedTools.length,
|
|
154
|
+
staleCommandMentions: staleCommandMentions.length,
|
|
155
|
+
};
|
|
156
|
+
return {
|
|
157
|
+
undocumentedCommands,
|
|
158
|
+
undocumentedTools,
|
|
159
|
+
staleCommandMentions,
|
|
160
|
+
counts,
|
|
161
|
+
ok:
|
|
162
|
+
counts.undocumentedCommands === 0 &&
|
|
163
|
+
counts.undocumentedTools === 0 &&
|
|
164
|
+
counts.staleCommandMentions === 0,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import { execSync } from "node:child_process";
|
|
27
27
|
import { join, basename } from "node:path";
|
|
28
28
|
import { getHomeDir, getConfigPath } from "./paths.js";
|
|
29
|
+
import { checkAgenda, checkInstructionFiles } from "./runtime-checkup.js";
|
|
29
30
|
|
|
30
31
|
export const CHECK_LEVELS = Object.freeze({
|
|
31
32
|
OK: "ok",
|
|
@@ -540,6 +541,80 @@ async function worktreeSection(opts, deps) {
|
|
|
540
541
|
return { id: "worktrees", title: "Git worktrees", checks };
|
|
541
542
|
}
|
|
542
543
|
|
|
544
|
+
// ── runtime checkup: agenda expiry + verbose instruction files ──────────────
|
|
545
|
+
// Only the gaps the OTHER sections don't already cover. Stale sessions,
|
|
546
|
+
// worktrees, orphan processes and lost background agents are handled by
|
|
547
|
+
// transcriptSection / worktreeSection / backgroundSection — surfacing them here
|
|
548
|
+
// too would double-report, so this section deliberately covers just the agenda
|
|
549
|
+
// schedule store and the instruction files (cc.md / AGENTS.md / CLAUDE.md).
|
|
550
|
+
const SEVERITY_TO_LEVEL = {
|
|
551
|
+
error: CHECK_LEVELS.ERR,
|
|
552
|
+
warn: CHECK_LEVELS.WARN,
|
|
553
|
+
info: CHECK_LEVELS.INFO,
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
async function runtimeSection(opts, deps) {
|
|
557
|
+
const checks = [];
|
|
558
|
+
const now = deps.now();
|
|
559
|
+
|
|
560
|
+
// Agenda schedule store — overdue/never-fired wakeups, retirable leftovers.
|
|
561
|
+
try {
|
|
562
|
+
const { AgentScheduleStore } = await import("./agent-schedule-store.js");
|
|
563
|
+
const store = new AgentScheduleStore();
|
|
564
|
+
const entries = store.list().map((e) => ({
|
|
565
|
+
id: e.id,
|
|
566
|
+
dueAt: e.kind === "wakeup" ? e.dueAt : e.nextAt,
|
|
567
|
+
recurring: e.kind !== "wakeup",
|
|
568
|
+
}));
|
|
569
|
+
for (const f of checkAgenda(entries, now)) {
|
|
570
|
+
checks.push(
|
|
571
|
+
check(
|
|
572
|
+
f.id,
|
|
573
|
+
`agenda ${f.ref}`,
|
|
574
|
+
SEVERITY_TO_LEVEL[f.severity] || CHECK_LEVELS.INFO,
|
|
575
|
+
`${f.message} — ${f.remediation}`,
|
|
576
|
+
),
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
} catch (err) {
|
|
580
|
+
checks.push(failedCheck("agenda", "agenda schedule store", err));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Instruction files — flag verbose ones (byte size only; a derivability
|
|
584
|
+
// heuristic would need code analysis and isn't computed here).
|
|
585
|
+
try {
|
|
586
|
+
const cwd = opts.cwd || process.cwd();
|
|
587
|
+
const files = [];
|
|
588
|
+
for (const name of ["cc.md", "AGENTS.md", "CLAUDE.md"]) {
|
|
589
|
+
const p = join(cwd, name);
|
|
590
|
+
if (deps.existsSync(p)) {
|
|
591
|
+
try {
|
|
592
|
+
files.push({ path: name, bytes: deps.statSync(p).size });
|
|
593
|
+
} catch {
|
|
594
|
+
// raced/unreadable — skip
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
for (const f of checkInstructionFiles(files)) {
|
|
599
|
+
checks.push(
|
|
600
|
+
check(
|
|
601
|
+
f.id,
|
|
602
|
+
f.ref,
|
|
603
|
+
SEVERITY_TO_LEVEL[f.severity] || CHECK_LEVELS.INFO,
|
|
604
|
+
`${f.message} — ${f.remediation}`,
|
|
605
|
+
),
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
} catch (err) {
|
|
609
|
+
checks.push(failedCheck("instructions", "instruction files", err));
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (checks.length === 0) {
|
|
613
|
+
checks.push(check("runtime-clean", "runtime hygiene", CHECK_LEVELS.OK, ""));
|
|
614
|
+
}
|
|
615
|
+
return { id: "runtime", title: "Runtime checkup", checks };
|
|
616
|
+
}
|
|
617
|
+
|
|
543
618
|
/**
|
|
544
619
|
* Collect all checkup sections. Never throws — a failing subsystem becomes an
|
|
545
620
|
* `err` check inside its section.
|
|
@@ -557,6 +632,7 @@ export async function collectCheckupSections(opts = {}) {
|
|
|
557
632
|
transcriptSection,
|
|
558
633
|
backgroundSection,
|
|
559
634
|
worktreeSection,
|
|
635
|
+
runtimeSection,
|
|
560
636
|
]) {
|
|
561
637
|
try {
|
|
562
638
|
sections.push(await build(opts, deps));
|