@namzu/sdk 51.1.0 → 52.1.0

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 (395) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/dist/agents/QueryAgent.d.ts.map +1 -1
  3. package/dist/agents/QueryAgent.js +2 -0
  4. package/dist/agents/QueryAgent.js.map +1 -1
  5. package/dist/agents/SupervisorAgent.d.ts +1 -1
  6. package/dist/agents/SupervisorAgent.d.ts.map +1 -1
  7. package/dist/agents/SupervisorAgent.js.map +1 -1
  8. package/dist/agents/examples/SupervisorAgent.d.ts +42 -0
  9. package/dist/agents/examples/SupervisorAgent.d.ts.map +1 -1
  10. package/dist/agents/examples/SupervisorAgent.js +80 -4
  11. package/dist/agents/examples/SupervisorAgent.js.map +1 -1
  12. package/dist/agents/index.d.ts +1 -1
  13. package/dist/agents/index.d.ts.map +1 -1
  14. package/dist/agents/index.js.map +1 -1
  15. package/dist/authorization/protected-paths.d.ts +36 -0
  16. package/dist/authorization/protected-paths.d.ts.map +1 -0
  17. package/dist/authorization/protected-paths.js +107 -0
  18. package/dist/authorization/protected-paths.js.map +1 -0
  19. package/dist/authorization/readonly-commands.d.ts +39 -0
  20. package/dist/authorization/readonly-commands.d.ts.map +1 -0
  21. package/dist/authorization/readonly-commands.js +797 -0
  22. package/dist/authorization/readonly-commands.js.map +1 -0
  23. package/dist/authorization/readonly-flags.d.ts +19 -0
  24. package/dist/authorization/readonly-flags.d.ts.map +1 -0
  25. package/dist/authorization/readonly-flags.js +178 -0
  26. package/dist/authorization/readonly-flags.js.map +1 -0
  27. package/dist/authorization/removal.d.ts +52 -0
  28. package/dist/authorization/removal.d.ts.map +1 -0
  29. package/dist/authorization/removal.js +283 -0
  30. package/dist/authorization/removal.js.map +1 -0
  31. package/dist/authorization/shell-paths.d.ts +66 -0
  32. package/dist/authorization/shell-paths.d.ts.map +1 -0
  33. package/dist/authorization/shell-paths.js +697 -0
  34. package/dist/authorization/shell-paths.js.map +1 -0
  35. package/dist/bridge/acp/update.d.ts.map +1 -1
  36. package/dist/bridge/acp/update.js +1 -0
  37. package/dist/bridge/acp/update.js.map +1 -1
  38. package/dist/compaction/context-window.d.ts +24 -0
  39. package/dist/compaction/context-window.d.ts.map +1 -1
  40. package/dist/compaction/context-window.js +34 -0
  41. package/dist/compaction/context-window.js.map +1 -1
  42. package/dist/compaction/manual.d.ts.map +1 -1
  43. package/dist/compaction/manual.js +10 -2
  44. package/dist/compaction/manual.js.map +1 -1
  45. package/dist/compaction/plan.d.ts +4 -0
  46. package/dist/compaction/plan.d.ts.map +1 -1
  47. package/dist/compaction/plan.js +6 -2
  48. package/dist/compaction/plan.js.map +1 -1
  49. package/dist/compaction/verifier.d.ts +11 -1
  50. package/dist/compaction/verifier.d.ts.map +1 -1
  51. package/dist/compaction/verifier.js +75 -23
  52. package/dist/compaction/verifier.js.map +1 -1
  53. package/dist/config/runtime.d.ts +55 -7
  54. package/dist/config/runtime.d.ts.map +1 -1
  55. package/dist/config/runtime.js +23 -2
  56. package/dist/config/runtime.js.map +1 -1
  57. package/dist/connector/web/guarded-fetch.d.ts.map +1 -1
  58. package/dist/connector/web/guarded-fetch.js +24 -0
  59. package/dist/connector/web/guarded-fetch.js.map +1 -1
  60. package/dist/connector/web/index.d.ts +1 -1
  61. package/dist/connector/web/index.d.ts.map +1 -1
  62. package/dist/connector/web/page.d.ts +59 -0
  63. package/dist/connector/web/page.d.ts.map +1 -0
  64. package/dist/connector/web/page.js +112 -0
  65. package/dist/connector/web/page.js.map +1 -0
  66. package/dist/connector/web/types.d.ts +47 -0
  67. package/dist/connector/web/types.d.ts.map +1 -1
  68. package/dist/connector/web/types.js.map +1 -1
  69. package/dist/constants/continuation.d.ts +8 -0
  70. package/dist/constants/continuation.d.ts.map +1 -1
  71. package/dist/constants/continuation.js +8 -0
  72. package/dist/constants/continuation.js.map +1 -1
  73. package/dist/constants/plugin/index.d.ts +1 -1
  74. package/dist/constants/plugin/index.d.ts.map +1 -1
  75. package/dist/constants/plugin/index.js +1 -1
  76. package/dist/constants/plugin/index.js.map +1 -1
  77. package/dist/contracts/a2a.d.ts +2 -2
  78. package/dist/manager/agent/lifecycle.d.ts +7 -0
  79. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  80. package/dist/manager/agent/lifecycle.js +11 -2
  81. package/dist/manager/agent/lifecycle.js.map +1 -1
  82. package/dist/manager/session/turn-recorder.d.ts.map +1 -1
  83. package/dist/manager/session/turn-recorder.js +4 -2
  84. package/dist/manager/session/turn-recorder.js.map +1 -1
  85. package/dist/plugin/index.d.ts +1 -1
  86. package/dist/plugin/index.d.ts.map +1 -1
  87. package/dist/plugin/index.js +1 -1
  88. package/dist/plugin/index.js.map +1 -1
  89. package/dist/plugin/lifecycle.d.ts +6 -0
  90. package/dist/plugin/lifecycle.d.ts.map +1 -1
  91. package/dist/plugin/lifecycle.js +98 -114
  92. package/dist/plugin/lifecycle.js.map +1 -1
  93. package/dist/plugin/shell-hook.d.ts +90 -22
  94. package/dist/plugin/shell-hook.d.ts.map +1 -1
  95. package/dist/plugin/shell-hook.js +434 -41
  96. package/dist/plugin/shell-hook.js.map +1 -1
  97. package/dist/provider/capabilities.d.ts +9 -0
  98. package/dist/provider/capabilities.d.ts.map +1 -1
  99. package/dist/provider/capabilities.js +15 -0
  100. package/dist/provider/capabilities.js.map +1 -1
  101. package/dist/provider/errors.d.ts.map +1 -1
  102. package/dist/provider/errors.js +13 -0
  103. package/dist/provider/errors.js.map +1 -1
  104. package/dist/provider/fallback.d.ts.map +1 -1
  105. package/dist/provider/fallback.js +10 -0
  106. package/dist/provider/fallback.js.map +1 -1
  107. package/dist/provider/idle-timeout.d.ts.map +1 -1
  108. package/dist/provider/idle-timeout.js +5 -0
  109. package/dist/provider/idle-timeout.js.map +1 -1
  110. package/dist/provider/image-input-refusal.d.ts +9 -0
  111. package/dist/provider/image-input-refusal.d.ts.map +1 -0
  112. package/dist/provider/image-input-refusal.js +54 -0
  113. package/dist/provider/image-input-refusal.js.map +1 -0
  114. package/dist/provider/index.d.ts +2 -1
  115. package/dist/provider/index.d.ts.map +1 -1
  116. package/dist/provider/index.js +2 -1
  117. package/dist/provider/index.js.map +1 -1
  118. package/dist/provider/retry.d.ts +7 -2
  119. package/dist/provider/retry.d.ts.map +1 -1
  120. package/dist/provider/retry.js +38 -4
  121. package/dist/provider/retry.js.map +1 -1
  122. package/dist/public-runtime.d.ts +5 -2
  123. package/dist/public-runtime.d.ts.map +1 -1
  124. package/dist/public-runtime.js +5 -2
  125. package/dist/public-runtime.js.map +1 -1
  126. package/dist/public-types.d.ts +2 -2
  127. package/dist/public-types.d.ts.map +1 -1
  128. package/dist/runtime/jobs/registry.d.ts +21 -0
  129. package/dist/runtime/jobs/registry.d.ts.map +1 -1
  130. package/dist/runtime/jobs/registry.js +15 -2
  131. package/dist/runtime/jobs/registry.js.map +1 -1
  132. package/dist/runtime/query/deferred-sandbox.d.ts +54 -0
  133. package/dist/runtime/query/deferred-sandbox.d.ts.map +1 -0
  134. package/dist/runtime/query/deferred-sandbox.js +75 -0
  135. package/dist/runtime/query/deferred-sandbox.js.map +1 -0
  136. package/dist/runtime/query/executor/tool-call-admission.d.ts +10 -0
  137. package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -1
  138. package/dist/runtime/query/executor/tool-call-admission.js +74 -13
  139. package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
  140. package/dist/runtime/query/executor.d.ts +51 -2
  141. package/dist/runtime/query/executor.d.ts.map +1 -1
  142. package/dist/runtime/query/executor.js +214 -9
  143. package/dist/runtime/query/executor.js.map +1 -1
  144. package/dist/runtime/query/finalize-turn.d.ts +6 -0
  145. package/dist/runtime/query/finalize-turn.d.ts.map +1 -1
  146. package/dist/runtime/query/finalize-turn.js +1 -1
  147. package/dist/runtime/query/finalize-turn.js.map +1 -1
  148. package/dist/runtime/query/index.d.ts +29 -0
  149. package/dist/runtime/query/index.d.ts.map +1 -1
  150. package/dist/runtime/query/index.js +108 -28
  151. package/dist/runtime/query/index.js.map +1 -1
  152. package/dist/runtime/query/iteration/index.d.ts +57 -0
  153. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  154. package/dist/runtime/query/iteration/index.js +380 -28
  155. package/dist/runtime/query/iteration/index.js.map +1 -1
  156. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +61 -0
  157. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts.map +1 -0
  158. package/dist/runtime/query/iteration/phases/compaction-breaker.js +94 -0
  159. package/dist/runtime/query/iteration/phases/compaction-breaker.js.map +1 -0
  160. package/dist/runtime/query/iteration/phases/compaction.d.ts +42 -1
  161. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  162. package/dist/runtime/query/iteration/phases/compaction.js +203 -23
  163. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  164. package/dist/runtime/query/iteration/phases/context.d.ts +10 -0
  165. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  166. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  167. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  168. package/dist/runtime/query/iteration/phases/tool-review.js +165 -2
  169. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  170. package/dist/runtime/query/iteration/provider-rejected-image.d.ts +21 -1
  171. package/dist/runtime/query/iteration/provider-rejected-image.d.ts.map +1 -1
  172. package/dist/runtime/query/iteration/provider-rejected-image.js +53 -7
  173. package/dist/runtime/query/iteration/provider-rejected-image.js.map +1 -1
  174. package/dist/runtime/query/iteration/request-repair.d.ts +108 -0
  175. package/dist/runtime/query/iteration/request-repair.d.ts.map +1 -0
  176. package/dist/runtime/query/iteration/request-repair.js +593 -0
  177. package/dist/runtime/query/iteration/request-repair.js.map +1 -0
  178. package/dist/runtime/query/iteration/stream-turn.d.ts +6 -0
  179. package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
  180. package/dist/runtime/query/iteration/stream-turn.js +2 -2
  181. package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
  182. package/dist/runtime/query/path-escalation.d.ts +36 -0
  183. package/dist/runtime/query/path-escalation.d.ts.map +1 -0
  184. package/dist/runtime/query/path-escalation.js +184 -0
  185. package/dist/runtime/query/path-escalation.js.map +1 -0
  186. package/dist/runtime/query/plugin-hooks.d.ts.map +1 -1
  187. package/dist/runtime/query/plugin-hooks.js +8 -0
  188. package/dist/runtime/query/plugin-hooks.js.map +1 -1
  189. package/dist/runtime/query/reminders.d.ts +35 -0
  190. package/dist/runtime/query/reminders.d.ts.map +1 -0
  191. package/dist/runtime/query/reminders.js +7 -0
  192. package/dist/runtime/query/reminders.js.map +1 -0
  193. package/dist/runtime/query/request-rich-content.d.ts +5 -1
  194. package/dist/runtime/query/request-rich-content.d.ts.map +1 -1
  195. package/dist/runtime/query/request-rich-content.js +67 -25
  196. package/dist/runtime/query/request-rich-content.js.map +1 -1
  197. package/dist/runtime/query/resume-pending.d.ts.map +1 -1
  198. package/dist/runtime/query/resume-pending.js +26 -0
  199. package/dist/runtime/query/resume-pending.js.map +1 -1
  200. package/dist/runtime/query/review-policy.d.ts +26 -0
  201. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  202. package/dist/runtime/query/review-policy.js +79 -5
  203. package/dist/runtime/query/review-policy.js.map +1 -1
  204. package/dist/runtime/query/tool-output-budget.d.ts +46 -0
  205. package/dist/runtime/query/tool-output-budget.d.ts.map +1 -1
  206. package/dist/runtime/query/tool-output-budget.js +85 -1
  207. package/dist/runtime/query/tool-output-budget.js.map +1 -1
  208. package/dist/runtime/query/tooling.d.ts +1 -0
  209. package/dist/runtime/query/tooling.d.ts.map +1 -1
  210. package/dist/runtime/query/tooling.js +5 -0
  211. package/dist/runtime/query/tooling.js.map +1 -1
  212. package/dist/session/paths.d.ts +5 -1
  213. package/dist/session/paths.d.ts.map +1 -1
  214. package/dist/session/paths.js +8 -0
  215. package/dist/session/paths.js.map +1 -1
  216. package/dist/store/memory/disk.d.ts.map +1 -1
  217. package/dist/store/memory/disk.js +2 -2
  218. package/dist/store/memory/disk.js.map +1 -1
  219. package/dist/store/memory/markdown.d.ts.map +1 -1
  220. package/dist/store/memory/markdown.js +2 -2
  221. package/dist/store/memory/markdown.js.map +1 -1
  222. package/dist/store/memory/naming.d.ts +9 -0
  223. package/dist/store/memory/naming.d.ts.map +1 -1
  224. package/dist/store/memory/naming.js +12 -0
  225. package/dist/store/memory/naming.js.map +1 -1
  226. package/dist/tools/builtins/bash.d.ts +30 -1
  227. package/dist/tools/builtins/bash.d.ts.map +1 -1
  228. package/dist/tools/builtins/bash.js +170 -26
  229. package/dist/tools/builtins/bash.js.map +1 -1
  230. package/dist/tools/builtins/computer-use.d.ts +1 -1
  231. package/dist/tools/builtins/computer-use.d.ts.map +1 -1
  232. package/dist/tools/builtins/computer-use.js +12 -5
  233. package/dist/tools/builtins/computer-use.js.map +1 -1
  234. package/dist/tools/builtins/content-fingerprint.d.ts +2 -0
  235. package/dist/tools/builtins/content-fingerprint.d.ts.map +1 -1
  236. package/dist/tools/builtins/content-fingerprint.js +4 -0
  237. package/dist/tools/builtins/content-fingerprint.js.map +1 -1
  238. package/dist/tools/builtins/edit.d.ts.map +1 -1
  239. package/dist/tools/builtins/edit.js +60 -18
  240. package/dist/tools/builtins/edit.js.map +1 -1
  241. package/dist/tools/builtins/grep.d.ts +8 -1
  242. package/dist/tools/builtins/grep.d.ts.map +1 -1
  243. package/dist/tools/builtins/grep.js +152 -33
  244. package/dist/tools/builtins/grep.js.map +1 -1
  245. package/dist/tools/builtins/read-file.d.ts +1 -0
  246. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  247. package/dist/tools/builtins/read-file.js +44 -5
  248. package/dist/tools/builtins/read-file.js.map +1 -1
  249. package/dist/tools/builtins/read-render.d.ts +52 -0
  250. package/dist/tools/builtins/read-render.d.ts.map +1 -1
  251. package/dist/tools/builtins/read-render.js +143 -0
  252. package/dist/tools/builtins/read-render.js.map +1 -1
  253. package/dist/tools/builtins/web.d.ts +13 -2
  254. package/dist/tools/builtins/web.d.ts.map +1 -1
  255. package/dist/tools/builtins/web.js +225 -32
  256. package/dist/tools/builtins/web.js.map +1 -1
  257. package/dist/tools/coordinator/ask-user-question.d.ts +2 -0
  258. package/dist/tools/coordinator/ask-user-question.d.ts.map +1 -1
  259. package/dist/tools/coordinator/ask-user-question.js +256 -92
  260. package/dist/tools/coordinator/ask-user-question.js.map +1 -1
  261. package/dist/tools/coordinator/question-options.d.ts +1 -0
  262. package/dist/tools/coordinator/question-options.d.ts.map +1 -1
  263. package/dist/tools/coordinator/question-options.js +6 -1
  264. package/dist/tools/coordinator/question-options.js.map +1 -1
  265. package/dist/tools/defineTool.d.ts +4 -1
  266. package/dist/tools/defineTool.d.ts.map +1 -1
  267. package/dist/tools/defineTool.js +6 -1
  268. package/dist/tools/defineTool.js.map +1 -1
  269. package/dist/tools/paths.d.ts +6 -0
  270. package/dist/tools/paths.d.ts.map +1 -1
  271. package/dist/tools/paths.js +1 -1
  272. package/dist/tools/paths.js.map +1 -1
  273. package/dist/types/agent/query.d.ts +3 -0
  274. package/dist/types/agent/query.d.ts.map +1 -1
  275. package/dist/types/errors/catalog.js +1 -1
  276. package/dist/types/errors/catalog.js.map +1 -1
  277. package/dist/types/hitl/index.d.ts +104 -0
  278. package/dist/types/hitl/index.d.ts.map +1 -1
  279. package/dist/types/hitl/index.js.map +1 -1
  280. package/dist/types/message/index.d.ts +1 -1
  281. package/dist/types/message/index.d.ts.map +1 -1
  282. package/dist/types/message/index.js +2 -0
  283. package/dist/types/message/index.js.map +1 -1
  284. package/dist/types/plugin/index.d.ts +93 -19
  285. package/dist/types/plugin/index.d.ts.map +1 -1
  286. package/dist/types/plugin/index.js +3 -0
  287. package/dist/types/plugin/index.js.map +1 -1
  288. package/dist/types/provider/errors.d.ts +19 -0
  289. package/dist/types/provider/errors.d.ts.map +1 -1
  290. package/dist/types/provider/errors.js +54 -5
  291. package/dist/types/provider/errors.js.map +1 -1
  292. package/dist/types/provider/interface.d.ts +12 -0
  293. package/dist/types/provider/interface.d.ts.map +1 -1
  294. package/dist/types/provider/stream.d.ts +1 -1
  295. package/dist/types/session/config.d.ts +14 -0
  296. package/dist/types/session/config.d.ts.map +1 -1
  297. package/dist/types/session/events.d.ts +27 -4
  298. package/dist/types/session/events.d.ts.map +1 -1
  299. package/dist/types/session/events.js.map +1 -1
  300. package/dist/types/session/records.d.ts +9 -9
  301. package/dist/types/session/records.d.ts.map +1 -1
  302. package/dist/types/session/records.js +1 -0
  303. package/dist/types/session/records.js.map +1 -1
  304. package/dist/types/session/stop-reason.d.ts +5 -0
  305. package/dist/types/session/stop-reason.d.ts.map +1 -1
  306. package/dist/types/tool/index.d.ts +52 -0
  307. package/dist/types/tool/index.d.ts.map +1 -1
  308. package/dist/types/tool/index.js.map +1 -1
  309. package/package.json +4 -2
  310. package/src/agents/QueryAgent.ts +5 -1
  311. package/src/agents/SupervisorAgent.ts +1 -1
  312. package/src/agents/examples/SupervisorAgent.ts +217 -106
  313. package/src/agents/index.ts +1 -1
  314. package/src/authorization/protected-paths.ts +110 -0
  315. package/src/authorization/readonly-commands.ts +771 -0
  316. package/src/authorization/readonly-flags.ts +196 -0
  317. package/src/authorization/removal.ts +326 -0
  318. package/src/authorization/shell-paths.ts +927 -0
  319. package/src/bridge/acp/update.ts +1 -0
  320. package/src/compaction/context-window.ts +41 -0
  321. package/src/compaction/manual.ts +10 -2
  322. package/src/compaction/plan.ts +11 -2
  323. package/src/compaction/verifier.ts +84 -22
  324. package/src/config/runtime.ts +23 -2
  325. package/src/connector/web/guarded-fetch.ts +26 -0
  326. package/src/connector/web/index.ts +3 -0
  327. package/src/connector/web/page.ts +130 -0
  328. package/src/connector/web/types.ts +47 -0
  329. package/src/constants/continuation.ts +10 -0
  330. package/src/constants/plugin/index.ts +1 -1
  331. package/src/manager/agent/lifecycle.ts +12 -4
  332. package/src/manager/session/turn-recorder.ts +10 -2
  333. package/src/plugin/index.ts +1 -0
  334. package/src/plugin/lifecycle.ts +107 -119
  335. package/src/plugin/shell-hook.ts +477 -43
  336. package/src/provider/capabilities.ts +17 -0
  337. package/src/provider/errors.ts +13 -0
  338. package/src/provider/fallback.ts +10 -0
  339. package/src/provider/idle-timeout.ts +5 -0
  340. package/src/provider/image-input-refusal.ts +54 -0
  341. package/src/provider/index.ts +2 -0
  342. package/src/provider/retry.ts +42 -4
  343. package/src/public-runtime.ts +11 -0
  344. package/src/public-types.ts +4 -0
  345. package/src/runtime/jobs/registry.ts +39 -2
  346. package/src/runtime/query/deferred-sandbox.ts +110 -0
  347. package/src/runtime/query/executor/tool-call-admission.ts +79 -13
  348. package/src/runtime/query/executor.ts +267 -11
  349. package/src/runtime/query/finalize-turn.ts +7 -1
  350. package/src/runtime/query/index.ts +169 -52
  351. package/src/runtime/query/iteration/index.ts +439 -35
  352. package/src/runtime/query/iteration/phases/compaction-breaker.ts +108 -0
  353. package/src/runtime/query/iteration/phases/compaction.ts +241 -38
  354. package/src/runtime/query/iteration/phases/context.ts +11 -0
  355. package/src/runtime/query/iteration/phases/tool-review.ts +177 -2
  356. package/src/runtime/query/iteration/provider-rejected-image.ts +71 -5
  357. package/src/runtime/query/iteration/request-repair.ts +743 -0
  358. package/src/runtime/query/iteration/stream-turn.ts +11 -1
  359. package/src/runtime/query/path-escalation.ts +204 -0
  360. package/src/runtime/query/plugin-hooks.ts +8 -0
  361. package/src/runtime/query/reminders.ts +45 -0
  362. package/src/runtime/query/request-rich-content.ts +77 -27
  363. package/src/runtime/query/resume-pending.ts +27 -0
  364. package/src/runtime/query/review-policy.ts +106 -5
  365. package/src/runtime/query/tool-output-budget.ts +115 -1
  366. package/src/runtime/query/tooling.ts +6 -0
  367. package/src/session/paths.ts +11 -1
  368. package/src/store/memory/disk.ts +2 -1
  369. package/src/store/memory/markdown.ts +2 -1
  370. package/src/store/memory/naming.ts +13 -0
  371. package/src/tools/builtins/bash.ts +203 -34
  372. package/src/tools/builtins/computer-use.ts +13 -4
  373. package/src/tools/builtins/content-fingerprint.ts +5 -0
  374. package/src/tools/builtins/edit.ts +69 -19
  375. package/src/tools/builtins/grep.ts +159 -38
  376. package/src/tools/builtins/read-file.ts +48 -5
  377. package/src/tools/builtins/read-render.ts +193 -0
  378. package/src/tools/builtins/web.ts +258 -33
  379. package/src/tools/coordinator/ask-user-question.ts +309 -103
  380. package/src/tools/coordinator/question-options.ts +8 -1
  381. package/src/tools/defineTool.ts +11 -2
  382. package/src/tools/paths.ts +1 -1
  383. package/src/types/agent/query.ts +3 -0
  384. package/src/types/errors/catalog.ts +1 -1
  385. package/src/types/hitl/index.ts +104 -0
  386. package/src/types/message/index.ts +2 -0
  387. package/src/types/plugin/index.ts +101 -18
  388. package/src/types/provider/errors.ts +66 -5
  389. package/src/types/provider/interface.ts +13 -0
  390. package/src/types/provider/stream.ts +1 -1
  391. package/src/types/session/config.ts +14 -0
  392. package/src/types/session/events.ts +32 -4
  393. package/src/types/session/records.ts +1 -0
  394. package/src/types/session/stop-reason.ts +5 -0
  395. package/src/types/tool/index.ts +53 -2
@@ -23,13 +23,19 @@
23
23
  * one inherited from the host's own environment. A hook that needs "which
24
24
  * turn" has it exactly when there is one.
25
25
  *
26
- * Its exit code is its answer:
26
+ * Its answer is the exit code and, on exit `0`, an optional JSON object on
27
+ * stdout. The exit codes:
27
28
  *
28
- * - `0` — carry on.
29
- * - `2` — **block.** Before a tool (`pre_tool_use`) the call is skipped and the
30
- * model is told why, with the hook's stderr as the reason. On any other
31
- * event a 2 is reported and the turn carries on: there is nothing left to
32
- * block once the tool has run or the turn has started.
29
+ * - `0` — carry on. On `user_prompt_submit`, plain (non-JSON) stdout becomes
30
+ * context for the model.
31
+ * - `2` — **block**, read per event: before a tool (`pre_tool_use`) the call is
32
+ * skipped and the model is told why, with the hook's stderr as the reason;
33
+ * on `user_prompt_submit` the turn ends; on `turn_end` and `subagent_stop`
34
+ * the stderr is fed back to the model and the turn goes on; on
35
+ * `pre_compact` a compaction the context did not force is cancelled; on
36
+ * `post_tool_use` and `post_tool_use_failure` the stderr is shown to the
37
+ * model with that call's result. On every other event a 2 is reported and
38
+ * the turn carries on.
33
39
  * - anything else — the hook's own failure. Reported through the logger,
34
40
  * never blocking. A formatter that crashed must not stop the agent editing,
35
41
  * and a script missing its interpreter (127) must not read as "forbidden".
@@ -37,12 +43,41 @@
37
43
  * `2` rather than any non-zero, because a verdict and a failure are different
38
44
  * facts and one exit code cannot carry both.
39
45
  *
40
- * ## What a hook cannot do here
46
+ * ## The JSON answer
41
47
  *
42
- * Modify a tool's input or replace its result. The hook protocol allows both
43
- * (`modify`, `replace`); this adapter exposes neither, because a shell command
44
- * that rewrites what the model asked for deserves its own design and audit,
45
- * not a JSON field on stdout nobody reviewed.
48
+ * Exit `0` with a JSON object on stdout carries the fields below. Output that
49
+ * does not start with `{` is plain text and is not read as JSON; output that
50
+ * starts with `{` but does not parse, or has a field of the wrong type, is
51
+ * reported and ignored, and the call carries on.
52
+ *
53
+ * - `continue: false` with `stopReason` — stop the turn after the current
54
+ * step; `stopReason` is shown to the operator.
55
+ * - `decision: "block"` with `reason` — refuse; `decision: "approve"` allows a
56
+ * tool call or a permission request.
57
+ * - `systemMessage` — a warning for the operator, reported through the logger.
58
+ * - `suppressOutput` — keep this hook's stdout out of the debug log.
59
+ * - `hookSpecificOutput` with `hookEventName` naming this event (Namzu's
60
+ * name or the reference spelling `PreToolUse`, `Stop`, ...):
61
+ * `permissionDecision` (`allow`, `deny` or `ask`) with
62
+ * `permissionDecisionReason`, `updatedInput` and `additionalContext` for
63
+ * `pre_tool_use`; `additionalContext` for `user_prompt_submit`,
64
+ * `post_tool_use`, `post_tool_use_failure`, `turn_end`, `subagent_stop`,
65
+ * `session_start`, `notification`; and for `permission_request` a `decision`
66
+ * `{ behavior: "allow" }` or `{ behavior: "deny", message, interrupt }`.
67
+ *
68
+ * ## What a hook may decide, and what it may not
69
+ *
70
+ * A hook may rewrite a tool call's input (`updatedInput`) and answer a
71
+ * permission question. Both stay inside the kernel's own rules:
72
+ *
73
+ * - `updatedInput` goes through the same validation as a model-supplied
74
+ * input, and the rewritten call is then checked by the authorization gate
75
+ * and the review policy as it now reads.
76
+ * - `allow` stands in for a person's yes only on a call that would have been
77
+ * asked about because it changes something. It never covers a sandbox
78
+ * escape, a path outside the roots, a call the tool always asks about, a
79
+ * call a rule sent to review explicitly, or a destructive one, and it
80
+ * never reopens what a rule denied. `deny` always wins over `allow`.
46
81
  *
47
82
  * ## Bounds
48
83
  *
@@ -53,13 +88,18 @@
53
88
  * {@link attachShellHooks}, and the refusal names the new one: a hook that
54
89
  * silently stopped firing is worse than a config that fails to load.
55
90
  *
56
- * Each hook has a deadline (`timeoutMs`, default 30 s, capped at ten minutes)
91
+ * Each hook has a deadline (`timeoutMs`, default ten minutes, also the cap)
57
92
  * and its output is captured to a bound. A hook that times out is reported
58
93
  * and does not block: one that could not answer in time has not answered no.
59
94
  */
60
95
 
61
96
  import type { spawn } from 'node:child_process'
62
97
 
98
+ import { decodedCommands, decomposeCommandLine } from '../authorization/command-line.js'
99
+ import {
100
+ SKILL_TOOL_NAME_ALIASES,
101
+ permissionPatternToRegExpSource,
102
+ } from '../authorization/skill-grant.js'
63
103
  import { spawnHostShell } from '../tools/command-shell.js'
64
104
  import type { PluginId } from '../types/ids/index.js'
65
105
  import type {
@@ -67,7 +107,9 @@ import type {
67
107
  PluginHookContext,
68
108
  PluginHookDefinition,
69
109
  PluginHookEvent,
110
+ PluginHookPermission,
70
111
  PluginHookResult,
112
+ PluginNotificationInfo,
71
113
  } from '../types/plugin/index.js'
72
114
  import { RENAMED_PLUGIN_HOOK_EVENTS } from '../types/plugin/index.js'
73
115
  import { asPluginId } from '../utils/id.js'
@@ -87,6 +129,9 @@ export type ShellHookEvent = Extract<
87
129
  | 'session_end'
88
130
  | 'pre_tool_use'
89
131
  | 'post_tool_use'
132
+ | 'post_tool_use_failure'
133
+ | 'permission_request'
134
+ | 'notification'
90
135
  | 'pre_compact'
91
136
  | 'post_compact'
92
137
  | 'subagent_stop'
@@ -100,6 +145,9 @@ export const SHELL_HOOK_EVENTS: readonly ShellHookEvent[] = [
100
145
  'session_end',
101
146
  'pre_tool_use',
102
147
  'post_tool_use',
148
+ 'post_tool_use_failure',
149
+ 'permission_request',
150
+ 'notification',
103
151
  'pre_compact',
104
152
  'post_compact',
105
153
  'subagent_stop',
@@ -111,10 +159,27 @@ export const SHELL_HOOK_EVENTS: readonly ShellHookEvent[] = [
111
159
  export interface ShellHookEntry {
112
160
  /** Run with `sh -c`, in the working directory, with the event JSON on stdin. */
113
161
  readonly command: string
114
- /** Tool names this applies to: `*`, a name, or `a|b|prefix*`. Absent means every tool. */
162
+ /**
163
+ * What this applies to. On a tool event (and `permission_request`) the tool
164
+ * name; on `notification` the notification type. `*` or absent matches
165
+ * everything; a list of names (`a|b`, `a,b`, each optionally ending in `*`
166
+ * as a prefix) matches those; anything else is a regular expression tested
167
+ * against the name.
168
+ */
115
169
  readonly matcher?: string
116
170
  /** Deadline in milliseconds; default `DEFAULT_SHELL_HOOK_TIMEOUT_MS`, capped at `MAX_SHELL_HOOK_TIMEOUT_MS`. */
117
171
  readonly timeoutMs?: number
172
+ /**
173
+ * A permission-rule filter on a tool event: `bash(git *)` runs the hook
174
+ * only for a `bash` call whose command matches, `edit` for any `edit`.
175
+ * Checked before the process is spawned. A filter that cannot be read
176
+ * runs the hook, so a guard is never switched off by a typo.
177
+ */
178
+ readonly if?: string
179
+ /** Run once in a session, then never again in it. */
180
+ readonly once?: boolean
181
+ /** Text for the operator's status line while the hook runs. */
182
+ readonly statusMessage?: string
118
183
  }
119
184
 
120
185
  /** Event → entries, the shape a host's config file carries. */
@@ -125,15 +190,20 @@ export type ShellHooksConfig = {
125
190
  /** The plugin id host shell hooks register under when the host names none. */
126
191
  export const SHELL_HOOKS_PLUGIN_ID: PluginId = asPluginId('6d6f6bec-9d54-485b-90ca-52edbac1009d')
127
192
 
128
- export const DEFAULT_SHELL_HOOK_TIMEOUT_MS = 30_000
193
+ export const DEFAULT_SHELL_HOOK_TIMEOUT_MS = 600_000
129
194
  export const MAX_SHELL_HOOK_TIMEOUT_MS = 10 * 60_000
130
195
  /** Captured output bound per stream. */
131
196
  const MAX_CAPTURE_BYTES = 64 * 1024
132
197
 
133
198
  /**
134
- * `matcher` against a tool name: `*`, or a `|`-separated list of names, each
135
- * of which may end in `*`. Case-sensitive, because tool names are. A matcher
136
- * on an event with no tool (`turn_start`) matches nothing unless it is `*`.
199
+ * `matcher` against a name (a tool's, or a notification's type).
200
+ *
201
+ * Empty or `*` matches everything. A list of names separated by `|` or `,`
202
+ * matches exactly, each part optionally ending in `*` as a prefix (the
203
+ * spelling this adapter has always taken). Anything else is a regular
204
+ * expression, tested unanchored against the name; one that does not compile
205
+ * matches nothing. Case-sensitive, because tool names are. A matcher on an
206
+ * event that has no name (`turn_start`) matches nothing unless it is `*`.
137
207
  */
138
208
  export function shellHookMatches(
139
209
  matcher: string | undefined,
@@ -141,13 +211,64 @@ export function shellHookMatches(
141
211
  ): boolean {
142
212
  if (matcher === undefined || matcher.trim() === '' || matcher.trim() === '*') return true
143
213
  if (toolName === undefined) return false
144
- return matcher.split('|').some((part) => {
145
- const candidate = part.trim()
146
- if (candidate.length === 0) return false
147
- return candidate.endsWith('*')
148
- ? toolName.startsWith(candidate.slice(0, -1))
149
- : toolName === candidate
150
- })
214
+ const parts = matcher.split(/[|,]/).map((part) => part.trim())
215
+ if (parts.every((part) => /^[A-Za-z0-9_.:-]+\*?$/.test(part))) {
216
+ return parts.some((candidate) =>
217
+ candidate.endsWith('*')
218
+ ? toolName.startsWith(candidate.slice(0, -1))
219
+ : toolName === candidate,
220
+ )
221
+ }
222
+ try {
223
+ return new RegExp(matcher).test(toolName)
224
+ } catch {
225
+ return false
226
+ }
227
+ }
228
+
229
+ /**
230
+ * A hook's `if` against one tool call: `Tool` or `Tool(pattern)`. The tool
231
+ * part is case-insensitive and takes the names skills write (`Bash`,
232
+ * `Read`). A pattern is matched against a shell tool's command line and only
233
+ * there; for a tool with no command line the filter cannot be read, which
234
+ * runs the hook.
235
+ */
236
+ export function shellHookIfMatches(
237
+ filter: string | undefined,
238
+ toolName: string | undefined,
239
+ toolInput: unknown,
240
+ ): boolean {
241
+ if (filter === undefined || filter.trim() === '') return true
242
+ if (toolName === undefined) return true
243
+ const shape = /^([A-Za-z0-9_.-]+)(?:\(([\s\S]*)\))?$/.exec(filter.trim())
244
+ if (!shape) return true
245
+ const written = (shape[1] ?? '').toLowerCase()
246
+ const tool = SKILL_TOOL_NAME_ALIASES[written] ?? written
247
+ if (tool !== toolName.toLowerCase()) return false
248
+ const specifier = shape[2]?.trim()
249
+ if (specifier === undefined || specifier === '' || specifier.replaceAll('*', '') === '')
250
+ return true
251
+ if (tool !== 'bash') return true
252
+ const line =
253
+ toolInput !== null && typeof toolInput === 'object'
254
+ ? (toolInput as Record<string, unknown>).command
255
+ : undefined
256
+ if (typeof line !== 'string') return false
257
+ const pattern = specifier.endsWith(':*') ? `${specifier.slice(0, -2)} *` : specifier
258
+ try {
259
+ const matcher = new RegExp(permissionPatternToRegExpSource(pattern))
260
+ const decomposed = decomposeCommandLine(line, 'sh')
261
+ // A line the reader cannot see through may run the very command the
262
+ // filter names, so the hook runs. A filter only ever narrows when the
263
+ // whole line is visible and none of its commands matches.
264
+ if (decomposed.opaque) return true
265
+ const candidates = [...decomposed.segments, ...decodedCommands(line, 'sh')].map((each) =>
266
+ each.trim(),
267
+ )
268
+ return candidates.some((each) => matcher.test(each))
269
+ } catch {
270
+ return true
271
+ }
151
272
  }
152
273
 
153
274
  /** What one hook run came back with, before it is mapped to a verdict. */
@@ -180,6 +301,10 @@ export interface ShellHookInput {
180
301
  readonly output: string
181
302
  readonly error?: string
182
303
  }
304
+ /** On `notification`: what the host is telling the operator. */
305
+ readonly notification?: PluginNotificationInfo
306
+ /** On `turn_end` and `subagent_stop`: an earlier answer already kept this turn going. */
307
+ readonly stopHookActive?: boolean
183
308
  }
184
309
 
185
310
  /** Run one hook command to completion, bounded by its deadline and capture. */
@@ -204,6 +329,16 @@ export function runShellHook(
204
329
  ...(input.toolName !== undefined ? { tool_name: input.toolName } : {}),
205
330
  ...(input.toolInput !== undefined ? { tool_input: input.toolInput } : {}),
206
331
  ...(input.toolResult !== undefined ? { tool_result: input.toolResult } : {}),
332
+ ...(input.event === 'post_tool_use_failure' && input.toolResult?.error !== undefined
333
+ ? { error: input.toolResult.error }
334
+ : {}),
335
+ ...(input.notification !== undefined
336
+ ? {
337
+ notification_type: input.notification.type,
338
+ message: input.notification.message,
339
+ }
340
+ : {}),
341
+ ...(input.stopHookActive === true ? { stop_hook_active: true } : {}),
207
342
  })
208
343
  const toolPath = pathOf(input.toolInput)
209
344
  // A hook outside a turn must not see a turn id, including one the host
@@ -323,6 +458,167 @@ function pathOf(toolInput: unknown): string | undefined {
323
458
  return undefined
324
459
  }
325
460
 
461
+ /** The reference spelling of each event name, which a hook's JSON may use. */
462
+ const REFERENCE_EVENT_NAMES: Readonly<Partial<Record<ShellHookEvent, string>>> = {
463
+ user_prompt_submit: 'UserPromptSubmit',
464
+ session_start: 'SessionStart',
465
+ session_end: 'SessionEnd',
466
+ pre_tool_use: 'PreToolUse',
467
+ post_tool_use: 'PostToolUse',
468
+ post_tool_use_failure: 'PostToolUseFailure',
469
+ permission_request: 'PermissionRequest',
470
+ notification: 'Notification',
471
+ pre_compact: 'PreCompact',
472
+ post_compact: 'PostCompact',
473
+ subagent_stop: 'SubagentStop',
474
+ turn_end: 'Stop',
475
+ }
476
+
477
+ /** What a hook's stdout JSON said, before it is mapped to a verdict for an event. */
478
+ interface ShellHookReading {
479
+ /** Refuse (or, on a stop event, keep going; on `pre_compact`, cancel). */
480
+ block?: string
481
+ permission?: PluginHookPermission
482
+ updatedInput?: Record<string, unknown>
483
+ context?: string
484
+ halt?: { reason: string }
485
+ notice?: string
486
+ /** The hook asked for its stdout to stay out of the debug log. */
487
+ suppressOutput?: boolean
488
+ /** Why the JSON was set aside, when it was. */
489
+ invalid?: string
490
+ }
491
+
492
+ function isRecord(value: unknown): value is Record<string, unknown> {
493
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
494
+ }
495
+
496
+ /**
497
+ * Read a hook's stdout as its JSON answer, or `undefined` when it is plain
498
+ * text. Strict about types the way the contract is: a wrong type anywhere
499
+ * sets the whole object aside rather than half-obeying it, so a typo in a
500
+ * guard's `decision` is reported instead of silently becoming "continue".
501
+ */
502
+ function readShellHookJson(event: ShellHookEvent, stdout: string): ShellHookReading | undefined {
503
+ const text = stdout.trim()
504
+ if (!text.startsWith('{')) return undefined
505
+ let parsed: unknown
506
+ try {
507
+ parsed = JSON.parse(text)
508
+ } catch (error) {
509
+ return {
510
+ invalid: `stdout starts with "{" but is not valid JSON (${(error as Error).message})`,
511
+ }
512
+ }
513
+ if (!isRecord(parsed)) return { invalid: 'stdout JSON is not an object' }
514
+ const optionalString = (
515
+ key: string,
516
+ from: Record<string, unknown> = parsed,
517
+ ): string | undefined => (typeof from[key] === 'string' ? (from[key] as string) : undefined)
518
+ const wrongType = (
519
+ key: string,
520
+ type: 'string' | 'boolean',
521
+ from: Record<string, unknown> = parsed,
522
+ ) => {
523
+ const value = from[key]
524
+ if (value === undefined) return undefined
525
+ const fits = type === 'string' ? typeof value === 'string' : typeof value === 'boolean'
526
+ return fits ? undefined : `\`${key}\` must be a ${type}`
527
+ }
528
+
529
+ for (const [key, type] of [
530
+ ['continue', 'boolean'],
531
+ ['suppressOutput', 'boolean'],
532
+ ['stopReason', 'string'],
533
+ ['reason', 'string'],
534
+ ['systemMessage', 'string'],
535
+ ] as const) {
536
+ const problem = wrongType(key, type)
537
+ if (problem) return { invalid: problem }
538
+ }
539
+ if (
540
+ parsed.decision !== undefined &&
541
+ parsed.decision !== 'approve' &&
542
+ parsed.decision !== 'block'
543
+ ) {
544
+ return { invalid: '`decision` must be "approve" or "block"' }
545
+ }
546
+
547
+ const reading: ShellHookReading = {}
548
+ if (parsed.suppressOutput === true) reading.suppressOutput = true
549
+ const systemMessage = optionalString('systemMessage')
550
+ if (systemMessage) reading.notice = systemMessage
551
+ if (parsed.continue === false) {
552
+ reading.halt = {
553
+ reason: optionalString('stopReason') ?? 'Stopped by a hook',
554
+ }
555
+ }
556
+ const reason = optionalString('reason')
557
+ if (parsed.decision === 'block') reading.block = reason ?? 'Blocked by hook'
558
+ if (parsed.decision === 'approve')
559
+ reading.permission = { decision: 'allow', ...(reason ? { reason } : {}) }
560
+
561
+ const specific = parsed.hookSpecificOutput
562
+ if (specific === undefined) return reading
563
+ if (!isRecord(specific)) return { invalid: '`hookSpecificOutput` must be an object' }
564
+ const named = specific.hookEventName
565
+ if (typeof named !== 'string') {
566
+ return { invalid: '`hookSpecificOutput` is missing its `hookEventName`' }
567
+ }
568
+ if (named !== event && named !== REFERENCE_EVENT_NAMES[event]) {
569
+ return {
570
+ invalid: `\`hookSpecificOutput.hookEventName\` is "${named}" but this hook ran for ${event}`,
571
+ }
572
+ }
573
+ for (const [key, type] of [
574
+ ['permissionDecisionReason', 'string'],
575
+ ['additionalContext', 'string'],
576
+ ] as const) {
577
+ const problem = wrongType(key, type, specific)
578
+ if (problem) return { invalid: `hookSpecificOutput: ${problem}` }
579
+ }
580
+ const additional = optionalString('additionalContext', specific)
581
+ if (additional && additional.trim().length > 0) reading.context = additional.trim()
582
+
583
+ if (event === 'pre_tool_use') {
584
+ const decision = specific.permissionDecision
585
+ const why = optionalString('permissionDecisionReason', specific)
586
+ if (decision !== undefined) {
587
+ if (decision === 'deny') reading.block = why ?? reason ?? 'Blocked by hook'
588
+ else if (decision === 'allow' || decision === 'ask') {
589
+ reading.permission = { decision, ...(why ? { reason: why } : {}) }
590
+ } else if (decision !== 'defer') {
591
+ return {
592
+ invalid: '`permissionDecision` must be "allow", "deny" or "ask"',
593
+ }
594
+ }
595
+ // "defer" hands the decision to a separate permission service the kernel
596
+ // does not have: nothing is decided, and the ordinary rules apply.
597
+ }
598
+ if (specific.updatedInput !== undefined) {
599
+ if (!isRecord(specific.updatedInput)) return { invalid: '`updatedInput` must be an object' }
600
+ reading.updatedInput = specific.updatedInput
601
+ }
602
+ }
603
+ if (event === 'permission_request') {
604
+ const decision = specific.decision
605
+ if (!isRecord(decision)) {
606
+ return {
607
+ invalid: '`decision` must be {"behavior":"allow"} or {"behavior":"deny","message":"..."}',
608
+ }
609
+ }
610
+ if (decision.behavior === 'allow') {
611
+ reading.permission = { decision: 'allow' }
612
+ } else if (decision.behavior === 'deny') {
613
+ reading.block = optionalString('message', decision) ?? 'Denied by hook'
614
+ if (decision.interrupt === true) reading.halt = { reason: reading.block }
615
+ } else {
616
+ return { invalid: '`decision.behavior` must be "allow" or "deny"' }
617
+ }
618
+ }
619
+ return reading
620
+ }
621
+
326
622
  /**
327
623
  * The verdict, from the outcome and the event. Pure apart from the logger,
328
624
  * so the mapping — the part that decides what the model is told — is
@@ -349,31 +645,157 @@ export function shellHookVerdict(
349
645
  log.warn('hook timed out', attrs)
350
646
  return { action: 'continue' }
351
647
  }
352
- if (outcome.exitCode === 0) {
648
+ if (outcome.exitCode === 2) {
649
+ const reason =
650
+ outcome.stderr.trim() || outcome.stdout.trim() || `blocked by hook \`${entry.command}\``
651
+ switch (event) {
652
+ case 'pre_tool_use':
653
+ case 'user_prompt_submit':
654
+ // Fed back to the model, and the turn goes on: the model is not
655
+ // finished until the hook is satisfied.
656
+ case 'turn_end':
657
+ case 'subagent_stop':
658
+ case 'pre_compact':
659
+ return { action: 'skip', reason }
660
+ // The call has run. What the hook has to say rides with its result.
661
+ case 'post_tool_use':
662
+ case 'post_tool_use_failure':
663
+ return { action: 'continue', context: reason }
664
+ default:
665
+ log.warn('hook returned 2 on an event it cannot block', {
666
+ ...attrs,
667
+ 'namzu.hook.reason': reason,
668
+ })
669
+ return { action: 'continue' }
670
+ }
671
+ }
672
+ if (outcome.exitCode !== 0) {
673
+ log.warn('hook failed', {
674
+ ...attrs,
675
+ 'namzu.hook.exit': outcome.exitCode ?? -1,
676
+ 'namzu.hook.stderr': outcome.stderr.trim().slice(0, 500),
677
+ })
678
+ return { action: 'continue' }
679
+ }
680
+
681
+ // Exit 0.
682
+ const text = outcome.stdout.trim()
683
+ const reading = readShellHookJson(event, outcome.stdout)
684
+ if (reading === undefined) {
353
685
  // On the prompt event, what the hook printed is context for the
354
686
  // model — the branch, the time, a ticket. Everywhere else stdout is
355
687
  // the operator's to read and the model's business not at all.
356
- const text = outcome.stdout.trim()
357
688
  if (event === 'user_prompt_submit' && text.length > 0) return { action: 'annotate', text }
358
689
  return { action: 'continue' }
359
690
  }
360
- if (outcome.exitCode === 2) {
361
- const reason =
362
- outcome.stderr.trim() || outcome.stdout.trim() || `blocked by hook \`${entry.command}\``
363
- if (event === 'pre_tool_use' || event === 'user_prompt_submit')
364
- return { action: 'skip', reason }
365
- log.warn('hook returned 2 on an event it cannot block', {
691
+ if (reading.invalid !== undefined) {
692
+ log.warn('hook output ignored', {
366
693
  ...attrs,
367
- 'namzu.hook.reason': reason,
694
+ 'namzu.hook.problem': reading.invalid,
368
695
  })
369
696
  return { action: 'continue' }
370
697
  }
371
- log.warn('hook failed', {
372
- ...attrs,
373
- 'namzu.hook.exit': outcome.exitCode ?? -1,
374
- 'namzu.hook.stderr': outcome.stderr.trim().slice(0, 500),
375
- })
376
- return { action: 'continue' }
698
+ if (!reading.suppressOutput) {
699
+ log.debug('hook output', {
700
+ ...attrs,
701
+ 'namzu.hook.stdout': text.slice(0, 500),
702
+ })
703
+ }
704
+ if (reading.notice) {
705
+ log.warn('hook warning', { ...attrs, 'namzu.hook.notice': reading.notice })
706
+ }
707
+ const notes = {
708
+ ...(reading.context !== undefined ? { context: reading.context } : {}),
709
+ ...(reading.halt !== undefined ? { halt: reading.halt } : {}),
710
+ ...(reading.notice !== undefined ? { notice: reading.notice } : {}),
711
+ }
712
+ switch (event) {
713
+ case 'pre_tool_use': {
714
+ // A stop request also withholds the call: the hook asked for nothing
715
+ // further to happen.
716
+ const block = reading.block ?? reading.halt?.reason
717
+ if (block !== undefined) return { action: 'skip', reason: block, ...notes }
718
+ if (reading.updatedInput !== undefined) {
719
+ return {
720
+ action: 'modify',
721
+ input: reading.updatedInput,
722
+ ...(reading.permission ? { permission: reading.permission } : {}),
723
+ ...notes,
724
+ }
725
+ }
726
+ return {
727
+ action: 'continue',
728
+ ...(reading.permission ? { permission: reading.permission } : {}),
729
+ ...notes,
730
+ }
731
+ }
732
+ case 'user_prompt_submit': {
733
+ const block = reading.block ?? reading.halt?.reason
734
+ if (block !== undefined) return { action: 'skip', reason: block }
735
+ return reading.context !== undefined
736
+ ? { action: 'annotate', text: reading.context }
737
+ : { action: 'continue' }
738
+ }
739
+ case 'post_tool_use':
740
+ case 'post_tool_use_failure': {
741
+ // The call has run, so a "block" cannot undo it: its reason is what the
742
+ // model is told about the result.
743
+ const context = [reading.block, reading.context].filter(Boolean).join('\n')
744
+ return {
745
+ action: 'continue',
746
+ ...(context.length > 0 ? { context } : {}),
747
+ ...(reading.halt ? { halt: reading.halt } : {}),
748
+ ...(reading.notice ? { notice: reading.notice } : {}),
749
+ }
750
+ }
751
+ case 'turn_end':
752
+ case 'subagent_stop': {
753
+ // A stop request outranks keeping the turn going.
754
+ if (reading.halt) {
755
+ return {
756
+ action: 'continue',
757
+ halt: reading.halt,
758
+ ...(reading.notice ? { notice: reading.notice } : {}),
759
+ }
760
+ }
761
+ const feedback = reading.block ?? reading.context
762
+ return feedback !== undefined
763
+ ? {
764
+ action: 'skip',
765
+ reason: feedback,
766
+ ...(reading.notice ? { notice: reading.notice } : {}),
767
+ }
768
+ : {
769
+ action: 'continue',
770
+ ...(reading.notice ? { notice: reading.notice } : {}),
771
+ }
772
+ }
773
+ case 'pre_compact':
774
+ return reading.block !== undefined
775
+ ? { action: 'skip', reason: reading.block, ...notes }
776
+ : { action: 'continue', ...notes }
777
+ case 'permission_request': {
778
+ if (reading.block !== undefined) {
779
+ return {
780
+ action: 'skip',
781
+ reason: reading.block,
782
+ ...(reading.halt ? { halt: reading.halt } : {}),
783
+ }
784
+ }
785
+ return {
786
+ action: 'continue',
787
+ ...(reading.permission ? { permission: reading.permission } : {}),
788
+ }
789
+ }
790
+ default:
791
+ // Nothing here can be changed by a verdict, but a stop request still
792
+ // can end the turn, and a warning still reaches the operator.
793
+ return {
794
+ action: 'continue',
795
+ ...(reading.halt ? { halt: reading.halt } : {}),
796
+ ...(reading.notice ? { notice: reading.notice } : {}),
797
+ }
798
+ }
377
799
  }
378
800
 
379
801
  export interface ShellHookOptions {
@@ -389,12 +811,23 @@ export function createShellHook(
389
811
  options: ShellHookOptions,
390
812
  ): PluginHookDefinition {
391
813
  const log = options.log ?? NOOP_LOGGER
814
+ // `once` is per definition, and a definition is per session: the manager
815
+ // is built for one session and its hooks are attached to it.
816
+ let spent = false
392
817
  // The shell verdict branches on `event`, but TypeScript cannot retain that
393
818
  // correlation across the async handler closure and the union of events.
394
819
  return {
395
820
  event,
821
+ ...(entry.statusMessage !== undefined ? { statusMessage: entry.statusMessage } : {}),
396
822
  handler: async (context: PluginHookContext) => {
397
- if (!shellHookMatches(entry.matcher, context.toolName)) return { action: 'continue' }
823
+ if (spent) return { action: 'continue' }
824
+ // A notification is matched on its type; every other event on the tool.
825
+ const subject = event === 'notification' ? context.notification?.type : context.toolName
826
+ if (!shellHookMatches(entry.matcher, subject)) return { action: 'continue' }
827
+ if (!shellHookIfMatches(entry.if, context.toolName, context.toolInput)) {
828
+ return { action: 'continue' }
829
+ }
830
+ if (entry.once === true) spent = true
398
831
  const outcome = await runShellHook(
399
832
  entry,
400
833
  {
@@ -423,6 +856,8 @@ export function createShellHook(
423
856
  },
424
857
  }
425
858
  : {}),
859
+ ...(context.notification !== undefined ? { notification: context.notification } : {}),
860
+ ...(context.stopHookActive === true ? { stopHookActive: true } : {}),
426
861
  },
427
862
  context.signal,
428
863
  )
@@ -433,9 +868,8 @@ export function createShellHook(
433
868
 
434
869
  /**
435
870
  * Register every entry on the manager, in file order within an event. The
436
- * manager runs `post_*` hooks in reverse, as it does for plugins, so a
437
- * formatter registered last runs first after a write. Returns how many were
438
- * attached.
871
+ * manager starts all of an event's hooks together and reads their answers in
872
+ * that order. Returns how many were attached.
439
873
  *
440
874
  * Throws, before registering anything, when the config names a renamed event
441
875
  * (`run_start` and the rest); the message names the event that replaced it.
@@ -65,6 +65,23 @@ export function resolveProviderCapabilities(
65
65
  }
66
66
  }
67
67
 
68
+ /**
69
+ * Whether a request for `model` through `provider` may carry images.
70
+ *
71
+ * False when the driver cannot map them at all, or when the driver's own
72
+ * catalogue says this model is text-only. Anything the driver does not know is
73
+ * assumed to read images: a wrong "no" silently blinds a vision model, while a
74
+ * wrong "yes" is recovered from when the provider refuses the request.
75
+ */
76
+ export function modelReadsImages(
77
+ provider: Pick<LLMProvider, 'capabilities' | 'supportsImageInputFor'>,
78
+ model: string | undefined,
79
+ ): boolean {
80
+ if (provider.capabilities?.supportsVision === false) return false
81
+ if (model === undefined || model === '') return true
82
+ return provider.supportsImageInputFor?.(model) ?? true
83
+ }
84
+
68
85
  /** Require explicit wire support before dispatching a native JSON Schema request. */
69
86
  export function assertNativeStructuredOutputSupported(
70
87
  provider: Pick<LLMProvider, 'id' | 'capabilities'>,