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.
Files changed (192) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-ClI2ALgv.js → AIOps-BQ0kgfA1.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-BDjRcJaK.js → ActionButton-Dr1MDANe.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-DikSRsaT.js → Analytics-CZ5vsaXW.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-9mLhMFmz.js → AppLayout-BIBga881.js} +3 -3
  6. package/src/assets/web-panel/assets/{Artifacts-BSr88BrH.js → Artifacts-vpXHRU3u.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-CdvyimOF.js → Audit-B8exuoub.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-D3_hCedh.js → BackgroundAgents-923evRRN.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-Cyvv9RHf.js → Backup-sr_sswlQ.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-BZM14kt8.js → BaseInput-CnW-4XKT.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-DrQSokVz.js → Chat-FZCsTZIL.js} +6 -6
  12. package/src/assets/web-panel/assets/ChatBubbleRenderer-DDPd9qT0.js +1 -0
  13. package/src/assets/web-panel/assets/{Checkbox-bFLYeWMV.js → Checkbox-DFrxB59H.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-7mN7xJXT.js → Codegen-_b6WEel8.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-DAB5ZWm7.js → Col-CkbnnZ-K.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-B_1TymaK.js → Community-BxM_82zW.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-Bq1uSWLJ.js → Compact-DYinP45R.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-CyiE1jEe.js → Compliance-DyXp3sSP.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-Cpk0D7vm.js → Cowork-BEr2DhW7.js} +2 -2
  20. package/src/assets/web-panel/assets/{Cron-d_Lv3rdB.js → Cron-B-Vwo1hN.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-DMqhHjMR.js → Crosschain-DbLxKddd.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-Br-oPuBw.js → DID-BsQ_LFw9.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-CfLW_RaP.js → Dashboard-lVlXQHu5.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-DZnTOExV.js → Dropdown-Beo_foO7.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-DHIUHDv1.js → EmailListRenderer-DszHIXED.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-Bw3t3myu.js → FamilyGuardDashboard-DcAwoh81.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-D9cpQPAY.js → Federation-CuJLa2eb.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-Jdczr9Mr.js → FormItemContext-Cxd1lxhc.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-BT8ZMpC5.js → GenericCardRenderer-BBvq5Jsw.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-DjARip2b.js → Git-WqPKetx6.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-BgWbSRcj.js → Governance-CWYtY3ke.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-CfouJciM.js → Inference-BnX1iiNV.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-50l7mrAr.js → KnowledgeGraph-BJuM8GN5.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-rq50H84B.js → Logs-DqLMNntm.js} +2 -2
  35. package/src/assets/web-panel/assets/{MarkdownRenderer-DUPIEdQH.js → MarkdownRenderer-BqYwEYou.js} +1 -1
  36. package/src/assets/web-panel/assets/{Marketplace-BYgPDmHU.js → Marketplace-_vGfmhNl.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-5DVgcFdx.js → McpTools-BeJDOkDj.js} +3 -3
  38. package/src/assets/web-panel/assets/{Memory-B70LPk7f.js → Memory-Da0EEZDO.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-De8ssA5Y.js → MobileBridge-DSuzcCCy.js} +1 -1
  40. package/src/assets/web-panel/assets/MobileProjects-D0RAk4e8.js +1 -0
  41. package/src/assets/web-panel/assets/{Mtc-RJsOTsIA.js → Mtc-BE9YgdA0.js} +2 -2
  42. package/src/assets/web-panel/assets/{MtcAudit-Crh-6BEK.js → MtcAudit-fC2BpO5S.js} +5 -5
  43. package/src/assets/web-panel/assets/{Multisig-wuoR5l3x.js → Multisig-D24Xxqw3.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-U2Mji4fZ.js → NLProgramming-Bs3oGvS8.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-CdTZAyDH.js → Notes-BFjmAA-3.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-C-FucWE7.js → NotificationSettings-BX_9Bhv0.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-DCYFTeaI.js → OrderTableRenderer-wjkZwJXI.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-an3OF85L.js → Organization-DBcdsVEP.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-C-1vmYFe.js → Overflow-DDyaF9vC.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-Cwh47xKg.js → P2P-BB78L4he.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-Dm38GT0k.js → PdhVaultBrowser-DqTiZFum.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-DS2Y8awD.js → Permissions-INbnWv4j.js} +4 -4
  53. package/src/assets/web-panel/assets/{PersonalDataHub-BQNziA0I.js → PersonalDataHub-BST-VmTq.js} +3 -3
  54. package/src/assets/web-panel/assets/{Pipeline-DF85L1ue.js → Pipeline-CE7_tS2k.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-BuLornF-.js → Privacy-hK5L8dOB.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-C3Y31IwX.js → ProjectInit-BvkkUoDn.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-BCPiI8aD.js → ProjectSettings-u7_EUtkm.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-BZ6s4k6g.js → Projects-5Fmt_8Nw.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-D3Wp022X.js → Providers-B9eanarR.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-CUJfQkH4.js → QrScannerModal-C0dX5N2f.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-UAc0EO8l.js → QuickAsk-CKTpG-PZ.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-BaxKupcs.js → Recommend-Px_w_XQM.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-C75NNnXZ.js → RemoteSession-Di0fnYkM.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-BVXynZAk.js → Reputation-vesnWjXt.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-C5MRZ0bi.js → Row-exbY6LkT.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-DAKkEYka.js → RssFeed-B3O3x70t.js} +3 -3
  67. package/src/assets/web-panel/assets/{Search-DvWv9YOA.js → Search-uRA0DhNz.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-B5peafW1.js → Security-CViqdH6K.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-UQmvPEaG.js → Services-CkXS97WN.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-C3bhWpuF.js → Skeleton-DxW5WYnE.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-DfkKf9_H.js → Skills-CLxmGFse.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-DqMSjlBM.js → Sla-Cr5fziXq.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-C-WbxWUX.js → SpeechSettings-H0JSb3cp.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-CYuHZ4N2.js → SyncSettings-DMYWMae7.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-CyeAyB9d.js → Tasks-CbYyDdF4.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-BTCeQnpq.js → Templates-Cpc8JjiK.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-BifQuA9Y.js → Tenant-CRh77Jda.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-Bx_9Bocf.js → Terminal-4D8tNBO5.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-BX0RFCOm.js → TimelineRenderer-D41jzzbG.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-CGHrCmUS.js → Tokens-C_jWEy6b.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-DlqaY4SO.js → Trigger-Dk1ZUhsy.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-DfOmj-ay.js → Trust-CLVy4l7m.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-B72jH5fE.js → UkeySign-C-FeqFDj.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-BT-63fqv.js → VideoEditing-CqVZzyMm.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-Dsul3sHr.js → Wallet-DX9BW9pu.js} +4 -4
  86. package/src/assets/web-panel/assets/{WebAuthn-Izkwwdgy.js → WebAuthn-7hQDpWw_.js} +2 -2
  87. package/src/assets/web-panel/assets/{WorkflowEditor-D3FHSiH1.js → WorkflowEditor-6XqUAO0j.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-CmVz5z3U.js → chat-5R_vkaJG.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-CuLgyJBp.js → colors-huUPZdC-.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-Ml1YkXpw.js → compact-item-CfJlgisE.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-BvjJ48bs.js → createContext-jO8heofr.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-DeIrCfH-.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-DVC0157C.js → hasIn-DY8Vxqx7.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-DJyN28Et.js → index-A48PtXw9.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-CudmwNBV.js → index-B300RPnT.js} +3 -3
  96. package/src/assets/web-panel/assets/{index-BbepULSc.js → index-B74OXFxC.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-DYXHFr4r.js → index-BAON7TzU.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-BzG-x9p4.js → index-BCqxyvqp.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-wvb_FtaV.js → index-BHE8OIls.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-QXzlT9KD.js → index-BQS-uPOK.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DukDVXCv.js → index-BRXOP09X.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-CFYVQMvu.js → index-BYLBPpt8.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-BydjdZnl.js → index-BfCx4pFT.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CmFNvNLZ.js → index-BliaT02i.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-KMqhcF7l.js → index-BmqGerpC.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-Ck_52dz_.js → index-BsBtRWbA.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-C229ABfy.js → index-BtEXSoPL.js} +1 -1
  108. package/src/assets/web-panel/assets/index-BxUKMRpP.js +1 -0
  109. package/src/assets/web-panel/assets/{index-C_H_S-DY.js → index-Bxax9vQ6.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-B0S8NzO6.js → index-C2iVSFpy.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BjHBpRYZ.js → index-CA6sO5VD.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-Bzr0rB5T.js → index-CA90TZxG.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-DDGaOWBV.js → index-CLhBh-qE.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-Dt5KMjwJ.js → index-CRElRhyS.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-BbPtzB8M.js → index-Cbe9G0V4.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-C_vRgTl9.js → index-ChvHubX3.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BpFJHiTv.js → index-Cis_Ip7H.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-KjgKdZe-.js → index-Cjxbu68P.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-vG4XSJ1d.js → index-CzKmteNS.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-BUt8kTIB.js → index-D0pet5Uy.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-BfuKP4qe.js → index-D1VLuww0.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-4SBta3d7.js → index-DXBcvUnf.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-DC_OI9H2.js → index-Db1cLWir.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-DuGszaWY.js → index-DoI2sZm7.js} +1 -1
  125. package/src/assets/web-panel/assets/index-DscU9iPI.js +1 -0
  126. package/src/assets/web-panel/assets/{index-4wcgvPoF.js → index-KRHroryI.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-D3lD5qap.js → index-d1xkbtNz.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-B_EvG_GW.js → index-dKx57io7.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-CXkJH-lK.js → index-j3O2EEJe.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-BiC6dQZG.js → index-p85YaaKw.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-Cw0Rjy87.js → index-v3pTaEQm.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-C_XuRgGn.js → index-vXs3B8GJ.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-BDgd2YZ8.js → initDefaultProps-MCB7-raZ.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-ErtrXdYe.js → motion-UWDF9swV.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-_GPPNCgs.js → move-CsTmgXZU.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-DRLL-8V2.js → mtc-parser-fQy6KGLg.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-p86sl023.js → omit-woPnEjd1.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-BTu7cMLH.js → pickAttrs-Bs2dZ1gf.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-BEKz6Isr.js → placementArrow-C7EVgPkg.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-Ck42c9MP.js → responsiveObserve-BH2kYCTN.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-1CEvuPsB.js → slide-BFBP2V_e.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-BDfdx1GJ.js → statusUtils-xdDxDYse.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-B4K46m5w.js → styleChecker-aAWgPPQf.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-DoIVpJNq.js → useFlexGapSupport-DkGSAWoO.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-DdAWi-ln.js → useFs-BrOih40N.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-iUu6k8dx.js → usePersonalDataHub-eIYrgMqO.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-DZz4R6Tw.js → vnode-B8lutNSE.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-DXlf9sA3.js → zoom-CffNre7V.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/commands/agenda.js +254 -23
  151. package/src/commands/agent.js +105 -11
  152. package/src/commands/context.js +125 -14
  153. package/src/commands/hook.js +181 -7
  154. package/src/commands/plugin.js +239 -1
  155. package/src/commands/review.js +347 -2
  156. package/src/data/changelog.json +9 -4
  157. package/src/gateways/ws/remote-session-protocol.js +39 -0
  158. package/src/gateways/ws/session-protocol.js +5 -2
  159. package/src/lib/agent-schedule-store.js +289 -10
  160. package/src/lib/channels/channel-manager.js +54 -2
  161. package/src/lib/cli-context-engineering.js +9 -2
  162. package/src/lib/context-breakdown.js +116 -0
  163. package/src/lib/doctor-checkup.js +52 -0
  164. package/src/lib/hook-event-log.cjs +193 -0
  165. package/src/lib/interaction-adapter.js +34 -4
  166. package/src/lib/json-schema-output.js +46 -12
  167. package/src/lib/json-schema-validate.js +143 -3
  168. package/src/lib/plugin-runtime/capability-consent.js +175 -0
  169. package/src/lib/plugin-runtime/policy.js +62 -0
  170. package/src/lib/plugin-runtime/scopes.js +20 -4
  171. package/src/lib/project-instructions.js +128 -0
  172. package/src/lib/repl-goal.js +132 -0
  173. package/src/lib/repl-rewind.js +52 -0
  174. package/src/lib/schedule-planner.js +17 -1
  175. package/src/lib/settings-hook-events.cjs +13 -0
  176. package/src/lib/settings-loader.cjs +42 -0
  177. package/src/lib/side-effect-ledger-store.js +88 -0
  178. package/src/lib/side-effect-ledger.js +296 -0
  179. package/src/lib/sub-agent-context.js +74 -5
  180. package/src/lib/subagent-inheritance.js +112 -0
  181. package/src/lib/turn-binding-store.js +119 -0
  182. package/src/repl/agent-repl.js +136 -0
  183. package/src/repl/permission-tier.js +28 -0
  184. package/src/runtime/agent-core.js +376 -10
  185. package/src/runtime/coding-agent-contract-shared.cjs +60 -1
  186. package/src/runtime/headless-runner.js +267 -0
  187. package/src/runtime/headless-stream.js +60 -1
  188. package/src/assets/web-panel/assets/ChatBubbleRenderer-HwJR-C6B.js +0 -1
  189. package/src/assets/web-panel/assets/MobileProjects-D10VISps.js +0 -1
  190. package/src/assets/web-panel/assets/devWarning-DhiGrbsh.js +0 -1
  191. package/src/assets/web-panel/assets/index-B7a1TrU6.js +0 -1
  192. 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 && isGitRepo(this._repoDir)) {
181
- return this._runInWorktree(userPrompt, loopOptions);
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
+ }