chainlesschain 0.162.35 → 0.162.37

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 (193) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-CJn02U42.js → AIOps-_oxz4VHy.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-ewURAAoy.js → ActionButton-uaeqFuDj.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-BiSadESb.js → Analytics-BPVV0OUf.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-BR0WOEug.js → AppLayout-ppCYKm3I.js} +4 -4
  6. package/src/assets/web-panel/assets/{Audit-CrqcYx0e.js → Audit-DFAY6umk.js} +1 -1
  7. package/src/assets/web-panel/assets/{Backup-DtbSBn4e.js → Backup-pAPBFDyP.js} +1 -1
  8. package/src/assets/web-panel/assets/{BaseInput-BjSc9j0o.js → BaseInput-BbBl0uT2.js} +1 -1
  9. package/src/assets/web-panel/assets/{Chat-ixzrlCJE.js → Chat-Ct22JUnT.js} +6 -6
  10. package/src/assets/web-panel/assets/{ChatBubbleRenderer-B78nEq05.js → ChatBubbleRenderer-DPlsLl22.js} +1 -1
  11. package/src/assets/web-panel/assets/{Checkbox-UGYeSsgr.js → Checkbox-DEkCollc.js} +1 -1
  12. package/src/assets/web-panel/assets/{Codegen-B97OOAg4.js → Codegen-Tor-de39.js} +1 -1
  13. package/src/assets/web-panel/assets/{Col-D9aGkaZ6.js → Col-ojNrLQU7.js} +1 -1
  14. package/src/assets/web-panel/assets/{Community-Dc2v2RGS.js → Community-CLOGhqMF.js} +1 -1
  15. package/src/assets/web-panel/assets/{Compact-B_FYlUQR.js → Compact-CYKNlSZ4.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compliance-C4FiTHyC.js → Compliance-C5E6ABuA.js} +1 -1
  17. package/src/assets/web-panel/assets/{Cowork-CQ8j3LIg.js → Cowork-CHeEsZ3W.js} +2 -2
  18. package/src/assets/web-panel/assets/{Cron-Dzjs9Z9Z.js → Cron-B4e1n2e7.js} +2 -2
  19. package/src/assets/web-panel/assets/{Crosschain-BXI24uzI.js → Crosschain-DbNV8P9R.js} +1 -1
  20. package/src/assets/web-panel/assets/{DID-C-I4_d07.js → DID-C5_Tk3nC.js} +2 -2
  21. package/src/assets/web-panel/assets/{Dashboard-BzzGh5mo.js → Dashboard-BhdV_c4N.js} +2 -2
  22. package/src/assets/web-panel/assets/{Dropdown-Bh8H70De.js → Dropdown-CEi5AMtM.js} +1 -1
  23. package/src/assets/web-panel/assets/{EmailListRenderer-DI_qybJP.js → EmailListRenderer-DOhPiYng.js} +1 -1
  24. package/src/assets/web-panel/assets/{FamilyGuardDashboard-DkKTsfc4.js → FamilyGuardDashboard-fu4NRP3X.js} +1 -1
  25. package/src/assets/web-panel/assets/{Federation-DS7CmvVG.js → Federation-B7BtIWKL.js} +1 -1
  26. package/src/assets/web-panel/assets/{FormItemContext-CI97WsB5.js → FormItemContext-BmPWZVLP.js} +1 -1
  27. package/src/assets/web-panel/assets/GenericCardRenderer-hsOPNJq8.js +1 -0
  28. package/src/assets/web-panel/assets/{Git-CEh0gR2W.js → Git-Bi_EFBUH.js} +2 -2
  29. package/src/assets/web-panel/assets/{Governance-kIr3tls2.js → Governance-emf2ubDK.js} +1 -1
  30. package/src/assets/web-panel/assets/{Inference-CC1GzyC1.js → Inference-B7KjKzkI.js} +1 -1
  31. package/src/assets/web-panel/assets/{KnowledgeGraph-BNgTiWOB.js → KnowledgeGraph-uAaBK0F3.js} +1 -1
  32. package/src/assets/web-panel/assets/{Logs-B2P10gB1.js → Logs-utK7hNpj.js} +2 -2
  33. package/src/assets/web-panel/assets/{Marketplace-HPfBvbFZ.js → Marketplace-CzQe6n3z.js} +1 -1
  34. package/src/assets/web-panel/assets/{McpTools-ByYotSKb.js → McpTools-CuAaJr51.js} +5 -5
  35. package/src/assets/web-panel/assets/{Memory-BGIAzFVS.js → Memory-CRuZZJ75.js} +2 -2
  36. package/src/assets/web-panel/assets/{MobileBridge-CroNYTAH.js → MobileBridge-Cp06wunh.js} +2 -2
  37. package/src/assets/web-panel/assets/MobileProjects-DJEdUwhr.js +1 -0
  38. package/src/assets/web-panel/assets/{Mtc-BqhyIwo9.js → Mtc-8YY4dR7g.js} +2 -2
  39. package/src/assets/web-panel/assets/{MtcAudit-BpEKOvx9.js → MtcAudit-BmPJYHar.js} +2 -2
  40. package/src/assets/web-panel/assets/{Multisig-DST1d_Qo.js → Multisig-d-ydyVdq.js} +3 -3
  41. package/src/assets/web-panel/assets/{NLProgramming-DlMsZcK_.js → NLProgramming-DA_ikw_n.js} +1 -1
  42. package/src/assets/web-panel/assets/{Notes-C734UJvD.js → Notes-DIyF-fRe.js} +3 -3
  43. package/src/assets/web-panel/assets/{NotificationSettings-C0-pPxvk.js → NotificationSettings-CzPZXEtK.js} +1 -1
  44. package/src/assets/web-panel/assets/OrderTableRenderer-BiLtg-LY.js +1 -0
  45. package/src/assets/web-panel/assets/{Organization-C5iHC_yW.js → Organization-DdDZ_Ap6.js} +4 -4
  46. package/src/assets/web-panel/assets/{Overflow-CovuHHVR.js → Overflow-BnMBkttv.js} +1 -1
  47. package/src/assets/web-panel/assets/{P2P-Dx9QL-Gy.js → P2P-Es1050f-.js} +2 -2
  48. package/src/assets/web-panel/assets/{PdhVaultBrowser-IP1dEt6-.js → PdhVaultBrowser-CKkRmyn9.js} +4 -4
  49. package/src/assets/web-panel/assets/{Permissions-BrR1XZG5.js → Permissions-zU9n9cAD.js} +4 -4
  50. package/src/assets/web-panel/assets/{PersonalDataHub-BgqxVE5m.js → PersonalDataHub-BZi5Xwas.js} +2 -2
  51. package/src/assets/web-panel/assets/{Pipeline-DzMk5HAz.js → Pipeline-CRfeGiFc.js} +1 -1
  52. package/src/assets/web-panel/assets/{Privacy-CDoLa6tk.js → Privacy-CQA_IgLA.js} +1 -1
  53. package/src/assets/web-panel/assets/{ProjectInit-Dy5gc6ve.js → ProjectInit-C9hmEvoT.js} +2 -2
  54. package/src/assets/web-panel/assets/{ProjectSettings-DXy-k4hG.js → ProjectSettings-yXA72ws4.js} +2 -2
  55. package/src/assets/web-panel/assets/Projects-BpWS-qam.js +1 -0
  56. package/src/assets/web-panel/assets/Providers-Cxe55dRD.js +1 -0
  57. package/src/assets/web-panel/assets/{QuickAsk-B8KEHCnd.js → QuickAsk-Do0aUTQr.js} +1 -1
  58. package/src/assets/web-panel/assets/{Recommend-DNVHGYYZ.js → Recommend--ysZHjyA.js} +1 -1
  59. package/src/assets/web-panel/assets/{Reputation-CaDhWP03.js → Reputation-BOBU8JrH.js} +1 -1
  60. package/src/assets/web-panel/assets/{Row-CrGLI02x.js → Row-C6X7bRKE.js} +1 -1
  61. package/src/assets/web-panel/assets/{RssFeed-BX7P8I6i.js → RssFeed-D8AwqlkQ.js} +3 -3
  62. package/src/assets/web-panel/assets/Search-Bi3rCZD4.js +1 -0
  63. package/src/assets/web-panel/assets/{Security-B6J7IFc1.js → Security-DxUDVrtY.js} +4 -4
  64. package/src/assets/web-panel/assets/{Services-vvdcO3mM.js → Services-BXXN7yC1.js} +2 -2
  65. package/src/assets/web-panel/assets/{Skeleton-BoAoPTzZ.js → Skeleton-B3BR34tZ.js} +1 -1
  66. package/src/assets/web-panel/assets/{Skills-CyIQV5b3.js → Skills-BjYu8OQ1.js} +1 -1
  67. package/src/assets/web-panel/assets/{Sla-BAQVgdZV.js → Sla-DDkCtD8w.js} +1 -1
  68. package/src/assets/web-panel/assets/{SpeechSettings-Bxcn1Jkj.js → SpeechSettings-CGhYzP7V.js} +1 -1
  69. package/src/assets/web-panel/assets/{SyncSettings-Dpaj3hDM.js → SyncSettings-CYNKVAHA.js} +2 -2
  70. package/src/assets/web-panel/assets/{Tasks-Bwqo89En.js → Tasks-DavmlJpd.js} +1 -1
  71. package/src/assets/web-panel/assets/{Templates-Bowcqifn.js → Templates-CQuYFf2C.js} +1 -1
  72. package/src/assets/web-panel/assets/{Tenant-DOkf85uG.js → Tenant-DdzZh8vE.js} +1 -1
  73. package/src/assets/web-panel/assets/Terminal-D75WeG9d.js +3 -0
  74. package/src/assets/web-panel/assets/{TimelineRenderer-B9A3zDXA.js → TimelineRenderer-DKOARnc_.js} +1 -1
  75. package/src/assets/web-panel/assets/{Tokens-jtVVqKFr.js → Tokens-D7QRNG8y.js} +1 -1
  76. package/src/assets/web-panel/assets/{Trigger-26Iw-iIl.js → Trigger-BCsqLZl4.js} +1 -1
  77. package/src/assets/web-panel/assets/{Trust-DqY5ORrH.js → Trust-BarGUa6p.js} +1 -1
  78. package/src/assets/web-panel/assets/{UkeySign-BFsbr3y7.js → UkeySign-pHrg5a8E.js} +1 -1
  79. package/src/assets/web-panel/assets/{VideoEditing-BtDbj3oa.js → VideoEditing-Dug3m1py.js} +1 -1
  80. package/src/assets/web-panel/assets/{Wallet-BAwmwHbk.js → Wallet-BfK3Z_Ez.js} +4 -4
  81. package/src/assets/web-panel/assets/{WebAuthn-DINJTsfq.js → WebAuthn-CYRdl9td.js} +5 -5
  82. package/src/assets/web-panel/assets/{WorkflowEditor-BEorm8SK.js → WorkflowEditor-DTW5AcqM.js} +1 -1
  83. package/src/assets/web-panel/assets/{chat-CE39-Dxg.js → chat-CCXz4j38.js} +1 -1
  84. package/src/assets/web-panel/assets/{colors-C_cLZ93a.js → colors-BJBOhAqa.js} +1 -1
  85. package/src/assets/web-panel/assets/{compact-item-BSioWA2c.js → compact-item-E9M6BQcM.js} +1 -1
  86. package/src/assets/web-panel/assets/{createContext-CGTk4mhN.js → createContext-Cg9CAws4.js} +1 -1
  87. package/src/assets/web-panel/assets/devWarning-BrsbTJUv.js +1 -0
  88. package/src/assets/web-panel/assets/{hasIn-Dl1fRwS_.js → hasIn-DhVtqv5L.js} +1 -1
  89. package/src/assets/web-panel/assets/{index-pngH1and.js → index--7o5YdL6.js} +1 -1
  90. package/src/assets/web-panel/assets/{index-BmbVyhk1.js → index-4N5lNXGP.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-BnEPB1Mz.js → index-6-04M2Nx.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-Cxw3p73X.js → index-B111fZ21.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-CST381Qf.js → index-B4NBF4Sa.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-ChwpS1f0.js → index-B8bjEHrQ.js} +1 -1
  95. package/src/assets/web-panel/assets/{index--SWvw6yW.js → index-BAB0nGP7.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-CAwVwBOL.js → index-BFZPRd0T.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-hv4jUdG3.js → index-B_SMPD4L.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-Qj2x55mz.js → index-BxSzyly9.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-BWpfxzVm.js → index-ByazO4Q9.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-Di6nvW1N.js → index-C-2dUIli.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-BhqOTuMW.js → index-CFarAlXj.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-CA6K7lZB.js → index-CFp-wdrQ.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DTKEXyaW.js → index-CJ8nNT8h.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-iiZfONfx.js → index-CSiyjCYi.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DTpCUi0m.js → index-CUp_c8Le.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-Bvi14vJ7.js → index-CVR_s-pT.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DKEipmR8.js → index-Ca8BYV1g.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-DJyeeygd.js → index-CeRlLp3F.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-C9tq8Da8.js → index-ChsSljaN.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-B2QiUEgK.js → index-CkTeBHI9.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-OCxo0X6J.js → index-Cm1m7BJh.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-DrWERr8C.js → index-ComyTKz-.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-B016Fsqr.js → index-CznfPnOx.js} +3 -3
  114. package/src/assets/web-panel/assets/{index-CisXVbSt.js → index-D5yC2Ps8.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-C-VVk1Jg.js → index-D7DXdf7x.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DDQx2YFc.js → index-DDcJO27F.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-Ds2RzRG0.js → index-DSQazU6J.js} +1 -1
  118. package/src/assets/web-panel/assets/index-DSTQDO-Y.js +1 -0
  119. package/src/assets/web-panel/assets/{index-C4JXchTG.js → index-DaFe1aqY.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-BAhinBPR.js → index-DdhnGez0.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-9_mmaR42.js → index-Di5LBXcE.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-D9D4q-qI.js → index-Dwvewrul.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CbXnyoSO.js → index-MdXEhfdJ.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-II3JhQu2.js → index-_PNqQ5mE.js} +1 -1
  125. package/src/assets/web-panel/assets/index-c2U6LV3Q.js +1 -0
  126. package/src/assets/web-panel/assets/{index-C2ly7sCw.js → index-kz1oXl1a.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-Ceo9P9tQ.js → index-wkt-o5q5.js} +1 -1
  128. package/src/assets/web-panel/assets/{initDefaultProps-GOhLA2-f.js → initDefaultProps-iyBaePF-.js} +1 -1
  129. package/src/assets/web-panel/assets/{motion-jqxFzHTx.js → motion-RWtj4rgu.js} +1 -1
  130. package/src/assets/web-panel/assets/{move-CSLsp6TA.js → move-CqPRVzpH.js} +1 -1
  131. package/src/assets/web-panel/assets/{omit-Cnlrb25c.js → omit-DsvJze25.js} +1 -1
  132. package/src/assets/web-panel/assets/{pickAttrs-CLqlxWWD.js → pickAttrs-B4tfZBhc.js} +1 -1
  133. package/src/assets/web-panel/assets/{placementArrow-BAWIWtul.js → placementArrow-KvHUwXMA.js} +1 -1
  134. package/src/assets/web-panel/assets/{responsiveObserve-CSR1DayS.js → responsiveObserve-DGdJ-b7W.js} +1 -1
  135. package/src/assets/web-panel/assets/{slide-CNhoPJOp.js → slide-Cd6ebRmw.js} +1 -1
  136. package/src/assets/web-panel/assets/{statusUtils-BZiYHRHW.js → statusUtils-Bg9GcIAn.js} +1 -1
  137. package/src/assets/web-panel/assets/{styleChecker-BMoY-Fm5.js → styleChecker-MQjKsG84.js} +1 -1
  138. package/src/assets/web-panel/assets/{useFlexGapSupport-DhtNdlaS.js → useFlexGapSupport-C241WujP.js} +1 -1
  139. package/src/assets/web-panel/assets/{useFs-DNPtDOZ4.js → useFs-CMpy7RS4.js} +1 -1
  140. package/src/assets/web-panel/assets/{usePersonalDataHub-DTdjNvAI.js → usePersonalDataHub-BLHtapKb.js} +1 -1
  141. package/src/assets/web-panel/assets/{vnode-C9zW9IJ2.js → vnode-DmcTV67c.js} +1 -1
  142. package/src/assets/web-panel/assets/{zoom-D-6RYJJr.js → zoom-DHL8_0Y8.js} +1 -1
  143. package/src/assets/web-panel/index.html +1 -1
  144. package/src/commands/agent.js +161 -6
  145. package/src/commands/agents.js +8 -2
  146. package/src/commands/cli-anything.js +14 -6
  147. package/src/commands/command.js +7 -2
  148. package/src/commands/hook.js +136 -28
  149. package/src/commands/ide.js +168 -0
  150. package/src/commands/loop.js +450 -0
  151. package/src/commands/mcp.js +92 -0
  152. package/src/commands/output-style.js +127 -0
  153. package/src/commands/permissions.js +211 -0
  154. package/src/commands/statusline.js +93 -0
  155. package/src/index.js +10 -2
  156. package/src/lib/agent-core.js +7 -0
  157. package/src/lib/agents.js +5 -0
  158. package/src/lib/hook-manager.js +1 -0
  159. package/src/lib/hook-runner.cjs +183 -0
  160. package/src/lib/ide-bridge.js +310 -0
  161. package/src/lib/image-input.js +156 -0
  162. package/src/lib/loop.js +198 -0
  163. package/src/lib/mcp-oauth.js +415 -0
  164. package/src/lib/output-styles.js +179 -0
  165. package/src/lib/permission-rules.cjs +325 -0
  166. package/src/lib/provider-options.js +11 -7
  167. package/src/lib/settings-hook-events.cjs +102 -0
  168. package/src/lib/settings-hooks.cjs +163 -0
  169. package/src/lib/settings-loader.cjs +244 -0
  170. package/src/lib/slash-commands.js +4 -0
  171. package/src/lib/status-line.cjs +204 -0
  172. package/src/lib/sub-agent-profiles.js +3 -0
  173. package/src/lib/web-search.js +487 -0
  174. package/src/repl/agent-repl.js +450 -35
  175. package/src/repl/slash-macro.js +45 -0
  176. package/src/runtime/agent-core.js +799 -21
  177. package/src/runtime/coding-agent-contract-shared.cjs +94 -4
  178. package/src/runtime/coding-agent-policy.cjs +24 -0
  179. package/src/runtime/headless-runner.js +162 -6
  180. package/src/runtime/headless-stream.js +133 -7
  181. package/src/runtime/mcp-config.js +161 -15
  182. package/src/runtime/policies/agent-policy.js +4 -0
  183. package/src/runtime/system-prompt.js +6 -1
  184. package/src/assets/web-panel/assets/GenericCardRenderer-Da27EdR4.js +0 -1
  185. package/src/assets/web-panel/assets/MobileProjects-CH-qnGEV.js +0 -1
  186. package/src/assets/web-panel/assets/OrderTableRenderer-C7zT9eFc.js +0 -1
  187. package/src/assets/web-panel/assets/Projects-DvsaEbZR.js +0 -1
  188. package/src/assets/web-panel/assets/Providers-Demck9PO.js +0 -1
  189. package/src/assets/web-panel/assets/Search-laS6rz8M.js +0 -1
  190. package/src/assets/web-panel/assets/Terminal-v4MM9dCj.js +0 -3
  191. package/src/assets/web-panel/assets/devWarning-PObcVnJR.js +0 -1
  192. package/src/assets/web-panel/assets/index-BNwIzLyX.js +0 -1
  193. package/src/assets/web-panel/assets/index-Dh6FxR9B.js +0 -1
@@ -16,10 +16,15 @@
16
16
 
17
17
  import fs from "fs";
18
18
  import path from "path";
19
- import { execSync } from "child_process";
19
+ import { execSync, spawn } from "child_process";
20
20
  import os from "os";
21
21
  import sharedCodingAgentPolicy from "./coding-agent-policy.cjs";
22
22
  import sharedShellPolicy from "./coding-agent-shell-policy.cjs";
23
+ import sharedPermissionRules from "../lib/permission-rules.cjs";
24
+ import sharedSettingsHooks from "../lib/settings-hooks.cjs";
25
+ import sharedHookRunner from "../lib/hook-runner.cjs";
26
+ import sharedHookEvents from "../lib/settings-hook-events.cjs";
27
+ import { mergeProviderOptions } from "../lib/provider-options.js";
23
28
  import { getPlanModeManager } from "../lib/plan-mode.js";
24
29
  import { CLISkillLoader } from "../lib/skill-loader.js";
25
30
  import { executeHooks, HookEvents } from "../lib/hook-manager.js";
@@ -47,6 +52,11 @@ import {
47
52
  mountSkillMcpServers,
48
53
  unmountSkillMcpServers,
49
54
  } from "../lib/skill-mcp.js";
55
+ import {
56
+ hasImageContent,
57
+ toOllamaMessages,
58
+ imageUrlBlockToAnthropic,
59
+ } from "../lib/image-input.js";
50
60
 
51
61
  /**
52
62
  * Names of MCP servers currently mounted by an in-flight run_skill call.
@@ -62,6 +72,182 @@ export function getActiveMcpServers() {
62
72
 
63
73
  const { isReadOnlyGitCommand, normalizeGitCommand } = sharedCodingAgentPolicy;
64
74
  const { evaluateShellCommandPolicy } = sharedShellPolicy;
75
+ const { evaluatePermissionRules } = sharedPermissionRules;
76
+ const { collectHooks, umbrellaFor } = sharedSettingsHooks;
77
+ const { runHooks: runCommandHooks } = sharedHookRunner;
78
+ const { runObserveHooks } = sharedHookEvents;
79
+
80
+ // ─── Background shell tasks ────────────────────────────────────────────────
81
+ //
82
+ // run_shell is synchronous (execSync) and capped at a foreground timeout, which
83
+ // is the right default for quick commands but blocks the whole agent loop on
84
+ // long-running ones (builds, full test suites, `npm run dev`). When the model
85
+ // passes run_in_background:true the command is spawned instead, returns a
86
+ // task_id immediately, and streams its output into this registry. The agent
87
+ // then polls completion + incremental output via the check_shell tool — the
88
+ // run_in_background + BashOutput pattern from Claude Code.
89
+ //
90
+ // In-memory, process-lifetime: a task_id is only valid within the agent process
91
+ // that spawned it, which is exactly the polling window (one REPL session / one
92
+ // headless run). Buffers are bounded (MAX_BG_BUFFER per stream, tail-retained)
93
+ // so a chatty long task can't exhaust memory.
94
+ const MAX_BG_BUFFER = 1024 * 1024; // 1 MB retained tail per stream
95
+ const _backgroundShellTasks = new Map();
96
+ let _backgroundTaskSeq = 0;
97
+
98
+ function _newBgStream() {
99
+ return { buf: "", total: 0, dropped: 0, cursor: 0 };
100
+ }
101
+
102
+ function _appendBgStream(stream, text) {
103
+ stream.buf += text;
104
+ stream.total += text.length;
105
+ if (stream.buf.length > MAX_BG_BUFFER) {
106
+ const over = stream.buf.length - MAX_BG_BUFFER;
107
+ stream.buf = stream.buf.slice(over);
108
+ stream.dropped += over;
109
+ }
110
+ }
111
+
112
+ // Read everything produced since the last read and advance the cursor. When the
113
+ // cursor points into a region already dropped from the retained tail, the gap
114
+ // is reported so the caller knows output was lost to the buffer cap.
115
+ function _readBgStream(stream) {
116
+ const bufStart = stream.total - stream.buf.length;
117
+ let from = stream.cursor;
118
+ let droppedGap = 0;
119
+ if (from < bufStart) {
120
+ droppedGap = bufStart - from;
121
+ from = bufStart;
122
+ }
123
+ const text = stream.buf.slice(from - bufStart);
124
+ stream.cursor = stream.total;
125
+ return { text, droppedGap };
126
+ }
127
+
128
+ /**
129
+ * Snapshot of background shell tasks (for REPL/host status surfaces).
130
+ * @returns {Array<{id:string,status:string,command:string,exitCode:number|null}>}
131
+ */
132
+ export function listBackgroundShellTasks() {
133
+ return Array.from(_backgroundShellTasks.values()).map((t) => ({
134
+ id: t.id,
135
+ status: t.status,
136
+ command: t.command,
137
+ exitCode: t.exitCode,
138
+ startedAt: t.startedAt,
139
+ endedAt: t.endedAt,
140
+ }));
141
+ }
142
+
143
+ // Kill a background task's whole process tree. Because tasks are spawned with
144
+ // shell:true, the child is a shell whose real command runs as a grandchild — a
145
+ // plain child.kill() on POSIX only signals the shell (and often orphans the
146
+ // command), so a backgrounded `npm run dev` would survive. POSIX: the task is
147
+ // spawned detached (its own process group), so signal the group via the
148
+ // negative pid. Windows: `taskkill /T` walks and kills the whole tree.
149
+ // Returns true if a running task was signalled.
150
+ function _killTask(task) {
151
+ const child = task?.child;
152
+ if (!child || child.killed || task?.status !== "running") return false;
153
+ try {
154
+ if (process.platform === "win32") {
155
+ if (child.pid) {
156
+ spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
157
+ windowsHide: true,
158
+ });
159
+ } else {
160
+ child.kill();
161
+ }
162
+ } else if (child.pid) {
163
+ // Negative pid → signal the whole process group (requires detached spawn).
164
+ try {
165
+ process.kill(-child.pid, "SIGTERM");
166
+ } catch (_err) {
167
+ child.kill("SIGTERM");
168
+ }
169
+ } else {
170
+ child.kill("SIGTERM");
171
+ }
172
+ return true;
173
+ } catch (_err) {
174
+ return false;
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Kill every still-running background shell task. Callers (REPL exit, headless
180
+ * shutdown) invoke this so a backgrounded `npm run dev` doesn't outlive the
181
+ * agent. Best-effort: kill failures are swallowed.
182
+ * @returns {number} count of tasks signalled
183
+ */
184
+ export function killAllBackgroundShellTasks() {
185
+ let killed = 0;
186
+ for (const task of _backgroundShellTasks.values()) {
187
+ if (_killTask(task)) {
188
+ killed += 1;
189
+ }
190
+ }
191
+ return killed;
192
+ }
193
+
194
+ // Foreground (synchronous) run_shell timeout. Configurable per-call via the
195
+ // optional `timeout` arg; defaults to 60s and is hard-capped at 10 min so a
196
+ // synchronous call can never wedge the loop indefinitely (use run_in_background
197
+ // for genuinely long work).
198
+ const DEFAULT_SHELL_TIMEOUT_MS = 60000;
199
+ const MAX_SHELL_TIMEOUT_MS = 600000;
200
+ function _resolveShellTimeout(raw) {
201
+ if (raw == null) return DEFAULT_SHELL_TIMEOUT_MS;
202
+ const n = Number(raw);
203
+ if (!Number.isFinite(n) || n <= 0) return DEFAULT_SHELL_TIMEOUT_MS;
204
+ return Math.min(Math.floor(n), MAX_SHELL_TIMEOUT_MS);
205
+ }
206
+
207
+ /**
208
+ * Run settings.json `PreToolUse` hooks (decision-capable). DB hooks are handled
209
+ * separately + stay observe-only. A `block` decision stops the tool; an `ask`
210
+ * routes to the confirmer (headless without one falls closed). spawnSync is
211
+ * synchronous but each hook is timeout-capped.
212
+ * @returns {Promise<{blocked:boolean, reason?:string, hook?:string}>}
213
+ */
214
+ async function runSettingsPreToolUseHooks(name, args, context, cwd) {
215
+ const matched = collectHooks(context.settingsHooks, "PreToolUse", name);
216
+ if (!matched || matched.length === 0) return { blocked: false };
217
+ const payload = {
218
+ hook_event_name: "PreToolUse",
219
+ tool_name: umbrellaFor(name),
220
+ raw_tool_name: name,
221
+ tool_input: args,
222
+ cwd,
223
+ session_id: context.sessionId || null,
224
+ };
225
+ const outcome = runCommandHooks(matched, payload, { cwd, event: "PreToolUse" });
226
+ if (outcome.decision === "block") {
227
+ return { blocked: true, reason: outcome.reason, hook: outcome.hook };
228
+ }
229
+ if (outcome.decision === "ask") {
230
+ const confirm = context.permissionConfirm || context.shellConfirm || null;
231
+ const ok =
232
+ typeof confirm === "function"
233
+ ? await confirm({
234
+ tool: name,
235
+ args,
236
+ rule: `hook:${outcome.hook}`,
237
+ reason:
238
+ outcome.reason || "a PreToolUse hook requests confirmation",
239
+ })
240
+ : false;
241
+ return ok
242
+ ? { blocked: false }
243
+ : {
244
+ blocked: true,
245
+ reason: outcome.reason || "PreToolUse hook ask denied",
246
+ hook: outcome.hook,
247
+ };
248
+ }
249
+ return { blocked: false };
250
+ }
65
251
 
66
252
  // ─── Tool definitions ────────────────────────────────────────────────────
67
253
 
@@ -220,6 +406,7 @@ Key behaviors:
220
406
  - When asked to modify code, read the file first, then edit it
221
407
  - When asked to create something, use write_file to create it
222
408
  - When asked to run/test something, use run_shell to execute it
409
+ - For long-running commands (builds, full test suites, dev servers) set run_shell { run_in_background: true } to get a task_id back immediately, then poll output and completion with check_shell { task_id }. Kill a backgrounded server with check_shell { task_id, kill: true } when finished
223
410
  - When asked about git status, diff, log, or other repository operations, use the git tool instead of run_shell
224
411
  - When asked about files or code, use read_file and search_files to find information
225
412
  - You have multi-layer skills (built-in, marketplace, global, project-level) — use list_skills to discover them and run_skill to execute them
@@ -451,6 +638,44 @@ export async function executeTool(name, args, context = {}) {
451
638
  };
452
639
  }
453
640
 
641
+ // ── Permission resolution (most-restrictive-wins; denies before prompts) ──
642
+ // Two policy sources gate a tool call: the user's .claude/settings.json rules
643
+ // (deny/ask/allow) and the desktop host's synced policy (hostManagedToolPolicy,
644
+ // usually null in CLI). Precedence, evaluated in this exact order:
645
+ // 1. settings `deny` → block.
646
+ // 2. host `deny` → block. A settings `allow` NEVER relaxes a host deny
647
+ // (the desktop runtime authority outranks project
648
+ // config); symmetrically a settings `deny` (step 1)
649
+ // outranks a host `allow`. Net effect: any deny wins.
650
+ // 3. settings `ask` → confirm (headless w/o confirmer falls closed).
651
+ // Reached only after BOTH denies clear, so a denied
652
+ // tool never wastes a confirmation round-trip.
653
+ // 4. settings `allow` → pre-authorize (ruleAllowed): short-circuit the
654
+ // plan-mode block + run_shell ApprovalGate. The hard
655
+ // shell-policy denylist still applies — allow never
656
+ // re-enables an unsafe `rm -rf /`.
657
+ // No matching rule + no host policy → every existing layer runs unchanged
658
+ // (default behaviour is byte-for-byte).
659
+ const settingsVerdict = context.permissionRules
660
+ ? evaluatePermissionRules({
661
+ tool: name,
662
+ args,
663
+ cwd,
664
+ rules: context.permissionRules,
665
+ })
666
+ : { decision: null, rule: null };
667
+
668
+ // 1. settings deny
669
+ if (settingsVerdict.decision === "deny") {
670
+ return {
671
+ error: `[Permission] Tool "${name}" denied by settings rule: ${settingsVerdict.rule}`,
672
+ policy: { decision: "deny", rule: settingsVerdict.rule, via: "settings" },
673
+ };
674
+ }
675
+
676
+ // Resolve the host policy (needed for the host-deny check + the plan-mode
677
+ // block below). Computed once here so a host deny can short-circuit before
678
+ // any settings `ask` prompt.
454
679
  const toolPolicies =
455
680
  context.hostManagedToolPolicy?.tools ||
456
681
  context.hostManagedToolPolicy?.toolPolicies ||
@@ -471,6 +696,8 @@ export async function executeTool(name, args, context = {}) {
471
696
  isExternalLocalTool &&
472
697
  planManager.isActive() &&
473
698
  localToolDescriptor?.isReadOnly === true;
699
+
700
+ // 2. host deny (a settings `allow` does not relax this)
474
701
  if (
475
702
  hostToolPolicy &&
476
703
  hostToolPolicy.allowed === false &&
@@ -487,9 +714,34 @@ export async function executeTool(name, args, context = {}) {
487
714
  };
488
715
  }
489
716
 
490
- // Plan mode: check if tool is allowed
717
+ // 3 + 4. settings ask / allow (only reached when neither layer denied)
718
+ let ruleAllowed = false;
719
+ if (settingsVerdict.decision === "ask") {
720
+ const confirm = context.permissionConfirm || context.shellConfirm || null;
721
+ const ok =
722
+ typeof confirm === "function"
723
+ ? await confirm({
724
+ tool: name,
725
+ args,
726
+ rule: settingsVerdict.rule,
727
+ reason: `settings rule ${settingsVerdict.rule} requires confirmation`,
728
+ })
729
+ : false;
730
+ if (!ok) {
731
+ return {
732
+ error: `[Permission] Tool "${name}" requires confirmation (settings rule: ${settingsVerdict.rule}) — denied.`,
733
+ policy: { decision: "ask", rule: settingsVerdict.rule, via: "settings" },
734
+ };
735
+ }
736
+ ruleAllowed = true; // confirmed → treat like allow downstream
737
+ } else if (settingsVerdict.decision === "allow") {
738
+ ruleAllowed = true;
739
+ }
740
+
741
+ // Plan mode: check if tool is allowed (a settings `allow` rule pre-authorizes)
491
742
  if (
492
743
  planManager.isActive() &&
744
+ !ruleAllowed &&
493
745
  !(name === "git" && isReadOnlyGitCommand(args.command)) &&
494
746
  !planManager.isToolAllowed(name) &&
495
747
  !(isExternalHostTool && hostToolPolicy?.allowed === true) &&
@@ -514,7 +766,11 @@ export async function executeTool(name, args, context = {}) {
514
766
  };
515
767
  }
516
768
 
517
- // PreToolUse hook
769
+ // PreToolUse hooks. DB hooks (cc hook add) stay observe-only — a failure
770
+ // never blocks. settings.json hooks (context.settingsHooks) are decision-
771
+ // capable: a `block` (exit 2 / {decision:block}) stops the tool here, an
772
+ // `ask` routes to the confirmer. Runs after permission resolution so a
773
+ // settings deny / host deny short-circuits before any hook process spawns.
518
774
  if (hookDb) {
519
775
  try {
520
776
  await executeHooks(hookDb, HookEvents.PreToolUse, {
@@ -528,6 +784,15 @@ export async function executeTool(name, args, context = {}) {
528
784
  // Hook failure should not block tool execution
529
785
  }
530
786
  }
787
+ if (context.settingsHooks) {
788
+ const pre = await runSettingsPreToolUseHooks(name, args, context, cwd);
789
+ if (pre.blocked) {
790
+ return {
791
+ error: `[Hook] PreToolUse blocked "${name}"${pre.reason ? ": " + pre.reason : ""}`,
792
+ policy: { decision: "block", via: "hook", hook: pre.hook || null },
793
+ };
794
+ }
795
+ }
531
796
 
532
797
  const startTime = Date.now();
533
798
  let toolResult;
@@ -546,6 +811,7 @@ export async function executeTool(name, args, context = {}) {
546
811
  approvalGate: context.approvalGate || null,
547
812
  shellConfirm: context.shellConfirm || null,
548
813
  additionalDirectories: context.additionalDirectories || null,
814
+ ruleAllowed,
549
815
  });
550
816
  } catch (err) {
551
817
  if (hookDb) {
@@ -592,6 +858,36 @@ export async function executeTool(name, args, context = {}) {
592
858
  // Non-critical
593
859
  }
594
860
  }
861
+ // settings.json PostToolUse hooks: can't un-run the tool, but a `block`
862
+ // reason is attached as `hookFeedback` to be surfaced back to the model.
863
+ if (context.settingsHooks && toolResult && typeof toolResult === "object") {
864
+ try {
865
+ const matched = collectHooks(context.settingsHooks, "PostToolUse", name);
866
+ if (matched && matched.length > 0) {
867
+ const outcome = runCommandHooks(
868
+ matched,
869
+ {
870
+ hook_event_name: "PostToolUse",
871
+ tool_name: umbrellaFor(name),
872
+ raw_tool_name: name,
873
+ tool_input: args,
874
+ tool_response:
875
+ typeof toolResult === "object"
876
+ ? JSON.stringify(toolResult).substring(0, 2000)
877
+ : String(toolResult).substring(0, 2000),
878
+ cwd,
879
+ session_id: context.sessionId || null,
880
+ },
881
+ { cwd, event: "PostToolUse" },
882
+ );
883
+ if (outcome.decision === "block" && outcome.reason) {
884
+ toolResult.hookFeedback = outcome.reason;
885
+ }
886
+ }
887
+ } catch (_err) {
888
+ // PostToolUse hooks are best-effort
889
+ }
890
+ }
595
891
 
596
892
  return toolResult;
597
893
  }
@@ -612,10 +908,12 @@ async function executeToolInner(
612
908
  externalToolDescriptors,
613
909
  externalToolExecutors,
614
910
  mcpClient,
911
+ llmOptions,
615
912
  shellPolicyOverrides,
616
913
  approvalGate,
617
914
  shellConfirm,
618
915
  additionalDirectories,
916
+ ruleAllowed = false,
619
917
  },
620
918
  ) {
621
919
  const localToolDescriptor =
@@ -760,7 +1058,10 @@ async function executeToolInner(
760
1058
  const override = getRuntimeToolDescriptorByCommand(args.command);
761
1059
  let shellPolicy;
762
1060
  let approvalOutcome = null;
763
- if (approvalGate) {
1061
+ // A settings `allow` rule (ruleAllowed) pre-authorizes: skip the
1062
+ // ApprovalGate tier confirm, but still run the hard shell-policy denylist
1063
+ // below so an allow rule can never re-enable a blocked unsafe command.
1064
+ if (approvalGate && !ruleAllowed) {
764
1065
  const { evaluateShellCommandWithApproval } =
765
1066
  await import("../lib/shell-approval.js");
766
1067
  const gated = await evaluateShellCommandWithApproval({
@@ -802,11 +1103,86 @@ async function executeToolInner(
802
1103
  }
803
1104
  }
804
1105
 
1106
+ // Background: spawn, register, return a task_id immediately. The agent
1107
+ // polls output + completion via check_shell. No timeout — that's the whole
1108
+ // point of backgrounding (builds, test suites, dev servers).
1109
+ if (args.run_in_background === true) {
1110
+ const id = `bg_${++_backgroundTaskSeq}`;
1111
+ const task = {
1112
+ id,
1113
+ command: args.command,
1114
+ cwd: args.cwd || cwd,
1115
+ status: "running",
1116
+ exitCode: null,
1117
+ signal: null,
1118
+ error: null,
1119
+ startedAt: new Date().toISOString(),
1120
+ endedAt: null,
1121
+ out: _newBgStream(),
1122
+ err: _newBgStream(),
1123
+ child: null,
1124
+ };
1125
+ try {
1126
+ const child = spawn(args.command, {
1127
+ cwd: task.cwd,
1128
+ shell: true,
1129
+ windowsHide: true,
1130
+ // POSIX: own process group so check_shell{kill}/teardown can signal
1131
+ // the whole tree (shell + its grandchild command). No-op on Windows
1132
+ // where the tree is killed via taskkill /T instead.
1133
+ detached: process.platform !== "win32",
1134
+ });
1135
+ task.child = child;
1136
+ if (child.stdout) {
1137
+ child.stdout.setEncoding("utf8");
1138
+ child.stdout.on("data", (d) => _appendBgStream(task.out, d));
1139
+ }
1140
+ if (child.stderr) {
1141
+ child.stderr.setEncoding("utf8");
1142
+ child.stderr.on("data", (d) => _appendBgStream(task.err, d));
1143
+ }
1144
+ child.on("error", (err) => {
1145
+ task.status = "error";
1146
+ task.error = String(err?.message || err).substring(0, 2000);
1147
+ task.endedAt = new Date().toISOString();
1148
+ });
1149
+ // 'close' (not 'exit') so stdout/stderr are fully drained before the
1150
+ // status leaves "running" — otherwise a poll can observe completion
1151
+ // with the final output chunk not yet buffered.
1152
+ child.on("close", (code, signal) => {
1153
+ // 'error' may have already set a terminal state; don't overwrite it.
1154
+ if (task.status === "running") {
1155
+ task.status = code === 0 ? "exited" : "failed";
1156
+ }
1157
+ task.exitCode = code;
1158
+ task.signal = signal;
1159
+ task.endedAt = new Date().toISOString();
1160
+ });
1161
+ } catch (err) {
1162
+ task.status = "error";
1163
+ task.error = String(err?.message || err).substring(0, 2000);
1164
+ task.endedAt = new Date().toISOString();
1165
+ }
1166
+ _backgroundShellTasks.set(id, task);
1167
+ return attachDescriptor(
1168
+ {
1169
+ background: true,
1170
+ task_id: id,
1171
+ status: task.status,
1172
+ command: task.command,
1173
+ hint: "Poll output and completion with the check_shell tool using this task_id. Kill long-lived servers with check_shell { task_id, kill: true } when done.",
1174
+ shellCommandPolicy: shellPolicy,
1175
+ approval: approvalOutcome,
1176
+ },
1177
+ override || runtimeDescriptor,
1178
+ );
1179
+ }
1180
+
805
1181
  try {
806
1182
  const output = execSync(args.command, {
807
1183
  cwd: args.cwd || cwd,
808
1184
  encoding: "utf8",
809
- timeout: 60000,
1185
+ timeout: _resolveShellTimeout(args.timeout),
810
1186
  maxBuffer: 1024 * 1024,
811
1187
  });
812
1188
  return attachDescriptor(
@@ -831,6 +1207,51 @@ async function executeToolInner(
831
1207
  }
832
1208
  }
833
1209
 
1210
+ case "check_shell": {
1211
+ const taskId = args.task_id;
1212
+ // No task_id → list known background tasks (lightweight status surface).
1213
+ if (!taskId) {
1214
+ return attachDescriptor({
1215
+ tasks: listBackgroundShellTasks(),
1216
+ });
1217
+ }
1218
+ const task = _backgroundShellTasks.get(taskId);
1219
+ if (!task) {
1220
+ return attachDescriptor({
1221
+ error: `No background shell task with id "${taskId}".`,
1222
+ tasks: listBackgroundShellTasks(),
1223
+ });
1224
+ }
1225
+ let killed = false;
1226
+ if (args.kill === true) {
1227
+ // _killTask signals the whole process tree (see its doc); the close
1228
+ // handler flips status. Best-effort.
1229
+ killed = _killTask(task);
1230
+ }
1231
+ const out = _readBgStream(task.out);
1232
+ const err = _readBgStream(task.err);
1233
+ return attachDescriptor({
1234
+ task_id: task.id,
1235
+ status: task.status,
1236
+ running: task.status === "running",
1237
+ command: task.command,
1238
+ exitCode: task.exitCode,
1239
+ signal: task.signal,
1240
+ ...(task.error ? { error: task.error } : {}),
1241
+ stdout: out.text.substring(0, 30000),
1242
+ stderr: err.text.substring(0, 30000),
1243
+ ...(out.droppedGap
1244
+ ? { stdout_dropped_bytes: out.droppedGap }
1245
+ : {}),
1246
+ ...(err.droppedGap
1247
+ ? { stderr_dropped_bytes: err.droppedGap }
1248
+ : {}),
1249
+ ...(killed ? { killed: true } : {}),
1250
+ startedAt: task.startedAt,
1251
+ endedAt: task.endedAt,
1252
+ });
1253
+ }
1254
+
834
1255
  case "git": {
835
1256
  const normalizedCommand = normalizeGitCommand(args.command);
836
1257
  if (!normalizedCommand) {
@@ -874,6 +1295,7 @@ async function executeToolInner(
874
1295
  parentMessages,
875
1296
  interaction,
876
1297
  sessionId,
1298
+ llmOptions,
877
1299
  }),
878
1300
  );
879
1301
  }
@@ -905,6 +1327,33 @@ async function executeToolInner(
905
1327
  }
906
1328
  }
907
1329
 
1330
+ case "web_search": {
1331
+ try {
1332
+ const { webSearch } = await import("../lib/web-search.js");
1333
+ let webSearchConfig = {};
1334
+ try {
1335
+ const { loadProjectConfig: _lpc, findProjectRoot: _fpr } =
1336
+ await import("../lib/project-detector.js");
1337
+ const projectRoot = _fpr(cwd);
1338
+ if (projectRoot) {
1339
+ const cfg = _lpc(projectRoot);
1340
+ webSearchConfig = cfg?.webSearch || {};
1341
+ }
1342
+ } catch (_err) {
1343
+ // Config optional — use defaults (auto provider / keyless fallback)
1344
+ }
1345
+ const result = await webSearch(args.query, {
1346
+ provider: args.provider,
1347
+ maxResults: args.maxResults,
1348
+ timeout: args.timeout,
1349
+ config: webSearchConfig,
1350
+ });
1351
+ return attachDescriptor(result);
1352
+ } catch (err) {
1353
+ return attachDescriptor({ error: `web_search failed: ${err.message}` });
1354
+ }
1355
+ }
1356
+
908
1357
  case "todo_write": {
909
1358
  try {
910
1359
  const { writeTodos } = await import("../lib/todo-manager.js");
@@ -1470,7 +1919,7 @@ async function _executeRunCode(args, cwd) {
1470
1919
  * @returns {Promise<object>}
1471
1920
  */
1472
1921
  async function _executeSpawnSubAgent(args, ctx) {
1473
- const {
1922
+ let {
1474
1923
  role,
1475
1924
  task,
1476
1925
  context: inheritedContext,
@@ -1478,8 +1927,37 @@ async function _executeSpawnSubAgent(args, ctx) {
1478
1927
  profile: profileName,
1479
1928
  } = args;
1480
1929
 
1481
- if (!role || !task) {
1482
- return { error: "Both 'role' and 'task' are required for spawn_sub_agent" };
1930
+ // Named subagent delegation (cc agents / .chainlesschain|.claude/agents/*.md):
1931
+ // load the agent's persona (its body = system prompt) + tool allow-list.
1932
+ // Explicit role/tools still win over the agent file's values.
1933
+ let mdProfile = null;
1934
+ let mdModel = null;
1935
+ if (args.agent) {
1936
+ try {
1937
+ const { getAgent } = await import("../lib/agents.js");
1938
+ const md = getAgent(args.agent, ctx.cwd);
1939
+ if (!md) {
1940
+ return {
1941
+ error: `Unknown subagent "${args.agent}". List them with: cc agents list`,
1942
+ };
1943
+ }
1944
+ role = role || md.name;
1945
+ if (!explicitTools && Array.isArray(md.tools)) explicitTools = md.tools;
1946
+ if (md.model) mdModel = md.model;
1947
+ if (md.systemPrompt) {
1948
+ mdProfile = { name: md.name, systemPrompt: md.systemPrompt };
1949
+ }
1950
+ } catch (err) {
1951
+ return {
1952
+ error: `Failed to load subagent "${args.agent}": ${err.message}`,
1953
+ };
1954
+ }
1955
+ }
1956
+
1957
+ if (!task || (!role && !args.agent)) {
1958
+ return {
1959
+ error: "spawn_sub_agent requires 'task' and either 'role' or 'agent'",
1960
+ };
1483
1961
  }
1484
1962
 
1485
1963
  // Phase 3: resolve declarative profile if requested. Explicit tools/context
@@ -1500,6 +1978,10 @@ async function _executeSpawnSubAgent(args, ctx) {
1500
1978
  }
1501
1979
  }
1502
1980
 
1981
+ // A named subagent's body becomes the sub-agent system prompt (via the
1982
+ // profile.systemPrompt seam) when no declarative profile was requested.
1983
+ if (!profile && mdProfile) profile = mdProfile;
1984
+
1503
1985
  const allowedTools = Array.isArray(explicitTools)
1504
1986
  ? explicitTools
1505
1987
  : profile?.toolAllowlist || null;
@@ -1521,6 +2003,14 @@ async function _executeSpawnSubAgent(args, ctx) {
1521
2003
  const parentSessionId = ctx.sessionId || null;
1522
2004
  const interaction = ctx.interaction || null;
1523
2005
 
2006
+ // Inherit the parent's provider / base-url / key; a named subagent's `model:`
2007
+ // frontmatter (mdModel) overrides just the model, else keep the parent's.
2008
+ const parentLlm = ctx.llmOptions || {};
2009
+ const subLlmOptions = {
2010
+ ...parentLlm,
2011
+ model: mdModel || parentLlm.model || undefined,
2012
+ };
2013
+
1524
2014
  const subCtx = SubAgentContext.create({
1525
2015
  role,
1526
2016
  task,
@@ -1529,6 +2019,7 @@ async function _executeSpawnSubAgent(args, ctx) {
1529
2019
  allowedTools: allowedTools || null,
1530
2020
  cwd: ctx.cwd,
1531
2021
  profile: profile || null,
2022
+ llmOptions: subLlmOptions,
1532
2023
  });
1533
2024
 
1534
2025
  const emit = (type, payload) => {
@@ -1664,6 +2155,12 @@ export async function chatWithTools(rawMessages, options) {
1664
2155
 
1665
2156
  if (provider === "ollama") {
1666
2157
  const apiUrl = `${baseUrl}/api/chat`;
2158
+ // Multimodal (`cc agent --image`): ollama wants `{content, images:[base64]}`
2159
+ // not OpenAI-style `image_url` blocks. Convert only when an image part is
2160
+ // present so text-only runs keep the identical request shape.
2161
+ const ollamaMessages = hasImageContent(messages)
2162
+ ? toOllamaMessages(messages)
2163
+ : messages;
1667
2164
  // Real-time token deltas (Claude-Code `--include-partial-messages`): when
1668
2165
  // the caller supplies an onToken hook, stream the response and forward each
1669
2166
  // content chunk as it arrives. Tool calls + usage are accumulated and the
@@ -1672,7 +2169,7 @@ export async function chatWithTools(rawMessages, options) {
1672
2169
  if (typeof options.onToken === "function") {
1673
2170
  return await _chatOllamaStreaming(
1674
2171
  apiUrl,
1675
- { model, messages, tools },
2172
+ { model, messages: ollamaMessages, tools },
1676
2173
  options.onToken,
1677
2174
  signal,
1678
2175
  );
@@ -1683,7 +2180,7 @@ export async function chatWithTools(rawMessages, options) {
1683
2180
  signal,
1684
2181
  body: JSON.stringify({
1685
2182
  model,
1686
- messages,
2183
+ messages: ollamaMessages,
1687
2184
  tools,
1688
2185
  stream: false,
1689
2186
  }),
@@ -1714,12 +2211,35 @@ export async function chatWithTools(rawMessages, options) {
1714
2211
  input_schema: t.function.parameters,
1715
2212
  }));
1716
2213
 
2214
+ // Model-aware max_tokens (Opus → 16384, Haiku → 4096, else 8192) via
2215
+ // provider-options. We read ONLY maxTokens: the module's `temperature`
2216
+ // default is never forwarded (400s on Opus 4.7/4.8).
2217
+ const effModel = model || "claude-sonnet-4-20250514";
2218
+ const { maxTokens: anthropicMaxTokens } = mergeProviderOptions(
2219
+ "anthropic",
2220
+ effModel,
2221
+ );
1717
2222
  const body = {
1718
- model: model || "claude-sonnet-4-20250514",
1719
- max_tokens: 8192,
1720
- messages: otherMsgs,
2223
+ model: effModel,
2224
+ max_tokens: anthropicMaxTokens || 8192,
2225
+ // Convert cc's internal OpenAI-shaped history (role:"tool" results,
2226
+ // assistant tool_calls[]) into Anthropic content blocks. Without this,
2227
+ // multi-turn tool use 400s on turn 2 (Anthropic rejects role:"tool" and
2228
+ // assistant `tool_calls`). Also replays preserved thinking blocks.
2229
+ messages: _toAnthropicMessages(otherMsgs),
1721
2230
  tools: anthropicTools,
1722
2231
  };
2232
+ // Extended thinking — OPT-IN via options.thinking; off by default so the
2233
+ // request is byte-identical to before. Model-aware (adaptive+effort on Opus
2234
+ // 4.6+/Sonnet 4.6, legacy enabled+budget else; nothing on Haiku). temperature
2235
+ // is never sent. RUNTIME-UNVERIFIED: no Anthropic key here to E2E the
2236
+ // thinking-block signature replay (see cli_claude_code_parity_landed memory).
2237
+ const thinkingParams = _anthropicThinkingParams(
2238
+ effModel,
2239
+ options,
2240
+ body.max_tokens,
2241
+ );
2242
+ if (thinkingParams) Object.assign(body, thinkingParams);
1723
2243
  if (systemMsgs.length > 0) {
1724
2244
  body.system = systemMsgs.map((m) => m.content).join("\n");
1725
2245
  }
@@ -1999,7 +2519,11 @@ function _anthropicReduceLine(state, raw, onToken) {
1999
2519
  state.blocks[obj.index] =
2000
2520
  cb.type === "tool_use"
2001
2521
  ? { type: "tool_use", id: cb.id, name: cb.name, json: "" }
2002
- : { type: "text" };
2522
+ : cb.type === "thinking"
2523
+ ? { type: "thinking", thinking: "", signature: "" }
2524
+ : cb.type === "redacted_thinking"
2525
+ ? { type: "redacted_thinking", data: cb.data || "" }
2526
+ : { type: "text" };
2003
2527
  } else if (obj.type === "content_block_delta") {
2004
2528
  const d = obj.delta || {};
2005
2529
  if (d.type === "text_delta" && d.text) {
@@ -2013,6 +2537,12 @@ function _anthropicReduceLine(state, raw, onToken) {
2013
2537
  }
2014
2538
  } else if (d.type === "input_json_delta" && state.blocks[obj.index]) {
2015
2539
  state.blocks[obj.index].json += d.partial_json || "";
2540
+ } else if (d.type === "thinking_delta" && state.blocks[obj.index]) {
2541
+ state.blocks[obj.index].thinking =
2542
+ (state.blocks[obj.index].thinking || "") + (d.thinking || "");
2543
+ } else if (d.type === "signature_delta" && state.blocks[obj.index]) {
2544
+ state.blocks[obj.index].signature =
2545
+ (state.blocks[obj.index].signature || "") + (d.signature || "");
2016
2546
  }
2017
2547
  } else if (obj.type === "message_delta") {
2018
2548
  state.outputTokens = Number(obj.usage?.output_tokens) || state.outputTokens;
@@ -2022,7 +2552,10 @@ function _anthropicReduceLine(state, raw, onToken) {
2022
2552
 
2023
2553
  function _anthropicFinalize(state) {
2024
2554
  const toolCalls = [];
2025
- for (const k of Object.keys(state.blocks)) {
2555
+ const thinkingBlocks = [];
2556
+ for (const k of Object.keys(state.blocks).sort(
2557
+ (a, b) => Number(a) - Number(b),
2558
+ )) {
2026
2559
  const b = state.blocks[k];
2027
2560
  if (b.type === "tool_use") {
2028
2561
  let input = {};
@@ -2036,10 +2569,21 @@ function _anthropicFinalize(state) {
2036
2569
  type: "function",
2037
2570
  function: { name: b.name, arguments: JSON.stringify(input) },
2038
2571
  });
2572
+ } else if (b.type === "thinking") {
2573
+ thinkingBlocks.push({
2574
+ type: "thinking",
2575
+ thinking: b.thinking || "",
2576
+ signature: b.signature || "",
2577
+ });
2578
+ } else if (b.type === "redacted_thinking") {
2579
+ thinkingBlocks.push({ type: "redacted_thinking", data: b.data || "" });
2039
2580
  }
2040
2581
  }
2041
2582
  const message = { role: "assistant", content: state.text };
2042
2583
  if (toolCalls.length) message.tool_calls = toolCalls;
2584
+ // Preserve thinking blocks verbatim (incl. signature) for replay on the next
2585
+ // tool turn — required by the API when extended thinking is on.
2586
+ if (thinkingBlocks.length) message._thinkingBlocks = thinkingBlocks;
2043
2587
  const out = { message };
2044
2588
  if (state.inputTokens || state.outputTokens) {
2045
2589
  out.usage = {
@@ -2187,10 +2731,154 @@ async function _chatOpenAIStreaming(apiUrl, body, apiKey, onToken, signal, provi
2187
2731
  return _openaiFinalize(state);
2188
2732
  }
2189
2733
 
2190
- function _normalizeAnthropicResponse(data) {
2734
+ /**
2735
+ * Convert cc's internal OpenAI-shaped messages into Anthropic content-block
2736
+ * messages. Internal shape: {role:"user"|"assistant"|"tool", content,
2737
+ * tool_calls?, _thinkingBlocks?}. Anthropic shape: {role:"user"|"assistant",
2738
+ * content: string | block[]} — assistant tool calls become {type:"tool_use"}
2739
+ * blocks; tool results become {type:"tool_result"} blocks inside a USER turn,
2740
+ * with consecutive results merged. Preserved thinking blocks (with signature)
2741
+ * are replayed FIRST in the assistant turn (the API requires them ahead of
2742
+ * tool_use when continuing a thinking+tool turn). Exported for tests.
2743
+ */
2744
+ export function _toAnthropicMessages(msgs) {
2745
+ const out = [];
2746
+ let pendingResults = [];
2747
+ const flush = () => {
2748
+ if (pendingResults.length) {
2749
+ out.push({ role: "user", content: pendingResults });
2750
+ pendingResults = [];
2751
+ }
2752
+ };
2753
+ for (const m of msgs || []) {
2754
+ if (!m) continue;
2755
+ if (m.role === "tool") {
2756
+ pendingResults.push({
2757
+ type: "tool_result",
2758
+ tool_use_id: m.tool_call_id,
2759
+ content:
2760
+ typeof m.content === "string"
2761
+ ? m.content
2762
+ : JSON.stringify(m.content ?? ""),
2763
+ });
2764
+ continue;
2765
+ }
2766
+ flush();
2767
+ if (m.role === "assistant") {
2768
+ const blocks = [];
2769
+ if (Array.isArray(m._thinkingBlocks)) {
2770
+ for (const tb of m._thinkingBlocks) blocks.push(tb);
2771
+ }
2772
+ if (typeof m.content === "string" && m.content.trim()) {
2773
+ blocks.push({ type: "text", text: m.content });
2774
+ } else if (Array.isArray(m.content)) {
2775
+ for (const b of m.content) blocks.push(b);
2776
+ }
2777
+ if (Array.isArray(m.tool_calls)) {
2778
+ for (const tc of m.tool_calls) {
2779
+ const raw = tc.function?.arguments;
2780
+ let input = {};
2781
+ try {
2782
+ input =
2783
+ typeof raw === "string" ? JSON.parse(raw || "{}") : raw || {};
2784
+ } catch {
2785
+ input = {};
2786
+ }
2787
+ blocks.push({
2788
+ type: "tool_use",
2789
+ id: tc.id,
2790
+ name: tc.function?.name,
2791
+ input,
2792
+ });
2793
+ }
2794
+ }
2795
+ out.push({
2796
+ role: "assistant",
2797
+ content: blocks.length ? blocks : m.content || "",
2798
+ });
2799
+ } else {
2800
+ // user turn: pass content through (string or already-block array). When
2801
+ // it carries OpenAI-style `image_url` parts (`cc agent --image`), convert
2802
+ // them to Anthropic `image` blocks; text parts and any other blocks pass
2803
+ // through unchanged.
2804
+ let content = m.content;
2805
+ if (Array.isArray(content)) {
2806
+ content = content.map((b) =>
2807
+ b?.type === "image_url" ? imageUrlBlockToAnthropic(b) || b : b,
2808
+ );
2809
+ }
2810
+ out.push({ role: "user", content });
2811
+ }
2812
+ }
2813
+ flush();
2814
+ return out;
2815
+ }
2816
+
2817
+ /** Map a Claude-Code-style intensity to an Anthropic effort level. */
2818
+ function _intensityToEffort(want) {
2819
+ switch (String(want)) {
2820
+ case "ultra":
2821
+ case "ultrathink":
2822
+ return "xhigh";
2823
+ case "hard":
2824
+ case "think-hard":
2825
+ case "harder":
2826
+ return "high";
2827
+ case "think":
2828
+ return "medium";
2829
+ default:
2830
+ return "high"; // bare `true` → a sensible default for intelligence work
2831
+ }
2832
+ }
2833
+
2834
+ /**
2835
+ * Decide the Anthropic `thinking` request params for a model. Returns null
2836
+ * (off) unless the caller opts in via options.thinking (true | "think" |
2837
+ * "hard" | "ultra"). Model-aware per the Claude API:
2838
+ * - Opus 4.6/4.7/4.8, Sonnet 4.6 → adaptive thinking + output_config.effort
2839
+ * - Sonnet 4.5 / Opus 4.0-4.5 / older → legacy enabled+budget (< max_tokens)
2840
+ * - anything else (e.g. Haiku) → null (no thinking)
2841
+ * temperature is never added (it 400s on Opus 4.7/4.8). Note: `xhigh`/`max`
2842
+ * effort are Opus-tier — on Sonnet they may error; left to the caller's intent.
2843
+ * RUNTIME-UNVERIFIED — no Anthropic key to validate the wire shape live.
2844
+ * Exported for tests.
2845
+ */
2846
+ export function _anthropicThinkingParams(model, options = {}, maxTokens = 8192) {
2847
+ const want = options?.thinking;
2848
+ if (!want) return null; // off by default → request unchanged
2849
+ const m = String(model || "").toLowerCase();
2850
+ const adaptive = /opus-4-(6|7|8)/.test(m) || /sonnet-4-6/.test(m);
2851
+ const legacy =
2852
+ /sonnet-4-5/.test(m) ||
2853
+ /opus-4-(0|1|5)/.test(m) ||
2854
+ /sonnet-4-0/.test(m) ||
2855
+ /sonnet-3/.test(m) ||
2856
+ /opus-3/.test(m);
2857
+ if (adaptive) {
2858
+ const params = { thinking: { type: "adaptive" } };
2859
+ const effort =
2860
+ typeof options.thinkingEffort === "string"
2861
+ ? options.thinkingEffort
2862
+ : _intensityToEffort(want);
2863
+ if (effort) params.output_config = { effort };
2864
+ return params;
2865
+ }
2866
+ if (legacy) {
2867
+ let budget = Number(options.thinkingBudget) || 8000;
2868
+ // budget_tokens must be strictly < max_tokens (min 1024) on legacy models
2869
+ if (budget >= maxTokens) budget = Math.max(1024, Math.floor(maxTokens / 2));
2870
+ return { thinking: { type: "enabled", budget_tokens: budget } };
2871
+ }
2872
+ return null; // unknown / Haiku → no thinking
2873
+ }
2874
+
2875
+ export function _normalizeAnthropicResponse(data) {
2191
2876
  const content = data.content || [];
2192
2877
  const textBlocks = content.filter((b) => b.type === "text");
2193
2878
  const toolBlocks = content.filter((b) => b.type === "tool_use");
2879
+ const thinkingBlocks = content.filter(
2880
+ (b) => b.type === "thinking" || b.type === "redacted_thinking",
2881
+ );
2194
2882
 
2195
2883
  const message = {
2196
2884
  role: "assistant",
@@ -2208,6 +2896,13 @@ function _normalizeAnthropicResponse(data) {
2208
2896
  }));
2209
2897
  }
2210
2898
 
2899
+ // Preserve thinking blocks VERBATIM (incl. signature) so the agent loop can
2900
+ // replay them on the next tool turn — required when extended thinking is on,
2901
+ // harmless (absent) otherwise. _toAnthropicMessages re-emits them first.
2902
+ if (thinkingBlocks.length > 0) {
2903
+ message._thinkingBlocks = thinkingBlocks;
2904
+ }
2905
+
2211
2906
  return { message };
2212
2907
  }
2213
2908
 
@@ -2322,12 +3017,23 @@ export async function* agentLoop(messages, options) {
2322
3017
  externalToolDescriptors: options.externalToolDescriptors || null,
2323
3018
  externalToolExecutors: options.externalToolExecutors || null,
2324
3019
  mcpClient: options.mcpClient || null,
3020
+ // Parent LLM config — forwarded to spawn_sub_agent so a delegated subagent
3021
+ // inherits the provider/key and can override just the model (cc agents `model:`).
3022
+ llmOptions: {
3023
+ provider: options.provider || null,
3024
+ model: options.model || null,
3025
+ baseUrl: options.baseUrl || null,
3026
+ apiKey: options.apiKey || null,
3027
+ },
2325
3028
  parentMessages: messages, // pass parent messages for sub-agent auto-condensation
2326
3029
  interaction: options.interaction || null,
2327
3030
  shellPolicyOverrides: options.shellPolicyOverrides || null,
2328
3031
  approvalGate: options.approvalGate || null,
2329
3032
  shellConfirm: options.shellConfirm || null,
2330
3033
  additionalDirectories: options.additionalDirectories || null,
3034
+ permissionRules: options.permissionRules || null,
3035
+ permissionConfirm: options.permissionConfirm || null,
3036
+ settingsHooks: options.settingsHooks || null,
2331
3037
  autoCheckpoint: options.autoCheckpoint || false,
2332
3038
  checkpointSession:
2333
3039
  options.checkpointSession || options.sessionId || "agent",
@@ -2401,6 +3107,10 @@ export async function* agentLoop(messages, options) {
2401
3107
  sessionId: options.sessionId || null,
2402
3108
  };
2403
3109
 
3110
+ // True once a Stop hook has forced a continuation — passed to the next Stop
3111
+ // hook as `stop_hook_active` so a well-behaved hook won't block forever.
3112
+ let stopHookActive = false;
3113
+
2404
3114
  while (budget.hasRemaining()) {
2405
3115
  budget.consume();
2406
3116
  throwIfAborted(signal);
@@ -2445,10 +3155,37 @@ export async function* agentLoop(messages, options) {
2445
3155
  try {
2446
3156
  const compactor = await _getAutoCompactor(options);
2447
3157
  if (compactor && compactor.shouldAutoCompact(messages)) {
2448
- const { messages: compacted, stats } = await compactor.compress(
2449
- messages,
2450
- { preserveToolPairs: true },
2451
- );
3158
+ // settings.json PreCompact hooks: a `block` decision SKIPS this
3159
+ // compaction round (e.g. the hook archived / owns the history). Fires
3160
+ // right before the history would be compacted.
3161
+ let preCompactBlocked = false;
3162
+ let preCompactReason = null;
3163
+ if (options.settingsHooks) {
3164
+ try {
3165
+ const pc = runObserveHooks(
3166
+ options.settingsHooks,
3167
+ "PreCompact",
3168
+ {
3169
+ trigger: "auto",
3170
+ message_count: messages.length,
3171
+ session_id: options.sessionId || null,
3172
+ },
3173
+ { cwd: options.cwd || process.cwd() },
3174
+ );
3175
+ if (pc && pc.decision === "block") {
3176
+ preCompactBlocked = true;
3177
+ preCompactReason = pc.reason || null;
3178
+ }
3179
+ } catch (_err) {
3180
+ // observe-only
3181
+ }
3182
+ }
3183
+ if (preCompactBlocked) {
3184
+ yield { type: "compaction-skipped", runId, reason: preCompactReason };
3185
+ }
3186
+ const { messages: compacted, stats } = preCompactBlocked
3187
+ ? { messages, stats: { saved: 0 } }
3188
+ : await compactor.compress(messages, { preserveToolPairs: true });
2452
3189
  if (stats.saved > 0 && compacted.length < messages.length) {
2453
3190
  messages.splice(0, messages.length, ...compacted);
2454
3191
  // Persist the compaction so a later --resume rebuilds from the
@@ -2538,6 +3275,43 @@ export async function* agentLoop(messages, options) {
2538
3275
 
2539
3276
  if (!toolCalls || toolCalls.length === 0) {
2540
3277
  yield { type: "response-complete", content: msg.content || "" };
3278
+ // settings.json Stop hooks: a `block` decision FORCES the agent to keep
3279
+ // going instead of stopping — the reason is injected as a new instruction.
3280
+ // `stop_hook_active` lets the hook avoid an infinite loop; the iteration
3281
+ // budget is the hard backstop.
3282
+ if (options.settingsHooks) {
3283
+ let stopOutcome = null;
3284
+ try {
3285
+ stopOutcome = runObserveHooks(
3286
+ options.settingsHooks,
3287
+ "Stop",
3288
+ {
3289
+ stop_hook_active: stopHookActive,
3290
+ final_response: String(msg.content || "").substring(0, 2000),
3291
+ session_id: options.sessionId || null,
3292
+ },
3293
+ { cwd: options.cwd || process.cwd() },
3294
+ );
3295
+ } catch (_err) {
3296
+ stopOutcome = null; // never affect the run outcome
3297
+ }
3298
+ if (stopOutcome && stopOutcome.decision === "block") {
3299
+ stopHookActive = true;
3300
+ messages.push({ role: "assistant", content: msg.content || "" });
3301
+ messages.push({
3302
+ role: "user",
3303
+ content:
3304
+ stopOutcome.reason ||
3305
+ "A Stop hook requested that you keep working.",
3306
+ });
3307
+ yield {
3308
+ type: "stop-hook-continue",
3309
+ runId,
3310
+ reason: stopOutcome.reason || null,
3311
+ };
3312
+ continue;
3313
+ }
3314
+ }
2541
3315
  yield { type: "run-ended", runId, reason: "complete" };
2542
3316
  return;
2543
3317
  }
@@ -2626,7 +3400,11 @@ export function formatToolArgs(name, args) {
2626
3400
  case "edit_file_hashed":
2627
3401
  return `${args.path} @${args.anchor_hash}`;
2628
3402
  case "run_shell":
2629
- return args.command;
3403
+ return args.run_in_background ? `${args.command} (background)` : args.command;
3404
+ case "check_shell":
3405
+ return args.task_id
3406
+ ? `${args.task_id}${args.kill ? " (kill)" : ""}`
3407
+ : "list";
2630
3408
  case "git":
2631
3409
  return args.command;
2632
3410
  case "search_files":