chainlesschain 0.162.162 → 0.162.164
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -1
- package/src/assets/web-panel/assets/{AIOps-ClI2ALgv.js → AIOps-BqRkaHGb.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BDjRcJaK.js → ActionButton-DWO7OZLC.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DikSRsaT.js → Analytics-C5oaVezM.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-9mLhMFmz.js → AppLayout-CsUESDoZ.js} +3 -3
- package/src/assets/web-panel/assets/{Artifacts-BSr88BrH.js → Artifacts-Dragqu5k.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CdvyimOF.js → Audit-CQsaHctY.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-D3_hCedh.js → BackgroundAgents-DdG1jNSM.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-Cyvv9RHf.js → Backup-DN6sjK4z.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-BZM14kt8.js → BaseInput-Cxm_ZTCl.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DrQSokVz.js → Chat-DVXGmA56.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-HwJR-C6B.js → ChatBubbleRenderer-BdaCL_iE.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-bFLYeWMV.js → Checkbox-DEc4vD_A.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-7mN7xJXT.js → Codegen-CKiTcv36.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DAB5ZWm7.js → Col-Br9UUNN0.js} +1 -1
- package/src/assets/web-panel/assets/{Community-B_1TymaK.js → Community-NeUH_70T.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-Bq1uSWLJ.js → Compact-BqR-z2Df.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CyiE1jEe.js → Compliance-DcWB_SgZ.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-Cpk0D7vm.js → Cowork-BfhgcudF.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-d_Lv3rdB.js → Cron-LaU29LgD.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DMqhHjMR.js → Crosschain-B2ZB9uwW.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Br-oPuBw.js → DID-BuHl38A2.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CfLW_RaP.js → Dashboard-m627yqvZ.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-DZnTOExV.js → Dropdown-C7bxF8F5.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-DHIUHDv1.js → EmailListRenderer-DPLPQzLp.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-Bw3t3myu.js → FamilyGuardDashboard-cOiH6ike.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-D9cpQPAY.js → Federation-Bvkzg88z.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Jdczr9Mr.js → FormItemContext-BTsiA7h_.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BT8ZMpC5.js → GenericCardRenderer-BGnVE5qu.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DjARip2b.js → Git-DS_j6r25.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BgWbSRcj.js → Governance-CKRG3Rsl.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CfouJciM.js → Inference-DW8INoNQ.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-50l7mrAr.js → KnowledgeGraph-BhEYeFhC.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-rq50H84B.js → Logs-wqi0uxuR.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-DUPIEdQH.js → MarkdownRenderer-DS8dE8ty.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-BYgPDmHU.js → Marketplace-CMlTuPLT.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-5DVgcFdx.js → McpTools-CSx4m5JJ.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-B70LPk7f.js → Memory-vnQCUy-_.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-De8ssA5Y.js → MobileBridge-BV2NA742.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-Bw0k8e1B.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-RJsOTsIA.js → Mtc-CzK_FPB1.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-Crh-6BEK.js → MtcAudit-Cb7G9oL3.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-wuoR5l3x.js → Multisig-qJxlUyzr.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-U2Mji4fZ.js → NLProgramming-DqaBJ7Te.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CdTZAyDH.js → Notes-CwhTJkRi.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-C-FucWE7.js → NotificationSettings-YDs4Fal4.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-DCYFTeaI.js → OrderTableRenderer-CvEALpq7.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-an3OF85L.js → Organization-CEmQm9n8.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-C-1vmYFe.js → Overflow-zby_P_LA.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-Cwh47xKg.js → P2P-DsmDNFNT.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-Dm38GT0k.js → PdhVaultBrowser-FWwFlZGF.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-DS2Y8awD.js → Permissions-DuzQ6me5.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BQNziA0I.js → PersonalDataHub-DZbeN613.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-DF85L1ue.js → Pipeline-CoNSVAUS.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BuLornF-.js → Privacy-OYJEAaMC.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-C3Y31IwX.js → ProjectInit-CWIapV--.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BCPiI8aD.js → ProjectSettings-CX8xMxZn.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-BZ6s4k6g.js → Projects-BLCN8YyY.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-D3Wp022X.js → Providers-BV_sxp75.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-CUJfQkH4.js → QrScannerModal-BTnRrHRp.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-UAc0EO8l.js → QuickAsk-D1KZg018.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BaxKupcs.js → Recommend-COm5cuNg.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-C75NNnXZ.js → RemoteSession-0saWEkYI.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-BVXynZAk.js → Reputation-Dr7iKLS2.js} +1 -1
- package/src/assets/web-panel/assets/{Row-C5MRZ0bi.js → Row-C1vpPBcr.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-DAKkEYka.js → RssFeed-CxlWrtNj.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DvWv9YOA.js → Search-BT2V8dPN.js} +1 -1
- package/src/assets/web-panel/assets/{Security-B5peafW1.js → Security-bm9V3c5k.js} +4 -4
- package/src/assets/web-panel/assets/{Services-UQmvPEaG.js → Services-CocH1yyX.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-C3bhWpuF.js → Skeleton-DHItlMdd.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DfkKf9_H.js → Skills-C59sLc4w.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DqMSjlBM.js → Sla-Crj4BMc8.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-C-WbxWUX.js → SpeechSettings-DX4KFceY.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CYuHZ4N2.js → SyncSettings-BD_iwQLr.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-CyeAyB9d.js → Tasks-BmCTzYsH.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-BTCeQnpq.js → Templates-BGUaxZYl.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BifQuA9Y.js → Tenant-BS0tJx2O.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-Bx_9Bocf.js → Terminal--tHSAsld.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-BX0RFCOm.js → TimelineRenderer-D9-MFxS4.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-CGHrCmUS.js → Tokens-CiCB485f.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DlqaY4SO.js → Trigger-SmCFuW_l.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DfOmj-ay.js → Trust-DkK3Rqe0.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-B72jH5fE.js → UkeySign-CDM4LGdB.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BT-63fqv.js → VideoEditing-IDB7DJXt.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-Dsul3sHr.js → Wallet-Bikj5cmt.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-Izkwwdgy.js → WebAuthn-C9abWlAw.js} +2 -2
- package/src/assets/web-panel/assets/{WorkflowEditor-D3FHSiH1.js → WorkflowEditor-DrbRKVky.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CmVz5z3U.js → chat-CccCSDQf.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CuLgyJBp.js → colors-CXObXLhb.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-Ml1YkXpw.js → compact-item-B5kfYAxC.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BvjJ48bs.js → createContext-DmiPBfip.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CvKrADNa.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DVC0157C.js → hasIn-Q6lYteAc.js} +1 -1
- package/src/assets/web-panel/assets/{index-C229ABfy.js → index-25Wlqps4.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ck_52dz_.js → index-33U29UNV.js} +1 -1
- package/src/assets/web-panel/assets/{index-QXzlT9KD.js → index-4oXBDczR.js} +1 -1
- package/src/assets/web-panel/assets/{index-4SBta3d7.js → index-BGYXvMc5.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0S8NzO6.js → index-BJCCa0s_.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJyN28Et.js → index-BJJ5DhKI.js} +1 -1
- package/src/assets/web-panel/assets/{index-DC_OI9H2.js → index-BLOzViJ6.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_XuRgGn.js → index-BRrbUMoG.js} +1 -1
- package/src/assets/web-panel/assets/{index-wvb_FtaV.js → index-BUFpT17Q.js} +1 -1
- package/src/assets/web-panel/assets/{index-KMqhcF7l.js → index-BkRDiqGc.js} +1 -1
- package/src/assets/web-panel/assets/{index-vG4XSJ1d.js → index-BsfvW6my.js} +1 -1
- package/src/assets/web-panel/assets/{index-DYXHFr4r.js → index-By-kfPED.js} +1 -1
- package/src/assets/web-panel/assets/{index-DuGszaWY.js → index-BzFTcQsc.js} +1 -1
- package/src/assets/web-panel/assets/{index-CXkJH-lK.js → index-C2FXdboq.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dt5KMjwJ.js → index-C5XOlTHN.js} +1 -1
- package/src/assets/web-panel/assets/{index-4wcgvPoF.js → index-CLhBDS2w.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_H_S-DY.js → index-CMq7UeoT.js} +1 -1
- package/src/assets/web-panel/assets/{index-BydjdZnl.js → index-CVMTWNKl.js} +1 -1
- package/src/assets/web-panel/assets/{index-BjHBpRYZ.js → index-Caghi-yL.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bzr0rB5T.js → index-CdB6FuSH.js} +1 -1
- package/src/assets/web-panel/assets/{index-BbPtzB8M.js → index-CoK8-kWQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BUt8kTIB.js → index-CsO3GUAY.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_vRgTl9.js → index-CwZBa20Z.js} +1 -1
- package/src/assets/web-panel/assets/{index-DukDVXCv.js → index-D-kASucw.js} +1 -1
- package/src/assets/web-panel/assets/{index-BbepULSc.js → index-D8eBw7vW.js} +1 -1
- package/src/assets/web-panel/assets/{index-BfuKP4qe.js → index-D9wgifk4.js} +1 -1
- package/src/assets/web-panel/assets/{index-BiC6dQZG.js → index-DAYCTc07.js} +1 -1
- package/src/assets/web-panel/assets/{index-CmFNvNLZ.js → index-DF6Q8l2h.js} +1 -1
- package/src/assets/web-panel/assets/index-DTdFUWc_.js +1 -0
- package/src/assets/web-panel/assets/{index-KjgKdZe-.js → index-DgTLApS9.js} +1 -1
- package/src/assets/web-panel/assets/{index-BpFJHiTv.js → index-DgxVJ8pg.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cw0Rjy87.js → index-DlJvIqoR.js} +1 -1
- package/src/assets/web-panel/assets/{index-CFYVQMvu.js → index-Dw4eHfsG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CudmwNBV.js → index-Rh-ytwow.js} +3 -3
- package/src/assets/web-panel/assets/{index-DDGaOWBV.js → index-StAEt2pq.js} +1 -1
- package/src/assets/web-panel/assets/{index-D3lD5qap.js → index-bzfw951k.js} +1 -1
- package/src/assets/web-panel/assets/{index-BzG-x9p4.js → index-fSUMbMvv.js} +1 -1
- package/src/assets/web-panel/assets/{index-B_EvG_GW.js → index-tFHy5Wol.js} +1 -1
- package/src/assets/web-panel/assets/index-zQ0M_kz-.js +1 -0
- package/src/assets/web-panel/assets/{initDefaultProps-BDgd2YZ8.js → initDefaultProps-BNq7X72N.js} +1 -1
- package/src/assets/web-panel/assets/{motion-ErtrXdYe.js → motion-DOf9viTc.js} +1 -1
- package/src/assets/web-panel/assets/{move-_GPPNCgs.js → move-CRHTcW4x.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DRLL-8V2.js → mtc-parser-Cyoozgm2.js} +1 -1
- package/src/assets/web-panel/assets/{omit-p86sl023.js → omit-6-TjFNru.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-BTu7cMLH.js → pickAttrs-DlUmO1zi.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BEKz6Isr.js → placementArrow-CFOL8mIR.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-Ck42c9MP.js → responsiveObserve-BtaFK5zd.js} +1 -1
- package/src/assets/web-panel/assets/{slide-1CEvuPsB.js → slide-BIbc2bur.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BDfdx1GJ.js → statusUtils-BXX9d6-w.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-B4K46m5w.js → styleChecker-BGv10Bpn.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DoIVpJNq.js → useFlexGapSupport-bKnKT8tw.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-DdAWi-ln.js → useFs-FY4IL4jn.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-iUu6k8dx.js → usePersonalDataHub-Bwl9TEgu.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DZz4R6Tw.js → vnode-BM3OHxAT.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DXlf9sA3.js → zoom-D1fbFiHB.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agenda.js +320 -32
- package/src/commands/agent.js +105 -11
- package/src/commands/context.js +125 -14
- package/src/commands/doctor.js +85 -1
- package/src/commands/hook.js +181 -7
- package/src/commands/plugin.js +456 -1
- package/src/commands/review.js +363 -2
- package/src/commands/session.js +191 -0
- package/src/commands/team.js +19 -1
- package/src/data/changelog.json +10 -4
- package/src/gateways/ws/remote-session-protocol.js +39 -0
- package/src/gateways/ws/session-protocol.js +55 -2
- package/src/gateways/ws/ws-agent-handler.js +50 -0
- package/src/harness/jsonl-session-store.js +60 -0
- package/src/harness/worktree-isolator.js +140 -5
- package/src/lib/agent-schedule-store.js +318 -10
- package/src/lib/agent-team/team-worktree.js +37 -2
- package/src/lib/agent-tool-admission.js +125 -0
- package/src/lib/app-preview.js +360 -0
- package/src/lib/async-hook-supervisor.cjs +54 -0
- package/src/lib/capability-manifest.js +319 -0
- package/src/lib/capability-negotiation.js +23 -12
- package/src/lib/channels/channel-manager.js +54 -2
- package/src/lib/cli-context-engineering.js +9 -2
- package/src/lib/context-breakdown.js +116 -0
- package/src/lib/diagnostic-bundle.js +230 -0
- package/src/lib/doctor-checkup.js +275 -4
- package/src/lib/execution-location.js +286 -0
- package/src/lib/headless-manifest.js +13 -40
- package/src/lib/hook-event-log.cjs +193 -0
- package/src/lib/hook-stats-store.cjs +197 -0
- package/src/lib/idempotency.js +109 -0
- package/src/lib/interaction-adapter.js +34 -4
- package/src/lib/json-schema-output.js +46 -12
- package/src/lib/json-schema-validate.js +143 -3
- package/src/lib/jsonl-session-store.js +1 -0
- package/src/lib/lsp/__tests__/lsp-manager.test.js +58 -0
- package/src/lib/lsp/lsp-manager.js +56 -0
- package/src/lib/monitor-event.js +290 -0
- package/src/lib/operation-fingerprint.js +217 -0
- package/src/lib/plugin-runtime/bin.js +37 -0
- package/src/lib/plugin-runtime/capabilities.js +19 -0
- package/src/lib/plugin-runtime/capability-consent.js +203 -0
- package/src/lib/plugin-runtime/hooks.js +37 -0
- package/src/lib/plugin-runtime/lsp.js +38 -0
- package/src/lib/plugin-runtime/mcp.js +40 -0
- package/src/lib/plugin-runtime/monitors.js +37 -0
- package/src/lib/plugin-runtime/plugin-options.js +151 -0
- package/src/lib/plugin-runtime/policy.js +62 -0
- package/src/lib/plugin-runtime/scopes.js +20 -4
- package/src/lib/pr-automation-policy.js +224 -0
- package/src/lib/project-instructions.js +128 -0
- package/src/lib/remote-approval-bridge.js +66 -13
- package/src/lib/repl-goal.js +132 -0
- package/src/lib/repl-rewind.js +153 -0
- package/src/lib/review-comment-anchor.js +241 -0
- package/src/lib/review-pipeline.js +46 -7
- package/src/lib/runtime-checkup.js +221 -0
- package/src/lib/schedule-planner.js +17 -1
- package/src/lib/secret-scan.js +409 -0
- package/src/lib/session-branch.js +0 -0
- package/src/lib/settings-hook-events.cjs +13 -0
- package/src/lib/settings-loader.cjs +42 -0
- package/src/lib/side-effect-ledger-store.js +88 -0
- package/src/lib/side-effect-ledger.js +401 -0
- package/src/lib/skill-loader.js +32 -0
- package/src/lib/skill-path-scope.js +63 -0
- package/src/lib/sub-agent-context.js +110 -9
- package/src/lib/sub-agent-registry.js +19 -0
- package/src/lib/subagent-inheritance.js +112 -0
- package/src/lib/telemetry/span-recorder.js +17 -4
- package/src/lib/terminal-context.js +195 -0
- package/src/lib/turn-binding-store.js +119 -0
- package/src/lib/unattended-action-policy.js +182 -0
- package/src/lib/worktree-cleanup-safety.js +138 -0
- package/src/lib/worktree-isolator.js +8 -0
- package/src/lib/worktree-sparse.js +131 -0
- package/src/repl/agent-repl.js +280 -30
- package/src/repl/permission-tier.js +28 -0
- package/src/runtime/agent-core.js +565 -21
- package/src/runtime/coding-agent-contract-shared.cjs +60 -1
- package/src/runtime/diagnostics.js +60 -0
- package/src/runtime/headless-runner.js +393 -1
- package/src/runtime/headless-stream.js +60 -1
- package/src/assets/web-panel/assets/MobileProjects-D10VISps.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DhiGrbsh.js +0 -1
- package/src/assets/web-panel/assets/index-B7a1TrU6.js +0 -1
- package/src/assets/web-panel/assets/index-C2B1KRja.js +0 -1
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App-Preview auto-validation core — the pure-logic half of P1-3
|
|
3
|
+
* (App Preview 自动验证) of CLAUDE_CODE_IDE_INCREMENTAL_GAP_ANALYSIS_2026-07-13.md.
|
|
4
|
+
*
|
|
5
|
+
* The gap section asks for a project-level Launch config (start command, cwd,
|
|
6
|
+
* port, health check, env references, shutdown policy), a per-turn RISK-based
|
|
7
|
+
* choice of verification tier ("不要求所有改动都启动浏览器" — not every change
|
|
8
|
+
* needs a browser), an evidence-Artifact contract (screenshot / DOM summary /
|
|
9
|
+
* action sequence / test result), and login-state persistence that is OFF by
|
|
10
|
+
* default and must be explicitly scoped (project / session / user).
|
|
11
|
+
*
|
|
12
|
+
* This module is that decision + descriptor layer, NOT a runner: it never spawns
|
|
13
|
+
* a dev server, opens a browser, or touches fs / clock / RNG / process. A caller
|
|
14
|
+
* feeds observed facts (a package.json, a launch config, a change descriptor) and
|
|
15
|
+
* reads back a normalized, safe descriptor plus fail-closed policy verdicts.
|
|
16
|
+
*
|
|
17
|
+
* Two invariants give it teeth, mirroring [[execution-location.js]]:
|
|
18
|
+
* 1. Launch config carries env *references* only (names), never values — a
|
|
19
|
+
* leaked value is a violation, so the config can be rendered/logged/shipped.
|
|
20
|
+
* 2. Login-state persistence fails CLOSED: OFF unless explicitly enabled AND
|
|
21
|
+
* given an explicit {project|session|user} scope. An unknown/unscoped
|
|
22
|
+
* request never silently persists a session cookie jar.
|
|
23
|
+
*
|
|
24
|
+
* Free-text evidence fields are run through [[secret-scan.js]] `redactSecrets`
|
|
25
|
+
* so a captured console error or DOM summary can't smuggle a token into an
|
|
26
|
+
* Artifact.
|
|
27
|
+
*
|
|
28
|
+
* PURE: no fs / clock / RNG / process.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { redactSecrets, containsSecret } from "./secret-scan.js";
|
|
32
|
+
|
|
33
|
+
/** Verification tiers, cheapest → most expensive. Risk selects the floor. */
|
|
34
|
+
export const VERIFICATION_TIER = Object.freeze({
|
|
35
|
+
STATIC: "static-check", // lint/type/build — no server, no browser
|
|
36
|
+
API_PROBE: "api-probe", // hit an endpoint / health check
|
|
37
|
+
DOM_ASSERT: "dom-assert", // render + assert DOM state
|
|
38
|
+
VISUAL: "visual-screenshot", // capture a screenshot for human/vision review
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const TIER_ORDER = [
|
|
42
|
+
VERIFICATION_TIER.STATIC,
|
|
43
|
+
VERIFICATION_TIER.API_PROBE,
|
|
44
|
+
VERIFICATION_TIER.DOM_ASSERT,
|
|
45
|
+
VERIFICATION_TIER.VISUAL,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/** Evidence artifact kinds the Session persists for a verification run. */
|
|
49
|
+
export const EVIDENCE_KIND = Object.freeze({
|
|
50
|
+
SCREENSHOT: "screenshot",
|
|
51
|
+
DOM_SUMMARY: "dom-summary",
|
|
52
|
+
ACTION_SEQUENCE: "action-sequence",
|
|
53
|
+
TEST_RESULT: "test-result",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/** Login-state persistence scopes. Anything else → fail-closed (no persist). */
|
|
57
|
+
export const LOGIN_SCOPE = Object.freeze({
|
|
58
|
+
PROJECT: "project",
|
|
59
|
+
SESSION: "session",
|
|
60
|
+
USER: "user",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const LOGIN_SCOPES = new Set(Object.values(LOGIN_SCOPE));
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Known dev-server framework signatures → {framework, defaultPort}. Detection
|
|
67
|
+
* looks at dependencies first (most reliable), then falls back to scripts.
|
|
68
|
+
*/
|
|
69
|
+
const FRAMEWORK_SIGNATURES = Object.freeze([
|
|
70
|
+
{ dep: "next", framework: "next", defaultPort: 3000 },
|
|
71
|
+
{ dep: "nuxt", framework: "nuxt", defaultPort: 3000 },
|
|
72
|
+
{ dep: "@angular/cli", framework: "angular", defaultPort: 4200 },
|
|
73
|
+
{ dep: "@vue/cli-service", framework: "vue-cli", defaultPort: 8080 },
|
|
74
|
+
{ dep: "astro", framework: "astro", defaultPort: 4321 },
|
|
75
|
+
{ dep: "@sveltejs/kit", framework: "sveltekit", defaultPort: 5173 },
|
|
76
|
+
{ dep: "gatsby", framework: "gatsby", defaultPort: 8000 },
|
|
77
|
+
{ dep: "remix", framework: "remix", defaultPort: 3000 },
|
|
78
|
+
{ dep: "@remix-run/dev", framework: "remix", defaultPort: 3000 },
|
|
79
|
+
{ dep: "react-scripts", framework: "create-react-app", defaultPort: 3000 },
|
|
80
|
+
{ dep: "vite", framework: "vite", defaultPort: 5173 },
|
|
81
|
+
{ dep: "webpack-dev-server", framework: "webpack", defaultPort: 8080 },
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
/** Script names, in preference order, that typically start a dev server. */
|
|
85
|
+
const DEV_SCRIPT_NAMES = ["dev", "start", "serve", "develop"];
|
|
86
|
+
|
|
87
|
+
function asObject(v) {
|
|
88
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Auto-detect a dev-server launch suggestion from a package.json object. Returns
|
|
93
|
+
* `null` when nothing recognizable is present (caller must not guess a command).
|
|
94
|
+
* Never fabricates a port: only a known framework yields a defaultPort; an
|
|
95
|
+
* unknown-framework match from a script alone leaves `port: null`.
|
|
96
|
+
*
|
|
97
|
+
* @param {object} pkg parsed package.json
|
|
98
|
+
* @returns {null | {framework:string|null, script:string|null, startCommand:string|null, port:number|null, source:"dependency"|"script"}}
|
|
99
|
+
*/
|
|
100
|
+
export function detectDevServer(pkg) {
|
|
101
|
+
const p = asObject(pkg);
|
|
102
|
+
const deps = { ...asObject(p.dependencies), ...asObject(p.devDependencies) };
|
|
103
|
+
const scripts = asObject(p.scripts);
|
|
104
|
+
|
|
105
|
+
// Prefer a dependency signature — it tells us both framework and default port.
|
|
106
|
+
for (const sig of FRAMEWORK_SIGNATURES) {
|
|
107
|
+
if (Object.prototype.hasOwnProperty.call(deps, sig.dep)) {
|
|
108
|
+
const script =
|
|
109
|
+
DEV_SCRIPT_NAMES.find((name) => typeof scripts[name] === "string") ||
|
|
110
|
+
null;
|
|
111
|
+
return {
|
|
112
|
+
framework: sig.framework,
|
|
113
|
+
script,
|
|
114
|
+
startCommand: script ? `npm run ${script}` : null,
|
|
115
|
+
port: sig.defaultPort,
|
|
116
|
+
source: "dependency",
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Fall back to a recognizable dev script with no known framework — we can
|
|
122
|
+
// suggest the command but must not invent a port.
|
|
123
|
+
const script = DEV_SCRIPT_NAMES.find(
|
|
124
|
+
(name) => typeof scripts[name] === "string",
|
|
125
|
+
);
|
|
126
|
+
if (script) {
|
|
127
|
+
return {
|
|
128
|
+
framework: null,
|
|
129
|
+
script,
|
|
130
|
+
startCommand: `npm run ${script}`,
|
|
131
|
+
port: null,
|
|
132
|
+
source: "script",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function normalizeEnvRefs(env) {
|
|
140
|
+
// Accept an array of names, or an object whose KEYS are the referenced names.
|
|
141
|
+
// Values are intentionally discarded — a launch config references env by name.
|
|
142
|
+
if (Array.isArray(env)) {
|
|
143
|
+
return env.filter((e) => typeof e === "string" && e.trim() !== "");
|
|
144
|
+
}
|
|
145
|
+
if (env && typeof env === "object") {
|
|
146
|
+
return Object.keys(env);
|
|
147
|
+
}
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const SHUTDOWN_POLICIES = new Set(["kill", "graceful", "keep"]);
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Normalize a raw launch config into a safe descriptor. Env is reduced to
|
|
155
|
+
* reference NAMES only; a port is coerced to a valid TCP port or null; a health
|
|
156
|
+
* check keeps only {path, timeoutMs}. Pure — no validation verdict here (see
|
|
157
|
+
* validateLaunchConfig).
|
|
158
|
+
*/
|
|
159
|
+
export function normalizeLaunchConfig(input = {}) {
|
|
160
|
+
const cfg = asObject(input);
|
|
161
|
+
const rawPort = Number(cfg.port);
|
|
162
|
+
const port =
|
|
163
|
+
Number.isInteger(rawPort) && rawPort > 0 && rawPort <= 65535
|
|
164
|
+
? rawPort
|
|
165
|
+
: null;
|
|
166
|
+
const health = asObject(cfg.healthCheck);
|
|
167
|
+
const rawTimeout = Number(health.timeoutMs);
|
|
168
|
+
return {
|
|
169
|
+
startCommand:
|
|
170
|
+
typeof cfg.startCommand === "string" && cfg.startCommand.trim() !== ""
|
|
171
|
+
? cfg.startCommand.trim()
|
|
172
|
+
: null,
|
|
173
|
+
cwd: cfg.cwd == null ? null : String(cfg.cwd),
|
|
174
|
+
port,
|
|
175
|
+
healthCheck:
|
|
176
|
+
health.path || health.url
|
|
177
|
+
? {
|
|
178
|
+
path: String(health.path || health.url),
|
|
179
|
+
timeoutMs:
|
|
180
|
+
Number.isFinite(rawTimeout) && rawTimeout > 0
|
|
181
|
+
? rawTimeout
|
|
182
|
+
: 30000,
|
|
183
|
+
}
|
|
184
|
+
: null,
|
|
185
|
+
envRefs: normalizeEnvRefs(cfg.env),
|
|
186
|
+
shutdown: SHUTDOWN_POLICIES.has(String(cfg.shutdown))
|
|
187
|
+
? String(cfg.shutdown)
|
|
188
|
+
: "graceful",
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Keys on an env entry that would mean a raw value leaked into the config. */
|
|
193
|
+
const ENV_VALUE_LEAK_KEYS = ["value", "secret", "token", "password"];
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Validate a raw launch config, fail-closed and EXHAUSTIVE (collects every
|
|
197
|
+
* violation). Reads:
|
|
198
|
+
* - no start command → nothing to launch (`missing-start-command`)
|
|
199
|
+
* - an env entry object carrying a value/secret/token/password → the config
|
|
200
|
+
* must reference env by name only (`env-value-present`)
|
|
201
|
+
* - a health check with no path/url → can't tell when the server is ready
|
|
202
|
+
* (`health-check-missing-target`) — only flagged when a healthCheck object
|
|
203
|
+
* was supplied at all
|
|
204
|
+
* - a startCommand string that itself embeds a secret (`secret-in-command`)
|
|
205
|
+
*
|
|
206
|
+
* @returns {{ok:boolean, violations:string[], config:object}}
|
|
207
|
+
*/
|
|
208
|
+
export function validateLaunchConfig(input = {}) {
|
|
209
|
+
const violations = [];
|
|
210
|
+
const config = normalizeLaunchConfig(input);
|
|
211
|
+
|
|
212
|
+
if (!config.startCommand) {
|
|
213
|
+
violations.push("missing-start-command");
|
|
214
|
+
} else if (containsSecret(config.startCommand)) {
|
|
215
|
+
violations.push("secret-in-command");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Env-as-object with a value field is a leak — the config should list names.
|
|
219
|
+
const rawEnv = asObject(input).env;
|
|
220
|
+
if (rawEnv && typeof rawEnv === "object" && !Array.isArray(rawEnv)) {
|
|
221
|
+
for (const v of Object.values(rawEnv)) {
|
|
222
|
+
if (v && typeof v === "object") {
|
|
223
|
+
for (const k of ENV_VALUE_LEAK_KEYS) {
|
|
224
|
+
if (v[k] != null) {
|
|
225
|
+
violations.push("env-value-present");
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (asObject(input).healthCheck && !config.healthCheck) {
|
|
234
|
+
violations.push("health-check-missing-target");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return { ok: violations.length === 0, violations, config };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Pick the verification tier for a change, by risk — the "不要求所有改动都启动
|
|
242
|
+
* 浏览器" rule. Escalation floor:
|
|
243
|
+
* - touches nothing runnable / docs-only → static-check
|
|
244
|
+
* - touches server/API code → api-probe
|
|
245
|
+
* - touches UI/render code → dom-assert
|
|
246
|
+
* - high risk OR explicit visual concern → visual-screenshot
|
|
247
|
+
* The caller may pass an explicit `minTier` to raise (never lower) the floor.
|
|
248
|
+
* A change with no signal defaults to the cheapest tier (static-check), never a
|
|
249
|
+
* browser launch.
|
|
250
|
+
*
|
|
251
|
+
* @param {object} change {touchesUi?, touchesApi?, touchesServer?, risk?, visual?, filesTouched?}
|
|
252
|
+
* @param {object} [opts] {minTier?}
|
|
253
|
+
* @returns {{tier:string, reasons:string[]}}
|
|
254
|
+
*/
|
|
255
|
+
export function selectVerificationTier(change = {}, opts = {}) {
|
|
256
|
+
const c = asObject(change);
|
|
257
|
+
const reasons = [];
|
|
258
|
+
let idx = 0; // static-check floor
|
|
259
|
+
|
|
260
|
+
const bump = (tier, reason) => {
|
|
261
|
+
const i = TIER_ORDER.indexOf(tier);
|
|
262
|
+
if (i > idx) idx = i;
|
|
263
|
+
reasons.push(reason);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
if (c.touchesApi === true || c.touchesServer === true) {
|
|
267
|
+
bump(VERIFICATION_TIER.API_PROBE, "server-or-api-change");
|
|
268
|
+
}
|
|
269
|
+
if (c.touchesUi === true) {
|
|
270
|
+
bump(VERIFICATION_TIER.DOM_ASSERT, "ui-change");
|
|
271
|
+
}
|
|
272
|
+
const risk = String(c.risk || "").toLowerCase();
|
|
273
|
+
if (c.visual === true || risk === "high" || risk === "critical") {
|
|
274
|
+
bump(
|
|
275
|
+
VERIFICATION_TIER.VISUAL,
|
|
276
|
+
c.visual === true ? "visual-concern" : "high-risk",
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// An explicit minimum tier can only raise the floor.
|
|
281
|
+
const minIdx = TIER_ORDER.indexOf(String(opts.minTier));
|
|
282
|
+
if (minIdx > idx) {
|
|
283
|
+
idx = minIdx;
|
|
284
|
+
reasons.push("explicit-min-tier");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return { tier: TIER_ORDER[idx], reasons };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Build a secret-safe evidence artifact record. Unknown kinds are rejected
|
|
292
|
+
* (returns null) rather than stored blindly. Every free-text field (summary,
|
|
293
|
+
* console/network errors, action labels) is passed through `redactSecrets`, so
|
|
294
|
+
* a captured error string can't leak a token into a persisted Artifact.
|
|
295
|
+
*
|
|
296
|
+
* @param {string} kind one of EVIDENCE_KIND
|
|
297
|
+
* @param {object} data kind-specific payload
|
|
298
|
+
* @returns {null | object} `{kind, tier?, ...redacted fields}`
|
|
299
|
+
*/
|
|
300
|
+
export function buildEvidenceArtifact(kind, data = {}) {
|
|
301
|
+
const d = asObject(data);
|
|
302
|
+
const redact = (s) => (typeof s === "string" ? redactSecrets(s) : null);
|
|
303
|
+
const base = {
|
|
304
|
+
kind,
|
|
305
|
+
tier: TIER_ORDER.includes(d.tier) ? d.tier : null,
|
|
306
|
+
label: redact(d.label),
|
|
307
|
+
};
|
|
308
|
+
switch (kind) {
|
|
309
|
+
case EVIDENCE_KIND.SCREENSHOT:
|
|
310
|
+
return {
|
|
311
|
+
...base,
|
|
312
|
+
// The image bytes/path are opaque to this layer; only metadata here.
|
|
313
|
+
ref: d.ref == null ? null : String(d.ref),
|
|
314
|
+
width: Number.isFinite(Number(d.width)) ? Number(d.width) : null,
|
|
315
|
+
height: Number.isFinite(Number(d.height)) ? Number(d.height) : null,
|
|
316
|
+
};
|
|
317
|
+
case EVIDENCE_KIND.DOM_SUMMARY:
|
|
318
|
+
return { ...base, summary: redact(d.summary) };
|
|
319
|
+
case EVIDENCE_KIND.ACTION_SEQUENCE:
|
|
320
|
+
return {
|
|
321
|
+
...base,
|
|
322
|
+
steps: Array.isArray(d.steps)
|
|
323
|
+
? d.steps.map((s) => redact(String(s)))
|
|
324
|
+
: [],
|
|
325
|
+
};
|
|
326
|
+
case EVIDENCE_KIND.TEST_RESULT:
|
|
327
|
+
return {
|
|
328
|
+
...base,
|
|
329
|
+
passed: d.passed === true,
|
|
330
|
+
total: Number.isFinite(Number(d.total)) ? Number(d.total) : null,
|
|
331
|
+
failed: Number.isFinite(Number(d.failed)) ? Number(d.failed) : null,
|
|
332
|
+
output: redact(d.output),
|
|
333
|
+
};
|
|
334
|
+
default:
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Resolve whether login-state (cookie jar / auth session) may be persisted for a
|
|
341
|
+
* preview run, and at what scope. Fail-closed:
|
|
342
|
+
* - persistence is OFF unless `enabled === true`
|
|
343
|
+
* - even when enabled, an unknown/absent scope → NOT persisted
|
|
344
|
+
* Returns the reason when persistence is denied so the UI can prompt for an
|
|
345
|
+
* explicit scope. Mirrors the credential fail-closed posture elsewhere.
|
|
346
|
+
*
|
|
347
|
+
* @param {object} req {enabled?, scope?}
|
|
348
|
+
* @returns {{persist:boolean, scope:string|null, reason:string|null}}
|
|
349
|
+
*/
|
|
350
|
+
export function resolveLoginStateScope(req = {}) {
|
|
351
|
+
const r = asObject(req);
|
|
352
|
+
if (r.enabled !== true) {
|
|
353
|
+
return { persist: false, scope: null, reason: "not-enabled" };
|
|
354
|
+
}
|
|
355
|
+
const scope = String(r.scope || "").toLowerCase();
|
|
356
|
+
if (!LOGIN_SCOPES.has(scope)) {
|
|
357
|
+
return { persist: false, scope: null, reason: "scope-unspecified" };
|
|
358
|
+
}
|
|
359
|
+
return { persist: true, scope, reason: null };
|
|
360
|
+
}
|
|
@@ -60,6 +60,19 @@ class AsyncHookSupervisor {
|
|
|
60
60
|
this._results = []; // completed records awaiting drainResults()
|
|
61
61
|
this._rewakes = []; // failed asyncRewake records awaiting drainRewakes()
|
|
62
62
|
this._stopped = false;
|
|
63
|
+
// Reliability aggregate for `cc doctor` (慢/熔断 Hook). Folded in-memory on
|
|
64
|
+
// each completion and persisted ONCE on stopAll() — never per run, so the
|
|
65
|
+
// hook hot path takes no extra I/O. Persistence is opt-in: production passes
|
|
66
|
+
// `persistStats:true` (→ default path) or an explicit `hookStatsPath`; tests
|
|
67
|
+
// that pass neither stay hermetic (aggregation still runs, just no write).
|
|
68
|
+
this._statAgg = {};
|
|
69
|
+
this._statsStore = opts.statsStore || require("./hook-stats-store.cjs");
|
|
70
|
+
this._hookStatsPath =
|
|
71
|
+
opts.hookStatsPath ||
|
|
72
|
+
(opts.persistStats === true
|
|
73
|
+
? this._statsStore.defaultHookStatsPath()
|
|
74
|
+
: null);
|
|
75
|
+
this._statsFs = opts.statsFs || null; // injected fs for tests
|
|
63
76
|
// The exit reaper is installed LAZILY (on first spawn), not in the ctor:
|
|
64
77
|
// a supervisor that never dispatches a hook (or the many built in tests)
|
|
65
78
|
// must not leave a permanent `process.once('exit')` listener behind —
|
|
@@ -327,6 +340,20 @@ class AsyncHookSupervisor {
|
|
|
327
340
|
ms: fields.started != null ? now - fields.started : 0,
|
|
328
341
|
};
|
|
329
342
|
this._pushResult(rec);
|
|
343
|
+
// Fold this run into the reliability aggregate (in-memory, bounded by
|
|
344
|
+
// distinct hooks). Persisted once on stopAll(); best-effort — a stats bug
|
|
345
|
+
// must never affect hook execution.
|
|
346
|
+
try {
|
|
347
|
+
this._statsStore.aggregateRun(this._statAgg, {
|
|
348
|
+
command: rec.command,
|
|
349
|
+
event: rec.event,
|
|
350
|
+
ok: rec.ok,
|
|
351
|
+
ms: rec.ms,
|
|
352
|
+
now: rec.ts,
|
|
353
|
+
});
|
|
354
|
+
} catch {
|
|
355
|
+
/* stats are best-effort */
|
|
356
|
+
}
|
|
330
357
|
// A rewake fires only for a hook that OPTED IN and finished in a failure
|
|
331
358
|
// state — a passing async check should not re-engage the agent.
|
|
332
359
|
if (hook.asyncRewake && !rec.ok) {
|
|
@@ -381,6 +408,33 @@ class AsyncHookSupervisor {
|
|
|
381
408
|
/* ignore */
|
|
382
409
|
}
|
|
383
410
|
this._exitHookInstalled = false;
|
|
411
|
+
this._persistStats();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Best-effort persist of the session's hook reliability aggregate, merged with
|
|
416
|
+
* whatever is already on disk (accumulates across sessions). No-op when
|
|
417
|
+
* persistence wasn't enabled or nothing was recorded. Never throws — called
|
|
418
|
+
* from stopAll(), which also runs on process 'exit'.
|
|
419
|
+
*/
|
|
420
|
+
_persistStats() {
|
|
421
|
+
if (!this._hookStatsPath) return;
|
|
422
|
+
try {
|
|
423
|
+
const fs = this._statsFs || undefined;
|
|
424
|
+
this._statsStore.persistSessionStats(
|
|
425
|
+
this._statAgg,
|
|
426
|
+
this._hookStatsPath,
|
|
427
|
+
fs,
|
|
428
|
+
);
|
|
429
|
+
this._statAgg = {}; // folded in — don't double-count on a later stop
|
|
430
|
+
} catch {
|
|
431
|
+
/* persistence is best-effort */
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Current in-memory reliability aggregate (test/inspection accessor). */
|
|
436
|
+
getStatsAggregate() {
|
|
437
|
+
return this._statAgg;
|
|
384
438
|
}
|
|
385
439
|
}
|
|
386
440
|
|