chainlesschain 0.162.125 → 0.162.127

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 +2 -2
  2. package/src/assets/web-panel/assets/{AIOps-DcqNhZhA.js → AIOps-CsHJh6tT.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-BigtmPoq.js → ActionButton-ClDr78Wl.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-1J_X_HF4.js → Analytics-CQQBX5mv.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-DlcDG_a_.js → AppLayout-DIofBNeG.js} +5 -5
  6. package/src/assets/web-panel/assets/{Audit-Do-y8_3w.js → Audit-d8pxGJLK.js} +1 -1
  7. package/src/assets/web-panel/assets/{Backup-S2Df-50Y.js → Backup-ChZb31ek.js} +1 -1
  8. package/src/assets/web-panel/assets/{BaseInput-D5kgWJfk.js → BaseInput-CQsSXb5v.js} +1 -1
  9. package/src/assets/web-panel/assets/{Chat-aUcp3kN1.js → Chat-C_mjILx8.js} +6 -6
  10. package/src/assets/web-panel/assets/ChatBubbleRenderer-B545kWla.js +1 -0
  11. package/src/assets/web-panel/assets/{Checkbox-DTg1U7Xs.js → Checkbox-CWFmVuo8.js} +1 -1
  12. package/src/assets/web-panel/assets/{Codegen-YO9ZZ4Ky.js → Codegen-DwicePOQ.js} +1 -1
  13. package/src/assets/web-panel/assets/{Col-BHonE9fU.js → Col-B36SnRdL.js} +1 -1
  14. package/src/assets/web-panel/assets/{Community-DA2AlEFh.js → Community-C_sZ2HhK.js} +1 -1
  15. package/src/assets/web-panel/assets/{Compact-DULxLRNg.js → Compact-B2jzDcUl.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compliance-BZqfuuZL.js → Compliance-BD58_IHe.js} +1 -1
  17. package/src/assets/web-panel/assets/{Cowork-CJe3vyMS.js → Cowork-VIBdGc4D.js} +2 -2
  18. package/src/assets/web-panel/assets/{Cron-CeV8AtRu.js → Cron-CDTVWUmV.js} +2 -2
  19. package/src/assets/web-panel/assets/{Crosschain-CAg66zS5.js → Crosschain-D5DkGNKU.js} +1 -1
  20. package/src/assets/web-panel/assets/{DID-Dtup5JZm.js → DID-B23ae8PQ.js} +2 -2
  21. package/src/assets/web-panel/assets/{Dashboard-DAR_8PNy.js → Dashboard-DTG5MsSx.js} +2 -2
  22. package/src/assets/web-panel/assets/{Dropdown-K5bTaCYN.js → Dropdown-D35RRnMZ.js} +1 -1
  23. package/src/assets/web-panel/assets/{EmailListRenderer-C890uErx.js → EmailListRenderer-DRvY2nPk.js} +1 -1
  24. package/src/assets/web-panel/assets/{FamilyGuardDashboard-B-Tj_8yM.js → FamilyGuardDashboard-BT_eESkk.js} +1 -1
  25. package/src/assets/web-panel/assets/{Federation-C6WZ98ni.js → Federation-CxX9F7IL.js} +1 -1
  26. package/src/assets/web-panel/assets/{FormItemContext-D-LTfGWd.js → FormItemContext-C1xDz6D8.js} +1 -1
  27. package/src/assets/web-panel/assets/{GenericCardRenderer-C80DK4W7.js → GenericCardRenderer-CjBOAXcO.js} +1 -1
  28. package/src/assets/web-panel/assets/{Git-Cjvvv3zW.js → Git-I9I9NkPO.js} +2 -2
  29. package/src/assets/web-panel/assets/{Governance-C0BND77H.js → Governance-BiigWxQW.js} +1 -1
  30. package/src/assets/web-panel/assets/{Inference-BL9kTtSu.js → Inference-C0KrSCgh.js} +1 -1
  31. package/src/assets/web-panel/assets/{KnowledgeGraph-CnVTBmJf.js → KnowledgeGraph-B1Nd54ow.js} +1 -1
  32. package/src/assets/web-panel/assets/{Logs-CbCbg7K6.js → Logs-CeOtvYIo.js} +2 -2
  33. package/src/assets/web-panel/assets/{Marketplace-CG5On-fH.js → Marketplace-CRhgvAVQ.js} +1 -1
  34. package/src/assets/web-panel/assets/{McpTools-AYYDZZK7.js → McpTools-BIJNah-H.js} +5 -5
  35. package/src/assets/web-panel/assets/{Memory-tMWbMDQq.js → Memory-D6gsbo4e.js} +2 -2
  36. package/src/assets/web-panel/assets/{MobileBridge-BAOsf4wB.js → MobileBridge-DDkIKA6U.js} +1 -1
  37. package/src/assets/web-panel/assets/{MobileProjects-BT-2komQ.js → MobileProjects-BBkI02Lf.js} +1 -1
  38. package/src/assets/web-panel/assets/{Mtc-BFwfC63n.js → Mtc-BziHK7HS.js} +5 -5
  39. package/src/assets/web-panel/assets/{MtcAudit-DYG3CWdH.js → MtcAudit-CWXjbkeF.js} +2 -2
  40. package/src/assets/web-panel/assets/{Multisig-KJwd0yWT.js → Multisig-9jNF_VWa.js} +3 -3
  41. package/src/assets/web-panel/assets/{NLProgramming-BmL5dNWm.js → NLProgramming-BUdF7I2G.js} +1 -1
  42. package/src/assets/web-panel/assets/{Notes-DmkaVaMc.js → Notes-D5ls1r1T.js} +3 -3
  43. package/src/assets/web-panel/assets/{NotificationSettings-tmVQszDZ.js → NotificationSettings-B2WX_TMX.js} +1 -1
  44. package/src/assets/web-panel/assets/{OrderTableRenderer-BtrR7anf.js → OrderTableRenderer-COSzdTcE.js} +1 -1
  45. package/src/assets/web-panel/assets/{Organization-pppktQyW.js → Organization-tzWIgnbM.js} +4 -4
  46. package/src/assets/web-panel/assets/{Overflow-D3lBoQDA.js → Overflow-Ca1gp_3S.js} +1 -1
  47. package/src/assets/web-panel/assets/{P2P-BgE2qGlZ.js → P2P-XnfDfRK9.js} +2 -2
  48. package/src/assets/web-panel/assets/{PdhVaultBrowser-D26Bz5gS.js → PdhVaultBrowser-23Susb25.js} +3 -3
  49. package/src/assets/web-panel/assets/{Permissions-BR8no2Xe.js → Permissions-CnkhYPqt.js} +4 -4
  50. package/src/assets/web-panel/assets/{PersonalDataHub-DabbyQEF.js → PersonalDataHub-CZ1UasJc.js} +2 -2
  51. package/src/assets/web-panel/assets/{Pipeline-n4sNpEz8.js → Pipeline-DC1oC84K.js} +1 -1
  52. package/src/assets/web-panel/assets/{Privacy-CGNp4n8M.js → Privacy-CdqayaCE.js} +1 -1
  53. package/src/assets/web-panel/assets/{ProjectInit-CAVA5VsX.js → ProjectInit-DS7NbGQL.js} +2 -2
  54. package/src/assets/web-panel/assets/{ProjectSettings-GCgkfM7A.js → ProjectSettings-fdzwup3n.js} +2 -2
  55. package/src/assets/web-panel/assets/{Projects-BYK17p52.js → Projects-FvodeZob.js} +1 -1
  56. package/src/assets/web-panel/assets/{Providers-DpUT5W-d.js → Providers-IxYyzeSI.js} +1 -1
  57. package/src/assets/web-panel/assets/{QuickAsk-ZJxTb7vi.js → QuickAsk-BAtFET-B.js} +1 -1
  58. package/src/assets/web-panel/assets/{Recommend-CIzFRDk1.js → Recommend-DCNRV9Vr.js} +1 -1
  59. package/src/assets/web-panel/assets/{Reputation-lQ_WDNZn.js → Reputation-lg2JmIai.js} +1 -1
  60. package/src/assets/web-panel/assets/{Row-1MEtrgtF.js → Row-BAhyDuri.js} +1 -1
  61. package/src/assets/web-panel/assets/{RssFeed-SSgcPPl4.js → RssFeed-BA_3issD.js} +2 -2
  62. package/src/assets/web-panel/assets/{Search-CcTMOGNY.js → Search-CJheUJDl.js} +1 -1
  63. package/src/assets/web-panel/assets/{Security-BA1KMaDx.js → Security-D2VczQDD.js} +3 -3
  64. package/src/assets/web-panel/assets/{Services-_aj7czZn.js → Services-7ZFAzqVR.js} +2 -2
  65. package/src/assets/web-panel/assets/{Skeleton-BWgg_0Zr.js → Skeleton-CsBoTasD.js} +1 -1
  66. package/src/assets/web-panel/assets/{Skills-C2ZIziYM.js → Skills-DT_j_gj0.js} +1 -1
  67. package/src/assets/web-panel/assets/{Sla-CYxp4axY.js → Sla-C9D5geJx.js} +1 -1
  68. package/src/assets/web-panel/assets/{SpeechSettings-C7Csp1jV.js → SpeechSettings-CWCNVW42.js} +1 -1
  69. package/src/assets/web-panel/assets/{SyncSettings-svGeswiw.js → SyncSettings-TYCbf2kk.js} +2 -2
  70. package/src/assets/web-panel/assets/{Tasks-ClISj6oK.js → Tasks-CbRoXu1G.js} +1 -1
  71. package/src/assets/web-panel/assets/{Templates-CmO-Fp7r.js → Templates-CxBjIrkJ.js} +1 -1
  72. package/src/assets/web-panel/assets/{Tenant-a3Ac3lxz.js → Tenant-NKVyFsPM.js} +1 -1
  73. package/src/assets/web-panel/assets/{Terminal-DyJIRh81.js → Terminal-D5aAL5_H.js} +2 -2
  74. package/src/assets/web-panel/assets/TimelineRenderer-CD1JE0Rb.js +1 -0
  75. package/src/assets/web-panel/assets/{Tokens-DJW0KqV4.js → Tokens-CdepBSYe.js} +1 -1
  76. package/src/assets/web-panel/assets/{Trigger-0HeTi4r6.js → Trigger-Br3n_PEh.js} +1 -1
  77. package/src/assets/web-panel/assets/{Trust-aFym34eo.js → Trust-CSac0pMf.js} +1 -1
  78. package/src/assets/web-panel/assets/{UkeySign-CYhszHJv.js → UkeySign-BiWo43zc.js} +1 -1
  79. package/src/assets/web-panel/assets/{VideoEditing-BbJxPF9X.js → VideoEditing-Dj50GuoF.js} +1 -1
  80. package/src/assets/web-panel/assets/{Wallet-DDthEwiu.js → Wallet-CQK_g4_U.js} +4 -4
  81. package/src/assets/web-panel/assets/{WebAuthn-DQ6McYPg.js → WebAuthn-D8mBl1NC.js} +3 -3
  82. package/src/assets/web-panel/assets/{WorkflowEditor-CnF8zRsi.js → WorkflowEditor-CHeS_vab.js} +1 -1
  83. package/src/assets/web-panel/assets/{chat-Dzkx2gTP.js → chat-CbeOf_lH.js} +1 -1
  84. package/src/assets/web-panel/assets/{colors-35T51Oo5.js → colors-CTFiyfed.js} +1 -1
  85. package/src/assets/web-panel/assets/{compact-item-TtzNrlu1.js → compact-item-CZsh8FXE.js} +1 -1
  86. package/src/assets/web-panel/assets/{createContext-Y1LkWrYb.js → createContext-UPhnXsap.js} +1 -1
  87. package/src/assets/web-panel/assets/devWarning-BFRqfgsk.js +1 -0
  88. package/src/assets/web-panel/assets/{hasIn-BOYw1BgS.js → hasIn-CCqQtSY-.js} +1 -1
  89. package/src/assets/web-panel/assets/{index-D2od7Bgx.js → index-1d4pat4L.js} +1 -1
  90. package/src/assets/web-panel/assets/{index-NM9Oke2k.js → index-86dmMEZY.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-Dof7lWA_.js → index-B-OcSTNA.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-D7dCBw_M.js → index-B1tylGKq.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-CqhwG1ox.js → index-BAcem_Qs.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-CZ16GlOs.js → index-BKADY9Iw.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-CPGNc2tF.js → index-Be0uH7m1.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-DAizH273.js → index-Bga0UuWA.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-DFhlelzN.js → index-Blgzobgm.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-D6tG4B25.js → index-BwPULPuj.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-DjeUZxE5.js → index-CIQVlE9u.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CLqC2sRT.js → index-CMi91z74.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-hyqEKkBT.js → index-CNteMctn.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-Cs82BHAj.js → index-CQHQiYAO.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-zF77jnI2.js → index-CVKFSFkX.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-G4ClSmJc.js → index-CctoAd9I.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DhMSOd_2.js → index-CdiuRSqw.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-BWgNn9As.js → index-Cy83zglX.js} +3 -3
  107. package/src/assets/web-panel/assets/{index-Bw1iOGhM.js → index-DBJjqjxL.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-D4BgCBa3.js → index-DFbyobqi.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-BKRlWHUp.js → index-DcJxrM1F.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-B7aKAZzS.js → index-DgXn2sWa.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-D47robkX.js → index-Dkn1r09O.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-CVrUs6rf.js → index-DmScDPXc.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-Bs69SI96.js → index-DpTaXoo4.js} +1 -1
  114. package/src/assets/web-panel/assets/index-DqADH38Q.js +1 -0
  115. package/src/assets/web-panel/assets/{index-BxWUEFKy.js → index-DvaDSd9k.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-BtlJWaSP.js → index-LIFKNtNq.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BGPEaeB1.js → index-LyzbfmcN.js} +1 -1
  118. package/src/assets/web-panel/assets/index-Pbx8iJt_.js +1 -0
  119. package/src/assets/web-panel/assets/{index-KgXrlfNF.js → index-WGmri9tu.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-XIuZV9by.js → index-Yd1x-xhs.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-Dhfw-BXs.js → index-_OOipSkX.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-Dx0hIfC-.js → index-dwBroN51.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-C7Wt5feN.js → index-e6tj_vqt.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CdyFg4FI.js → index-lOxvi-gf.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-8jffdb6b.js → index-qTrskpW-.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-DmkG479D.js → index-uH-glcG6.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BSpFe6j5.js → index-z5pyzCXj.js} +1 -1
  128. package/src/assets/web-panel/assets/{initDefaultProps-DD5y5msp.js → initDefaultProps-K95PVa57.js} +1 -1
  129. package/src/assets/web-panel/assets/{motion-BuoutMia.js → motion-DwTSyvvW.js} +1 -1
  130. package/src/assets/web-panel/assets/{move-DkpjqOXg.js → move-BuRSogkN.js} +1 -1
  131. package/src/assets/web-panel/assets/{mtc-parser-BmGJD-v-.js → mtc-parser-twEnPl1V.js} +1 -1
  132. package/src/assets/web-panel/assets/{omit-DftUE0Sz.js → omit-CdPhebkX.js} +1 -1
  133. package/src/assets/web-panel/assets/{pickAttrs-DDRb9FIu.js → pickAttrs-CGcIpdPi.js} +1 -1
  134. package/src/assets/web-panel/assets/{placementArrow-DPgtxOOD.js → placementArrow-DixkX9VV.js} +1 -1
  135. package/src/assets/web-panel/assets/{responsiveObserve-BQFRzFeZ.js → responsiveObserve-C74t3rB5.js} +1 -1
  136. package/src/assets/web-panel/assets/{slide-DQ4lmUSz.js → slide-TcNt6K6l.js} +1 -1
  137. package/src/assets/web-panel/assets/{statusUtils-C1TR4ZiQ.js → statusUtils-mpTN9mrS.js} +1 -1
  138. package/src/assets/web-panel/assets/{styleChecker-B-Suj978.js → styleChecker-Bwg8zmIj.js} +1 -1
  139. package/src/assets/web-panel/assets/{useFlexGapSupport-Q7bDZ3EI.js → useFlexGapSupport-Cz4mN4sW.js} +1 -1
  140. package/src/assets/web-panel/assets/{useFs-DHjRLyQH.js → useFs-DF9zRfbI.js} +1 -1
  141. package/src/assets/web-panel/assets/{usePersonalDataHub-lQOvCvCr.js → usePersonalDataHub-BcQ4O2UK.js} +1 -1
  142. package/src/assets/web-panel/assets/{vnode-Ie0g4-p3.js → vnode-juM2dgAV.js} +1 -1
  143. package/src/assets/web-panel/assets/{zoom-CHP0YEoH.js → zoom-c4aJj-vC.js} +1 -1
  144. package/src/assets/web-panel/index.html +1 -1
  145. package/src/commands/agent.js +41 -2
  146. package/src/commands/compliance.js +3 -1
  147. package/src/commands/review.js +47 -17
  148. package/src/commands/session.js +11 -2
  149. package/src/gateways/ws/message-dispatcher.js +145 -165
  150. package/src/gateways/ws/ws-session-gateway.js +39 -5
  151. package/src/harness/jsonl-session-store.js +39 -12
  152. package/src/harness/worktree-isolator.js +5 -0
  153. package/src/lib/agent-core.js +1 -0
  154. package/src/lib/audit-logger.js +3 -1
  155. package/src/lib/chat-core.js +5 -2
  156. package/src/lib/chat-intent-service.js +68 -21
  157. package/src/lib/config-manager.js +25 -1
  158. package/src/lib/credential-guard.js +43 -1
  159. package/src/lib/git-integration.js +5 -0
  160. package/src/lib/interaction-adapter.js +32 -11
  161. package/src/lib/jsonl-session-store.js +1 -0
  162. package/src/lib/llm-config-defaults.js +37 -0
  163. package/src/lib/mcp-oauth.js +76 -10
  164. package/src/lib/permission-engine.js +4 -1
  165. package/src/lib/personal-data-hub-wiring.js +14 -0
  166. package/src/lib/pipeline-orchestrator.js +20 -2
  167. package/src/lib/project-detector.js +44 -3
  168. package/src/lib/project-instructions.js +13 -0
  169. package/src/lib/repl-denials.js +137 -0
  170. package/src/lib/safe-json.js +22 -0
  171. package/src/lib/sandbox-v2.js +7 -6
  172. package/src/lib/search-command.js +65 -0
  173. package/src/lib/settings-hooks.cjs +133 -0
  174. package/src/lib/settings-loader.cjs +16 -7
  175. package/src/lib/social-graph.js +3 -1
  176. package/src/lib/stream-retry.js +27 -0
  177. package/src/lib/turn-context.js +15 -5
  178. package/src/repl/agent-repl.js +81 -8
  179. package/src/runtime/__tests__/message-roles.test.js +36 -3
  180. package/src/runtime/agent-core.js +175 -50
  181. package/src/runtime/coding-agent-contract-shared.cjs +9 -2
  182. package/src/runtime/coding-agent-shell-policy.cjs +23 -2
  183. package/src/runtime/file-ref-expander.js +51 -7
  184. package/src/runtime/headless-runner.js +83 -2
  185. package/src/runtime/headless-stream.js +69 -3
  186. package/src/runtime/mcp-config.js +42 -1
  187. package/src/runtime/message-roles.js +14 -1
  188. package/src/assets/web-panel/assets/ChatBubbleRenderer-Pg7dIsiE.js +0 -1
  189. package/src/assets/web-panel/assets/TimelineRenderer-acXS5N5h.js +0 -1
  190. package/src/assets/web-panel/assets/devWarning-D_lwLZ6O.js +0 -1
  191. package/src/assets/web-panel/assets/index-CEwuCM44.js +0 -1
  192. package/src/assets/web-panel/assets/index-DzzgU4IU.js +0 -1
@@ -29,6 +29,11 @@ import {
29
29
  analyzeContinuation,
30
30
  joinContinuation,
31
31
  } from "../lib/repl-multiline.js";
32
+ import {
33
+ classifyDenial,
34
+ recordDenial,
35
+ formatDenials,
36
+ } from "../lib/repl-denials.js";
32
37
  import { bootstrap, shutdown } from "../runtime/bootstrap.js";
33
38
  import {
34
39
  createSession,
@@ -118,6 +123,14 @@ let _settingsHooks = null;
118
123
  // `!command` auto-triggers an assistant response to its output. undefined =
119
124
  // unset → defaults OFF (opt-in) in shouldRespondToBashCommands.
120
125
  let _respondToBash;
126
+ // .claude/settings.json `autoMode.classifyAllShell` (Claude-Code 2.1.193): route
127
+ // the built-in verification allowlist through the shell-policy classifier (→
128
+ // ApprovalGate confirm) instead of fast-pathing it. false = unset → off.
129
+ let _classifyAllShell = false;
130
+ // Bounded log of tool calls the agent was BLOCKED from running this session
131
+ // (shell-policy / ApprovalGate / settings rule / hook). Surfaced by
132
+ // `/permissions denials` (Claude-Code 2.1.193 "recent denials"). In-memory only.
133
+ const _recentDenials = [];
121
134
 
122
135
  /**
123
136
  * Fire settings.json Notification hooks (observe-only) — the agent needs the
@@ -180,13 +193,17 @@ async function executeTool(name, args) {
180
193
  permissionRules: _permissionRules,
181
194
  permissionConfirm: _permissionConfirm,
182
195
  settingsHooks: _settingsHooks,
196
+ classifyAllShell: _classifyAllShell,
183
197
  });
184
198
  }
185
199
 
186
200
  /**
187
201
  * Agentic loop — wraps agent-core's async generator with REPL display output.
202
+ * Exported so its event-translation contract (checkpoint-mark accuracy for
203
+ * `/rewind`, content/usage extraction, forced-off in-loop compaction) is
204
+ * unit-testable via the `options._coreLoop` injection seam.
188
205
  */
189
- async function agentLoop(messages, options) {
206
+ export async function agentLoop(messages, options) {
190
207
  // Resume-degenerate role merge (Claude Code 2.1.187 parity), gated by the
191
208
  // one-shot `mergeRoles` flag so it fires only on the first model call after
192
209
  // resuming a session whose prior run produced no assistant response. Collapse
@@ -212,12 +229,19 @@ async function agentLoop(messages, options) {
212
229
  `\n ⚠️ ${info.message || `已从 "${info.from}" 切换到 "${info.to}"`}\n`,
213
230
  ),
214
231
  ));
215
- // The REPL runs its own auto-compaction (after each turn, with metrics +
216
- // persisted compact events), so opt out of the agent loop's in-loop
217
- // compaction to avoid compacting the same history twice.
218
- for await (const event of coreAgentLoop(messages, {
219
- autoCompact: false,
232
+ // `_coreLoop` is an injectable seam (defaults to agent-core's loop) so the
233
+ // wrapper's event translation can be unit-tested without a live model.
234
+ const runCoreLoop = options._coreLoop || coreAgentLoop;
235
+ // The tool-result event carries no args; remember the last tool-executing
236
+ // pair so a recorded denial can show what was attempted (/permissions denials).
237
+ let _lastExec = null;
238
+ for await (const event of runCoreLoop(messages, {
220
239
  ...options,
240
+ // FORCE in-loop compaction off — the REPL runs its OWN post-turn compaction
241
+ // (with metrics + persisted compact events), so letting agent-core compact
242
+ // too would trim the same history twice. Placed AFTER the spread so a
243
+ // caller's options.autoCompact can never silently re-enable the double pass.
244
+ autoCompact: false,
221
245
  onProviderFallback,
222
246
  })) {
223
247
  if (event.type === "checkpoint") {
@@ -236,6 +260,7 @@ async function agentLoop(messages, options) {
236
260
  chalk.gray(` ⎌ checkpoint ${event.id} (before ${event.tool})\n`),
237
261
  );
238
262
  } else if (event.type === "tool-executing") {
263
+ _lastExec = { tool: event.tool, args: event.args };
239
264
  process.stdout.write(
240
265
  chalk.gray(
241
266
  ` [${event.tool}] ${formatToolArgs(event.tool, event.args)}\n`,
@@ -246,6 +271,24 @@ async function agentLoop(messages, options) {
246
271
  process.stdout.write(
247
272
  chalk.red(` Error: ${event.error || event.result?.error}\n`),
248
273
  );
274
+ // Record policy denials (not plain tool failures) into the caller's
275
+ // denial log for review via `/permissions denials` (Claude-Code 2.1.193
276
+ // recent denials). Caller passes the session log (mirrors checkpointMarks)
277
+ // so the wrapper stays unit-testable.
278
+ if (Array.isArray(options.denialLog)) {
279
+ const denial = classifyDenial({
280
+ tool: event.tool,
281
+ result: event.result,
282
+ error: event.error,
283
+ argsSummary:
284
+ _lastExec && _lastExec.tool === event.tool
285
+ ? formatToolArgs(event.tool, _lastExec.args)
286
+ : "",
287
+ });
288
+ if (denial) {
289
+ recordDenial(options.denialLog, { ...denial, at: Date.now() });
290
+ }
291
+ }
249
292
  // Parity with Desktop AIChatPage's `Switch to Trusted` button:
250
293
  // when the deny came from ApprovalGate (not shell-policy), surface
251
294
  // the exact CLI command the user can run to relax the per-session
@@ -503,6 +546,11 @@ export async function startAgentRepl(options = {}) {
503
546
  _respondToBash = readBooleanSetting("respondToBashCommands", {
504
547
  cwd: process.cwd(),
505
548
  });
549
+ // Claude-Code 2.1.193 autoMode.classifyAllShell (default OFF when unset).
550
+ _classifyAllShell =
551
+ readBooleanSetting("autoMode.classifyAllShell", {
552
+ cwd: process.cwd(),
553
+ }) === true;
506
554
  const total =
507
555
  loaded.rules.allow.length +
508
556
  loaded.rules.ask.length +
@@ -538,12 +586,21 @@ export async function startAgentRepl(options = {}) {
538
586
  // PostToolUse). The interactive _permissionConfirm above doubles as the
539
587
  // confirmer for a hook `ask` decision.
540
588
  try {
541
- const { loadHooks } = await import("../lib/settings-hooks.cjs");
589
+ const { loadHooks, projectHookTrustNotice } =
590
+ await import("../lib/settings-hooks.cjs");
542
591
  const loaded = loadHooks({ cwd: process.cwd() });
543
592
  _settingsHooks =
544
593
  loaded.hooks && Object.keys(loaded.hooks).length > 0
545
594
  ? loaded.hooks
546
595
  : null;
596
+ // First-run trust notice for an untrusted/cloned repo's shell-running
597
+ // hooks (Claude-Code 2.1.195 parity). Best-effort, stderr-only.
598
+ try {
599
+ const notice = projectHookTrustNotice({ cwd: process.cwd() });
600
+ if (notice) process.stderr.write(notice + "\n");
601
+ } catch {
602
+ /* trust notice is best-effort */
603
+ }
547
604
  } catch (_err) {
548
605
  _settingsHooks = null;
549
606
  }
@@ -1482,7 +1539,7 @@ export async function startAgentRepl(options = {}) {
1482
1539
  ` ${chalk.cyan("/cost")} Session token spend + estimated $ (per model & category)`,
1483
1540
  );
1484
1541
  logger.log(
1485
- ` ${chalk.cyan("/permissions")} Allow/ask/deny rules; set/cycle tier (/permissions <tier> · Shift+Tab cycles)`,
1542
+ ` ${chalk.cyan("/permissions")} Allow/ask/deny rules; set/cycle tier (/permissions <tier> · Shift+Tab); /permissions denials to review blocked calls`,
1486
1543
  );
1487
1544
  logger.log(
1488
1545
  ` ${chalk.cyan("/export")} Save this conversation to a Markdown file (/export [path])`,
@@ -3034,6 +3091,13 @@ export async function startAgentRepl(options = {}) {
3034
3091
  // parity): what the agent runs unprompted, asks about, or is blocked from.
3035
3092
  if (trimmed === "/permissions" || trimmed.startsWith("/permissions ")) {
3036
3093
  const arg = trimmed.slice("/permissions".length).trim();
3094
+ if (arg === "denials" || arg === "denied") {
3095
+ // Review what the agent was BLOCKED from running this session
3096
+ // (Claude-Code 2.1.193 "recent denials").
3097
+ logger.log(formatDenials(_recentDenials));
3098
+ prompt();
3099
+ return;
3100
+ }
3037
3101
  if (arg) {
3038
3102
  // Set this session's approval tier mid-session (Claude-Code
3039
3103
  // permission-mode / Shift+Tab parity; mirrors `cc session policy --set`).
@@ -3079,6 +3143,13 @@ export async function startAgentRepl(options = {}) {
3079
3143
  " Set tier mid-session: /permissions <strict|trusted|autopilot>",
3080
3144
  ),
3081
3145
  );
3146
+ if (_recentDenials.length) {
3147
+ logger.log(
3148
+ chalk.gray(
3149
+ ` ${_recentDenials.length} tool call(s) denied this session — /permissions denials to review`,
3150
+ ),
3151
+ );
3152
+ }
3082
3153
  prompt();
3083
3154
  return;
3084
3155
  }
@@ -3511,11 +3582,13 @@ export async function startAgentRepl(options = {}) {
3511
3582
  autoCheckpoint,
3512
3583
  checkpointSession: sessionId,
3513
3584
  checkpointMarks: _checkpointMarks,
3585
+ denialLog: _recentDenials,
3514
3586
  prepareCall,
3515
3587
  approvalGate: _approvalGate,
3516
3588
  permissionRules: _permissionRules,
3517
3589
  permissionConfirm: _permissionConfirm,
3518
3590
  settingsHooks: _settingsHooks,
3591
+ classifyAllShell: _classifyAllShell,
3519
3592
  // Interactive session: gate run_code through the ApprovalGate (like
3520
3593
  // run_shell) so a strict tier prompts before arbitrary code runs.
3521
3594
  interactiveApproval: true,
@@ -51,13 +51,38 @@ describe("mergeConsecutiveMessages", () => {
51
51
  // A multi-tool turn produces back-to-back `tool` results — folding them
52
52
  // would drop tool_call_ids and corrupt the tool-call pairing.
53
53
  const msgs = [
54
- { role: "assistant", content: "", tool_calls: [{ id: "a" }, { id: "b" }] },
54
+ {
55
+ role: "assistant",
56
+ content: "",
57
+ tool_calls: [{ id: "a" }, { id: "b" }],
58
+ },
55
59
  { role: "tool", content: "r1", tool_call_id: "a" },
56
60
  { role: "tool", content: "r2", tool_call_id: "b" },
57
61
  ];
58
62
  expect(mergeConsecutiveMessages(msgs)).toEqual(msgs);
59
63
  });
60
64
 
65
+ it("never merges a structured assistant tool-call turn (tool_calls preserved)", () => {
66
+ // Two consecutive assistants where the SECOND carries tool_calls — folding
67
+ // would drop them (orphaning the result) and the strict API would 400.
68
+ const msgs = [
69
+ { role: "assistant", content: "let me check" },
70
+ { role: "assistant", content: "", tool_calls: [{ id: "c1" }] },
71
+ { role: "tool", content: "result", tool_call_id: "c1" },
72
+ ];
73
+ expect(mergeConsecutiveMessages(msgs)).toEqual(msgs);
74
+ });
75
+
76
+ it("never folds two consecutive assistants when the FIRST has tool_calls", () => {
77
+ // e.g. after a compaction dropped the tool results — merging would mutate
78
+ // the structured turn's content and keep its now-unanswered tool_calls.
79
+ const msgs = [
80
+ { role: "assistant", content: "", tool_calls: [{ id: "a" }] },
81
+ { role: "assistant", content: "fallback text" },
82
+ ];
83
+ expect(mergeConsecutiveMessages(msgs)).toEqual(msgs);
84
+ });
85
+
61
86
  it("never merges system messages (leading base + context pair kept distinct)", () => {
62
87
  const msgs = [
63
88
  { role: "system", content: "base" },
@@ -202,14 +227,22 @@ describe("collapseConsecutiveMessagesInPlace", () => {
202
227
  const messages = [
203
228
  { role: "user", content: "u1" },
204
229
  { role: "user", content: "u2" },
205
- { role: "assistant", content: "", tool_calls: [{ id: "a" }, { id: "b" }] },
230
+ {
231
+ role: "assistant",
232
+ content: "",
233
+ tool_calls: [{ id: "a" }, { id: "b" }],
234
+ },
206
235
  { role: "tool", content: "r1", tool_call_id: "a" },
207
236
  { role: "tool", content: "r2", tool_call_id: "b" },
208
237
  ];
209
238
  expect(collapseConsecutiveMessagesInPlace(messages)).toBe(true);
210
239
  expect(messages).toEqual([
211
240
  { role: "user", content: "u1\n\nu2" },
212
- { role: "assistant", content: "", tool_calls: [{ id: "a" }, { id: "b" }] },
241
+ {
242
+ role: "assistant",
243
+ content: "",
244
+ tool_calls: [{ id: "a" }, { id: "b" }],
245
+ },
213
246
  { role: "tool", content: "r1", tool_call_id: "a" },
214
247
  { role: "tool", content: "r2", tool_call_id: "b" },
215
248
  ]);
@@ -43,10 +43,11 @@ import {
43
43
  import { createToolContext } from "../tools/tool-context.js";
44
44
  import { createToolTelemetryRecord } from "../tools/tool-telemetry.js";
45
45
  import { isAbortError, throwIfAborted } from "../lib/abort-utils.js";
46
+ import { buildSearchCommand } from "../lib/search-command.js";
46
47
  import {
47
48
  isRetryableStreamError,
48
- STREAM_RETRY_MAX,
49
49
  STREAM_RETRY_BASE_MS,
50
+ resolveStreamRetryMax,
50
51
  } from "../lib/stream-retry.js";
51
52
  import {
52
53
  annotateLines,
@@ -755,6 +756,27 @@ export function tokenizeShellWords(input) {
755
756
  return args;
756
757
  }
757
758
 
759
+ /**
760
+ * Literal string edit shared by the edit_file preview + execution paths.
761
+ * Uses split/join so `old_string` and `new_string` are treated as LITERAL text
762
+ * (no regex / `$&` / `$1` pattern interpretation — a plain `String.replace`
763
+ * with a string pattern still expands `$` sequences in the replacement).
764
+ * Returns the occurrence count so the caller can require a UNIQUE match
765
+ * (Claude-Code Edit parity): replacing the first of several identical strings
766
+ * silently edits the wrong place.
767
+ *
768
+ * @returns {{ count:number, newContent:string }}
769
+ */
770
+ function applyLiteralEdit(content, oldStr, newStr, replaceAll) {
771
+ const parts = content.split(oldStr);
772
+ const count = parts.length - 1;
773
+ if (count === 0) return { count: 0, newContent: content };
774
+ const newContent = replaceAll
775
+ ? parts.join(newStr)
776
+ : parts[0] + newStr + parts.slice(1).join(oldStr); // first occurrence only
777
+ return { count, newContent };
778
+ }
779
+
758
780
  /**
759
781
  * Compute the content an edit tool WOULD write, without writing it — the
760
782
  * left/right sides for an IDE diff review. Mirrors the corresponding
@@ -781,15 +803,22 @@ export function computeProposedEdit(name, args = {}, cwd = process.cwd()) {
781
803
  const content = fs.readFileSync(filePath, "utf8");
782
804
  if (
783
805
  typeof args.old_string !== "string" ||
784
- !content.includes(args.old_string)
806
+ args.old_string === "" ||
807
+ typeof args.new_string !== "string"
785
808
  ) {
786
809
  return null;
787
810
  }
788
- return {
789
- filePath,
790
- newContent: content.replace(args.old_string, () => args.new_string),
791
- originalText: content,
792
- };
811
+ const replaceAll = args.replace_all === true;
812
+ const { count, newContent } = applyLiteralEdit(
813
+ content,
814
+ args.old_string,
815
+ args.new_string,
816
+ replaceAll,
817
+ );
818
+ // No match, or a non-unique match without replace_all → the edit will be
819
+ // rejected, so show no (misleading) diff and let the tool report it.
820
+ if (count === 0 || (count > 1 && !replaceAll)) return null;
821
+ return { filePath, newContent, originalText: content };
793
822
  }
794
823
  if (name === "edit_file_hashed") {
795
824
  if (!fs.existsSync(filePath)) return null;
@@ -965,7 +994,7 @@ export async function executeTool(name, args, context = {}) {
965
994
  // 1. settings deny
966
995
  if (settingsVerdict.decision === "deny") {
967
996
  return {
968
- error: `[Permission] Tool "${name}" denied by settings rule: ${settingsVerdict.rule}`,
997
+ error: `[Permission] Tool "${name}" denied by settings rule: ${settingsVerdict.rule}. This is a configured policy — retrying won't help; tell the user if the task genuinely needs it.`,
969
998
  policy: { decision: "deny", rule: settingsVerdict.rule, via: "settings" },
970
999
  };
971
1000
  }
@@ -1037,7 +1066,7 @@ export async function executeTool(name, args, context = {}) {
1037
1066
  : false;
1038
1067
  if (!ok) {
1039
1068
  return {
1040
- error: `[Permission] Tool "${name}" requires confirmation (settings rule: ${settingsVerdict.rule}) — denied.`,
1069
+ error: `[Permission] Tool "${name}" requires confirmation (settings rule: ${settingsVerdict.rule}) but this run is non-interactive — denied. Do not retry; tell the user this action needs their approval.`,
1041
1070
  policy: {
1042
1071
  decision: "ask",
1043
1072
  rule: settingsVerdict.rule,
@@ -1594,6 +1623,47 @@ export function renderNotebook(text) {
1594
1623
  return lines.join("\n");
1595
1624
  }
1596
1625
 
1626
+ /**
1627
+ * Ingest the raw stdout of a search_files command into the shared hit
1628
+ * accumulator: split into lines, dedup via `seen`, redact credential-file
1629
+ * content hits, label multi-root results, and stop at the hit cap. Pure aside
1630
+ * from mutating the passed-in `seen`/`hits`/`redactedCreds` collectors.
1631
+ *
1632
+ * Shared by the success path AND the maxBuffer-overflow salvage path: a search
1633
+ * on a large tree (especially Windows, where `findstr`/`dir /s` have no `head`
1634
+ * cap) can exceed execSync's maxBuffer and throw ENOBUFS — but the error still
1635
+ * carries the first maxBuffer of matches in `err.stdout`, so the same ingest is
1636
+ * run on it instead of dropping every match as a false "no matches".
1637
+ *
1638
+ * @param {string} output raw command stdout (or a truncated partial)
1639
+ * @param {object} ctx { isContent, root, multiRoot, seen, hits, redactedCreds,
1640
+ * credentialFileReason, limit? }
1641
+ */
1642
+ export function _ingestSearchOutput(output, ctx) {
1643
+ const limit = ctx.limit ?? 20;
1644
+ for (const line of String(output || "")
1645
+ .trim()
1646
+ .split("\n")) {
1647
+ const v = line.trim();
1648
+ if (!v || ctx.seen.has(v)) continue;
1649
+ if (ctx.isContent) {
1650
+ // content hit is `file:line:text` (findstr /n) or a bare filename
1651
+ // (grep -l); pull the source file and redact credential matches.
1652
+ const src = v.match(/^(.+?):\d+:/)?.[1] ?? v;
1653
+ if (ctx.credentialFileReason(src)) {
1654
+ ctx.redactedCreds.add(src.replace(/\\/g, "/"));
1655
+ ctx.seen.add(v);
1656
+ continue;
1657
+ }
1658
+ }
1659
+ // Qualify with the root so multi-root results stay unambiguous.
1660
+ const labeled = ctx.multiRoot ? `${ctx.root}: ${v}` : v;
1661
+ ctx.seen.add(v);
1662
+ ctx.hits.push(labeled);
1663
+ if (ctx.hits.length >= limit) return;
1664
+ }
1665
+ }
1666
+
1597
1667
  /**
1598
1668
  * Inner tool execution — no hooks, no plan-mode checks.
1599
1669
  */
@@ -1612,6 +1682,7 @@ async function executeToolInner(
1612
1682
  mcpClient,
1613
1683
  llmOptions,
1614
1684
  shellPolicyOverrides,
1685
+ classifyAllShell = false,
1615
1686
  approvalGate,
1616
1687
  shellConfirm,
1617
1688
  additionalDirectories,
@@ -1663,6 +1734,13 @@ async function executeToolInner(
1663
1734
  if (!fs.existsSync(filePath)) {
1664
1735
  return attachDescriptor({ error: `File not found: ${filePath}` });
1665
1736
  }
1737
+ // A clear, self-correcting error beats the cryptic "EISDIR: illegal
1738
+ // operation on a directory" that readFileSync throws on a directory.
1739
+ if (fs.statSync(filePath).isDirectory()) {
1740
+ return attachDescriptor({
1741
+ error: `Path is a directory, not a file: ${filePath}. Use list_dir to see its contents.`,
1742
+ });
1743
+ }
1666
1744
  const content = fs.readFileSync(filePath, "utf8");
1667
1745
  // Jupyter notebooks: render a compact cell listing (index/id/type/source,
1668
1746
  // outputs summarized) so the model can find cells for notebook_edit
@@ -1763,20 +1841,40 @@ async function executeToolInner(
1763
1841
  if (!fs.existsSync(filePath)) {
1764
1842
  return attachDescriptor({ error: `File not found: ${filePath}` });
1765
1843
  }
1766
- const content = fs.readFileSync(filePath, "utf8");
1767
- if (!content.includes(args.old_string)) {
1768
- return attachDescriptor({ error: "old_string not found in file" });
1844
+ if (typeof args.old_string !== "string" || args.old_string === "") {
1845
+ return attachDescriptor({
1846
+ error: "old_string must be a non-empty string",
1847
+ });
1848
+ }
1849
+ if (typeof args.new_string !== "string") {
1850
+ return attachDescriptor({ error: "new_string must be a string" });
1769
1851
  }
1770
- const newContent = content.replace(
1852
+ const content = fs.readFileSync(filePath, "utf8");
1853
+ const replaceAll = args.replace_all === true;
1854
+ const { count, newContent } = applyLiteralEdit(
1855
+ content,
1771
1856
  args.old_string,
1772
- () => args.new_string,
1857
+ args.new_string,
1858
+ replaceAll,
1773
1859
  );
1860
+ if (count === 0) {
1861
+ return attachDescriptor({ error: "old_string not found in file" });
1862
+ }
1863
+ // Require a UNIQUE match (Claude-Code Edit parity) — replacing the first
1864
+ // of several identical strings silently edits the wrong occurrence.
1865
+ if (count > 1 && !replaceAll) {
1866
+ return attachDescriptor({
1867
+ error: `old_string is not unique — it appears ${count} times. Add surrounding context so it matches exactly one occurrence, or pass replace_all:true to change all of them.`,
1868
+ occurrences: count,
1869
+ });
1870
+ }
1774
1871
  const wrote = writeFileVerified(filePath, newContent);
1775
1872
  if (wrote.error) return attachDescriptor({ error: wrote.error });
1776
1873
  return attachDescriptor({
1777
1874
  success: true,
1778
1875
  path: filePath,
1779
1876
  size: wrote.size,
1877
+ replaced: replaceAll ? count : 1,
1780
1878
  });
1781
1879
  }
1782
1880
 
@@ -1831,9 +1929,12 @@ async function executeToolInner(
1831
1929
  }
1832
1930
 
1833
1931
  case "run_shell": {
1834
- const shellPolicyOpts = shellPolicyOverrides
1835
- ? { overrideRuleIds: shellPolicyOverrides }
1836
- : {};
1932
+ const shellPolicyOpts = {
1933
+ ...(shellPolicyOverrides
1934
+ ? { overrideRuleIds: shellPolicyOverrides }
1935
+ : {}),
1936
+ ...(classifyAllShell ? { classifyAllShell: true } : {}),
1937
+ };
1837
1938
  const override = getRuntimeToolDescriptorByCommand(args.command);
1838
1939
  let shellPolicy;
1839
1940
  let approvalOutcome = null;
@@ -1857,12 +1958,18 @@ async function executeToolInner(
1857
1958
  policy: gated.policy,
1858
1959
  };
1859
1960
  if (!gated.allowed) {
1961
+ // Make a policy denial ACTIONABLE for the model (Claude-Code 2.1.193
1962
+ // "denial reasons to transcripts"): tell it this won't change on
1963
+ // retry and to involve the user, so it stops re-issuing the same
1964
+ // blocked command (which otherwise just burns turns + tokens).
1965
+ const tierLabel =
1966
+ typeof gated.policy === "string" ? `"${gated.policy}" ` : "";
1860
1967
  return attachDescriptor(
1861
1968
  {
1862
1969
  error:
1863
1970
  gated.via === "shell-policy"
1864
- ? `[Shell Policy] ${gated.reason}`
1865
- : `[ApprovalGate] command denied (${gated.via})`,
1971
+ ? `[Shell Policy] ${gated.reason} This command is blocked by policy and will not run — do not retry it; find another approach.`
1972
+ : `[ApprovalGate] command denied by the ${tierLabel}approval policy (via ${gated.via}). Retrying the same command will not help — it needs user approval. Tell the user (they can run it themselves, approve it, or relax the policy) and continue with other work.`,
1866
1973
  shellCommandPolicy: shellPolicy,
1867
1974
  approval: approvalOutcome,
1868
1975
  },
@@ -1874,7 +1981,7 @@ async function executeToolInner(
1874
1981
  if (!shellPolicy.allowed) {
1875
1982
  return attachDescriptor(
1876
1983
  {
1877
- error: `[Shell Policy] ${shellPolicy.reason}`,
1984
+ error: `[Shell Policy] ${shellPolicy.reason} This command is blocked by policy and will not run — do not retry it; find another approach.`,
1878
1985
  shellCommandPolicy: shellPolicy,
1879
1986
  },
1880
1987
  override || runtimeDescriptor,
@@ -2137,12 +2244,17 @@ async function executeToolInner(
2137
2244
  args: { language: args.language },
2138
2245
  });
2139
2246
  if (gate.decision !== APPROVAL_DECISION.ALLOW) {
2247
+ // Actionable denial (matches run_shell, Claude-Code 2.1.193): tell the
2248
+ // model retrying won't help and to involve the user.
2249
+ const tierLabel =
2250
+ typeof gate.policy === "string" ? `"${gate.policy}" ` : "";
2140
2251
  return attachDescriptor({
2141
- error: `[ApprovalGate] run_code denied (${gate.via})`,
2252
+ error: `[ApprovalGate] run_code denied by the ${tierLabel}approval policy (via ${gate.via}). Retrying won't help — running arbitrary code needs user approval. Tell the user (they can approve it or relax the policy) and continue with other work.`,
2142
2253
  approval: {
2143
2254
  decision: gate.decision,
2144
2255
  via: gate.via,
2145
2256
  riskLevel: "high",
2257
+ policy: gate.policy,
2146
2258
  },
2147
2259
  });
2148
2260
  }
@@ -2303,13 +2415,14 @@ async function executeToolInner(
2303
2415
  ? [path.resolve(cwd, args.directory)]
2304
2416
  : [cwd, ...extraRoots];
2305
2417
  const isContent = Boolean(args.content_search);
2306
- const cmd = isContent
2307
- ? process.platform === "win32"
2308
- ? `findstr /s /i /n "${args.pattern}" *`
2309
- : `grep -r -l -i "${args.pattern}" . --include="*" 2>/dev/null | head -20`
2310
- : process.platform === "win32"
2311
- ? `dir /s /b *${args.pattern}* 2>NUL`
2312
- : `find . -name "*${args.pattern}*" -type f 2>/dev/null | head -20`;
2418
+ // Pattern is model/user-supplied and flows into a shell — build the
2419
+ // command with it SAFELY quoted (see search-command.js). Raw interpolation
2420
+ // here was a command-injection bypass of the run_shell guards.
2421
+ const built = buildSearchCommand({ pattern: args.pattern, isContent });
2422
+ if (built.error) {
2423
+ return attachDescriptor({ error: built.error });
2424
+ }
2425
+ const cmd = built.cmd;
2313
2426
 
2314
2427
  // Credential guard (Claude-Code 2.1.189 parity): a CONTENT search must not
2315
2428
  // become a side channel that exfils secrets the read_file / run_shell
@@ -2326,34 +2439,34 @@ async function executeToolInner(
2326
2439
  const redactedCreds = new Set();
2327
2440
  for (const root of roots) {
2328
2441
  if (hits.length >= 20) break;
2442
+ const ictx = {
2443
+ isContent,
2444
+ root,
2445
+ multiRoot: roots.length > 1,
2446
+ seen,
2447
+ hits,
2448
+ redactedCreds,
2449
+ credentialFileReason,
2450
+ };
2329
2451
  try {
2330
2452
  if (!fs.existsSync(root)) continue;
2331
2453
  const output = execSync(cmd, {
2332
2454
  cwd: root,
2333
2455
  encoding: "utf8",
2334
2456
  timeout: 10000,
2457
+ // Windows `findstr`/`dir /s` have no `head` cap (unlike the POSIX
2458
+ // `| head -20`), so a large tree can blow past execSync's 1 MB
2459
+ // default and throw ENOBUFS. Give it real headroom AND salvage the
2460
+ // partial below — otherwise a busy repo silently reports "no matches".
2461
+ maxBuffer: 8 * 1024 * 1024,
2335
2462
  });
2336
- for (const line of output.trim().split("\n")) {
2337
- const v = line.trim();
2338
- if (!v || seen.has(v)) continue;
2339
- if (isContent) {
2340
- // content hit is `file:line:text` (findstr /n) or a bare filename
2341
- // (grep -l); pull the source file and redact credential matches.
2342
- const src = v.match(/^(.+?):\d+:/)?.[1] ?? v;
2343
- if (credentialFileReason(src)) {
2344
- redactedCreds.add(src.replace(/\\/g, "/"));
2345
- seen.add(v);
2346
- continue;
2347
- }
2348
- }
2349
- // Qualify with the root so multi-root results stay unambiguous.
2350
- const labeled = roots.length > 1 ? `${root}: ${v}` : v;
2351
- seen.add(v);
2352
- hits.push(labeled);
2353
- if (hits.length >= 20) break;
2354
- }
2355
- } catch {
2356
- // No matches in this root — continue to the next.
2463
+ _ingestSearchOutput(output, ictx);
2464
+ } catch (err) {
2465
+ // A maxBuffer overflow still carries the first 8 MB of matches in
2466
+ // err.stdout — ingest those rather than dropping every hit as a false
2467
+ // "No matches found". A genuine no-match / command failure leaves
2468
+ // err.stdout empty, so hits stay unchanged (same as before).
2469
+ if (err && err.stdout) _ingestSearchOutput(err.stdout, ictx);
2357
2470
  }
2358
2471
  }
2359
2472
 
@@ -2376,6 +2489,12 @@ async function executeToolInner(
2376
2489
  if (!fs.existsSync(dirPath)) {
2377
2490
  return attachDescriptor({ error: `Directory not found: ${dirPath}` });
2378
2491
  }
2492
+ // Clear error instead of the cryptic "ENOTDIR" readdirSync throws on a file.
2493
+ if (!fs.statSync(dirPath).isDirectory()) {
2494
+ return attachDescriptor({
2495
+ error: `Path is a file, not a directory: ${dirPath}. Use read_file to read it.`,
2496
+ });
2497
+ }
2379
2498
  const entries = fs.readdirSync(dirPath, { withFileTypes: true });
2380
2499
  return attachDescriptor({
2381
2500
  entries: entries.map((e) => ({
@@ -3685,7 +3804,9 @@ const _STREAM_STALL = Symbol("stream-stall");
3685
3804
  * @param {object} opts { signal?, retries?, baseDelayMs?, onRetry?, sleep? }
3686
3805
  */
3687
3806
  export async function _retryStreamingChat(streamFn, opts = {}) {
3688
- const retries = opts.retries ?? STREAM_RETRY_MAX;
3807
+ // Default budget honors CC_MAX_RETRIES / CLAUDE_CODE_MAX_RETRIES (capped 15);
3808
+ // an explicit opts.retries still wins (tests / callers that pin it).
3809
+ const retries = opts.retries ?? resolveStreamRetryMax();
3689
3810
  const base = opts.baseDelayMs ?? STREAM_RETRY_BASE_MS;
3690
3811
  const signal = opts.signal;
3691
3812
  const sleep = opts.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
@@ -4501,6 +4622,10 @@ export async function* agentLoop(messages, options) {
4501
4622
  parentMessages: messages, // pass parent messages for sub-agent auto-condensation
4502
4623
  interaction: options.interaction || null,
4503
4624
  shellPolicyOverrides: options.shellPolicyOverrides || null,
4625
+ // autoMode.classifyAllShell (Claude-Code 2.1.193): when true, the built-in
4626
+ // verification allowlist (npm test / rg / …) is classified through the
4627
+ // ApprovalGate instead of fast-pathed, so no shell command auto-runs.
4628
+ classifyAllShell: options.classifyAllShell || false,
4504
4629
  approvalGate: options.approvalGate || null,
4505
4630
  shellConfirm: options.shellConfirm || null,
4506
4631
  // Interactive sessions (the REPL) set this so run_code is gated through the
@@ -141,19 +141,26 @@ const CODING_AGENT_TOOL_CONTRACTS = Object.freeze([
141
141
  title: "Edit File",
142
142
  kind: "filesystem",
143
143
  tier: "mvp",
144
- description: "Replace a specific string in a file with new content",
144
+ description:
145
+ "Replace a string in a file. old_string must match EXACTLY ONE place — include enough surrounding context to make it unique, or the edit is rejected. Pass replace_all:true to change every occurrence instead.",
145
146
  inputSchema: {
146
147
  type: "object",
147
148
  properties: {
148
149
  path: { type: "string", description: "File path" },
149
150
  old_string: {
150
151
  type: "string",
151
- description: "Exact string to find and replace",
152
+ description:
153
+ "Exact string to replace; must be unique in the file unless replace_all is true",
152
154
  },
153
155
  new_string: {
154
156
  type: "string",
155
157
  description: "Replacement string",
156
158
  },
159
+ replace_all: {
160
+ type: "boolean",
161
+ description:
162
+ "Replace every occurrence of old_string instead of requiring a unique match (default false)",
163
+ },
157
164
  },
158
165
  required: ["path", "old_string", "new_string"],
159
166
  },