chainlesschain 0.162.163 → 0.162.164
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -1
- package/src/assets/web-panel/assets/{AIOps-BQ0kgfA1.js → AIOps-BqRkaHGb.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-Dr1MDANe.js → ActionButton-DWO7OZLC.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CZ5vsaXW.js → Analytics-C5oaVezM.js} +2 -2
- package/src/assets/web-panel/assets/{AppLayout-BIBga881.js → AppLayout-CsUESDoZ.js} +3 -3
- package/src/assets/web-panel/assets/{Artifacts-vpXHRU3u.js → Artifacts-Dragqu5k.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-B8exuoub.js → Audit-CQsaHctY.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-923evRRN.js → BackgroundAgents-DdG1jNSM.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-sr_sswlQ.js → Backup-DN6sjK4z.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CnW-4XKT.js → BaseInput-Cxm_ZTCl.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-FZCsTZIL.js → Chat-DVXGmA56.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-DDPd9qT0.js → ChatBubbleRenderer-BdaCL_iE.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-DFrxB59H.js → Checkbox-DEc4vD_A.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-_b6WEel8.js → Codegen-CKiTcv36.js} +1 -1
- package/src/assets/web-panel/assets/{Col-CkbnnZ-K.js → Col-Br9UUNN0.js} +1 -1
- package/src/assets/web-panel/assets/{Community-BxM_82zW.js → Community-NeUH_70T.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DYinP45R.js → Compact-BqR-z2Df.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DyXp3sSP.js → Compliance-DcWB_SgZ.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BEr2DhW7.js → Cowork-BfhgcudF.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-B-Vwo1hN.js → Cron-LaU29LgD.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DbLxKddd.js → Crosschain-B2ZB9uwW.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BsQ_LFw9.js → DID-BuHl38A2.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-lVlXQHu5.js → Dashboard-m627yqvZ.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-Beo_foO7.js → Dropdown-C7bxF8F5.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-DszHIXED.js → EmailListRenderer-DPLPQzLp.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-DcAwoh81.js → FamilyGuardDashboard-cOiH6ike.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-CuJLa2eb.js → Federation-Bvkzg88z.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Cxd1lxhc.js → FormItemContext-BTsiA7h_.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BBvq5Jsw.js → GenericCardRenderer-BGnVE5qu.js} +1 -1
- package/src/assets/web-panel/assets/{Git-WqPKetx6.js → Git-DS_j6r25.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CWYtY3ke.js → Governance-CKRG3Rsl.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BnX1iiNV.js → Inference-DW8INoNQ.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-BJuM8GN5.js → KnowledgeGraph-BhEYeFhC.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-DqLMNntm.js → Logs-wqi0uxuR.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-BqYwEYou.js → MarkdownRenderer-DS8dE8ty.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-_vGfmhNl.js → Marketplace-CMlTuPLT.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BeJDOkDj.js → McpTools-CSx4m5JJ.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-Da0EEZDO.js → Memory-vnQCUy-_.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-DSuzcCCy.js → MobileBridge-BV2NA742.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-Bw0k8e1B.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-BE9YgdA0.js → Mtc-CzK_FPB1.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-fC2BpO5S.js → MtcAudit-Cb7G9oL3.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-D24Xxqw3.js → Multisig-qJxlUyzr.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-Bs3oGvS8.js → NLProgramming-DqaBJ7Te.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-BFjmAA-3.js → Notes-CwhTJkRi.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-BX_9Bhv0.js → NotificationSettings-YDs4Fal4.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-wjkZwJXI.js → OrderTableRenderer-CvEALpq7.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-DBcdsVEP.js → Organization-CEmQm9n8.js} +2 -2
- package/src/assets/web-panel/assets/{Overflow-DDyaF9vC.js → Overflow-zby_P_LA.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BB78L4he.js → P2P-DsmDNFNT.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-DqTiZFum.js → PdhVaultBrowser-FWwFlZGF.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-INbnWv4j.js → Permissions-DuzQ6me5.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BST-VmTq.js → PersonalDataHub-DZbeN613.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-CE7_tS2k.js → Pipeline-CoNSVAUS.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-hK5L8dOB.js → Privacy-OYJEAaMC.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BvkkUoDn.js → ProjectInit-CWIapV--.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-u7_EUtkm.js → ProjectSettings-CX8xMxZn.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-5Fmt_8Nw.js → Projects-BLCN8YyY.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-B9eanarR.js → Providers-BV_sxp75.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-C0dX5N2f.js → QrScannerModal-BTnRrHRp.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-CKTpG-PZ.js → QuickAsk-D1KZg018.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Px_w_XQM.js → Recommend-COm5cuNg.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-Di0fnYkM.js → RemoteSession-0saWEkYI.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-vesnWjXt.js → Reputation-Dr7iKLS2.js} +1 -1
- package/src/assets/web-panel/assets/{Row-exbY6LkT.js → Row-C1vpPBcr.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-B3O3x70t.js → RssFeed-CxlWrtNj.js} +2 -2
- package/src/assets/web-panel/assets/{Search-uRA0DhNz.js → Search-BT2V8dPN.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CViqdH6K.js → Security-bm9V3c5k.js} +4 -4
- package/src/assets/web-panel/assets/{Services-CkXS97WN.js → Services-CocH1yyX.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DxW5WYnE.js → Skeleton-DHItlMdd.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CLxmGFse.js → Skills-C59sLc4w.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-Cr5fziXq.js → Sla-Crj4BMc8.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-H0JSb3cp.js → SpeechSettings-DX4KFceY.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-DMYWMae7.js → SyncSettings-BD_iwQLr.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-CbYyDdF4.js → Tasks-BmCTzYsH.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-Cpc8JjiK.js → Templates-BGUaxZYl.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-CRh77Jda.js → Tenant-BS0tJx2O.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-4D8tNBO5.js → Terminal--tHSAsld.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-D41jzzbG.js → TimelineRenderer-D9-MFxS4.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-C_jWEy6b.js → Tokens-CiCB485f.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-Dk1ZUhsy.js → Trigger-SmCFuW_l.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-CLVy4l7m.js → Trust-DkK3Rqe0.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-C-FeqFDj.js → UkeySign-CDM4LGdB.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-CqVZzyMm.js → VideoEditing-IDB7DJXt.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DX9BW9pu.js → Wallet-Bikj5cmt.js} +2 -2
- package/src/assets/web-panel/assets/{WebAuthn-7hQDpWw_.js → WebAuthn-C9abWlAw.js} +2 -2
- package/src/assets/web-panel/assets/{WorkflowEditor-6XqUAO0j.js → WorkflowEditor-DrbRKVky.js} +1 -1
- package/src/assets/web-panel/assets/{chat-5R_vkaJG.js → chat-CccCSDQf.js} +1 -1
- package/src/assets/web-panel/assets/{colors-huUPZdC-.js → colors-CXObXLhb.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CfJlgisE.js → compact-item-B5kfYAxC.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-jO8heofr.js → createContext-DmiPBfip.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CvKrADNa.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DY8Vxqx7.js → hasIn-Q6lYteAc.js} +1 -1
- package/src/assets/web-panel/assets/{index-BtEXSoPL.js → index-25Wlqps4.js} +1 -1
- package/src/assets/web-panel/assets/{index-BsBtRWbA.js → index-33U29UNV.js} +1 -1
- package/src/assets/web-panel/assets/{index-BQS-uPOK.js → index-4oXBDczR.js} +1 -1
- package/src/assets/web-panel/assets/{index-DXBcvUnf.js → index-BGYXvMc5.js} +1 -1
- package/src/assets/web-panel/assets/{index-C2iVSFpy.js → index-BJCCa0s_.js} +1 -1
- package/src/assets/web-panel/assets/{index-A48PtXw9.js → index-BJJ5DhKI.js} +1 -1
- package/src/assets/web-panel/assets/{index-Db1cLWir.js → index-BLOzViJ6.js} +1 -1
- package/src/assets/web-panel/assets/{index-vXs3B8GJ.js → index-BRrbUMoG.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHE8OIls.js → index-BUFpT17Q.js} +1 -1
- package/src/assets/web-panel/assets/{index-BmqGerpC.js → index-BkRDiqGc.js} +1 -1
- package/src/assets/web-panel/assets/{index-CzKmteNS.js → index-BsfvW6my.js} +1 -1
- package/src/assets/web-panel/assets/{index-BAON7TzU.js → index-By-kfPED.js} +1 -1
- package/src/assets/web-panel/assets/{index-DoI2sZm7.js → index-BzFTcQsc.js} +1 -1
- package/src/assets/web-panel/assets/{index-j3O2EEJe.js → index-C2FXdboq.js} +1 -1
- package/src/assets/web-panel/assets/{index-CRElRhyS.js → index-C5XOlTHN.js} +1 -1
- package/src/assets/web-panel/assets/{index-KRHroryI.js → index-CLhBDS2w.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bxax9vQ6.js → index-CMq7UeoT.js} +1 -1
- package/src/assets/web-panel/assets/{index-BfCx4pFT.js → index-CVMTWNKl.js} +1 -1
- package/src/assets/web-panel/assets/{index-CA6sO5VD.js → index-Caghi-yL.js} +1 -1
- package/src/assets/web-panel/assets/{index-CA90TZxG.js → index-CdB6FuSH.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cbe9G0V4.js → index-CoK8-kWQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-D0pet5Uy.js → index-CsO3GUAY.js} +1 -1
- package/src/assets/web-panel/assets/{index-ChvHubX3.js → index-CwZBa20Z.js} +1 -1
- package/src/assets/web-panel/assets/{index-BRXOP09X.js → index-D-kASucw.js} +1 -1
- package/src/assets/web-panel/assets/{index-B74OXFxC.js → index-D8eBw7vW.js} +1 -1
- package/src/assets/web-panel/assets/{index-D1VLuww0.js → index-D9wgifk4.js} +1 -1
- package/src/assets/web-panel/assets/{index-p85YaaKw.js → index-DAYCTc07.js} +1 -1
- package/src/assets/web-panel/assets/{index-BliaT02i.js → index-DF6Q8l2h.js} +1 -1
- package/src/assets/web-panel/assets/index-DTdFUWc_.js +1 -0
- package/src/assets/web-panel/assets/{index-Cjxbu68P.js → index-DgTLApS9.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cis_Ip7H.js → index-DgxVJ8pg.js} +1 -1
- package/src/assets/web-panel/assets/{index-v3pTaEQm.js → index-DlJvIqoR.js} +1 -1
- package/src/assets/web-panel/assets/{index-BYLBPpt8.js → index-Dw4eHfsG.js} +1 -1
- package/src/assets/web-panel/assets/{index-B300RPnT.js → index-Rh-ytwow.js} +3 -3
- package/src/assets/web-panel/assets/{index-CLhBh-qE.js → index-StAEt2pq.js} +1 -1
- package/src/assets/web-panel/assets/{index-d1xkbtNz.js → index-bzfw951k.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCqxyvqp.js → index-fSUMbMvv.js} +1 -1
- package/src/assets/web-panel/assets/{index-dKx57io7.js → index-tFHy5Wol.js} +1 -1
- package/src/assets/web-panel/assets/index-zQ0M_kz-.js +1 -0
- package/src/assets/web-panel/assets/{initDefaultProps-MCB7-raZ.js → initDefaultProps-BNq7X72N.js} +1 -1
- package/src/assets/web-panel/assets/{motion-UWDF9swV.js → motion-DOf9viTc.js} +1 -1
- package/src/assets/web-panel/assets/{move-CsTmgXZU.js → move-CRHTcW4x.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-fQy6KGLg.js → mtc-parser-Cyoozgm2.js} +1 -1
- package/src/assets/web-panel/assets/{omit-woPnEjd1.js → omit-6-TjFNru.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-Bs2dZ1gf.js → pickAttrs-DlUmO1zi.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-C7EVgPkg.js → placementArrow-CFOL8mIR.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-BH2kYCTN.js → responsiveObserve-BtaFK5zd.js} +1 -1
- package/src/assets/web-panel/assets/{slide-BFBP2V_e.js → slide-BIbc2bur.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-xdDxDYse.js → statusUtils-BXX9d6-w.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-aAWgPPQf.js → styleChecker-BGv10Bpn.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DkGSAWoO.js → useFlexGapSupport-bKnKT8tw.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-BrOih40N.js → useFs-FY4IL4jn.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-eIYrgMqO.js → usePersonalDataHub-Bwl9TEgu.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-B8lutNSE.js → vnode-BM3OHxAT.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CffNre7V.js → zoom-D1fbFiHB.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agenda.js +71 -14
- package/src/commands/doctor.js +85 -1
- package/src/commands/plugin.js +226 -9
- package/src/commands/review.js +17 -1
- package/src/commands/session.js +191 -0
- package/src/commands/team.js +19 -1
- package/src/data/changelog.json +5 -4
- package/src/gateways/ws/session-protocol.js +50 -0
- package/src/gateways/ws/ws-agent-handler.js +50 -0
- package/src/harness/jsonl-session-store.js +60 -0
- package/src/harness/worktree-isolator.js +140 -5
- package/src/lib/agent-schedule-store.js +30 -1
- package/src/lib/agent-team/team-worktree.js +37 -2
- package/src/lib/agent-tool-admission.js +125 -0
- package/src/lib/app-preview.js +360 -0
- package/src/lib/async-hook-supervisor.cjs +54 -0
- package/src/lib/capability-manifest.js +319 -0
- package/src/lib/capability-negotiation.js +23 -12
- package/src/lib/diagnostic-bundle.js +230 -0
- package/src/lib/doctor-checkup.js +223 -4
- package/src/lib/execution-location.js +286 -0
- package/src/lib/headless-manifest.js +13 -40
- package/src/lib/hook-stats-store.cjs +197 -0
- package/src/lib/idempotency.js +109 -0
- package/src/lib/jsonl-session-store.js +1 -0
- package/src/lib/lsp/__tests__/lsp-manager.test.js +58 -0
- package/src/lib/lsp/lsp-manager.js +56 -0
- package/src/lib/monitor-event.js +290 -0
- package/src/lib/operation-fingerprint.js +217 -0
- package/src/lib/plugin-runtime/bin.js +37 -0
- package/src/lib/plugin-runtime/capabilities.js +19 -0
- package/src/lib/plugin-runtime/capability-consent.js +28 -0
- package/src/lib/plugin-runtime/hooks.js +37 -0
- package/src/lib/plugin-runtime/lsp.js +38 -0
- package/src/lib/plugin-runtime/mcp.js +40 -0
- package/src/lib/plugin-runtime/monitors.js +37 -0
- package/src/lib/plugin-runtime/plugin-options.js +151 -0
- package/src/lib/pr-automation-policy.js +224 -0
- package/src/lib/remote-approval-bridge.js +66 -13
- package/src/lib/repl-rewind.js +101 -0
- package/src/lib/review-comment-anchor.js +241 -0
- package/src/lib/review-pipeline.js +46 -7
- package/src/lib/runtime-checkup.js +221 -0
- package/src/lib/secret-scan.js +409 -0
- package/src/lib/session-branch.js +0 -0
- package/src/lib/side-effect-ledger.js +105 -0
- package/src/lib/skill-loader.js +32 -0
- package/src/lib/skill-path-scope.js +63 -0
- package/src/lib/sub-agent-context.js +36 -4
- package/src/lib/sub-agent-registry.js +19 -0
- package/src/lib/telemetry/span-recorder.js +17 -4
- package/src/lib/terminal-context.js +195 -0
- package/src/lib/unattended-action-policy.js +182 -0
- package/src/lib/worktree-cleanup-safety.js +138 -0
- package/src/lib/worktree-isolator.js +8 -0
- package/src/lib/worktree-sparse.js +131 -0
- package/src/repl/agent-repl.js +152 -38
- package/src/runtime/agent-core.js +191 -13
- package/src/runtime/diagnostics.js +60 -0
- package/src/runtime/headless-runner.js +126 -1
- package/src/assets/web-panel/assets/MobileProjects-D0RAk4e8.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DeIrCfH-.js +0 -1
- package/src/assets/web-panel/assets/index-BxUKMRpP.js +0 -1
- package/src/assets/web-panel/assets/index-DscU9iPI.js +0 -1
|
@@ -66,6 +66,82 @@ export function kindIsIdempotent(kind) {
|
|
|
66
66
|
return KIND_IDEMPOTENT.get(String(kind || "")) === true;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/** Trim a free-text descriptor to a short, log-safe key. */
|
|
70
|
+
function shortKey(v) {
|
|
71
|
+
if (v == null) return null;
|
|
72
|
+
const s = String(v);
|
|
73
|
+
return s.length > 80 ? s.slice(0, 80) + "…" : s;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Classify one agent tool call into a side-effect kind for the ledger, or null
|
|
78
|
+
* when the tool issues no irreversible external effect (reads, searches, plain
|
|
79
|
+
* git status, etc.). PURE. Conservative by design — an opaque shell command is
|
|
80
|
+
* NON-idempotent so a mid-flight crash forces INSPECT rather than a blind
|
|
81
|
+
* replay (the P0-2 "started_unknown 风险操作默认 needs_input" invariant).
|
|
82
|
+
*
|
|
83
|
+
* Only the tools that can leave a durable external mark are recorded; read-only
|
|
84
|
+
* and purely-local-recoverable tools return null so a resumed session is not
|
|
85
|
+
* spammed with false "verify before replay" warnings.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} toolName
|
|
88
|
+
* @param {object} [args]
|
|
89
|
+
* @returns {{kind:string, key:string|null}|null}
|
|
90
|
+
*/
|
|
91
|
+
export function classifyToolSideEffect(toolName, args = {}) {
|
|
92
|
+
const name = String(toolName || "");
|
|
93
|
+
const a = args && typeof args === "object" ? args : {};
|
|
94
|
+
switch (name) {
|
|
95
|
+
case "write_file":
|
|
96
|
+
case "notebook_edit":
|
|
97
|
+
case "edit_file":
|
|
98
|
+
return {
|
|
99
|
+
kind: "file-write",
|
|
100
|
+
key: shortKey(a.path || a.file || a.notebook_path),
|
|
101
|
+
};
|
|
102
|
+
case "edit_file_hashed":
|
|
103
|
+
// Hash-guarded: re-applying over an already-changed tree fails safely, so
|
|
104
|
+
// a replay cannot silently double-apply — idempotent-by-construction.
|
|
105
|
+
return {
|
|
106
|
+
kind: "file-write-checkpointed",
|
|
107
|
+
key: shortKey(a.path || a.file),
|
|
108
|
+
};
|
|
109
|
+
case "run_shell":
|
|
110
|
+
case "run_code":
|
|
111
|
+
// Opaque command: could be `npm publish`, `rm -rf`, or just `npm test`.
|
|
112
|
+
// We cannot tell, so fail-closed to a non-idempotent effect.
|
|
113
|
+
return { kind: "shell", key: shortKey(a.command || a.code) };
|
|
114
|
+
case "git": {
|
|
115
|
+
const cmd = String(a.command || "").trim();
|
|
116
|
+
const verb = cmd.split(/\s+/)[0] || "";
|
|
117
|
+
if (verb === "push") return { kind: "git-push", key: shortKey(cmd) };
|
|
118
|
+
// Every other git op (status/diff/commit/branch/fetch/pull/…) is either
|
|
119
|
+
// read-only or locally recoverable via git — not an irreversible effect.
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
case "publish_artifact":
|
|
123
|
+
return {
|
|
124
|
+
kind: "network-mutation",
|
|
125
|
+
key: shortKey(a.title || a.path || a.name),
|
|
126
|
+
};
|
|
127
|
+
case "schedule":
|
|
128
|
+
return {
|
|
129
|
+
kind: "network-mutation",
|
|
130
|
+
key: shortKey(a.name || a.cron || a.when),
|
|
131
|
+
};
|
|
132
|
+
case "notify":
|
|
133
|
+
return { kind: "network-mutation", key: shortKey(a.title || a.message) };
|
|
134
|
+
case "browser_act":
|
|
135
|
+
// A CDP action (click/type/submit) can mutate remote state.
|
|
136
|
+
return {
|
|
137
|
+
kind: "network-mutation",
|
|
138
|
+
key: shortKey(a.action || a.selector),
|
|
139
|
+
};
|
|
140
|
+
default:
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
69
145
|
function nowFrom(clock) {
|
|
70
146
|
return typeof clock === "function" ? Number(clock()) || 0 : 0;
|
|
71
147
|
}
|
|
@@ -271,6 +347,35 @@ export function planOpRecovery(op) {
|
|
|
271
347
|
}
|
|
272
348
|
}
|
|
273
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Count COMMITTED side-effects that share an idempotency key beyond the first —
|
|
352
|
+
* the "resume 后重复副作用 = 0" acceptance metric (P0-2 §11.2). A committed op's
|
|
353
|
+
* dedup key is `meta.idempotencyKey`; when absent it falls back to the (unique)
|
|
354
|
+
* `opId`, so un-keyed ops never register as duplicates. A correct resume never
|
|
355
|
+
* re-commits an already-committed effect, so this must stay `0`.
|
|
356
|
+
*
|
|
357
|
+
* @param {SideEffectLedger|{ops:Array}} ledger
|
|
358
|
+
* @returns {number}
|
|
359
|
+
*/
|
|
360
|
+
export function countDuplicateCommittedEffects(ledger) {
|
|
361
|
+
const ops =
|
|
362
|
+
ledger instanceof SideEffectLedger
|
|
363
|
+
? ledger.list()
|
|
364
|
+
: Array.isArray(ledger?.ops)
|
|
365
|
+
? ledger.ops
|
|
366
|
+
: [];
|
|
367
|
+
const seen = new Map();
|
|
368
|
+
let duplicates = 0;
|
|
369
|
+
for (const o of ops) {
|
|
370
|
+
if (o?.state !== SIDE_EFFECT_STATE.COMMITTED) continue;
|
|
371
|
+
const key = (o.meta && o.meta.idempotencyKey) || o.opId;
|
|
372
|
+
const n = (seen.get(key) || 0) + 1;
|
|
373
|
+
seen.set(key, n);
|
|
374
|
+
if (n > 1) duplicates++;
|
|
375
|
+
}
|
|
376
|
+
return duplicates;
|
|
377
|
+
}
|
|
378
|
+
|
|
274
379
|
/**
|
|
275
380
|
* Build a recovery plan for every op in a (crash-rebuilt) ledger.
|
|
276
381
|
*
|
package/src/lib/skill-loader.js
CHANGED
|
@@ -28,6 +28,7 @@ import { getElectronUserDataDir } from "./paths.js";
|
|
|
28
28
|
import { findProjectRoot } from "./project-detector.js";
|
|
29
29
|
import { findProjectRoot as findGitRoot } from "./project-instructions.js";
|
|
30
30
|
import { parseSkillMcpServers } from "./skill-mcp.js";
|
|
31
|
+
import { normalizeSkillPaths } from "./skill-path-scope.js";
|
|
31
32
|
import { discoverPluginSkillLayers } from "./plugin-runtime/skills.js";
|
|
32
33
|
import settingsLoader from "./settings-loader.cjs";
|
|
33
34
|
|
|
@@ -467,6 +468,10 @@ export class CLISkillLoader {
|
|
|
467
468
|
executionMode: data.executionMode || null,
|
|
468
469
|
cliDomain: data.cliDomain || null,
|
|
469
470
|
cliVersionHash: data.cliVersionHash || null,
|
|
471
|
+
// Optional per-directory path-scope: when set, the skill is only
|
|
472
|
+
// surfaced/runnable under a matching subtree (large-monorepo lazy
|
|
473
|
+
// context). null (the default) = applies everywhere.
|
|
474
|
+
paths: normalizeSkillPaths(data),
|
|
470
475
|
dirName: entry.name,
|
|
471
476
|
hasHandler: fs.existsSync(path.join(skillDir, "handler.js")),
|
|
472
477
|
body,
|
|
@@ -515,6 +520,33 @@ export class CLISkillLoader {
|
|
|
515
520
|
return this._cache;
|
|
516
521
|
}
|
|
517
522
|
|
|
523
|
+
/**
|
|
524
|
+
* Every skill found in every layer, WITHOUT dedup. loadAll() keeps only the
|
|
525
|
+
* winning copy per id (Map.set overwrite), so a skill id defined in more than
|
|
526
|
+
* one layer silently shadows — a caller (e.g. `cc doctor`) needs the full,
|
|
527
|
+
* un-deduped list to detect and report that. Same scan logic as loadAll, no
|
|
528
|
+
* override collapse.
|
|
529
|
+
* @returns {Array<{id:string, layer:string, skillDir:string}>}
|
|
530
|
+
*/
|
|
531
|
+
listSkillLayerEntries(options = {}) {
|
|
532
|
+
if (allSkillsDisabled(options)) return [];
|
|
533
|
+
const dropBundled = bundledSkillsDisabled(options);
|
|
534
|
+
const out = [];
|
|
535
|
+
for (const { layer, path: layerPath, exists } of this.getLayerPaths()) {
|
|
536
|
+
if (!exists) continue;
|
|
537
|
+
if (dropBundled && (layer === "bundled" || layer === "cli-bundled"))
|
|
538
|
+
continue;
|
|
539
|
+
for (const skill of this._loadFromDir(layerPath, layer)) {
|
|
540
|
+
out.push({
|
|
541
|
+
id: skill.id,
|
|
542
|
+
layer: skill.source || layer,
|
|
543
|
+
skillDir: skill.skillDir,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return out;
|
|
548
|
+
}
|
|
549
|
+
|
|
518
550
|
/**
|
|
519
551
|
* Get resolved skills (uses cache if available)
|
|
520
552
|
* @returns {object[]}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-directory skill path-scoping (P1 large-monorepo lazy context).
|
|
3
|
+
*
|
|
4
|
+
* A skill may declare a `paths:` (or `globs:` / `pathScope:`) frontmatter list
|
|
5
|
+
* so it is only surfaced when the agent is working under a matching subtree —
|
|
6
|
+
* e.g. a skill scoped to `packages/cli/**` should not clutter discovery or run
|
|
7
|
+
* when the agent is operating in `android-app/`. This keeps the on-demand skill
|
|
8
|
+
* surface small in a big monorepo.
|
|
9
|
+
*
|
|
10
|
+
* Pure (path math only; the matching semantics are reused from
|
|
11
|
+
* project-instructions.js's `ruleApplies`, the same prefix-overlap rule used for
|
|
12
|
+
* path-scoped `.claude/rules`). A skill with no `paths` applies everywhere, so a
|
|
13
|
+
* skill set with no path-scoped skills is filtered byte-for-byte unchanged.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { ruleApplies } from "./project-instructions.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Normalize a skill's raw path-scope frontmatter (`paths` / `globs` /
|
|
21
|
+
* `pathScope`, string or array) into a non-empty string[] of globs, or null
|
|
22
|
+
* when the skill declares no scope (→ applies everywhere).
|
|
23
|
+
*/
|
|
24
|
+
export function normalizeSkillPaths(data) {
|
|
25
|
+
const raw = data?.paths ?? data?.globs ?? data?.pathScope ?? null;
|
|
26
|
+
if (raw == null) return null;
|
|
27
|
+
const list = (Array.isArray(raw) ? raw : [raw])
|
|
28
|
+
.map((g) => String(g == null ? "" : g).trim())
|
|
29
|
+
.filter(Boolean);
|
|
30
|
+
return list.length > 0 ? list : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The agent's cwd expressed relative to the project root, forward-slashed — the
|
|
35
|
+
* form `ruleApplies` expects (skill `paths` globs are project-root-relative,
|
|
36
|
+
* like rule globs). A cwd at (or outside) the root resolves to "" → every skill
|
|
37
|
+
* applies (never hide skills just because we can't locate the root).
|
|
38
|
+
*/
|
|
39
|
+
export function relCwdForCwd(cwd, root) {
|
|
40
|
+
if (!cwd || !root) return "";
|
|
41
|
+
let rel;
|
|
42
|
+
try {
|
|
43
|
+
rel = path.relative(root, cwd);
|
|
44
|
+
} catch {
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) return "";
|
|
48
|
+
return rel.replace(/\\/g, "/");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Does a skill's path-scope apply when the agent runs at `relCwd`? A skill with
|
|
53
|
+
* no `paths` (null/empty) applies everywhere.
|
|
54
|
+
*/
|
|
55
|
+
export function skillAppliesToCwd(skill, relCwd) {
|
|
56
|
+
return ruleApplies(skill?.paths || [], relCwd);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Filter a skill list to those whose path-scope applies at `relCwd`. */
|
|
60
|
+
export function filterSkillsByRelCwd(skills, relCwd) {
|
|
61
|
+
if (!Array.isArray(skills)) return skills;
|
|
62
|
+
return skills.filter((s) => skillAppliesToCwd(s, relCwd));
|
|
63
|
+
}
|
|
@@ -140,8 +140,13 @@ export class SubAgentContext {
|
|
|
140
140
|
this._onUsage =
|
|
141
141
|
typeof options.onUsage === "function" ? options.onUsage : null;
|
|
142
142
|
|
|
143
|
-
// Optional abort signal for cancellation
|
|
143
|
+
// Optional EXTERNAL abort signal for cancellation (e.g. a parent-provided
|
|
144
|
+
// AbortSignal). In addition, every context owns an INTERNAL controller so a
|
|
145
|
+
// single sub-agent can be cancelled precisely by id via `abort()` (the
|
|
146
|
+
// registry's `cancel(id)`), independent of any external signal.
|
|
144
147
|
this._signal = options.signal || null;
|
|
148
|
+
this._abortController = new AbortController();
|
|
149
|
+
this._cancelReason = null;
|
|
145
150
|
|
|
146
151
|
// Optional MCP / external tool plumbing. These are forwarded into the
|
|
147
152
|
// agentLoop options so MCP-backed tools (e.g. from a cowork template's
|
|
@@ -430,9 +435,9 @@ export class SubAgentContext {
|
|
|
430
435
|
}
|
|
431
436
|
}
|
|
432
437
|
|
|
433
|
-
// Check abort signal
|
|
434
|
-
if (this.
|
|
435
|
-
this.forceComplete("cancelled", {
|
|
438
|
+
// Check abort signal (external OR this context's own `abort()`)
|
|
439
|
+
if (this.isAborted()) {
|
|
440
|
+
this.forceComplete(this._cancelReason || "cancelled", {
|
|
436
441
|
partialContent: lastContent,
|
|
437
442
|
artifacts,
|
|
438
443
|
});
|
|
@@ -581,6 +586,33 @@ export class SubAgentContext {
|
|
|
581
586
|
}
|
|
582
587
|
}
|
|
583
588
|
|
|
589
|
+
/**
|
|
590
|
+
* True when this sub-agent has been asked to stop — either via an external
|
|
591
|
+
* abort signal or via this context's own `abort()` (precise single-agent
|
|
592
|
+
* cancel). The agent loop checks this each iteration and winds down
|
|
593
|
+
* cooperatively (force-completes with the partial work it has).
|
|
594
|
+
*/
|
|
595
|
+
isAborted() {
|
|
596
|
+
return Boolean(
|
|
597
|
+
this._signal?.aborted || this._abortController.signal.aborted,
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Precisely cancel THIS sub-agent: trip its internal abort signal so the loop
|
|
603
|
+
* stops at its next iteration and hands back partial work. Idempotent. The
|
|
604
|
+
* `reason` surfaces in the force-completed summary. Returns true if the abort
|
|
605
|
+
* was newly requested, false if it was already aborted / not active.
|
|
606
|
+
*/
|
|
607
|
+
abort(reason = "cancelled") {
|
|
608
|
+
if (this.status !== "active" || this._abortController.signal.aborted) {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
this._cancelReason = reason;
|
|
612
|
+
this._abortController.abort();
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
615
|
+
|
|
584
616
|
/**
|
|
585
617
|
* Get a serializable snapshot of this context (for debugging/logging).
|
|
586
618
|
*/
|
|
@@ -188,6 +188,25 @@ export class SubAgentRegistry {
|
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Precisely cancel a SINGLE active sub-agent by id (the per-agent sibling of
|
|
193
|
+
* `forceCompleteAll`). Trips the context's abort signal so its loop stops at
|
|
194
|
+
* the next iteration and hands back partial work; the normal completion path
|
|
195
|
+
* then moves it to history. Cooperative — a loop blocked on a long call winds
|
|
196
|
+
* down at its next check.
|
|
197
|
+
*
|
|
198
|
+
* @param {string} id - Sub-agent id to cancel
|
|
199
|
+
* @param {string} [reason="cancelled"] - Surfaced in the force-completed summary
|
|
200
|
+
* @returns {{ found: boolean, cancelled: boolean }}
|
|
201
|
+
*/
|
|
202
|
+
cancel(id, reason = "cancelled") {
|
|
203
|
+
const subCtx = this._active.get(id);
|
|
204
|
+
if (!subCtx) return { found: false, cancelled: false };
|
|
205
|
+
const cancelled =
|
|
206
|
+
typeof subCtx.abort === "function" ? subCtx.abort(reason) : false;
|
|
207
|
+
return { found: true, cancelled };
|
|
208
|
+
}
|
|
209
|
+
|
|
191
210
|
/**
|
|
192
211
|
* Clean up stale entries older than maxAgeMs.
|
|
193
212
|
* @param {number} [maxAgeMs=600000] - Max age in ms (default: 10 minutes)
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* `summary()` renders them. The clock is injected so tests are deterministic.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { redactSecrets } from "../secret-scan.js";
|
|
16
|
+
|
|
15
17
|
let _seq = 0;
|
|
16
18
|
function spanId() {
|
|
17
19
|
_seq = (_seq + 1) % Number.MAX_SAFE_INTEGER;
|
|
@@ -187,8 +189,19 @@ export class TelemetryRecorder {
|
|
|
187
189
|
};
|
|
188
190
|
}
|
|
189
191
|
|
|
190
|
-
/**
|
|
191
|
-
|
|
192
|
+
/**
|
|
193
|
+
* OTLP/JSON-shaped export (resourceSpans) — ingestible by an OTel collector.
|
|
194
|
+
*
|
|
195
|
+
* §8.1 export gate: a span attribute or an exception message can carry a
|
|
196
|
+
* provider token / JWT / connection string (a tool `command` attribute, an
|
|
197
|
+
* error string, a redirect URL). A trace is shipped to a collector that may be
|
|
198
|
+
* external, so string values are secret-redacted by default (recall-first —
|
|
199
|
+
* an export must fail toward "no leak"). Only true secret substrings are
|
|
200
|
+
* replaced, so a span with no secret is byte-identical. `{ redact: false }`
|
|
201
|
+
* keeps raw values for a fully-trusted local collector.
|
|
202
|
+
*/
|
|
203
|
+
toOtlp({ redact = true } = {}) {
|
|
204
|
+
const scrub = redact ? (v) => redactSecrets(String(v)) : (v) => String(v);
|
|
192
205
|
return {
|
|
193
206
|
resourceSpans: [
|
|
194
207
|
{
|
|
@@ -219,14 +232,14 @@ export class TelemetryRecorder {
|
|
|
219
232
|
? { doubleValue: v }
|
|
220
233
|
: typeof v === "boolean"
|
|
221
234
|
? { boolValue: v }
|
|
222
|
-
: { stringValue:
|
|
235
|
+
: { stringValue: scrub(v) },
|
|
223
236
|
})),
|
|
224
237
|
events: s.events.map((e) => ({
|
|
225
238
|
name: e.name,
|
|
226
239
|
timeUnixNano: String(Math.round(e.time * 1e6)),
|
|
227
240
|
attributes: Object.entries(e.attributes).map(([k, v]) => ({
|
|
228
241
|
key: k,
|
|
229
|
-
value: { stringValue:
|
|
242
|
+
value: { stringValue: scrub(v) },
|
|
230
243
|
})),
|
|
231
244
|
})),
|
|
232
245
|
})),
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal-context policy core — the pure-logic half of P1-2 (交互终端) of
|
|
3
|
+
* CLAUDE_CODE_IDE_INCREMENTAL_GAP_ANALYSIS_2026-07-13.md.
|
|
4
|
+
*
|
|
5
|
+
* P1-2 asks for an embedded interactive PTY inside an Agent Session: continuous
|
|
6
|
+
* ANSI output, scrollback trimming, search/copy, user takeover, "把选中输出附加
|
|
7
|
+
* 到下一轮", background commands surfacing PID / port / health, and — critically —
|
|
8
|
+
* "Agent 不得把 Terminal 中的敏感输出默认加入模型上下文" (terminal output must NOT
|
|
9
|
+
* enter the model context by default).
|
|
10
|
+
*
|
|
11
|
+
* This module is that policy + text layer, NOT a PTY: it never spawns a shell,
|
|
12
|
+
* allocates a pseudo-terminal, or touches fs / clock / RNG / process. A caller
|
|
13
|
+
* (the real PTY host) feeds captured bytes and a selection intent and reads back
|
|
14
|
+
* what — if anything — is safe to hand the model.
|
|
15
|
+
*
|
|
16
|
+
* Two invariants give it teeth, mirroring the other gap cores:
|
|
17
|
+
* 1. Terminal output fails CLOSED out of the model context: `selectTerminal
|
|
18
|
+
* ContextForModel` returns null unless the user EXPLICITLY opted this turn's
|
|
19
|
+
* output in. Ambient scrollback never leaks into a prompt.
|
|
20
|
+
* 2. Whatever DOES get opted in is ANSI-stripped and secret-redacted (via
|
|
21
|
+
* [[secret-scan.js]]) and scrollback-capped, so a printed token or a control
|
|
22
|
+
* sequence can't ride into the context.
|
|
23
|
+
*
|
|
24
|
+
* PURE: no fs / clock / RNG / process / PTY.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { redactSecrets } from "./secret-scan.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Match ANSI/VT control sequences: CSI (ESC [ … final byte), OSC (ESC ] … BEL or
|
|
31
|
+
* ST) and the standalone two/three-byte escapes. Kept explicit rather than a
|
|
32
|
+
* dependency so the strip is auditable (ESC = U+001B, CSI = U+009B, BEL = U+0007).
|
|
33
|
+
* A fresh RegExp is compiled per call to avoid the /g lastIndex
|
|
34
|
+
* hazard across invocations.
|
|
35
|
+
*/
|
|
36
|
+
function ansiRegex() {
|
|
37
|
+
return new RegExp(
|
|
38
|
+
[
|
|
39
|
+
"[\\u001B\\u009B][[\\]()#;?]*" +
|
|
40
|
+
"(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\\u0007)" +
|
|
41
|
+
"|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))",
|
|
42
|
+
].join(""),
|
|
43
|
+
"g",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Remove ANSI/VT escape sequences from terminal output, leaving human-readable
|
|
49
|
+
* text (for search, copy, and — importantly — before the text ever reaches a
|
|
50
|
+
* model, so escape codes can't smuggle intent or bloat the context). Non-string
|
|
51
|
+
* input → "".
|
|
52
|
+
*/
|
|
53
|
+
export function stripAnsi(text) {
|
|
54
|
+
if (typeof text !== "string") return "";
|
|
55
|
+
return text.replace(ansiRegex(), "");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Default scrollback cap — how many trailing lines survive truncation. */
|
|
59
|
+
export const DEFAULT_SCROLLBACK_LINES = 400;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Cap a captured buffer to its last `maxLines` lines (the live tail is what
|
|
63
|
+
* matters for a running process). Marks the elision so the reader — and any
|
|
64
|
+
* model — knows output was dropped rather than seeing a silently-truncated head.
|
|
65
|
+
*
|
|
66
|
+
* @returns {{text:string, truncated:boolean, droppedLines:number, totalLines:number}}
|
|
67
|
+
*/
|
|
68
|
+
export function truncateScrollback(text, maxLines = DEFAULT_SCROLLBACK_LINES) {
|
|
69
|
+
const src = typeof text === "string" ? text : "";
|
|
70
|
+
const cap =
|
|
71
|
+
Number.isInteger(maxLines) && maxLines > 0
|
|
72
|
+
? maxLines
|
|
73
|
+
: DEFAULT_SCROLLBACK_LINES;
|
|
74
|
+
const lines = src.split("\n");
|
|
75
|
+
const total = lines.length;
|
|
76
|
+
if (total <= cap) {
|
|
77
|
+
return { text: src, truncated: false, droppedLines: 0, totalLines: total };
|
|
78
|
+
}
|
|
79
|
+
const kept = lines.slice(total - cap);
|
|
80
|
+
const dropped = total - cap;
|
|
81
|
+
return {
|
|
82
|
+
text: `…(${dropped} earlier lines elided)\n${kept.join("\n")}`,
|
|
83
|
+
truncated: true,
|
|
84
|
+
droppedLines: dropped,
|
|
85
|
+
totalLines: total,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Decide what terminal output may be attached to the next model turn — the
|
|
91
|
+
* fail-closed core of "Agent 不得把 Terminal 中的敏感输出默认加入模型上下文".
|
|
92
|
+
*
|
|
93
|
+
* Returns `null` (attach NOTHING) unless the user explicitly opted in this turn
|
|
94
|
+
* (`optIn === true`). When opted in, a supplied `selection` (the user's
|
|
95
|
+
* highlighted region) is preferred over the full `output` — "把选中输出附加到下
|
|
96
|
+
* 一轮". Whatever is chosen is ANSI-stripped, secret-redacted and scrollback-
|
|
97
|
+
* capped before it becomes context.
|
|
98
|
+
*
|
|
99
|
+
* @param {object} args
|
|
100
|
+
* @param {string} [args.output] full captured terminal buffer
|
|
101
|
+
* @param {string} [args.selection] user-highlighted region (preferred)
|
|
102
|
+
* @param {boolean} [args.optIn] explicit "include this in context" intent
|
|
103
|
+
* @param {number} [args.maxLines] scrollback cap for the attached text
|
|
104
|
+
* @returns {null | {source:"selection"|"output", text:string, truncated:boolean, redacted:boolean}}
|
|
105
|
+
*/
|
|
106
|
+
export function selectTerminalContextForModel(args = {}) {
|
|
107
|
+
const a = args && typeof args === "object" ? args : {};
|
|
108
|
+
if (a.optIn !== true) {
|
|
109
|
+
return null; // fail-closed: no ambient terminal output in the prompt
|
|
110
|
+
}
|
|
111
|
+
const hasSelection =
|
|
112
|
+
typeof a.selection === "string" && a.selection.trim() !== "";
|
|
113
|
+
const source = hasSelection ? "selection" : "output";
|
|
114
|
+
const raw = hasSelection ? a.selection : a.output;
|
|
115
|
+
if (typeof raw !== "string" || raw.trim() === "") {
|
|
116
|
+
return null; // opted in but nothing to attach
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const clean = stripAnsi(raw);
|
|
120
|
+
const redactedText = redactSecrets(clean);
|
|
121
|
+
const capped = truncateScrollback(redactedText, a.maxLines);
|
|
122
|
+
return {
|
|
123
|
+
source,
|
|
124
|
+
text: capped.text,
|
|
125
|
+
truncated: capped.truncated,
|
|
126
|
+
redacted: redactedText !== clean,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Canonical health states for a backgrounded terminal command. */
|
|
131
|
+
export const HEALTH_STATUS = Object.freeze({
|
|
132
|
+
STARTING: "starting",
|
|
133
|
+
RUNNING: "running",
|
|
134
|
+
HEALTHY: "healthy",
|
|
135
|
+
UNHEALTHY: "unhealthy",
|
|
136
|
+
EXITED: "exited",
|
|
137
|
+
UNKNOWN: "unknown",
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const HEALTH_ALIASES = new Map([
|
|
141
|
+
["starting", HEALTH_STATUS.STARTING],
|
|
142
|
+
["running", HEALTH_STATUS.RUNNING],
|
|
143
|
+
["up", HEALTH_STATUS.RUNNING],
|
|
144
|
+
["healthy", HEALTH_STATUS.HEALTHY],
|
|
145
|
+
["ok", HEALTH_STATUS.HEALTHY],
|
|
146
|
+
["unhealthy", HEALTH_STATUS.UNHEALTHY],
|
|
147
|
+
["degraded", HEALTH_STATUS.UNHEALTHY],
|
|
148
|
+
["exited", HEALTH_STATUS.EXITED],
|
|
149
|
+
["stopped", HEALTH_STATUS.EXITED],
|
|
150
|
+
["dead", HEALTH_STATUS.EXITED],
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
/** Normalize a health label; anything unrecognized → UNKNOWN (fail-closed). */
|
|
154
|
+
export function normalizeHealthStatus(value) {
|
|
155
|
+
if (typeof value !== "string") return HEALTH_STATUS.UNKNOWN;
|
|
156
|
+
return (
|
|
157
|
+
HEALTH_ALIASES.get(value.trim().toLowerCase()) || HEALTH_STATUS.UNKNOWN
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function normalizePorts(ports) {
|
|
162
|
+
const list = Array.isArray(ports) ? ports : [];
|
|
163
|
+
const out = [];
|
|
164
|
+
for (const p of list) {
|
|
165
|
+
const n = Number(p);
|
|
166
|
+
if (Number.isInteger(n) && n > 0 && n <= 65535 && !out.includes(n)) {
|
|
167
|
+
out.push(n);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Build a safe descriptor for a backgrounded terminal command — the "后台命令显示
|
|
175
|
+
* PID、端口、健康状态并可安全停止" surface. The command string is secret-redacted
|
|
176
|
+
* (a `FOO=token cmd` invocation must not leak the token into a status pill), PID
|
|
177
|
+
* is coerced to a positive integer or null, ports are validated, and the health
|
|
178
|
+
* status is normalized fail-closed. `stoppable` is true only when we hold a real
|
|
179
|
+
* PID to signal.
|
|
180
|
+
*
|
|
181
|
+
* @param {object} cmd {command?, pid?, ports?, status?}
|
|
182
|
+
* @returns {{command:string|null, pid:number|null, ports:number[], status:string, stoppable:boolean}}
|
|
183
|
+
*/
|
|
184
|
+
export function describeBackgroundCommand(cmd = {}) {
|
|
185
|
+
const c = cmd && typeof cmd === "object" ? cmd : {};
|
|
186
|
+
const pidNum = Number(c.pid);
|
|
187
|
+
const pid = Number.isInteger(pidNum) && pidNum > 0 ? pidNum : null;
|
|
188
|
+
return {
|
|
189
|
+
command: typeof c.command === "string" ? redactSecrets(c.command) : null,
|
|
190
|
+
pid,
|
|
191
|
+
ports: normalizePorts(c.ports),
|
|
192
|
+
status: normalizeHealthStatus(c.status),
|
|
193
|
+
stoppable: pid !== null,
|
|
194
|
+
};
|
|
195
|
+
}
|