chainlesschain 0.162.162 → 0.162.163
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-ClI2ALgv.js → AIOps-BQ0kgfA1.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BDjRcJaK.js → ActionButton-Dr1MDANe.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DikSRsaT.js → Analytics-CZ5vsaXW.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-9mLhMFmz.js → AppLayout-BIBga881.js} +3 -3
- package/src/assets/web-panel/assets/{Artifacts-BSr88BrH.js → Artifacts-vpXHRU3u.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CdvyimOF.js → Audit-B8exuoub.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-D3_hCedh.js → BackgroundAgents-923evRRN.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-Cyvv9RHf.js → Backup-sr_sswlQ.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-BZM14kt8.js → BaseInput-CnW-4XKT.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DrQSokVz.js → Chat-FZCsTZIL.js} +6 -6
- package/src/assets/web-panel/assets/ChatBubbleRenderer-DDPd9qT0.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-bFLYeWMV.js → Checkbox-DFrxB59H.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-7mN7xJXT.js → Codegen-_b6WEel8.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DAB5ZWm7.js → Col-CkbnnZ-K.js} +1 -1
- package/src/assets/web-panel/assets/{Community-B_1TymaK.js → Community-BxM_82zW.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-Bq1uSWLJ.js → Compact-DYinP45R.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CyiE1jEe.js → Compliance-DyXp3sSP.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-Cpk0D7vm.js → Cowork-BEr2DhW7.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-d_Lv3rdB.js → Cron-B-Vwo1hN.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DMqhHjMR.js → Crosschain-DbLxKddd.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Br-oPuBw.js → DID-BsQ_LFw9.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CfLW_RaP.js → Dashboard-lVlXQHu5.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-DZnTOExV.js → Dropdown-Beo_foO7.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-DHIUHDv1.js → EmailListRenderer-DszHIXED.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-Bw3t3myu.js → FamilyGuardDashboard-DcAwoh81.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-D9cpQPAY.js → Federation-CuJLa2eb.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Jdczr9Mr.js → FormItemContext-Cxd1lxhc.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BT8ZMpC5.js → GenericCardRenderer-BBvq5Jsw.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DjARip2b.js → Git-WqPKetx6.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BgWbSRcj.js → Governance-CWYtY3ke.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CfouJciM.js → Inference-BnX1iiNV.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-50l7mrAr.js → KnowledgeGraph-BJuM8GN5.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-rq50H84B.js → Logs-DqLMNntm.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-DUPIEdQH.js → MarkdownRenderer-BqYwEYou.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-BYgPDmHU.js → Marketplace-_vGfmhNl.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-5DVgcFdx.js → McpTools-BeJDOkDj.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-B70LPk7f.js → Memory-Da0EEZDO.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-De8ssA5Y.js → MobileBridge-DSuzcCCy.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-D0RAk4e8.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-RJsOTsIA.js → Mtc-BE9YgdA0.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-Crh-6BEK.js → MtcAudit-fC2BpO5S.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-wuoR5l3x.js → Multisig-D24Xxqw3.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-U2Mji4fZ.js → NLProgramming-Bs3oGvS8.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CdTZAyDH.js → Notes-BFjmAA-3.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-C-FucWE7.js → NotificationSettings-BX_9Bhv0.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-DCYFTeaI.js → OrderTableRenderer-wjkZwJXI.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-an3OF85L.js → Organization-DBcdsVEP.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-C-1vmYFe.js → Overflow-DDyaF9vC.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-Cwh47xKg.js → P2P-BB78L4he.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-Dm38GT0k.js → PdhVaultBrowser-DqTiZFum.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-DS2Y8awD.js → Permissions-INbnWv4j.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BQNziA0I.js → PersonalDataHub-BST-VmTq.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-DF85L1ue.js → Pipeline-CE7_tS2k.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BuLornF-.js → Privacy-hK5L8dOB.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-C3Y31IwX.js → ProjectInit-BvkkUoDn.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BCPiI8aD.js → ProjectSettings-u7_EUtkm.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-BZ6s4k6g.js → Projects-5Fmt_8Nw.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-D3Wp022X.js → Providers-B9eanarR.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-CUJfQkH4.js → QrScannerModal-C0dX5N2f.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-UAc0EO8l.js → QuickAsk-CKTpG-PZ.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BaxKupcs.js → Recommend-Px_w_XQM.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-C75NNnXZ.js → RemoteSession-Di0fnYkM.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-BVXynZAk.js → Reputation-vesnWjXt.js} +1 -1
- package/src/assets/web-panel/assets/{Row-C5MRZ0bi.js → Row-exbY6LkT.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-DAKkEYka.js → RssFeed-B3O3x70t.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DvWv9YOA.js → Search-uRA0DhNz.js} +1 -1
- package/src/assets/web-panel/assets/{Security-B5peafW1.js → Security-CViqdH6K.js} +4 -4
- package/src/assets/web-panel/assets/{Services-UQmvPEaG.js → Services-CkXS97WN.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-C3bhWpuF.js → Skeleton-DxW5WYnE.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DfkKf9_H.js → Skills-CLxmGFse.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DqMSjlBM.js → Sla-Cr5fziXq.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-C-WbxWUX.js → SpeechSettings-H0JSb3cp.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CYuHZ4N2.js → SyncSettings-DMYWMae7.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-CyeAyB9d.js → Tasks-CbYyDdF4.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-BTCeQnpq.js → Templates-Cpc8JjiK.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BifQuA9Y.js → Tenant-CRh77Jda.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-Bx_9Bocf.js → Terminal-4D8tNBO5.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-BX0RFCOm.js → TimelineRenderer-D41jzzbG.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-CGHrCmUS.js → Tokens-C_jWEy6b.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DlqaY4SO.js → Trigger-Dk1ZUhsy.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DfOmj-ay.js → Trust-CLVy4l7m.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-B72jH5fE.js → UkeySign-C-FeqFDj.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BT-63fqv.js → VideoEditing-CqVZzyMm.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-Dsul3sHr.js → Wallet-DX9BW9pu.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-Izkwwdgy.js → WebAuthn-7hQDpWw_.js} +2 -2
- package/src/assets/web-panel/assets/{WorkflowEditor-D3FHSiH1.js → WorkflowEditor-6XqUAO0j.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CmVz5z3U.js → chat-5R_vkaJG.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CuLgyJBp.js → colors-huUPZdC-.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-Ml1YkXpw.js → compact-item-CfJlgisE.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BvjJ48bs.js → createContext-jO8heofr.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DeIrCfH-.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DVC0157C.js → hasIn-DY8Vxqx7.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJyN28Et.js → index-A48PtXw9.js} +1 -1
- package/src/assets/web-panel/assets/{index-CudmwNBV.js → index-B300RPnT.js} +3 -3
- package/src/assets/web-panel/assets/{index-BbepULSc.js → index-B74OXFxC.js} +1 -1
- package/src/assets/web-panel/assets/{index-DYXHFr4r.js → index-BAON7TzU.js} +1 -1
- package/src/assets/web-panel/assets/{index-BzG-x9p4.js → index-BCqxyvqp.js} +1 -1
- package/src/assets/web-panel/assets/{index-wvb_FtaV.js → index-BHE8OIls.js} +1 -1
- package/src/assets/web-panel/assets/{index-QXzlT9KD.js → index-BQS-uPOK.js} +1 -1
- package/src/assets/web-panel/assets/{index-DukDVXCv.js → index-BRXOP09X.js} +1 -1
- package/src/assets/web-panel/assets/{index-CFYVQMvu.js → index-BYLBPpt8.js} +1 -1
- package/src/assets/web-panel/assets/{index-BydjdZnl.js → index-BfCx4pFT.js} +1 -1
- package/src/assets/web-panel/assets/{index-CmFNvNLZ.js → index-BliaT02i.js} +1 -1
- package/src/assets/web-panel/assets/{index-KMqhcF7l.js → index-BmqGerpC.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ck_52dz_.js → index-BsBtRWbA.js} +1 -1
- package/src/assets/web-panel/assets/{index-C229ABfy.js → index-BtEXSoPL.js} +1 -1
- package/src/assets/web-panel/assets/index-BxUKMRpP.js +1 -0
- package/src/assets/web-panel/assets/{index-C_H_S-DY.js → index-Bxax9vQ6.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0S8NzO6.js → index-C2iVSFpy.js} +1 -1
- package/src/assets/web-panel/assets/{index-BjHBpRYZ.js → index-CA6sO5VD.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bzr0rB5T.js → index-CA90TZxG.js} +1 -1
- package/src/assets/web-panel/assets/{index-DDGaOWBV.js → index-CLhBh-qE.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dt5KMjwJ.js → index-CRElRhyS.js} +1 -1
- package/src/assets/web-panel/assets/{index-BbPtzB8M.js → index-Cbe9G0V4.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_vRgTl9.js → index-ChvHubX3.js} +1 -1
- package/src/assets/web-panel/assets/{index-BpFJHiTv.js → index-Cis_Ip7H.js} +1 -1
- package/src/assets/web-panel/assets/{index-KjgKdZe-.js → index-Cjxbu68P.js} +1 -1
- package/src/assets/web-panel/assets/{index-vG4XSJ1d.js → index-CzKmteNS.js} +1 -1
- package/src/assets/web-panel/assets/{index-BUt8kTIB.js → index-D0pet5Uy.js} +1 -1
- package/src/assets/web-panel/assets/{index-BfuKP4qe.js → index-D1VLuww0.js} +1 -1
- package/src/assets/web-panel/assets/{index-4SBta3d7.js → index-DXBcvUnf.js} +1 -1
- package/src/assets/web-panel/assets/{index-DC_OI9H2.js → index-Db1cLWir.js} +1 -1
- package/src/assets/web-panel/assets/{index-DuGszaWY.js → index-DoI2sZm7.js} +1 -1
- package/src/assets/web-panel/assets/index-DscU9iPI.js +1 -0
- package/src/assets/web-panel/assets/{index-4wcgvPoF.js → index-KRHroryI.js} +1 -1
- package/src/assets/web-panel/assets/{index-D3lD5qap.js → index-d1xkbtNz.js} +1 -1
- package/src/assets/web-panel/assets/{index-B_EvG_GW.js → index-dKx57io7.js} +1 -1
- package/src/assets/web-panel/assets/{index-CXkJH-lK.js → index-j3O2EEJe.js} +1 -1
- package/src/assets/web-panel/assets/{index-BiC6dQZG.js → index-p85YaaKw.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cw0Rjy87.js → index-v3pTaEQm.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_XuRgGn.js → index-vXs3B8GJ.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BDgd2YZ8.js → initDefaultProps-MCB7-raZ.js} +1 -1
- package/src/assets/web-panel/assets/{motion-ErtrXdYe.js → motion-UWDF9swV.js} +1 -1
- package/src/assets/web-panel/assets/{move-_GPPNCgs.js → move-CsTmgXZU.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DRLL-8V2.js → mtc-parser-fQy6KGLg.js} +1 -1
- package/src/assets/web-panel/assets/{omit-p86sl023.js → omit-woPnEjd1.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-BTu7cMLH.js → pickAttrs-Bs2dZ1gf.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BEKz6Isr.js → placementArrow-C7EVgPkg.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-Ck42c9MP.js → responsiveObserve-BH2kYCTN.js} +1 -1
- package/src/assets/web-panel/assets/{slide-1CEvuPsB.js → slide-BFBP2V_e.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BDfdx1GJ.js → statusUtils-xdDxDYse.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-B4K46m5w.js → styleChecker-aAWgPPQf.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DoIVpJNq.js → useFlexGapSupport-DkGSAWoO.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-DdAWi-ln.js → useFs-BrOih40N.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-iUu6k8dx.js → usePersonalDataHub-eIYrgMqO.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DZz4R6Tw.js → vnode-B8lutNSE.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DXlf9sA3.js → zoom-CffNre7V.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agenda.js +254 -23
- package/src/commands/agent.js +105 -11
- package/src/commands/context.js +125 -14
- package/src/commands/hook.js +181 -7
- package/src/commands/plugin.js +239 -1
- package/src/commands/review.js +347 -2
- package/src/data/changelog.json +9 -4
- package/src/gateways/ws/remote-session-protocol.js +39 -0
- package/src/gateways/ws/session-protocol.js +5 -2
- package/src/lib/agent-schedule-store.js +289 -10
- 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/doctor-checkup.js +52 -0
- package/src/lib/hook-event-log.cjs +193 -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/plugin-runtime/capability-consent.js +175 -0
- package/src/lib/plugin-runtime/policy.js +62 -0
- package/src/lib/plugin-runtime/scopes.js +20 -4
- package/src/lib/project-instructions.js +128 -0
- package/src/lib/repl-goal.js +132 -0
- package/src/lib/repl-rewind.js +52 -0
- package/src/lib/schedule-planner.js +17 -1
- 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 +296 -0
- package/src/lib/sub-agent-context.js +74 -5
- package/src/lib/subagent-inheritance.js +112 -0
- package/src/lib/turn-binding-store.js +119 -0
- package/src/repl/agent-repl.js +136 -0
- package/src/repl/permission-tier.js +28 -0
- package/src/runtime/agent-core.js +376 -10
- package/src/runtime/coding-agent-contract-shared.cjs +60 -1
- package/src/runtime/headless-runner.js +267 -0
- package/src/runtime/headless-stream.js +60 -1
- package/src/assets/web-panel/assets/ChatBubbleRenderer-HwJR-C6B.js +0 -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
|
@@ -415,11 +415,53 @@ function readBooleanSetting(key, opts = {}) {
|
|
|
415
415
|
return value;
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
+
/**
|
|
419
|
+
* Read a string-array setting across the layered `.claude/settings.json` files
|
|
420
|
+
* plus managed settings. Unlike {@link readBooleanSetting} (last-layer wins),
|
|
421
|
+
* arrays UNION across layers (mirroring how `env`/`sandbox` accumulate in
|
|
422
|
+
* {@link loadSettingsConfig}) — a user-scope and a project-scope layer can each
|
|
423
|
+
* contribute entries. Non-array values and non-string members are ignored.
|
|
424
|
+
* Returns a de-duplicated array, or `undefined` when no layer sets it.
|
|
425
|
+
*
|
|
426
|
+
* @param {string} key settings key or dotted path (e.g. "instructionExcludes")
|
|
427
|
+
* @param {object} [opts]
|
|
428
|
+
* @param {string} [opts.cwd=process.cwd()]
|
|
429
|
+
* @param {string} [opts.settingsFile]
|
|
430
|
+
* @param {(msg:string)=>void} [opts.onWarn]
|
|
431
|
+
* @returns {string[]|undefined}
|
|
432
|
+
*/
|
|
433
|
+
function readStringArraySetting(key, opts = {}) {
|
|
434
|
+
const cwd = opts.cwd || process.cwd();
|
|
435
|
+
const parts = String(key).split(".");
|
|
436
|
+
const collected = [];
|
|
437
|
+
let sawAny = false;
|
|
438
|
+
const absorb = (data) => {
|
|
439
|
+
let node = data;
|
|
440
|
+
for (const p of parts) {
|
|
441
|
+
node = node && typeof node === "object" ? node[p] : undefined;
|
|
442
|
+
}
|
|
443
|
+
if (Array.isArray(node)) {
|
|
444
|
+
sawAny = true;
|
|
445
|
+
for (const item of node) {
|
|
446
|
+
if (typeof item === "string" && item.trim()) collected.push(item);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
for (const file of settingsPaths(cwd, opts.settingsFile)) {
|
|
451
|
+
absorb(readSettingsFile(file, { onWarn: opts.onWarn }));
|
|
452
|
+
}
|
|
453
|
+
const managed = loadManagedSettings(opts);
|
|
454
|
+
if (managed.settings) absorb(managed.settings);
|
|
455
|
+
if (!sawAny) return undefined;
|
|
456
|
+
return [...new Set(collected)];
|
|
457
|
+
}
|
|
458
|
+
|
|
418
459
|
module.exports = {
|
|
419
460
|
loadSettings,
|
|
420
461
|
loadSettingsConfig,
|
|
421
462
|
readSettingsFile,
|
|
422
463
|
readBooleanSetting,
|
|
464
|
+
readStringArraySetting,
|
|
423
465
|
settingsPaths,
|
|
424
466
|
managedSettingsPath,
|
|
425
467
|
loadManagedSettings,
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persist the side-effect ledger ([[side-effect-ledger.js]]) as a tamper-evident
|
|
3
|
+
* chained session event so a worker that crashed mid `git push` / `file-write` /
|
|
4
|
+
* `package-install` can rebuild its ledger on recovery and reconcile WITHOUT
|
|
5
|
+
* repeating a side-effect it already issued.
|
|
6
|
+
*
|
|
7
|
+
* Same shape as [[turn-binding-store.js]]: each persist writes the FULL ledger
|
|
8
|
+
* snapshot (newest wins), all I/O is delegated to the canonical session store
|
|
9
|
+
* via `_deps` so the module stays pure-testable.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
appendEvent as storeAppendEvent,
|
|
14
|
+
readEvents as storeReadEvents,
|
|
15
|
+
} from "../harness/jsonl-session-store.js";
|
|
16
|
+
import {
|
|
17
|
+
SideEffectLedger,
|
|
18
|
+
reconcileSideEffects,
|
|
19
|
+
} from "./side-effect-ledger.js";
|
|
20
|
+
|
|
21
|
+
/** Event type carrying a full SideEffectLedger snapshot. */
|
|
22
|
+
export const SIDE_EFFECT_LEDGER_EVENT = "side_effect_ledger";
|
|
23
|
+
|
|
24
|
+
export const _deps = {
|
|
25
|
+
appendEvent: storeAppendEvent,
|
|
26
|
+
readEvents: storeReadEvents,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Append the current ledger as a chained event. Best-effort — a persistence
|
|
31
|
+
* failure must not crash the operation it is guarding.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} sessionId
|
|
34
|
+
* @param {SideEffectLedger|object} ledger
|
|
35
|
+
* @returns {boolean}
|
|
36
|
+
*/
|
|
37
|
+
export function persistSideEffectLedger(sessionId, ledger) {
|
|
38
|
+
if (!sessionId || !ledger) return false;
|
|
39
|
+
const snapshot =
|
|
40
|
+
ledger instanceof SideEffectLedger ? ledger.toJSON() : ledger;
|
|
41
|
+
if (!snapshot || !Array.isArray(snapshot.ops)) return false;
|
|
42
|
+
try {
|
|
43
|
+
_deps.appendEvent(sessionId, SIDE_EFFECT_LEDGER_EVENT, snapshot);
|
|
44
|
+
return true;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Rebuild the ledger from the LATEST persisted snapshot, or an empty ledger when
|
|
52
|
+
* the session never persisted one (or on read error).
|
|
53
|
+
*
|
|
54
|
+
* @param {string} sessionId
|
|
55
|
+
* @param {{clock?:Function}} [opts]
|
|
56
|
+
* @returns {SideEffectLedger}
|
|
57
|
+
*/
|
|
58
|
+
export function loadSideEffectLedger(sessionId, opts = {}) {
|
|
59
|
+
let events = [];
|
|
60
|
+
try {
|
|
61
|
+
events = _deps.readEvents(sessionId) || [];
|
|
62
|
+
} catch {
|
|
63
|
+
return new SideEffectLedger(opts);
|
|
64
|
+
}
|
|
65
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
66
|
+
const e = events[i];
|
|
67
|
+
if (
|
|
68
|
+
e &&
|
|
69
|
+
e.type === SIDE_EFFECT_LEDGER_EVENT &&
|
|
70
|
+
e.data &&
|
|
71
|
+
Array.isArray(e.data.ops)
|
|
72
|
+
) {
|
|
73
|
+
return SideEffectLedger.fromJSON(e.data, opts);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return new SideEffectLedger(opts);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Load + reconcile in one step: the recovery entry point. Returns the plan a
|
|
81
|
+
* restarting worker uses to decide, per op, redo / inspect / skip.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} sessionId
|
|
84
|
+
* @returns {{plans:Array, redo:string[], inspect:string[], skip:string[]}}
|
|
85
|
+
*/
|
|
86
|
+
export function reconcileSessionSideEffects(sessionId) {
|
|
87
|
+
return reconcileSideEffects(loadSideEffectLedger(sessionId));
|
|
88
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Side-effect ledger — crash-safe accounting of irreversible actions so a
|
|
3
|
+
* recovered worker never repeats a side-effect it already issued (P0
|
|
4
|
+
* state-machine slice: CLAUDE_CODE_CLI_INCREMENTAL_GAP_ANALYSIS_2026-07-12
|
|
5
|
+
* §"后台 Agent 状态机" — "副作用台账(prepared→started→committed|failed|unknown)
|
|
6
|
+
* 与崩溃恢复重放").
|
|
7
|
+
*
|
|
8
|
+
* The gap's acceptance is concrete: kill the worker/supervisor mid `file-write`,
|
|
9
|
+
* `git push`, or `package-install`, and after recovery the effect must not be
|
|
10
|
+
* applied twice. The classic bug is a blind retry — the process died AFTER
|
|
11
|
+
* `git push` reached the remote but BEFORE it recorded success, so a naive
|
|
12
|
+
* "resume the turn" pushes again.
|
|
13
|
+
*
|
|
14
|
+
* The fix is a two-phase record around every side-effect:
|
|
15
|
+
*
|
|
16
|
+
* prepare(opId) → start(opId) → commit(opId) | fail(opId) | unknown(opId)
|
|
17
|
+
*
|
|
18
|
+
* and an HONEST recovery planner. The invariant: a `started`-but-not-settled op
|
|
19
|
+
* is INDETERMINATE — we cannot know if the effect landed — so unless the op is
|
|
20
|
+
* explicitly idempotent it must be INSPECTED by a human/verifier, never blindly
|
|
21
|
+
* redone. `prepared`-but-never-`started` ops are safe to redo (no effect issued
|
|
22
|
+
* yet); `committed` ops are skipped; cleanly `failed` ops redo only when the
|
|
23
|
+
* caller vouched they roll back atomically.
|
|
24
|
+
*
|
|
25
|
+
* Everything here is PURE (no timers, no I/O): the ledger is in-memory +
|
|
26
|
+
* serializable so a caller persists it alongside the session record. I/O lives
|
|
27
|
+
* in [[side-effect-ledger-store.js]].
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** Lifecycle states of one side-effecting operation. */
|
|
31
|
+
export const SIDE_EFFECT_STATE = Object.freeze({
|
|
32
|
+
PREPARED: "prepared", // intent recorded; nothing issued yet
|
|
33
|
+
STARTED: "started", // effect issued; outcome not yet known
|
|
34
|
+
COMMITTED: "committed", // effect confirmed applied
|
|
35
|
+
FAILED: "failed", // effect confirmed NOT applied (clean failure)
|
|
36
|
+
UNKNOWN: "unknown", // outcome indeterminate (e.g. crash mid-flight)
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/** Recovery actions the reconciler emits for each unsettled op. */
|
|
40
|
+
export const RECOVERY_ACTION = Object.freeze({
|
|
41
|
+
SKIP: "skip", // already committed — do nothing
|
|
42
|
+
REDO: "redo", // safe to run again (no effect issued, or idempotent)
|
|
43
|
+
INSPECT: "inspect", // indeterminate — verify before any repeat
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Default idempotency for well-known side-effect kinds. Only actions that are
|
|
48
|
+
* naturally safe to re-run without duplicating an external effect are `true`.
|
|
49
|
+
* `file-write` is idempotent ONLY when a pre-write checkpoint exists (the write
|
|
50
|
+
* can be re-applied from the same source over a restored tree); a raw append
|
|
51
|
+
* has no such guarantee, so the default is conservative `false`.
|
|
52
|
+
*/
|
|
53
|
+
const KIND_IDEMPOTENT = new Map([
|
|
54
|
+
["git-push", false],
|
|
55
|
+
["package-install", false],
|
|
56
|
+
["network-mutation", false],
|
|
57
|
+
["payment", false],
|
|
58
|
+
["file-write", false],
|
|
59
|
+
["file-write-checkpointed", true],
|
|
60
|
+
["read", true],
|
|
61
|
+
["compute", true],
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
/** Whether a kind is idempotent by default (unknown kinds → conservative false). */
|
|
65
|
+
export function kindIsIdempotent(kind) {
|
|
66
|
+
return KIND_IDEMPOTENT.get(String(kind || "")) === true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function nowFrom(clock) {
|
|
70
|
+
return typeof clock === "function" ? Number(clock()) || 0 : 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The in-memory, serializable ledger. A caller records the two-phase lifecycle
|
|
75
|
+
* around each side-effect and, after a crash, hands the rebuilt ledger to
|
|
76
|
+
* `reconcileSideEffects` for a recovery plan.
|
|
77
|
+
*
|
|
78
|
+
* Timestamps are optional and only stamped when a `clock` is injected — keeping
|
|
79
|
+
* the ledger deterministic in tests (no `Date.now()`).
|
|
80
|
+
*/
|
|
81
|
+
export class SideEffectLedger {
|
|
82
|
+
constructor({ clock } = {}) {
|
|
83
|
+
this._ops = new Map();
|
|
84
|
+
this._order = [];
|
|
85
|
+
this._clock = clock || null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
_rec(opId) {
|
|
89
|
+
return this._ops.get(String(opId)) || null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Record intent to perform a side-effect. Idempotent on opId (re-preparing an
|
|
94
|
+
* existing op is a no-op, so a retry loop can't reset a committed op).
|
|
95
|
+
*
|
|
96
|
+
* @param {string} opId stable id (survives restart — e.g. `${turnId}:${seq}`)
|
|
97
|
+
* @param {{kind?:string, key?:string, idempotent?:boolean, meta?:object}} [info]
|
|
98
|
+
*/
|
|
99
|
+
prepare(opId, info = {}) {
|
|
100
|
+
const id = String(opId);
|
|
101
|
+
if (this._ops.has(id)) return this;
|
|
102
|
+
const kind = info.kind == null ? null : String(info.kind);
|
|
103
|
+
this._ops.set(id, {
|
|
104
|
+
opId: id,
|
|
105
|
+
kind,
|
|
106
|
+
key: info.key == null ? null : String(info.key),
|
|
107
|
+
idempotent:
|
|
108
|
+
typeof info.idempotent === "boolean"
|
|
109
|
+
? info.idempotent
|
|
110
|
+
: kindIsIdempotent(kind),
|
|
111
|
+
state: SIDE_EFFECT_STATE.PREPARED,
|
|
112
|
+
reason: null,
|
|
113
|
+
meta: info.meta && typeof info.meta === "object" ? { ...info.meta } : {},
|
|
114
|
+
preparedAt: nowFrom(this._clock),
|
|
115
|
+
settledAt: null,
|
|
116
|
+
});
|
|
117
|
+
this._order.push(id);
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Mark the effect as ISSUED (outcome not yet known). Only from `prepared`. */
|
|
122
|
+
start(opId) {
|
|
123
|
+
return this._advance(opId, SIDE_EFFECT_STATE.STARTED, [
|
|
124
|
+
SIDE_EFFECT_STATE.PREPARED,
|
|
125
|
+
]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Confirm the effect applied. Only from `started`. */
|
|
129
|
+
commit(opId) {
|
|
130
|
+
return this._advance(
|
|
131
|
+
opId,
|
|
132
|
+
SIDE_EFFECT_STATE.COMMITTED,
|
|
133
|
+
[SIDE_EFFECT_STATE.STARTED],
|
|
134
|
+
{ settle: true },
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Confirm the effect did NOT apply (clean failure). Only from `started`. */
|
|
139
|
+
fail(opId, reason) {
|
|
140
|
+
return this._advance(
|
|
141
|
+
opId,
|
|
142
|
+
SIDE_EFFECT_STATE.FAILED,
|
|
143
|
+
[SIDE_EFFECT_STATE.STARTED],
|
|
144
|
+
{ settle: true, reason },
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Explicitly mark an op's outcome indeterminate. Accepts `prepared`/`started`
|
|
150
|
+
* (a recovery pass sweeping unsettled ops), never overrides a settled op.
|
|
151
|
+
*/
|
|
152
|
+
unknown(opId, reason) {
|
|
153
|
+
return this._advance(
|
|
154
|
+
opId,
|
|
155
|
+
SIDE_EFFECT_STATE.UNKNOWN,
|
|
156
|
+
[SIDE_EFFECT_STATE.PREPARED, SIDE_EFFECT_STATE.STARTED],
|
|
157
|
+
{ settle: true, reason },
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
_advance(opId, next, allowedFrom, { settle = false, reason } = {}) {
|
|
162
|
+
const rec = this._rec(opId);
|
|
163
|
+
if (!rec) return this;
|
|
164
|
+
if (!allowedFrom.includes(rec.state)) return this; // illegal transition → ignore
|
|
165
|
+
rec.state = next;
|
|
166
|
+
if (reason != null) rec.reason = String(reason);
|
|
167
|
+
if (settle) rec.settledAt = nowFrom(this._clock);
|
|
168
|
+
return this;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
get(opId) {
|
|
172
|
+
const rec = this._rec(opId);
|
|
173
|
+
return rec ? { ...rec, meta: { ...rec.meta } } : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
list() {
|
|
177
|
+
return this._order.map((id) => this.get(id));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Ops that never reached a terminal state — the recovery surface. */
|
|
181
|
+
unsettled() {
|
|
182
|
+
return this.list().filter(
|
|
183
|
+
(o) =>
|
|
184
|
+
o.state === SIDE_EFFECT_STATE.PREPARED ||
|
|
185
|
+
o.state === SIDE_EFFECT_STATE.STARTED,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
toJSON() {
|
|
190
|
+
return { ops: this._order.map((id) => this.get(id)) };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
static fromJSON(json, { clock } = {}) {
|
|
194
|
+
const ledger = new SideEffectLedger({ clock });
|
|
195
|
+
for (const o of json?.ops || []) {
|
|
196
|
+
if (!o || o.opId == null) continue;
|
|
197
|
+
const id = String(o.opId);
|
|
198
|
+
if (ledger._ops.has(id)) continue;
|
|
199
|
+
ledger._ops.set(id, {
|
|
200
|
+
opId: id,
|
|
201
|
+
kind: o.kind == null ? null : String(o.kind),
|
|
202
|
+
key: o.key == null ? null : String(o.key),
|
|
203
|
+
idempotent: o.idempotent === true,
|
|
204
|
+
state: Object.values(SIDE_EFFECT_STATE).includes(o.state)
|
|
205
|
+
? o.state
|
|
206
|
+
: SIDE_EFFECT_STATE.UNKNOWN,
|
|
207
|
+
reason: o.reason == null ? null : String(o.reason),
|
|
208
|
+
meta: o.meta && typeof o.meta === "object" ? { ...o.meta } : {},
|
|
209
|
+
preparedAt: Number(o.preparedAt) || 0,
|
|
210
|
+
settledAt: o.settledAt == null ? null : Number(o.settledAt) || 0,
|
|
211
|
+
});
|
|
212
|
+
ledger._order.push(id);
|
|
213
|
+
}
|
|
214
|
+
return ledger;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Plan recovery for one op record (from SideEffectLedger.get()/list()). HONEST:
|
|
220
|
+
* an indeterminate `started` op is INSPECT unless it is explicitly idempotent.
|
|
221
|
+
*
|
|
222
|
+
* @param {object} op
|
|
223
|
+
* @returns {{opId:string, action:string, reason:string}}
|
|
224
|
+
*/
|
|
225
|
+
export function planOpRecovery(op) {
|
|
226
|
+
const opId = op?.opId ?? null;
|
|
227
|
+
switch (op?.state) {
|
|
228
|
+
case SIDE_EFFECT_STATE.COMMITTED:
|
|
229
|
+
return {
|
|
230
|
+
opId,
|
|
231
|
+
action: RECOVERY_ACTION.SKIP,
|
|
232
|
+
reason: "already committed",
|
|
233
|
+
};
|
|
234
|
+
case SIDE_EFFECT_STATE.PREPARED:
|
|
235
|
+
// Intent only — no external effect was issued, so a clean re-run is safe.
|
|
236
|
+
return {
|
|
237
|
+
opId,
|
|
238
|
+
action: RECOVERY_ACTION.REDO,
|
|
239
|
+
reason: "prepared but never started — no effect issued",
|
|
240
|
+
};
|
|
241
|
+
case SIDE_EFFECT_STATE.FAILED:
|
|
242
|
+
// A clean failure means the effect did not land; idempotent ops re-run.
|
|
243
|
+
return op.idempotent
|
|
244
|
+
? {
|
|
245
|
+
opId,
|
|
246
|
+
action: RECOVERY_ACTION.REDO,
|
|
247
|
+
reason: "failed cleanly and idempotent — safe to retry",
|
|
248
|
+
}
|
|
249
|
+
: {
|
|
250
|
+
opId,
|
|
251
|
+
action: RECOVERY_ACTION.INSPECT,
|
|
252
|
+
reason: "failed — verify no partial effect before retrying",
|
|
253
|
+
};
|
|
254
|
+
case SIDE_EFFECT_STATE.STARTED:
|
|
255
|
+
case SIDE_EFFECT_STATE.UNKNOWN:
|
|
256
|
+
default:
|
|
257
|
+
// Crashed mid-flight: the effect MAY have landed. Only idempotent ops are
|
|
258
|
+
// safe to redo; everything else must be inspected, never blindly repeated.
|
|
259
|
+
return op?.idempotent
|
|
260
|
+
? {
|
|
261
|
+
opId,
|
|
262
|
+
action: RECOVERY_ACTION.REDO,
|
|
263
|
+
reason: "indeterminate but idempotent — safe to re-run",
|
|
264
|
+
}
|
|
265
|
+
: {
|
|
266
|
+
opId,
|
|
267
|
+
action: RECOVERY_ACTION.INSPECT,
|
|
268
|
+
reason:
|
|
269
|
+
"started but not confirmed — effect may have applied; do not repeat blindly",
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Build a recovery plan for every op in a (crash-rebuilt) ledger.
|
|
276
|
+
*
|
|
277
|
+
* @param {SideEffectLedger|{ops:Array}} ledger
|
|
278
|
+
* @returns {{plans:Array, redo:string[], inspect:string[], skip:string[]}}
|
|
279
|
+
*/
|
|
280
|
+
export function reconcileSideEffects(ledger) {
|
|
281
|
+
const ops =
|
|
282
|
+
ledger instanceof SideEffectLedger
|
|
283
|
+
? ledger.list()
|
|
284
|
+
: Array.isArray(ledger?.ops)
|
|
285
|
+
? ledger.ops
|
|
286
|
+
: [];
|
|
287
|
+
const plans = ops.map((o) => planOpRecovery(o));
|
|
288
|
+
const bucket = (action) =>
|
|
289
|
+
plans.filter((p) => p.action === action).map((p) => p.opId);
|
|
290
|
+
return {
|
|
291
|
+
plans,
|
|
292
|
+
redo: bucket(RECOVERY_ACTION.REDO),
|
|
293
|
+
inspect: bucket(RECOVERY_ACTION.INSPECT),
|
|
294
|
+
skip: bucket(RECOVERY_ACTION.SKIP),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
@@ -45,8 +45,10 @@ export class SubAgentContext {
|
|
|
45
45
|
* @param {number} [options.maxIterations] - Iteration limit (fallback if no budget)
|
|
46
46
|
* @param {import('./iteration-budget.js').IterationBudget} [options.iterationBudget] - Shared iteration budget (takes priority over maxIterations)
|
|
47
47
|
* @param {number} [options.tokenBudget] - Optional token budget
|
|
48
|
-
* @param {object} [options.db] - Database instance
|
|
48
|
+
* @param {object} [options.db] - Database instance (memory recall source)
|
|
49
49
|
* @param {object} [options.permanentMemory] - Permanent memory instance
|
|
50
|
+
* @param {boolean} [options.memoryEnabled=true] - When false, the child's
|
|
51
|
+
* context engine suppresses hierarchical-memory recall (contract memory:false)
|
|
50
52
|
* @param {object} [options.llmOptions] - LLM provider/model/key options
|
|
51
53
|
* @param {string} [options.cwd] - Working directory
|
|
52
54
|
* @param {boolean} [options.useWorktree] - Force worktree isolation (overrides flag)
|
|
@@ -77,6 +79,14 @@ export class SubAgentContext {
|
|
|
77
79
|
// Shared run-wide TOTAL-sub-agent counter (one object across the whole tree)
|
|
78
80
|
// so this sub-agent's own spawns draw from the same breadth pool.
|
|
79
81
|
this.subAgentBudget = options.subAgentBudget || null;
|
|
82
|
+
// This context's EFFECTIVE subagent contract — the ceiling handed to its OWN
|
|
83
|
+
// nested spawns (threaded into the loop options so a nested spawn_sub_agent
|
|
84
|
+
// reads it as ctx.subAgentContract).
|
|
85
|
+
this.subAgentContract = options.subAgentContract || null;
|
|
86
|
+
// Skill capability INTERSECT: null = unrestricted; a list (possibly empty)
|
|
87
|
+
// restricts run_skill/list_skills in this context's loop to those skills.
|
|
88
|
+
this.skillAllowlist =
|
|
89
|
+
options.skillAllowlist != null ? options.skillAllowlist : null;
|
|
80
90
|
this.cwd = options.cwd || process.cwd();
|
|
81
91
|
this.status = "active";
|
|
82
92
|
this.result = null;
|
|
@@ -93,10 +103,17 @@ export class SubAgentContext {
|
|
|
93
103
|
// Independent message history — never shared with parent
|
|
94
104
|
this.messages = [];
|
|
95
105
|
|
|
96
|
-
// Independent context engine — does not inherit parent's compaction/errors
|
|
106
|
+
// Independent context engine — does not inherit parent's compaction/errors.
|
|
107
|
+
// Memory recall is gated by the subagent contract: the spawn passes a `db`
|
|
108
|
+
// and memoryEnabled:true only when the resolved contract grants memory
|
|
109
|
+
// (context:fork from a memory-bearing parent, or explicit memory:true).
|
|
110
|
+
// Default (silent-`fresh`→memory:false) → no db + memoryEnabled:false → no
|
|
111
|
+
// recall, which is today's byte-identical behavior for a plain sub-agent.
|
|
112
|
+
this._memoryEnabled = options.memoryEnabled !== false;
|
|
97
113
|
this.contextEngine = new CLIContextEngineering({
|
|
98
114
|
db: options.db || null,
|
|
99
115
|
permanentMemory: options.permanentMemory || null,
|
|
116
|
+
memoryEnabled: this._memoryEnabled,
|
|
100
117
|
scope: {
|
|
101
118
|
taskId: this.id,
|
|
102
119
|
role: this.role,
|
|
@@ -145,6 +162,22 @@ export class SubAgentContext {
|
|
|
145
162
|
: {};
|
|
146
163
|
this._mcpClient = options.mcpClient || null;
|
|
147
164
|
|
|
165
|
+
// Inherited settings hooks (Pre/PostToolUse) for this child loop. null =
|
|
166
|
+
// no hooks (the spawn default). The spawn path passes the parent's hooks
|
|
167
|
+
// filtered by the contract's `hooks` allow-list; forwarded into agentLoop.
|
|
168
|
+
this._settingsHooks = options.settingsHooks || null;
|
|
169
|
+
|
|
170
|
+
// permissionMode confirmer for this child loop. null = no confirmer (the
|
|
171
|
+
// spawn default → the child denies ask/sensitive-file/git gates). The spawn
|
|
172
|
+
// sets this only for the autopilot (bypassPermissions) ALLOW confirmer.
|
|
173
|
+
this._permissionConfirm = options.permissionConfirm || null;
|
|
174
|
+
|
|
175
|
+
// permissionMode ApprovalGate for this child loop. null = ungated (the spawn
|
|
176
|
+
// default). The spawn sets a dedicated confirmer-less gate (seeded with the
|
|
177
|
+
// mode's tier) for the strict/trusted tiers so run_shell/browser_act are
|
|
178
|
+
// gated headlessly (CONFIRM→no-confirmer→DENY).
|
|
179
|
+
this._approvalGate = options.approvalGate || null;
|
|
180
|
+
|
|
148
181
|
// Build isolated system prompt
|
|
149
182
|
const basePrompt = buildSystemPrompt(this.cwd);
|
|
150
183
|
const profilePrompt = this._profile?.systemPrompt
|
|
@@ -176,9 +209,25 @@ export class SubAgentContext {
|
|
|
176
209
|
);
|
|
177
210
|
}
|
|
178
211
|
|
|
179
|
-
// If worktree isolation is enabled, wrap execution in isolated worktree
|
|
180
|
-
if (this._useWorktree
|
|
181
|
-
|
|
212
|
+
// If worktree isolation is enabled, wrap execution in isolated worktree.
|
|
213
|
+
if (this._useWorktree) {
|
|
214
|
+
if (isGitRepo(this._repoDir)) {
|
|
215
|
+
return this._runInWorktree(userPrompt, loopOptions);
|
|
216
|
+
}
|
|
217
|
+
// FAIL CLOSED: isolation was explicitly requested but this is not a git
|
|
218
|
+
// repo. Never silently fall back to the parent checkout — refuse.
|
|
219
|
+
this.status = "failed";
|
|
220
|
+
this.completedAt = new Date().toISOString();
|
|
221
|
+
this.result = {
|
|
222
|
+
summary:
|
|
223
|
+
"Worktree isolation was requested but the working directory is not a git repository — refusing to run in the parent checkout.",
|
|
224
|
+
artifacts: [],
|
|
225
|
+
tokenCount: this._tokenCount,
|
|
226
|
+
toolsUsed: [...new Set(this._toolsUsed)],
|
|
227
|
+
iterationCount: this._iterationCount,
|
|
228
|
+
isolationError: true,
|
|
229
|
+
};
|
|
230
|
+
return this.result;
|
|
182
231
|
}
|
|
183
232
|
|
|
184
233
|
return this._runCore(userPrompt, loopOptions);
|
|
@@ -270,6 +319,10 @@ export class SubAgentContext {
|
|
|
270
319
|
// Shared total-sub-agent counter so a nested spawn_sub_agent draws from
|
|
271
320
|
// (and is bounded by) the run's single breadth pool.
|
|
272
321
|
subAgentBudget: this.subAgentBudget,
|
|
322
|
+
// This context's effective contract = the ceiling for its nested spawns.
|
|
323
|
+
subAgentContract: this.subAgentContract,
|
|
324
|
+
// Skill allow-list restricting run_skill/list_skills in this loop.
|
|
325
|
+
skillAllowlist: this.skillAllowlist,
|
|
273
326
|
...loopOptions,
|
|
274
327
|
};
|
|
275
328
|
if (this.iterationBudget) {
|
|
@@ -298,6 +351,22 @@ export class SubAgentContext {
|
|
|
298
351
|
if (this._mcpClient) {
|
|
299
352
|
options.mcpClient = this._mcpClient;
|
|
300
353
|
}
|
|
354
|
+
// Inherited Pre/PostToolUse hooks (spawn passes the parent's, filtered by
|
|
355
|
+
// the contract's `hooks` allow-list). Only set when non-null so a plain
|
|
356
|
+
// sub-agent keeps its no-hooks default.
|
|
357
|
+
if (this._settingsHooks) {
|
|
358
|
+
options.settingsHooks = this._settingsHooks;
|
|
359
|
+
}
|
|
360
|
+
// permissionMode confirmer (spawn sets it only for bypassPermissions). Only
|
|
361
|
+
// forward when present so a plain sub-agent keeps its implicit-deny default.
|
|
362
|
+
if (this._permissionConfirm) {
|
|
363
|
+
options.permissionConfirm = this._permissionConfirm;
|
|
364
|
+
}
|
|
365
|
+
// permissionMode ApprovalGate (spawn sets it for strict/trusted tiers). Only
|
|
366
|
+
// forward when present so a plain sub-agent stays ungated (byte-identical).
|
|
367
|
+
if (this._approvalGate) {
|
|
368
|
+
options.approvalGate = this._approvalGate;
|
|
369
|
+
}
|
|
301
370
|
|
|
302
371
|
try {
|
|
303
372
|
// Use a separate messages array for the agent loop
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent capability INHERITANCE + INTERSECT (P1 §"补齐 Subagent 契约").
|
|
3
|
+
*
|
|
4
|
+
* A spawned sub-agent inherits the parent loop's MCP tools and Pre/PostToolUse
|
|
5
|
+
* hooks ONLY through these filters, keyed off the resolved contract's
|
|
6
|
+
* `mcpServers` / `hooks` allow-lists. The tri-state matches
|
|
7
|
+
* `resolveSubagentContract` (see subagent-contract.js):
|
|
8
|
+
*
|
|
9
|
+
* allow === null → inherit ALL (context:fork with an unrestricted parent)
|
|
10
|
+
* allow === [] → inherit NONE (the silent-`fresh` default = today's
|
|
11
|
+
* behavior: a spawned child gets no MCP tools / no hooks)
|
|
12
|
+
* allow === [...] → inherit only the named servers / hook matchers
|
|
13
|
+
*
|
|
14
|
+
* Because the default is `[]` = none, wiring these is byte-identical for a
|
|
15
|
+
* plain sub-agent and only grants capabilities when the contract explicitly
|
|
16
|
+
* opts in (fork context or an explicit list). Pure — no I/O.
|
|
17
|
+
*
|
|
18
|
+
* @module subagent-inheritance
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Extract the server segment from an `mcp__<server>__<tool>` wire name. */
|
|
22
|
+
export function mcpServerOf(name) {
|
|
23
|
+
const s = String(name || "");
|
|
24
|
+
if (!s.startsWith("mcp__")) return null;
|
|
25
|
+
const rest = s.slice(5);
|
|
26
|
+
const idx = rest.indexOf("__");
|
|
27
|
+
return idx === -1 ? rest || null : rest.slice(0, idx) || null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Filter the parent's MCP plumbing to the servers the child may use.
|
|
32
|
+
*
|
|
33
|
+
* @param {object} parent { extraToolDefinitions[], externalToolDescriptors{},
|
|
34
|
+
* externalToolExecutors{}, mcpClient }
|
|
35
|
+
* @param {string[]|null} allow null=all, []=none, list=subset of server names
|
|
36
|
+
* @returns {object|null} filtered plumbing, or null when nothing is inherited
|
|
37
|
+
*/
|
|
38
|
+
export function filterInheritedMcp(parent, allow) {
|
|
39
|
+
if (Array.isArray(allow) && allow.length === 0) return null; // none
|
|
40
|
+
const allowSet = Array.isArray(allow) ? new Set(allow) : null; // null = all
|
|
41
|
+
const serverAllowed = (server) =>
|
|
42
|
+
!allowSet || (server != null && allowSet.has(server));
|
|
43
|
+
|
|
44
|
+
const defs = Array.isArray(parent?.extraToolDefinitions)
|
|
45
|
+
? parent.extraToolDefinitions
|
|
46
|
+
: [];
|
|
47
|
+
const descs =
|
|
48
|
+
parent?.externalToolDescriptors &&
|
|
49
|
+
typeof parent.externalToolDescriptors === "object"
|
|
50
|
+
? parent.externalToolDescriptors
|
|
51
|
+
: {};
|
|
52
|
+
const execs =
|
|
53
|
+
parent?.externalToolExecutors &&
|
|
54
|
+
typeof parent.externalToolExecutors === "object"
|
|
55
|
+
? parent.externalToolExecutors
|
|
56
|
+
: {};
|
|
57
|
+
|
|
58
|
+
const outDefs = defs.filter((d) => {
|
|
59
|
+
const name = d?.function?.name;
|
|
60
|
+
return name && serverAllowed(mcpServerOf(name));
|
|
61
|
+
});
|
|
62
|
+
const outDescs = {};
|
|
63
|
+
for (const [name, desc] of Object.entries(descs)) {
|
|
64
|
+
const server = desc?.serverName || mcpServerOf(name);
|
|
65
|
+
if (serverAllowed(server)) outDescs[name] = desc;
|
|
66
|
+
}
|
|
67
|
+
const outExecs = {};
|
|
68
|
+
for (const [name, exec] of Object.entries(execs)) {
|
|
69
|
+
const server = exec?.serverName || mcpServerOf(name);
|
|
70
|
+
if (serverAllowed(server)) outExecs[name] = exec;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (
|
|
74
|
+
outDefs.length === 0 &&
|
|
75
|
+
Object.keys(outDescs).length === 0 &&
|
|
76
|
+
Object.keys(outExecs).length === 0
|
|
77
|
+
) {
|
|
78
|
+
return null; // nothing survived the filter — inherit no MCP
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
extraToolDefinitions: outDefs,
|
|
82
|
+
externalToolDescriptors: outDescs,
|
|
83
|
+
externalToolExecutors: outExecs,
|
|
84
|
+
mcpClient: parent?.mcpClient || null,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Filter the parent's settings hooks to the matchers the child may fire. Hooks
|
|
90
|
+
* have no name; a group's `matcher` (normalized: null/""→"*") is its identity.
|
|
91
|
+
*
|
|
92
|
+
* @param {object|null} settingsHooks { [event]: [{matcher, hooks:[...]}] }
|
|
93
|
+
* @param {string[]|null} allow null=all, []=none, list=subset of matchers
|
|
94
|
+
* @returns {object|null} filtered hooks, or null when nothing is inherited
|
|
95
|
+
*/
|
|
96
|
+
export function filterInheritedHooks(settingsHooks, allow) {
|
|
97
|
+
if (Array.isArray(allow) && allow.length === 0) return null; // none
|
|
98
|
+
if (!settingsHooks || typeof settingsHooks !== "object") return null;
|
|
99
|
+
if (!Array.isArray(allow)) return settingsHooks; // null = inherit all as-is
|
|
100
|
+
|
|
101
|
+
const allowSet = new Set(allow);
|
|
102
|
+
const matcherKey = (g) =>
|
|
103
|
+
g?.matcher == null || g.matcher === "" ? "*" : String(g.matcher);
|
|
104
|
+
|
|
105
|
+
const out = {};
|
|
106
|
+
for (const [event, groups] of Object.entries(settingsHooks)) {
|
|
107
|
+
if (!Array.isArray(groups)) continue;
|
|
108
|
+
const kept = groups.filter((g) => allowSet.has(matcherKey(g)));
|
|
109
|
+
if (kept.length > 0) out[event] = kept;
|
|
110
|
+
}
|
|
111
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
112
|
+
}
|