@sema-agent/server 7.131.0 → 7.133.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 (509) hide show
  1. package/MIGRATION.md +575 -0
  2. package/README.md +12 -9
  3. package/README.zh-CN.md +9 -6
  4. package/USAGE.md +326 -98
  5. package/deploy/sema-up/README.md +19 -1
  6. package/deploy/sema-up/chart/templates/server-v4pro.yaml +5 -2
  7. package/deploy/sema-up/chart/templates/server.yaml +5 -2
  8. package/deploy/sema-up/stack/compose.yaml +3 -0
  9. package/deploy/sema-up/stack/sema-stack-cluster.yaml +4 -2
  10. package/dist/adoption/plan.d.ts +3 -2
  11. package/dist/adoption/plan.js +7 -5
  12. package/dist/adoption/runner.js +3 -10
  13. package/dist/adoption/sql.d.ts +3 -2
  14. package/dist/adoption/sql.js +3 -5
  15. package/dist/adoption/wire.d.ts +4 -1
  16. package/dist/adoption/wire.js +1 -0
  17. package/dist/approval-ask-audit-store.d.ts +31 -13
  18. package/dist/approval-ask-audit-store.js +272 -74
  19. package/dist/approval-ask-machine.d.ts +15 -0
  20. package/dist/approval-ask-machine.js +6 -0
  21. package/dist/approval-ask-retention.d.ts +136 -0
  22. package/dist/approval-ask-retention.js +102 -0
  23. package/dist/approval-card.d.ts +82 -20
  24. package/dist/approval-card.js +46 -16
  25. package/dist/approval-deny-reasons.d.ts +1 -1
  26. package/dist/approval.d.ts +9 -0
  27. package/dist/approval.js +24 -3
  28. package/dist/audit.d.ts +33 -19
  29. package/dist/audit.js +12 -16
  30. package/dist/auth-bridge.d.ts +4 -0
  31. package/dist/auth-bridge.js +10 -11
  32. package/dist/bake-runner/main.js +3 -2
  33. package/dist/boot/deferred-sandbox-path-env.js +1 -1
  34. package/dist/boot/deployment-mods.d.ts +31 -0
  35. package/dist/boot/deployment-mods.js +26 -0
  36. package/dist/boot/execution-env.d.ts +1 -1
  37. package/dist/boot/execution-env.js +27 -12
  38. package/dist/boot/execution-lane.js +11 -0
  39. package/dist/boot/leader.js +2 -0
  40. package/dist/boot/memory-boundary.d.ts +2 -2
  41. package/dist/boot/memory-boundary.js +2 -2
  42. package/dist/boot/memory-consolidation.d.ts +5 -4
  43. package/dist/boot/memory-consolidation.js +7 -6
  44. package/dist/boot/org-delivery.d.ts +28 -0
  45. package/dist/boot/org-delivery.js +20 -0
  46. package/dist/boot/read-deny-anchor-audit.d.ts +17 -0
  47. package/dist/boot/read-deny-anchor-audit.js +36 -0
  48. package/dist/boot/reapers.d.ts +3 -0
  49. package/dist/boot/reapers.js +21 -1
  50. package/dist/boot/resolve-spec.d.ts +4 -5
  51. package/dist/boot/resolve-spec.js +74 -123
  52. package/dist/boot/runner-deps.d.ts +19 -8
  53. package/dist/boot/runner-deps.js +15 -2
  54. package/dist/boot/session-faces.d.ts +2 -7
  55. package/dist/boot/session-faces.js +4 -2
  56. package/dist/boot/stage-02-web-admin-pull.js +4 -1
  57. package/dist/boot/stage-03-store-backend.d.ts +1 -1
  58. package/dist/boot/stage-03-store-backend.js +2 -2
  59. package/dist/boot/stage-04-stores.d.ts +1 -1
  60. package/dist/boot/stage-05-execution-env.d.ts +4 -4
  61. package/dist/boot/stage-05-execution-env.js +7 -10
  62. package/dist/boot/stage-06-runners.d.ts +4 -2
  63. package/dist/boot/stage-06-runners.js +11 -7
  64. package/dist/boot/stage-07-capability-layer.d.ts +4 -2
  65. package/dist/boot/stage-07-capability-layer.js +27 -6
  66. package/dist/boot/stage-08-reapers.d.ts +4 -2
  67. package/dist/boot/stage-08-reapers.js +2 -2
  68. package/dist/boot/stage-09-leader.d.ts +6 -9
  69. package/dist/boot/stage-10-http-server.d.ts +6 -9
  70. package/dist/boot/stage-10-http-server.js +7 -9
  71. package/dist/boot/stores.d.ts +1 -1
  72. package/dist/boot/stores.js +7 -8
  73. package/dist/boot/web-admin.d.ts +4 -3
  74. package/dist/boot/web-admin.js +186 -98
  75. package/dist/boot/workflow-orchestration.d.ts +2 -0
  76. package/dist/boot/workflow-orchestration.js +2 -1
  77. package/dist/brain.js +25 -25
  78. package/dist/breaker-key.d.ts +6 -0
  79. package/dist/breaker-key.js +29 -0
  80. package/dist/budget.js +5 -4
  81. package/dist/caller-text-echo.d.ts +23 -0
  82. package/dist/caller-text-echo.js +21 -0
  83. package/dist/capabilities/brand-identity.d.ts +26 -0
  84. package/dist/capabilities/brand-identity.js +27 -0
  85. package/dist/capabilities/center-plugins.d.ts +7 -0
  86. package/dist/capabilities/center-plugins.js +1 -1
  87. package/dist/capabilities/center-prompts.d.ts +32 -59
  88. package/dist/capabilities/center-prompts.js +18 -32
  89. package/dist/capabilities/composite-members.d.ts +1 -1
  90. package/dist/capabilities/composite-members.js +1 -1
  91. package/dist/capabilities/engine-capabilities.js +2 -1
  92. package/dist/capabilities/execution-lane.js +2 -2
  93. package/dist/capabilities/hands-lane.d.ts +30 -6
  94. package/dist/capabilities/hands-lane.js +11 -1
  95. package/dist/capabilities/mcp-probe.d.ts +48 -3
  96. package/dist/capabilities/mcp-probe.js +47 -135
  97. package/dist/capabilities/scenarios.d.ts +95 -45
  98. package/dist/capabilities/scenarios.js +53 -38
  99. package/dist/capabilities/skills.d.ts +19 -4
  100. package/dist/capabilities/skills.js +30 -10
  101. package/dist/capability-facts.d.ts +34 -4
  102. package/dist/capability-facts.js +28 -2
  103. package/dist/center-config-tier.d.ts +69 -0
  104. package/dist/center-config-tier.js +42 -0
  105. package/dist/checkpoint-identity.d.ts +16 -0
  106. package/dist/checkpoint-identity.js +19 -0
  107. package/dist/config-catalog.d.ts +17 -10
  108. package/dist/config-catalog.js +47 -32
  109. package/dist/config-d-migration.d.ts +3 -1
  110. package/dist/config-d-migration.js +2 -2
  111. package/dist/config-dir-trust.d.ts +114 -0
  112. package/dist/config-dir-trust.js +388 -0
  113. package/dist/config-invariants.d.ts +18 -5
  114. package/dist/config-invariants.js +35 -3
  115. package/dist/config-lkg.d.ts +9 -9
  116. package/dist/config-lkg.js +20 -4
  117. package/dist/config-provider.d.ts +9 -2
  118. package/dist/config-provider.js +18 -6
  119. package/dist/config-types.d.ts +75 -20
  120. package/dist/config-types.js +8 -0
  121. package/dist/config.d.ts +13 -0
  122. package/dist/config.js +137 -62
  123. package/dist/delegated-session-owner.d.ts +185 -0
  124. package/dist/delegated-session-owner.js +119 -0
  125. package/dist/device-protocol.d.ts +5 -3
  126. package/dist/device-protocol.js +1 -1
  127. package/dist/device-ws-hub.js +21 -2
  128. package/dist/device-ws-protocol.d.ts +70 -6
  129. package/dist/device-ws-protocol.js +50 -22
  130. package/dist/elicitation.js +3 -2
  131. package/dist/entry-problems.d.ts +34 -0
  132. package/dist/entry-problems.js +14 -0
  133. package/dist/env-name-allowlist-knobs.d.ts +12 -2
  134. package/dist/env-name-allowlist-knobs.js +5 -4
  135. package/dist/exec-bounds.d.ts +15 -3
  136. package/dist/exec-bounds.js +4 -0
  137. package/dist/execution-lane-caps.d.ts +56 -10
  138. package/dist/execution-lane-caps.js +50 -0
  139. package/dist/field-conflict.d.ts +43 -0
  140. package/dist/field-conflict.js +20 -0
  141. package/dist/fleet/fleet-bus.d.ts +80 -6
  142. package/dist/fleet/fleet-bus.js +41 -9
  143. package/dist/hooks/classic-stations.d.ts +91 -0
  144. package/dist/hooks/classic-stations.js +27 -0
  145. package/dist/hooks/command-spawn.d.ts +57 -0
  146. package/dist/hooks/command-spawn.js +139 -0
  147. package/dist/hooks/hook-llm.d.ts +8 -5
  148. package/dist/hooks/hook-llm.js +79 -36
  149. package/dist/hooks/hook-runner.d.ts +349 -95
  150. package/dist/hooks/hook-runner.js +855 -568
  151. package/dist/hooks/hook-source.d.ts +44 -0
  152. package/dist/hooks/hook-source.js +40 -0
  153. package/dist/http/active-run-conflict.d.ts +5 -13
  154. package/dist/http/active-run-conflict.js +17 -27
  155. package/dist/http/admission.d.ts +8 -2
  156. package/dist/http/admission.js +188 -101
  157. package/dist/http/approval-sweeps.js +99 -21
  158. package/dist/http/caller-trust.d.ts +77 -0
  159. package/dist/http/caller-trust.js +143 -0
  160. package/dist/http/checkpoint-ctx.d.ts +41 -24
  161. package/dist/http/checkpoint-ctx.js +23 -16
  162. package/dist/http/claimed-run.d.ts +14 -0
  163. package/dist/http/claimed-run.js +15 -0
  164. package/dist/http/device-placement.d.ts +2 -4
  165. package/dist/http/device-placement.js +7 -11
  166. package/dist/http/dispatch.d.ts +0 -4
  167. package/dist/http/dispatch.js +15 -13
  168. package/dist/http/durable-run-leg.d.ts +6 -33
  169. package/dist/http/durable-run-leg.js +36 -10
  170. package/dist/http/health-self-report.d.ts +7 -5
  171. package/dist/http/health-self-report.js +4 -18
  172. package/dist/http/owner-access.d.ts +56 -0
  173. package/dist/http/owner-access.js +42 -0
  174. package/dist/http/principal-gate.d.ts +70 -28
  175. package/dist/http/principal-gate.js +61 -15
  176. package/dist/http/readiness.d.ts +11 -5
  177. package/dist/http/readiness.js +10 -3
  178. package/dist/http/refusal-body.d.ts +21 -0
  179. package/dist/http/refusal-body.js +4 -0
  180. package/dist/http/request-guards.d.ts +43 -23
  181. package/dist/http/request-guards.js +53 -17
  182. package/dist/http/resume-ctx-read.d.ts +2 -5
  183. package/dist/http/resume-ctx-read.js +7 -4
  184. package/dist/http/resume-legs.d.ts +28 -4
  185. package/dist/http/resume-legs.js +169 -91
  186. package/dist/http/retired-service-deps.d.ts +43 -0
  187. package/dist/http/retired-service-deps.js +18 -0
  188. package/dist/http/route-ctx.d.ts +44 -28
  189. package/dist/http/route-ctx.js +4 -0
  190. package/dist/http/route-table.js +2 -0
  191. package/dist/http/routes/a2a-serve.js +3 -4
  192. package/dist/http/routes/admin-config-refresh.js +1 -1
  193. package/dist/http/routes/admin-drain.js +1 -1
  194. package/dist/http/routes/admin-memory-optout.js +5 -1
  195. package/dist/http/routes/adoption.js +2 -9
  196. package/dist/http/routes/approvals-assistant.js +49 -48
  197. package/dist/http/routes/background.js +3 -2
  198. package/dist/http/routes/capabilities.js +21 -5
  199. package/dist/http/routes/config-catalog.d.ts +3 -3
  200. package/dist/http/routes/config-catalog.js +1 -1
  201. package/dist/http/routes/devices.js +3 -17
  202. package/dist/http/routes/diagnostics.js +6 -2
  203. package/dist/http/routes/images.js +6 -7
  204. package/dist/http/routes/leader.js +1 -8
  205. package/dist/http/routes/mcp-reconnect.d.ts +1 -1
  206. package/dist/http/routes/mcp-reconnect.js +3 -2
  207. package/dist/http/routes/memory-bundle.js +1 -1
  208. package/dist/http/routes/memory-compliance.js +1 -1
  209. package/dist/http/routes/memory-consolidation.js +1 -1
  210. package/dist/http/routes/memory-origin.js +1 -1
  211. package/dist/http/routes/memory-policy.d.ts +1 -1
  212. package/dist/http/routes/memory-policy.js +3 -2
  213. package/dist/http/routes/notify-wake.js +8 -21
  214. package/dist/http/routes/observability.js +2 -2
  215. package/dist/http/routes/principal-inbox.d.ts +39 -0
  216. package/dist/http/routes/principal-inbox.js +76 -0
  217. package/dist/http/routes/retention-ops.js +2 -9
  218. package/dist/http/routes/rules.js +5 -18
  219. package/dist/http/routes/runs.d.ts +5 -0
  220. package/dist/http/routes/runs.js +34 -107
  221. package/dist/http/routes/session-sync.js +15 -26
  222. package/dist/http/routes/sessions.js +30 -40
  223. package/dist/http/routes/shared-memory.js +2 -13
  224. package/dist/http/routes/side-query.js +8 -7
  225. package/dist/http/routes/tasks.js +67 -21
  226. package/dist/http/routes/workflows.d.ts +351 -0
  227. package/dist/http/routes/workflows.js +153 -119
  228. package/dist/http/send.d.ts +33 -10
  229. package/dist/http/send.js +21 -4
  230. package/dist/http/server.d.ts +27 -18
  231. package/dist/http/server.js +34 -6
  232. package/dist/http/stored-body-replay.js +17 -4
  233. package/dist/http/submit-ctx.d.ts +25 -0
  234. package/dist/http/submit-ctx.js +40 -0
  235. package/dist/http/submit-replay.d.ts +4 -3
  236. package/dist/http/submit-replay.js +5 -4
  237. package/dist/http/wire-types.d.ts +11 -8
  238. package/dist/index.d.ts +2 -2
  239. package/dist/index.js +1 -2
  240. package/dist/lane-home-probe.d.ts +112 -0
  241. package/dist/lane-home-probe.js +101 -0
  242. package/dist/leader/endpoint.d.ts +4 -1
  243. package/dist/leader/endpoint.js +11 -10
  244. package/dist/limit-refusal.d.ts +59 -0
  245. package/dist/limit-refusal.js +15 -0
  246. package/dist/main.js +1 -1
  247. package/dist/mcp-assertion-header.d.ts +32 -19
  248. package/dist/mcp-assertion-header.js +10 -9
  249. package/dist/mcp-tool-faces.d.ts +5 -0
  250. package/dist/mcp-tool-faces.js +6 -4
  251. package/dist/memory-project-layer.d.ts +37 -0
  252. package/dist/memory-project-layer.js +46 -0
  253. package/dist/memory-scope.d.ts +24 -17
  254. package/dist/memory-scope.js +24 -8
  255. package/dist/memory-sync-client.d.ts +27 -13
  256. package/dist/memory-sync-client.js +107 -18
  257. package/dist/model-entry-refusal.d.ts +9 -1
  258. package/dist/model-entry-refusal.js +6 -1
  259. package/dist/model-plane-face.d.ts +51 -0
  260. package/dist/model-plane-face.js +24 -0
  261. package/dist/model-provider.d.ts +44 -12
  262. package/dist/model-provider.js +21 -2
  263. package/dist/model-route-endpoint.d.ts +9 -12
  264. package/dist/model-route-endpoint.js +8 -11
  265. package/dist/model-route-env.d.ts +22 -0
  266. package/dist/model-route-env.js +2 -0
  267. package/dist/model-route-gap.d.ts +60 -30
  268. package/dist/model-route-gap.js +21 -15
  269. package/dist/observability/corrupt-read-seat.d.ts +7 -0
  270. package/dist/observability/corrupt-read-seat.js +9 -0
  271. package/dist/observability/cost-taxonomy.d.ts +7 -7
  272. package/dist/observability/err-text.d.ts +6 -0
  273. package/dist/observability/err-text.js +18 -4
  274. package/dist/observability/fail-open.d.ts +76 -14
  275. package/dist/observability/fail-open.js +76 -14
  276. package/dist/observability/metrics.js +4 -1
  277. package/dist/observability/prompt-manifest.d.ts +33 -82
  278. package/dist/observability/prompt-manifest.js +122 -17
  279. package/dist/observability/run-terminal.js +14 -6
  280. package/dist/observability/secret-env-scrub.d.ts +13 -1
  281. package/dist/observability/secret-env-scrub.js +13 -1
  282. package/dist/observability/tool-trace.d.ts +16 -21
  283. package/dist/observability/tool-trace.js +58 -49
  284. package/dist/operator-ask.d.ts +10 -0
  285. package/dist/operator-ask.js +3 -0
  286. package/dist/orchestration/workflow-agent-session-index.d.ts +12 -12
  287. package/dist/orchestration/workflow-agent-session-index.js +9 -13
  288. package/dist/orchestration/workflow-completion-inbox.d.ts +42 -15
  289. package/dist/orchestration/workflow-completion-inbox.js +13 -0
  290. package/dist/orchestration/workflow-notify-journal.d.ts +13 -32
  291. package/dist/orchestration/workflow-notify-journal.js +27 -34
  292. package/dist/orchestration/workflow-park-decision.d.ts +89 -0
  293. package/dist/orchestration/workflow-park-decision.js +118 -0
  294. package/dist/org-delivery-gate.d.ts +81 -0
  295. package/dist/org-delivery-gate.js +59 -0
  296. package/dist/panel-tool-names.d.ts +23 -0
  297. package/dist/panel-tool-names.js +8 -0
  298. package/dist/parked-decide.d.ts +39 -6
  299. package/dist/parked-decide.js +98 -51
  300. package/dist/parked-revive-surface.d.ts +9 -2
  301. package/dist/parked-revive-surface.js +11 -8
  302. package/dist/per-task-image.d.ts +7 -1
  303. package/dist/per-task-image.js +3 -3
  304. package/dist/permission-rule-list.d.ts +1 -1
  305. package/dist/permission-rule-list.js +2 -2
  306. package/dist/plain-string-record.d.ts +25 -0
  307. package/dist/plain-string-record.js +32 -0
  308. package/dist/plugins/approval-ask-store-file.d.ts +16 -1
  309. package/dist/plugins/approval-ask-store-file.js +21 -0
  310. package/dist/plugins/approval-ask-store-memory.d.ts +18 -1
  311. package/dist/plugins/approval-ask-store-memory.js +34 -2
  312. package/dist/plugins/approval-ask-store-sql.d.ts +25 -0
  313. package/dist/plugins/approval-ask-store-sql.js +37 -1
  314. package/dist/plugins/blob-backend.d.ts +83 -15
  315. package/dist/plugins/blob-backend.js +68 -15
  316. package/dist/plugins/breaker-state-sql.d.ts +2 -2
  317. package/dist/plugins/breaker-state-sql.js +4 -4
  318. package/dist/plugins/caching-session-store.d.ts +3 -0
  319. package/dist/plugins/caching-session-store.js +3 -0
  320. package/dist/plugins/checkpoint-store-sql.d.ts +28 -4
  321. package/dist/plugins/checkpoint-store-sql.js +59 -28
  322. package/dist/plugins/file-history-store-sql.d.ts +5 -2
  323. package/dist/plugins/file-history-store-sql.js +3 -1
  324. package/dist/plugins/file-run-store.d.ts +5 -13
  325. package/dist/plugins/file-run-store.js +9 -0
  326. package/dist/plugins/fork-routing-session-store.d.ts +9 -1
  327. package/dist/plugins/fork-routing-session-store.js +12 -1
  328. package/dist/plugins/host-platform.d.ts +2 -0
  329. package/dist/plugins/host-platform.js +2 -1
  330. package/dist/plugins/k8s-bg-scripts.d.ts +11 -0
  331. package/dist/plugins/k8s-bg-scripts.js +22 -0
  332. package/dist/plugins/local-checkpoint-store.d.ts +2 -0
  333. package/dist/plugins/local-checkpoint-store.js +11 -3
  334. package/dist/plugins/local-session-store.d.ts +1 -1
  335. package/dist/plugins/local-session-store.js +1 -1
  336. package/dist/plugins/local-session-window.d.ts +39 -0
  337. package/dist/plugins/local-session-window.js +54 -0
  338. package/dist/plugins/mailbox-recipient-lifecycle.d.ts +3 -2
  339. package/dist/plugins/mailbox-store-sql.d.ts +30 -48
  340. package/dist/plugins/mailbox-store-sql.js +77 -52
  341. package/dist/plugins/memory-embedder-fingerprint.js +2 -1
  342. package/dist/plugins/memory-run-store.d.ts +5 -13
  343. package/dist/plugins/memory-run-store.js +8 -0
  344. package/dist/plugins/one-time-migrations.d.ts +245 -93
  345. package/dist/plugins/one-time-migrations.js +668 -207
  346. package/dist/plugins/org-delivery-quota-store.d.ts +123 -0
  347. package/dist/plugins/org-delivery-quota-store.js +228 -0
  348. package/dist/plugins/permission-rule-store-sql.d.ts +13 -7
  349. package/dist/plugins/permission-rule-store-sql.js +4 -18
  350. package/dist/plugins/pg-pool.js +39 -7
  351. package/dist/plugins/pg-session-storage.d.ts +20 -8
  352. package/dist/plugins/pg-session-storage.js +34 -21
  353. package/dist/plugins/remote-env-adb.d.ts +12 -12
  354. package/dist/plugins/remote-env-adb.js +21 -6
  355. package/dist/plugins/remote-env-device.js +3 -0
  356. package/dist/plugins/remote-env-e2b.d.ts +1 -1
  357. package/dist/plugins/remote-env-e2b.js +21 -9
  358. package/dist/plugins/remote-env-host.d.ts +9 -9
  359. package/dist/plugins/remote-env-host.js +10 -32
  360. package/dist/plugins/remote-env-k8s.d.ts +103 -38
  361. package/dist/plugins/remote-env-k8s.js +288 -95
  362. package/dist/plugins/remote-env-local-docker.d.ts +27 -13
  363. package/dist/plugins/remote-env-local-docker.js +82 -11
  364. package/dist/plugins/remote-env-ssh.d.ts +21 -12
  365. package/dist/plugins/remote-env-ssh.js +117 -5
  366. package/dist/plugins/remote-shell.d.ts +0 -1
  367. package/dist/plugins/remote-shell.js +1 -2
  368. package/dist/plugins/retention-store-sql.js +6 -2
  369. package/dist/plugins/run-store-sql.d.ts +16 -14
  370. package/dist/plugins/run-store-sql.js +22 -2
  371. package/dist/plugins/schema-consistency.d.ts +54 -0
  372. package/dist/plugins/schema-consistency.js +52 -0
  373. package/dist/plugins/schema-indexes.js +2 -0
  374. package/dist/plugins/send-user-file.js +2 -2
  375. package/dist/plugins/session-epoch-anchor.d.ts +39 -0
  376. package/dist/plugins/session-epoch-anchor.js +22 -0
  377. package/dist/plugins/session-store.d.ts +18 -8
  378. package/dist/plugins/session-store.js +15 -4
  379. package/dist/plugins/shared-memory-store-sql.d.ts +9 -3
  380. package/dist/plugins/shared-memory-store-sql.js +5 -16
  381. package/dist/plugins/sql-driver.d.ts +205 -1
  382. package/dist/plugins/sql-driver.js +325 -13
  383. package/dist/plugins/sql-errors.d.ts +64 -0
  384. package/dist/plugins/sql-errors.js +57 -0
  385. package/dist/plugins/store-backend.d.ts +36 -1
  386. package/dist/plugins/store-backend.js +42 -15
  387. package/dist/plugins/store-contracts.d.ts +42 -0
  388. package/dist/plugins/store-contracts.js +1 -0
  389. package/dist/plugins/tidb-pool.d.ts +6 -1
  390. package/dist/plugins/tidb-pool.js +44 -10
  391. package/dist/plugins/tidb-session-storage.d.ts +8 -11
  392. package/dist/plugins/tidb-session-storage.js +6 -16
  393. package/dist/plugins/tidb-session-store.d.ts +24 -4
  394. package/dist/plugins/tidb-session-store.js +29 -5
  395. package/dist/plugins/workflow-journal-store-sql.d.ts +73 -19
  396. package/dist/plugins/workflow-journal-store-sql.js +19 -6
  397. package/dist/plugins/workflow-run-store-sql.d.ts +30 -8
  398. package/dist/plugins/workflow-run-store-sql.js +50 -9
  399. package/dist/posture-source.d.ts +18 -10
  400. package/dist/posture-source.js +4 -1
  401. package/dist/project-identity.d.ts +45 -6
  402. package/dist/project-identity.js +162 -8
  403. package/dist/project-layer-key.d.ts +28 -0
  404. package/dist/project-layer-key.js +29 -0
  405. package/dist/prompts-domain-validate.d.ts +17 -61
  406. package/dist/prompts-domain-validate.js +9 -119
  407. package/dist/question.js +3 -2
  408. package/dist/read-deny-entry.d.ts +25 -0
  409. package/dist/read-deny-entry.js +12 -0
  410. package/dist/read-face-posture.d.ts +37 -11
  411. package/dist/read-face-posture.js +32 -3
  412. package/dist/request-key-closure.d.ts +9 -2
  413. package/dist/request-key-closure.js +11 -5
  414. package/dist/request-value-closure.d.ts +5 -0
  415. package/dist/request-value-closure.js +11 -3
  416. package/dist/rule-entry-list.d.ts +38 -11
  417. package/dist/rule-entry-list.js +106 -30
  418. package/dist/rules-consent.js +3 -4
  419. package/dist/run-acceptance.d.ts +19 -3
  420. package/dist/run-acceptance.js +13 -1
  421. package/dist/run-local.js +54 -24
  422. package/dist/runs.d.ts +15 -8
  423. package/dist/runs.js +35 -1
  424. package/dist/runtime-caps-resolver.d.ts +16 -6
  425. package/dist/runtime-caps-resolver.js +18 -5
  426. package/dist/sandbox-pkg-source.d.ts +19 -1
  427. package/dist/sandbox-pkg-source.js +29 -7
  428. package/dist/security.d.ts +87 -9
  429. package/dist/security.js +52 -9
  430. package/dist/server-secret-env.d.ts +44 -18
  431. package/dist/server-secret-env.js +49 -8
  432. package/dist/session-error-wire.d.ts +19 -2
  433. package/dist/session-error-wire.js +5 -1
  434. package/dist/session-incarnation.d.ts +15 -5
  435. package/dist/session-incarnation.js +16 -9
  436. package/dist/session-titled-frame.d.ts +33 -0
  437. package/dist/session-titled-frame.js +4 -0
  438. package/dist/session-titler.d.ts +9 -1
  439. package/dist/session-titler.js +27 -6
  440. package/dist/spec-fields.d.ts +28 -16
  441. package/dist/spec-fields.js +66 -54
  442. package/dist/sql-engine-posture.d.ts +31 -4
  443. package/dist/sql-engine-posture.js +36 -1
  444. package/dist/stored-request-tool-axes-m19.d.ts +19 -4
  445. package/dist/stored-request-tool-axes-m19.js +30 -2
  446. package/dist/task-a2a.d.ts +9 -1
  447. package/dist/task-a2a.js +73 -47
  448. package/dist/task-cwd.d.ts +1 -1
  449. package/dist/task-mcp.d.ts +46 -58
  450. package/dist/task-mcp.js +82 -107
  451. package/dist/task-settings.d.ts +95 -108
  452. package/dist/task-settings.js +77 -98
  453. package/dist/text-bidi.d.ts +1 -48
  454. package/dist/text-bidi.js +3 -5
  455. package/dist/tighten-only-seats.d.ts +150 -0
  456. package/dist/tighten-only-seats.js +196 -0
  457. package/dist/tool-approval.d.ts +304 -88
  458. package/dist/tool-approval.js +623 -239
  459. package/dist/trace/core-keyset-guard.d.ts +122 -20
  460. package/dist/trace/engine-notice-wire.d.ts +11 -3
  461. package/dist/trace/engine-notice-wire.js +8 -1
  462. package/dist/trace/exit-seat.d.ts +28 -0
  463. package/dist/trace/exit-seat.js +76 -0
  464. package/dist/trace/ledger-events.d.ts +6 -3
  465. package/dist/trace/ledger-sink.js +4 -1
  466. package/dist/trace/model-segment.js +1 -1
  467. package/dist/trace/project.d.ts +98 -17
  468. package/dist/trace/project.js +98 -50
  469. package/dist/trace/projection-table.d.ts +100 -0
  470. package/dist/trace/projection-table.js +130 -0
  471. package/dist/trace/redact.d.ts +54 -0
  472. package/dist/trace/redact.js +778 -152
  473. package/dist/trace/self-minted-wire.d.ts +345 -1
  474. package/dist/trace/self-minted-wire.js +117 -1
  475. package/dist/trace/wire-projection-faces.d.ts +377 -3
  476. package/dist/trace/wire-projection-faces.js +164 -1
  477. package/dist/url-refusal.js +2 -0
  478. package/dist/web-admin-client/apply-effective.d.ts +12 -0
  479. package/dist/web-admin-client/apply-effective.js +56 -5
  480. package/dist/web-admin-client/candidate-gate.d.ts +6 -6
  481. package/dist/web-admin-client/candidate-gate.js +3 -3
  482. package/dist/web-admin-client/facade.d.ts +1 -1
  483. package/dist/web-admin-client/facade.js +1 -1
  484. package/dist/web-admin-client/http-client.js +0 -6
  485. package/dist/web-admin-client/read-face.js +5 -9
  486. package/dist/web-admin-client/read-warnings.d.ts +13 -0
  487. package/dist/web-admin-client/read-warnings.js +10 -0
  488. package/dist/web-admin-client/skills-mcp.d.ts +3 -0
  489. package/dist/web-admin-client/skills-mcp.js +11 -9
  490. package/dist/web-admin-client/types.d.ts +2 -3
  491. package/dist/workspace-cache.d.ts +35 -0
  492. package/dist/workspace-cache.js +36 -0
  493. package/dist/workspace-space.d.ts +3 -2
  494. package/dist/workspace-space.js +2 -0
  495. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +65 -10
  496. package/package.json +8 -4
  497. package/scripts/drain-check-local.mjs +34 -1
  498. package/scripts/drain-check.mysql.sql +30 -2
  499. package/scripts/drain-check.pg.sql +29 -2
  500. package/dist/capabilities/memory-notice.d.ts +0 -138
  501. package/dist/capabilities/memory-notice.js +0 -64
  502. package/dist/capabilities/prompt.d.ts +0 -17
  503. package/dist/capabilities/prompt.js +0 -6
  504. package/dist/mcp-content-origin.d.ts +0 -34
  505. package/dist/mcp-content-origin.js +0 -10
  506. package/dist/tool-axis-words.d.ts +0 -27
  507. package/dist/tool-axis-words.js +0 -9
  508. package/dist/wire-name-bound.d.ts +0 -14
  509. package/dist/wire-name-bound.js +0 -13
@@ -1,15 +1,23 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { composeHostSubprocessEnv, hostSubprocessEnv } from "../server-secret-env.js";
2
3
  import vm from "node:vm";
3
- import { hostShell, resolveHostShell, adoptHostForegroundChild } from "../plugins/host-platform.js";
4
+ import { hostShell, resolveHostShell, adoptHostForegroundChild, killTreeSoft, killTreeHard } from "../plugins/host-platform.js";
4
5
  import { classifyHooksConfig, DEFAULT_HOOK_TIMEOUT_SECONDS, HOOK_ENTRY_TYPES, HOOK_EVENTS, HOOK_EVENT_OWNER, HOOK_EVENT_MATCHER_FIELD, hookMatcherForm, hookMatcherSelects, interpolateHookEnvVars, } from "@sema-agent/settings-schema/hooks";
5
6
  import { formatConfigPath, outgoingPath } from "@sema-agent/settings-schema";
6
- import { describeToolCatalog, isEngineRefusal, receiveHookResult } from "@sema-agent/core";
7
- import { boundedNoticeText } from "../fleet/fleet-bus.js";
7
+ import { ENGINE_OWNED_HOOK_EVENTS } from "@sema-agent/settings-schema/hooks";
8
+ import { describeToolCatalog } from "@sema-agent/core";
9
+ import { hangClassic } from "./classic-stations.js";
10
+ import { MAX_HOOK_NOTICE_TEXT_CHARS, boundedNoticeText } from "../fleet/fleet-bus.js";
8
11
  import { recordFailOpen } from "../observability/fail-open.js";
12
+ import { LIMIT_REASONS } from "../limit-refusal.js";
13
+ import { clipDataText, echoCallerText } from "../caller-text-echo.js";
9
14
  import { redactSecrets, buildDisplaySafeUrl } from "../trace/redact.js";
15
+ import { canonicalJson } from "../canonical-json.js";
10
16
  import { ccPromptSystemFor, wrapCondition, parseCcVerdict, CC_EVALUATOR_MAX_OUTPUT_TOKENS } from "./cc-stop-prompt.js";
11
17
  import { ccAgentHookSystemFor } from "./cc-agent-hook-prompt.js";
12
18
  import { renderBranchTranscript } from "./branch-transcript.js";
19
+ import { HOOK_PHASES, HOOK_SOURCE_VOCABULARY_SENTENCE, hookPhaseOf, readHookGroupSource } from "./hook-source.js";
20
+ import { commandLineChars, composeHookEnvLayer, hookPluginChars, planCommandSpawn, readHookArgs, readHookPlugin } from "./command-spawn.js";
13
21
  export const MAX_HOOK_ENTRIES_PER_EVENT = 32;
14
22
  export const MAX_HOOK_COMMAND_CHARS = 8_192;
15
23
  export const MAX_HOOK_TIMEOUT_SECONDS = 600;
@@ -26,26 +34,41 @@ const HOOK_ENV_ALLOWLIST = [
26
34
  "PATH", "HOME", "SHELL", "USER", "LOGNAME", "TERM", "TZ",
27
35
  "LANG", "LC_ALL", "LC_CTYPE", "LC_MESSAGES", "TMPDIR",
28
36
  ];
37
+ function hookBaseEnv(spawning) {
38
+ return spawning
39
+ ? hostSubprocessEnv({ site: "server.hooks.spawn-env", policy: [...HOOK_ENV_ALLOWLIST], allow: [] })
40
+ : composeHostSubprocessEnv({ policy: [...HOOK_ENV_ALLOWLIST], allow: [] });
41
+ }
42
+ function hookEnvOverBase(layer, spawning) {
43
+ return { ...hookBaseEnv(spawning), ...layer };
44
+ }
29
45
  function hookEnv(ctx) {
30
- const env = { CLAUDE_PROJECT_DIR: ctx.cwd };
31
- for (const k of HOOK_ENV_ALLOWLIST) {
32
- const v = process.env[k];
33
- if (typeof v === "string")
34
- env[k] = v;
35
- }
36
- return { ...env, ...ctx.shellEnv };
46
+ return hookEnvOverBase(composeHookEnvLayer(ctx.shellEnv, ctx.cwd, undefined), false);
37
47
  }
38
- const LIT_EVENTS = [
39
- "PreToolUse",
40
- "PostToolUse",
41
- "PostToolUseFailure",
42
- "PostToolBatch",
43
- "UserPromptSubmit",
44
- "Stop",
45
- "StopFailure",
46
- "PreCompact",
47
- "PostCompact",
48
- ];
48
+ export const SETTINGS_HOOK_STATIONS = {
49
+ PreToolUse: ["classic.PreToolUse"],
50
+ PostToolUse: ["classic.PostToolUse"],
51
+ PostToolUseFailure: ["classic.PostToolUseFailure"],
52
+ PostToolBatch: ["classic.PostToolBatch"],
53
+ UserPromptSubmit: ["classic.UserPromptSubmit"],
54
+ Stop: ["classic.Stop", "classic.SubagentStop"],
55
+ StopFailure: ["classic.StopFailure"],
56
+ PreCompact: ["classic.PreCompact"],
57
+ PostCompact: ["classic.PostCompact"],
58
+ };
59
+ function hangTranslator(on, event, translators) {
60
+ const translate = translators[event];
61
+ if (translate === undefined)
62
+ return;
63
+ for (const station of SETTINGS_HOOK_STATIONS[event])
64
+ hangClassic(on, station, translate);
65
+ }
66
+ export const SETTINGS_HOOKS_REGISTRATION = "settings.hooks";
67
+ export const DEFAULT_SETTINGS_HOOKS_BUDGET_MS = MAX_HOOK_TIMEOUT_SECONDS * 1000;
68
+ export function settingsHooksBudgetMs(config) {
69
+ return config.hooksTimeoutMs ?? DEFAULT_SETTINGS_HOOKS_BUDGET_MS;
70
+ }
71
+ export const HOOK_KILL_GRACE_MS = 1_000;
49
72
  const BRAIN_TO_TURN_ERROR = {
50
73
  auth: "authentication_failed",
51
74
  rate_limit: "rate_limit",
@@ -76,8 +99,12 @@ function isRunnableHookEntry(entry) {
76
99
  return isSupportedHookEntryType(entry.type);
77
100
  }
78
101
  export const MAX_MATCHER_EVAL_CHARS = MAX_HOOK_COMMAND_CHARS;
79
- export const ADMISSION_MATCHER_EVAL_BUDGET_MS = 1_000;
102
+ export const ADMISSION_MATCHER_BUDGET = { budgetMs: 1_000, totalMs: 5_000 };
80
103
  export const MATCHER_ECHO_CHARS = 200;
104
+ export const IF_ECHO_CHARS = 200;
105
+ const LOG_PREVIEW_ECHO_CHARS = 200;
106
+ const LOG_ERROR_ECHO_CHARS = 300;
107
+ const LOG_MESSAGE_ECHO_CHARS = 500;
81
108
  const MATCHER_UNJUDGED_TAG = "server.hooks.matcher-unjudged";
82
109
  function matcherReadsWireToolName(event) {
83
110
  return HOOK_EVENT_OWNER[event] === "engine" && HOOK_EVENT_MATCHER_FIELD[event] === "tool_name";
@@ -130,7 +157,11 @@ const MATCHER_EVALUATION = {
130
157
  regex: { regex: "budgeted", literal: "not_applicable" },
131
158
  invalid: { regex: "unreadable", literal: "not_applicable" },
132
159
  };
133
- function judgeMatcher(matcher, depth, budgetMs) {
160
+ const RUNTIME_EVAL_DEPTH = "regex";
161
+ function matcherSelectsBy(evaluation, matcher, name, budgetMs) {
162
+ return evaluation === "inline" ? hookMatcherSelects(matcher, name) : matcherSelectsWithin(matcher, name, budgetMs);
163
+ }
164
+ function judgeMatcher(matcher, depth, ledger, judgesAlias) {
134
165
  if (matcher.length > MAX_MATCHER_EVAL_CHARS)
135
166
  return { kind: "unjudged", reason: "oversize" };
136
167
  const evaluate = MATCHER_EVALUATION[hookMatcherForm(matcher)][depth];
@@ -141,14 +172,34 @@ function judgeMatcher(matcher, depth, budgetMs) {
141
172
  return { kind: "invalid_regex" };
142
173
  case "inline":
143
174
  case "budgeted":
175
+ if (!judgesAlias)
176
+ return undefined;
144
177
  break;
145
178
  default: {
146
179
  const unreachable = evaluate;
147
180
  return unreachable;
148
181
  }
149
182
  }
150
- const selects = (name) => (evaluate === "inline" ? hookMatcherSelects(matcher, name) : matcherSelectsWithin(matcher, name, budgetMs));
151
- const unanswered = (verdict) => (verdict === "over_budget" ? { kind: "over_budget" } : { kind: "invalid_regex" });
183
+ const selects = (name) => {
184
+ if (evaluate === "inline")
185
+ return matcherSelectsBy(evaluate, matcher, name, ledger.budgetMs);
186
+ const startedAt = ledger.now();
187
+ const answer = matcherSelectsBy(evaluate, matcher, name, ledger.budgetMs);
188
+ ledger.spentMs += ledger.now() - startedAt;
189
+ if (answer === "over_budget" || answer === "invalid")
190
+ return answer;
191
+ return ledger.spentMs > ledger.totalMs ? "budget_exceeded" : answer;
192
+ };
193
+ const unanswered = (verdict) => {
194
+ switch (verdict) {
195
+ case "over_budget":
196
+ return { kind: "over_budget" };
197
+ case "budget_exceeded":
198
+ return { kind: "budget_exceeded" };
199
+ case "invalid":
200
+ return { kind: "invalid_regex" };
201
+ }
202
+ };
152
203
  for (const [alias, canonical] of engineToolAliases()) {
153
204
  const selectsAlias = selects(alias);
154
205
  if (typeof selectsAlias === "string")
@@ -163,50 +214,111 @@ function judgeMatcher(matcher, depth, budgetMs) {
163
214
  }
164
215
  return undefined;
165
216
  }
166
- export function buildToolMatcherRefusal(issue) {
217
+ const HOOKS_CAPS = {
218
+ matcher_groups: { measure: "entries", cap: MAX_HOOK_MATCHER_GROUPS, sentence: (event, count) => `settings.hooks.${event}: ${count} matcher groups exceeds cap ${MAX_HOOK_MATCHER_GROUPS}` },
219
+ event_entries: { measure: "entries", cap: MAX_HOOK_ENTRIES_PER_EVENT, sentence: (event, count) => `settings.hooks.${event}: ${count} entries exceeds cap ${MAX_HOOK_ENTRIES_PER_EVENT}` },
220
+ command: { measure: "characters", cap: MAX_HOOK_COMMAND_CHARS, sentence: (event) => `settings.hooks.${event}: command exceeds ${MAX_HOOK_COMMAND_CHARS} chars` },
221
+ url: { measure: "characters", cap: MAX_HOOK_COMMAND_CHARS, sentence: (event) => `settings.hooks.${event}: url exceeds ${MAX_HOOK_COMMAND_CHARS} chars` },
222
+ headers: { measure: "entries", cap: MAX_HOOK_HTTP_HEADERS, sentence: (event, count) => `settings.hooks.${event}: ${count} headers exceeds cap ${MAX_HOOK_HTTP_HEADERS}` },
223
+ header_chars: { measure: "characters", cap: MAX_HOOK_COMMAND_CHARS, sentence: (event) => `settings.hooks.${event}: headers exceed ${MAX_HOOK_COMMAND_CHARS} chars total` },
224
+ allowed_env_vars: { measure: "entries", cap: MAX_HOOK_HTTP_HEADERS, sentence: (event) => `settings.hooks.${event}: allowedEnvVars exceeds cap ${MAX_HOOK_HTTP_HEADERS}` },
225
+ plugin: { measure: "characters", cap: MAX_HOOK_COMMAND_CHARS, sentence: (event) => `settings.hooks.${event}: plugin context exceeds ${MAX_HOOK_COMMAND_CHARS} chars total` },
226
+ };
227
+ const HOOKS_WIRE_KEY_SHAPE = {
228
+ source: `source must be one of ${HOOK_SOURCE_VOCABULARY_SENTENCE}`,
229
+ plugin: "plugin must be { name, root, dataDir?, options? } — name a non-empty string, root and dataDir absolute paths with no \"..\" segment and no NUL byte, " +
230
+ "options an object of string values whose keys are identifiers ([A-Za-z_][A-Za-z0-9_]*); nothing else",
231
+ args: "args must be an array of strings without NUL bytes (exec form: command is the executable, args its arguments)",
232
+ };
233
+ function hooksCapIssue(limit, event, path, count) {
234
+ const c = HOOKS_CAPS[limit];
235
+ return { reason: LIMIT_REASONS[c.measure], limit, event, path, count, cap: c.cap };
236
+ }
237
+ const matcherPath = (event, groupIndex) => ["hooks", event, groupIndex, "matcher"];
238
+ export function buildHooksRefusal(issue) {
239
+ const field = "settings.hooks";
167
240
  switch (issue.reason) {
168
241
  case "dead_alias":
169
242
  return {
170
243
  message: `settings.hooks.${issue.event}: matcher "${issue.alias}" is an alias; tool-event matchers match the tool's wire name (PostToolUse / PostToolUseFailure since 7.107.0) — use "${issue.canonical}"`,
171
- field: "settings.hooks",
172
- detail: { reason: issue.reason, event: issue.event, alias: issue.alias, canonical: issue.canonical },
244
+ field,
245
+ detail: { reason: issue.reason, event: issue.event, alias: issue.alias, canonical: issue.canonical, path: matcherPath(issue.event, issue.groupIndex) },
173
246
  };
174
247
  case "over_budget":
175
248
  return {
176
249
  message: `settings.hooks.${issue.event}[${issue.groupIndex}].matcher: evaluating the pattern did not finish within ${issue.budgetMs} ms (catastrophic regex backtracking) — rewrite it as a tool-name list or a pattern without nested repetition`,
177
- field: "settings.hooks",
178
- detail: { reason: issue.reason, event: issue.event, groupIndex: issue.groupIndex, budgetMs: issue.budgetMs },
250
+ field,
251
+ detail: { reason: issue.reason, event: issue.event, groupIndex: issue.groupIndex, budgetMs: issue.budgetMs, path: matcherPath(issue.event, issue.groupIndex) },
252
+ };
253
+ case "budget_exceeded":
254
+ return {
255
+ message: `settings.hooks.${issue.event}[${issue.groupIndex}].matcher: evaluating this configuration's regular-expression matchers took longer than ${issue.totalMs} ms in total (very slow patterns) — write tool names as a list such as "Read|Edit", or simplify the patterns`,
256
+ field,
257
+ detail: { reason: issue.reason, event: issue.event, groupIndex: issue.groupIndex, totalMs: issue.totalMs, path: matcherPath(issue.event, issue.groupIndex) },
179
258
  };
180
259
  case "invalid_regex":
181
260
  return {
182
261
  message: `settings.hooks.${issue.event}[${issue.groupIndex}].matcher: ${JSON.stringify(issue.matcher)} is not a valid regular expression (${issue.compileError}) — fix the pattern, or write the tool names as a list such as "Read|Edit"`,
183
- field: "settings.hooks",
184
- detail: { reason: issue.reason, event: issue.event, groupIndex: issue.groupIndex },
262
+ field,
263
+ detail: { reason: issue.reason, event: issue.event, groupIndex: issue.groupIndex, path: matcherPath(issue.event, issue.groupIndex) },
264
+ };
265
+ case "unsupported_entry_type":
266
+ return {
267
+ message: `${hooksCoordinate(issue.path)}: this server does not run hook entries of type ${issue.type}; supported: ${SUPPORTED_HOOK_ENTRY_TYPES.join(" / ")}`,
268
+ field,
269
+ detail: { reason: issue.reason, path: hooksOutgoingPath(issue.path), type: issue.type },
185
270
  };
271
+ case "over_cap":
272
+ case "too_long": {
273
+ const cap = issue;
274
+ return { message: HOOKS_CAPS[cap.limit].sentence(cap.event, cap.count), field, detail: { reason: cap.reason, path: hooksOutgoingPath(cap.path), count: cap.count, cap: cap.cap } };
275
+ }
276
+ case "unnamed_refusal":
277
+ return { message: `${hooksCoordinate(issue.path)}: the hooks reader refused this configuration without naming a piece`, field, detail: { reason: issue.reason, path: hooksOutgoingPath(issue.path) } };
278
+ case "wire_key_invalid":
279
+ return { message: `${hooksCoordinate(issue.path)}: ${HOOKS_WIRE_KEY_SHAPE[issue.key]}`, field, detail: { reason: issue.reason, path: hooksOutgoingPath(issue.path), key: issue.key } };
186
280
  default: {
187
- const unreachable = issue;
188
- return unreachable;
281
+ const piece = issue;
282
+ const key = piece.key !== undefined ? hooksOutgoingKey(piece.key) : undefined;
283
+ return {
284
+ message: hooksPieceRefusal(piece),
285
+ field,
286
+ detail: { reason: piece.reason, path: hooksOutgoingPath(piece.path), ...(key !== undefined ? { key } : {}) },
287
+ };
189
288
  }
190
289
  }
191
290
  }
192
- export function toolMatcherIssue(config, depth, budget = {}) {
193
- const budgetMs = budget.budgetMs ?? ADMISSION_MATCHER_EVAL_BUDGET_MS;
291
+ export function hooksMatcherIssue(config, depth, budget = {}) {
292
+ const ledger = {
293
+ budgetMs: budget.budgetMs ?? ADMISSION_MATCHER_BUDGET.budgetMs,
294
+ totalMs: budget.totalMs ?? ADMISSION_MATCHER_BUDGET.totalMs,
295
+ now: budget.now ?? (() => performance.now()),
296
+ spentMs: 0,
297
+ };
298
+ const budgetMs = ledger.budgetMs;
194
299
  for (const [event, groups] of Object.entries(config)) {
195
- if (!matcherReadsWireToolName(event))
196
- continue;
300
+ const judgesAlias = matcherReadsWireToolName(event);
197
301
  for (const [groupIndex, group] of (groups ?? []).entries()) {
198
302
  if (group.matcher === undefined)
199
303
  continue;
200
- const verdict = judgeMatcher(group.matcher, depth, budgetMs);
304
+ const verdict = judgeMatcher(group.matcher, depth, ledger, judgesAlias);
201
305
  if (verdict === undefined)
202
306
  continue;
203
307
  switch (verdict.kind) {
204
308
  case "dead":
205
- return { reason: "dead_alias", event, alias: verdict.alias, canonical: verdict.canonical };
309
+ return { reason: "dead_alias", event, groupIndex, alias: verdict.alias, canonical: verdict.canonical };
206
310
  case "over_budget":
207
311
  return { reason: "over_budget", event, groupIndex, budgetMs };
312
+ case "budget_exceeded":
313
+ return { reason: "budget_exceeded", event, groupIndex, totalMs: ledger.totalMs };
208
314
  case "invalid_regex":
209
- return { reason: "invalid_regex", event, groupIndex, matcher: clip(group.matcher, MATCHER_ECHO_CHARS), compileError: clip(matcherCompileSentence(group.matcher), MATCHER_ECHO_CHARS) };
315
+ return {
316
+ reason: "invalid_regex",
317
+ event,
318
+ groupIndex,
319
+ matcher: echoCallerText(group.matcher, { maxChars: MATCHER_ECHO_CHARS }),
320
+ compileError: echoCallerText(matcherCompileSentence(group.matcher), { maxChars: MATCHER_ECHO_CHARS }),
321
+ };
210
322
  case "unjudged":
211
323
  recordFailOpen(MATCHER_UNJUDGED_TAG, `event=${event} reason=${verdict.reason} matcherChars=${group.matcher.length}`);
212
324
  break;
@@ -232,80 +344,121 @@ const HOOKS_PIECE_MEANING = {
232
344
  "guard-matcher-invalid": "a PreToolUse / PermissionRequest matcher group is not valid",
233
345
  "guard-entry-invalid": "a PreToolUse / PermissionRequest hook entry is not valid",
234
346
  };
347
+ export function hooksOutgoingPath(path) {
348
+ return outgoingPath(path).map((seg) => (typeof seg === "string" ? echoCallerText(seg) : seg));
349
+ }
235
350
  export function hooksCoordinate(path) {
236
- return `settings.${formatConfigPath(outgoingPath(path))}`;
351
+ return `settings.${formatConfigPath(hooksOutgoingPath(path))}`;
352
+ }
353
+ function hooksOutgoingKey(key) {
354
+ return echoCallerText(String(outgoingPath([key])[0] ?? ""));
355
+ }
356
+ function hooksPieceIssue(piece) {
357
+ return piece.field !== undefined ? { reason: piece.code, path: piece.path, key: piece.field } : { reason: piece.code, path: piece.path };
237
358
  }
238
359
  function hooksPieceRefusal(piece) {
239
- const field = piece.field !== undefined ? ` (field: ${piece.field})` : "";
240
- return `${hooksCoordinate(piece.path)}: ${piece.code} — ${HOOKS_PIECE_MEANING[piece.code]}${field}`;
360
+ const field = piece.key !== undefined ? ` (field: ${hooksOutgoingKey(piece.key)})` : "";
361
+ return `${hooksCoordinate(piece.path)}: ${piece.reason} — ${HOOKS_PIECE_MEANING[piece.reason]}${field}`;
241
362
  }
242
- function unsupportedHookEntryTypeRefusal(path, type) {
243
- return `${hooksCoordinate(path)}: this server does not run hook entries of type ${type}; supported: ${SUPPORTED_HOOK_ENTRY_TYPES.join(" / ")}`;
363
+ function hooksRefused(issue, pieces) {
364
+ const error = buildHooksRefusal(issue).message;
365
+ return pieces === undefined ? { issue, error } : { issue, error, pieces };
244
366
  }
245
367
  export function parseHooksConfig(raw) {
246
368
  const classified = classifyHooksConfig({ hooks: raw });
247
369
  if (classified.outcome !== "loaded") {
248
370
  const first = classified.outcome === "rejected" ? classified.rejections[0] : classified.dropped[0];
249
371
  const pieces = classified.outcome === "rejected" ? classified.rejections : classified.dropped;
250
- return { error: first !== undefined ? hooksPieceRefusal(first) : `${hooksCoordinate(["hooks"])}: the hooks reader refused this configuration without naming a piece`, pieces };
372
+ return hooksRefused(first !== undefined ? hooksPieceIssue(first) : { reason: "unnamed_refusal", path: ["hooks"] }, pieces);
251
373
  }
252
374
  const loaded = classified.hooks;
253
375
  if (loaded === undefined) {
254
- return { error: hooksPieceRefusal({ code: "hooks-not-object", path: ["hooks"] }) };
376
+ return hooksRefused({ reason: "hooks-not-object", path: ["hooks"] });
255
377
  }
256
378
  const config = {};
379
+ const rawMember = (node, key) => node !== null && typeof node === "object" && Object.hasOwn(node, key) ? node[key] : undefined;
257
380
  for (const key of Object.keys(loaded)) {
258
381
  if (!isHookEventName(key)) {
259
- return { error: hooksPieceRefusal({ code: "unknown-event", path: ["hooks", key] }) };
382
+ return hooksRefused({ reason: "unknown-event", path: ["hooks", key] });
260
383
  }
261
384
  const event = key;
262
385
  const groups = loaded[event];
263
386
  if (!groups)
264
387
  continue;
265
- if (groups.length > MAX_HOOK_MATCHER_GROUPS) {
266
- return { error: `settings.hooks.${event}: ${groups.length} matcher groups exceeds cap ${MAX_HOOK_MATCHER_GROUPS}` };
267
- }
388
+ if (groups.length > MAX_HOOK_MATCHER_GROUPS)
389
+ return hooksRefused(hooksCapIssue("matcher_groups", event, ["hooks", event], groups.length));
268
390
  let entries = 0;
391
+ let firstPastCap;
269
392
  const kept = [];
393
+ const rawGroups = rawMember(raw, event);
270
394
  for (const [groupIndex, group] of groups.entries()) {
395
+ const rawGroup = rawMember(rawGroups, groupIndex);
396
+ const rawSource = rawMember(rawGroup, "source");
397
+ const source = rawSource === undefined ? undefined : readHookGroupSource(rawSource);
398
+ if (rawSource !== undefined && source === undefined)
399
+ return hooksRefused({ reason: "wire_key_invalid", path: ["hooks", event, groupIndex, "source"], key: "source" });
271
400
  const hooks = [];
272
- for (const [entryIndex, entry] of group.hooks.entries()) {
401
+ for (const [entryIndex, contractEntry] of group.hooks.entries()) {
273
402
  entries++;
274
- if (!isRunnableHookEntry(entry)) {
275
- return { error: unsupportedHookEntryTypeRefusal(["hooks", event, groupIndex, "hooks", entryIndex, "type"], entry.type) };
276
- }
277
- if (entry.type === "command" && entry.command.length > MAX_HOOK_COMMAND_CHARS) {
278
- return { error: `settings.hooks.${event}: command exceeds ${MAX_HOOK_COMMAND_CHARS} chars` };
403
+ const at = ["hooks", event, groupIndex, "hooks", entryIndex];
404
+ if (entries === MAX_HOOK_ENTRIES_PER_EVENT + 1)
405
+ firstPastCap = at;
406
+ if (!isRunnableHookEntry(contractEntry))
407
+ return hooksRefused({ reason: "unsupported_entry_type", path: [...at, "type"], type: contractEntry.type });
408
+ let entry = contractEntry;
409
+ if (entry.type === "command") {
410
+ const rawEntry = rawMember(rawMember(rawGroup, "hooks"), entryIndex);
411
+ const rawArgs = rawMember(rawEntry, "args");
412
+ const rawPlugin = rawMember(rawEntry, "plugin");
413
+ const args = rawArgs === undefined ? undefined : readHookArgs(rawArgs);
414
+ if (rawArgs !== undefined && args === undefined)
415
+ return hooksRefused({ reason: "wire_key_invalid", path: [...at, "args"], key: "args" });
416
+ const plugin = rawPlugin === undefined ? undefined : readHookPlugin(rawPlugin);
417
+ if (rawPlugin !== undefined && plugin === undefined)
418
+ return hooksRefused({ reason: "wire_key_invalid", path: [...at, "plugin"], key: "plugin" });
419
+ entry = { ...entry, ...(args !== undefined ? { args } : {}), ...(plugin !== undefined ? { plugin } : {}) };
420
+ const line = commandLineChars(entry);
421
+ if (line > MAX_HOOK_COMMAND_CHARS)
422
+ return hooksRefused(hooksCapIssue("command", event, [...at, "command"], line));
423
+ if (plugin !== undefined && hookPluginChars(plugin) > MAX_HOOK_COMMAND_CHARS)
424
+ return hooksRefused(hooksCapIssue("plugin", event, [...at, "plugin"], hookPluginChars(plugin)));
279
425
  }
280
426
  if (entry.type === "http") {
281
- if (entry.url.length > MAX_HOOK_COMMAND_CHARS) {
282
- return { error: `settings.hooks.${event}: url exceeds ${MAX_HOOK_COMMAND_CHARS} chars` };
283
- }
427
+ if (entry.url.length > MAX_HOOK_COMMAND_CHARS)
428
+ return hooksRefused(hooksCapIssue("url", event, [...at, "url"], entry.url.length));
284
429
  const headerEntries = Object.entries(entry.headers ?? {});
285
- if (headerEntries.length > MAX_HOOK_HTTP_HEADERS) {
286
- return { error: `settings.hooks.${event}: ${headerEntries.length} headers exceeds cap ${MAX_HOOK_HTTP_HEADERS}` };
287
- }
430
+ if (headerEntries.length > MAX_HOOK_HTTP_HEADERS)
431
+ return hooksRefused(hooksCapIssue("headers", event, [...at, "headers"], headerEntries.length));
288
432
  const headerChars = headerEntries.reduce((n, [k, v]) => n + k.length + v.length, 0);
289
- if (headerChars > MAX_HOOK_COMMAND_CHARS) {
290
- return { error: `settings.hooks.${event}: headers exceed ${MAX_HOOK_COMMAND_CHARS} chars total` };
291
- }
292
- if ((entry.allowedEnvVars?.length ?? 0) > MAX_HOOK_HTTP_HEADERS) {
293
- return { error: `settings.hooks.${event}: allowedEnvVars exceeds cap ${MAX_HOOK_HTTP_HEADERS}` };
294
- }
433
+ if (headerChars > MAX_HOOK_COMMAND_CHARS)
434
+ return hooksRefused(hooksCapIssue("header_chars", event, [...at, "headers"], headerChars));
435
+ const envVars = entry.allowedEnvVars?.length ?? 0;
436
+ if (envVars > MAX_HOOK_HTTP_HEADERS)
437
+ return hooksRefused(hooksCapIssue("allowed_env_vars", event, [...at, "allowedEnvVars"], envVars));
295
438
  }
296
439
  hooks.push(entry);
297
440
  }
298
- kept.push(group.matcher !== undefined ? { matcher: group.matcher, hooks } : { hooks });
299
- }
300
- if (entries > MAX_HOOK_ENTRIES_PER_EVENT) {
301
- return { error: `settings.hooks.${event}: ${entries} entries exceeds cap ${MAX_HOOK_ENTRIES_PER_EVENT}` };
441
+ kept.push({ ...(group.matcher !== undefined ? { matcher: group.matcher } : {}), ...(source !== undefined ? { source } : {}), hooks });
302
442
  }
443
+ if (firstPastCap !== undefined)
444
+ return hooksRefused(hooksCapIssue("event_entries", event, firstPastCap, entries));
303
445
  config[event] = kept;
304
446
  }
305
447
  return { config };
306
448
  }
307
449
  export function hookMatcherMatches(matcher, value) {
308
- return hookMatcherSelects(matcher, value);
450
+ const evaluate = MATCHER_EVALUATION[hookMatcherForm(matcher)][RUNTIME_EVAL_DEPTH];
451
+ switch (evaluate) {
452
+ case "inline":
453
+ case "budgeted":
454
+ return matcherSelectsBy(evaluate, matcher, value, ADMISSION_MATCHER_BUDGET.budgetMs);
455
+ case "unreadable":
456
+ return "invalid";
457
+ default: {
458
+ const unreachable = evaluate;
459
+ return unreachable;
460
+ }
461
+ }
309
462
  }
310
463
  export function hookIfMatches(cond, payload) {
311
464
  if (cond.length > 500 || (cond.match(/\*/g)?.length ?? 0) > 16)
@@ -318,10 +471,20 @@ export function hookIfMatches(cond, payload) {
318
471
  if (!m)
319
472
  return "unevaluable";
320
473
  const nameOk = hookMatcherMatches(m[1].trim(), toolName);
321
- if (nameOk === "invalid")
322
- return "unevaluable";
323
- if (!nameOk)
324
- return "not_matched";
474
+ switch (nameOk) {
475
+ case "invalid":
476
+ return "unevaluable";
477
+ case "over_budget":
478
+ return "over_budget";
479
+ case false:
480
+ return "not_matched";
481
+ case true:
482
+ break;
483
+ default: {
484
+ const unreachable = nameOk;
485
+ return unreachable;
486
+ }
487
+ }
325
488
  const spec = m[2];
326
489
  if (spec === undefined || spec === "*" || spec === "")
327
490
  return true;
@@ -340,8 +503,11 @@ export function hookIfMatches(cond, payload) {
340
503
  const MAX_LLM_ENTRIES_PER_EVENT = 4;
341
504
  const DEFAULT_PROMPT_HOOK_TIMEOUT_SECONDS = 30;
342
505
  const DEFAULT_AGENT_HOOK_TIMEOUT_SECONDS = 120;
343
- function clip(s, n) {
344
- return s.length > n ? `${s.slice(0, n)}…(${s.length})` : s;
506
+ function logHookCommandFailed(ctx, event, run) {
507
+ ctx.logger.warn("hook_command_failed", { event, code: run.code, stderr: echoCallerText(run.stderr, { maxChars: LOG_ERROR_ECHO_CHARS }) });
508
+ }
509
+ function logHookSystemMessage(ctx, event, message) {
510
+ ctx.logger.info("hook_system_message", { event, message: echoCallerText(message, { maxChars: LOG_MESSAGE_ECHO_CHARS }) });
345
511
  }
346
512
  function hookDisplayName(entry) {
347
513
  if (typeof entry.statusMessage === "string" && entry.statusMessage.length > 0)
@@ -370,7 +536,7 @@ function emitHookNotice(ctx, n) {
370
536
  }
371
537
  catch (e) {
372
538
  recordFailOpen("server.hooks.notice-sink-threw", `kind=${n.kind} event=${n.event}`);
373
- ctx.logger.warn("hook_notice_sink_threw", { kind: n.kind, event: n.event, error: clip(String(e), 200) });
539
+ ctx.logger.warn("hook_notice_sink_threw", { kind: n.kind, event: n.event, error: echoCallerText(String(e), { maxChars: LOG_PREVIEW_ECHO_CHARS }) });
374
540
  }
375
541
  }
376
542
  function noteHookFailure(ctx, args) {
@@ -389,84 +555,212 @@ function noteHookFailure(ctx, args) {
389
555
  ...(args.detail !== undefined ? { detail: boundedNoticeText(args.detail) } : {}),
390
556
  });
391
557
  }
558
+ function noteMatcherOverBudget(ctx, args) {
559
+ const budgetMs = ADMISSION_MATCHER_BUDGET.budgetMs;
560
+ const shown = (text) => echoCallerText(boundedNoticeText(text), { maxChars: MATCHER_ECHO_CHARS });
561
+ const expression = shown(args.expression);
562
+ const value = shown(args.value);
563
+ ctx.logger.warn("hook_matcher_over_budget", { event: args.event, scope: args.scope, expression, value, budgetMs, selected: true });
564
+ const first = args.entries[0];
565
+ if (first === undefined)
566
+ return;
567
+ const what = args.scope === "if" ? "if condition" : "matcher";
568
+ const count = args.entries.length;
569
+ noteHookFailure(ctx, {
570
+ event: args.event,
571
+ entry: first,
572
+ reason: "matcher_over_budget",
573
+ ...(args.toolName !== undefined ? { toolName: args.toolName } : {}),
574
+ detail: `${what} \`${expression}\` did not finish within ${budgetMs} ms on \`${value}\`; treated as selected (${count} ${count === 1 ? "entry" : "entries"})`,
575
+ });
576
+ }
577
+ function matcherGroupSelected(ctx, event, group, value, toolName) {
578
+ const answer = hookMatcherMatches(group.matcher, value);
579
+ switch (answer) {
580
+ case true:
581
+ return true;
582
+ case false:
583
+ return false;
584
+ case "invalid":
585
+ ctx.logger.warn("hook_matcher_invalid", { event, matcher: echoCallerText(group.matcher ?? "", { maxChars: MATCHER_ECHO_CHARS }) });
586
+ return "invalid";
587
+ case "over_budget":
588
+ noteMatcherOverBudget(ctx, { event, scope: "matcher", expression: group.matcher ?? "", value, entries: group.hooks, ...(toolName !== undefined ? { toolName } : {}) });
589
+ return "over_budget";
590
+ default: {
591
+ const unreachable = answer;
592
+ return unreachable;
593
+ }
594
+ }
595
+ }
596
+ export const PERMISSIVE_VERDICT_EVENTS = {
597
+ PreToolUse: true,
598
+ PostToolUse: false,
599
+ PostToolUseFailure: false,
600
+ PostToolBatch: false,
601
+ Notification: false,
602
+ UserPromptSubmit: false,
603
+ UserPromptExpansion: false,
604
+ SessionStart: false,
605
+ SessionEnd: false,
606
+ Stop: false,
607
+ StopFailure: false,
608
+ SubagentStart: false,
609
+ SubagentStop: false,
610
+ PreCompact: false,
611
+ PostCompact: false,
612
+ PermissionRequest: false,
613
+ PermissionDenied: false,
614
+ Setup: false,
615
+ TeammateIdle: false,
616
+ TaskCreated: false,
617
+ TaskCompleted: false,
618
+ Elicitation: false,
619
+ ElicitationResult: false,
620
+ ConfigChange: false,
621
+ WorktreeCreate: false,
622
+ WorktreeRemove: false,
623
+ InstructionsLoaded: false,
624
+ CwdChanged: false,
625
+ FileChanged: false,
626
+ MessageDisplay: false,
627
+ DirectoryAdded: false,
628
+ PreModelSwitch: false,
629
+ PostModelSwitch: false,
630
+ };
631
+ const DISTRUST_DETAIL = {
632
+ input_truncated: `the hook did not receive the whole payload (tool_input over ${MAX_TOOL_INPUT_CHARS} characters was replaced by a truncation marker, or a prompt / agent entry's payload text was clipped at that size); its allow / updatedInput are not honored`,
633
+ matcher_over_budget: "the matcher (or the if condition's tool-name part) did not finish within its budget; the entry ran as selected, so its allow / updatedInput are not honored",
634
+ };
635
+ function noteVerdictUntrusted(ctx, args) {
636
+ const notice = {
637
+ kind: "hook_verdict_untrusted",
638
+ event: args.event,
639
+ reason: args.reason,
640
+ lane: hookPhaseOf(args.source) === "unmanaged" ? "unmanaged" : "managed",
641
+ hookName: boundedNoticeText(hookDisplayName(args.entry)),
642
+ entryType: args.entry.type,
643
+ ...(args.toolName !== undefined ? { toolName: args.toolName } : {}),
644
+ ...(args.source !== undefined ? { source: boundedNoticeText(args.source) } : {}),
645
+ detail: boundedNoticeText(DISTRUST_DETAIL[args.reason]),
646
+ };
647
+ const logEcho = (text) => echoCallerText(text, { maxChars: MAX_HOOK_NOTICE_TEXT_CHARS });
648
+ ctx.logger.warn("hook_verdict_not_honored", {
649
+ event: notice.event,
650
+ reason: notice.reason,
651
+ lane: notice.lane,
652
+ hookName: logEcho(notice.hookName),
653
+ entryType: notice.entryType,
654
+ ...(notice.toolName !== undefined ? { toolName: notice.toolName } : {}),
655
+ ...(notice.source !== undefined ? { source: logEcho(notice.source) } : {}),
656
+ detail: notice.detail,
657
+ });
658
+ if (!ctx.onHookNotice)
659
+ return;
660
+ emitHookNotice(ctx, notice);
661
+ }
662
+ function createUnrunLedger(ctx, event, toolName) {
663
+ let managedWithoutVerdict = false;
664
+ return {
665
+ untrusted(source, entry, reason) {
666
+ if (hookPhaseOf(source) === "unmanaged")
667
+ return;
668
+ managedWithoutVerdict = true;
669
+ noteVerdictUntrusted(ctx, { event, reason, entry, source, toolName });
670
+ },
671
+ unrun(source, entry, reason, detail) {
672
+ if (source === undefined || hookPhaseOf(source) === "unmanaged")
673
+ return;
674
+ managedWithoutVerdict = true;
675
+ ctx.logger.warn("hook_managed_unrun", { event, reason, source: echoCallerText(source), type: entry.type, ...(toolName !== undefined ? { tool: toolName } : {}) });
676
+ if (!ctx.onHookNotice)
677
+ return;
678
+ emitHookNotice(ctx, {
679
+ kind: "hook_managed_unrun",
680
+ event,
681
+ reason,
682
+ source: boundedNoticeText(source),
683
+ hookName: boundedNoticeText(hookDisplayName(entry)),
684
+ entryType: entry.type,
685
+ ...(toolName !== undefined ? { toolName } : {}),
686
+ ...(detail !== undefined ? { detail: boundedNoticeText(detail) } : {}),
687
+ });
688
+ },
689
+ get managedWithoutVerdict() {
690
+ return managedWithoutVerdict;
691
+ },
692
+ };
693
+ }
694
+ function noteExitNonzero(ctx, ledger, event, ran, toolName, detail) {
695
+ logHookCommandFailed(ctx, event, ran.run);
696
+ noteHookFailure(ctx, { event, entry: ran.entry, reason: "exit_nonzero", exitCode: ran.run.code, stderr: ran.run.stderr, ...(toolName !== undefined ? { toolName } : {}), ...(detail !== undefined ? { detail } : {}) });
697
+ ledger.unrun(ran.source, ran.entry, "exit_nonzero");
698
+ }
392
699
  export function buildHookContext(parts) {
393
- const present = parts.filter((p) => p.length > 0).map(readHookContextPart);
700
+ const present = parts.filter((p) => p.length > 0);
394
701
  if (present.length === 0)
395
702
  return undefined;
396
703
  const kept = [];
397
704
  let used = 0;
398
705
  let truncating = false;
399
- let messages = 0;
400
706
  let omitted = 0;
401
707
  let droppedChars = 0;
402
708
  for (const part of present) {
403
- messages += part.messages;
404
- omitted += part.omitted;
405
- droppedChars += part.dropped;
406
709
  if (!truncating) {
407
710
  const sep = kept.length > 0 ? 1 : 0;
408
711
  const room = MAX_HOOK_CONTEXT_TOTAL_CHARS - used - sep;
409
- if (room >= part.body.length) {
410
- if (part.body.length > 0) {
411
- kept.push(part.body);
412
- used += sep + part.body.length;
413
- }
712
+ if (room >= part.length) {
713
+ kept.push(part);
714
+ used += sep + part.length;
414
715
  continue;
415
716
  }
416
717
  truncating = true;
417
- if (part.messages === 1 && room >= MIN_HOOK_CONTEXT_PARTIAL_CHARS) {
418
- kept.push(part.body.slice(0, room));
718
+ if (room >= MIN_HOOK_CONTEXT_PARTIAL_CHARS) {
719
+ kept.push(part.slice(0, room));
419
720
  used += sep + room;
420
- droppedChars += part.body.length - room;
721
+ droppedChars += part.length - room;
421
722
  continue;
422
723
  }
423
724
  }
424
- omitted += part.messages - part.omitted;
425
- droppedChars += part.body.length;
725
+ omitted += 1;
726
+ droppedChars += part.length;
426
727
  }
427
728
  const body = kept.join("\n");
428
729
  if (droppedChars === 0)
429
730
  return body;
430
- const marker = `[hook context truncated at ${MAX_HOOK_CONTEXT_TOTAL_CHARS} chars: ${omitted} of ${messages} hook messages omitted, ${droppedChars} characters dropped]`;
731
+ const marker = `[hook context truncated at ${MAX_HOOK_CONTEXT_TOTAL_CHARS} chars: ${omitted} of ${present.length} hook messages omitted, ${droppedChars} characters dropped]`;
431
732
  return kept.length > 0 ? `${body}\n${marker}` : marker;
432
733
  }
433
- const HOOK_CONTEXT_MARKER_RE = new RegExp(`\\n\\[hook context truncated at ${MAX_HOOK_CONTEXT_TOTAL_CHARS} chars: (\\d+) of (\\d+) hook messages omitted, (\\d+) characters dropped\\]$`);
434
- const MAX_HOOK_CONTEXT_PARSED_COUNT = 2 ** 42;
435
- function readHookContextPart(part) {
436
- const m = HOOK_CONTEXT_MARKER_RE.exec(part);
437
- if (!m)
438
- return { body: part, messages: 1, omitted: 0, dropped: 0 };
439
- const omitted = Number(m[1]);
440
- const messages = Number(m[2]);
441
- const dropped = Number(m[3]);
442
- const sane = (n) => Number.isSafeInteger(n) && n <= MAX_HOOK_CONTEXT_PARSED_COUNT;
443
- if (!sane(omitted) || !sane(messages) || !sane(dropped) || messages < 1 || omitted > messages || dropped < 1) {
444
- return { body: part, messages: 1, omitted: 0, dropped: 0 };
445
- }
446
- return { body: part.slice(0, part.length - m[0].length), messages, omitted, dropped };
447
- }
448
734
  function buildHookContextField(parts) {
449
735
  const text = buildHookContext(parts);
450
736
  return text === undefined ? {} : { additionalContext: text };
451
737
  }
452
- async function runCommandHook(entry, payload, ctx) {
453
- try {
454
- await resolveHostShell();
455
- }
456
- catch (e) {
457
- return { code: null, stdout: "", stderr: "", timedOut: false, spawnError: String(e) };
738
+ function spawnPlannedCommand(plan, ctx, stdio) {
739
+ const env = hookEnvOverBase(plan.env, true);
740
+ if (plan.kind === "exec")
741
+ return spawn(plan.file, [...plan.args], { cwd: ctx.cwd, env, stdio, detached: true });
742
+ const sh = hostShell();
743
+ return spawn(sh.shell, [...sh.args, plan.command], { cwd: ctx.cwd, env, stdio, detached: true });
744
+ }
745
+ async function runCommandHook(entry, payload, ctx, halt) {
746
+ const plan = await planCommandSpawn(entry, ctx.cwd, ctx.shellEnv);
747
+ if (plan.kind === "refused")
748
+ return { code: null, stdout: "", stderr: "", timedOut: false, spawnError: plan.error };
749
+ if (plan.kind === "shell") {
750
+ try {
751
+ await resolveHostShell();
752
+ }
753
+ catch (e) {
754
+ return { code: null, stdout: "", stderr: "", timedOut: false, spawnError: String(e) };
755
+ }
458
756
  }
757
+ if (halt.aborted)
758
+ return { code: null, stdout: "", stderr: "", timedOut: false, aborted: {} };
459
759
  return new Promise((resolve) => {
460
760
  const timeoutMs = Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) * 1000;
461
761
  let child;
462
762
  try {
463
- const sh = hostShell();
464
- child = spawn(sh.shell, [...sh.args, entry.command], {
465
- cwd: ctx.cwd,
466
- env: hookEnv(ctx),
467
- stdio: ["pipe", "pipe", "pipe"],
468
- detached: true,
469
- });
763
+ child = spawnPlannedCommand(plan, ctx, ["pipe", "pipe", "pipe"]);
470
764
  }
471
765
  catch (e) {
472
766
  resolve({ code: null, stdout: "", stderr: "", timedOut: false, spawnError: String(e) });
@@ -477,16 +771,20 @@ async function runCommandHook(entry, payload, ctx) {
477
771
  let stderr = "";
478
772
  let timedOut = false;
479
773
  let settled = false;
774
+ let sent;
480
775
  const settle = (code) => {
481
776
  if (settled)
482
777
  return;
483
778
  settled = true;
484
779
  clearTimeout(timer);
780
+ halt.removeEventListener("abort", onHalt);
485
781
  ticket?.release();
486
- resolve({ code, stdout, stderr, timedOut });
782
+ resolve({ code, stdout, stderr, timedOut, ...(sent !== undefined ? { aborted: { signal: sent } } : {}) });
487
783
  };
488
784
  const timer = setTimeout(() => {
489
785
  timedOut = true;
786
+ if (sent !== undefined)
787
+ sent = "SIGKILL";
490
788
  try {
491
789
  if (child.pid)
492
790
  process.kill(-child.pid, "SIGKILL");
@@ -498,6 +796,31 @@ async function runCommandHook(entry, payload, ctx) {
498
796
  }
499
797
  settle(null);
500
798
  }, timeoutMs);
799
+ const signalGroup = (sig) => {
800
+ try {
801
+ if (child.pid)
802
+ (sig === "SIGTERM" ? killTreeSoft : killTreeHard)(child.pid);
803
+ else
804
+ child.kill(sig);
805
+ }
806
+ catch {
807
+ child.kill(sig);
808
+ }
809
+ };
810
+ const onHalt = () => {
811
+ if (settled)
812
+ return;
813
+ sent = "SIGTERM";
814
+ signalGroup("SIGTERM");
815
+ const escalate = setTimeout(() => {
816
+ if (child.exitCode !== null || child.signalCode !== null)
817
+ return;
818
+ sent = "SIGKILL";
819
+ signalGroup("SIGKILL");
820
+ }, HOOK_KILL_GRACE_MS);
821
+ escalate.unref();
822
+ };
823
+ halt.addEventListener("abort", onHalt, { once: true });
501
824
  child.stdout?.on("data", (d) => {
502
825
  if (stdout.length < MAX_HOOK_OUTPUT_BYTES)
503
826
  stdout += d.toString("utf8").slice(0, MAX_HOOK_OUTPUT_BYTES - stdout.length);
@@ -508,6 +831,7 @@ async function runCommandHook(entry, payload, ctx) {
508
831
  });
509
832
  child.on("error", (e) => {
510
833
  clearTimeout(timer);
834
+ halt.removeEventListener("abort", onHalt);
511
835
  if (!settled) {
512
836
  settled = true;
513
837
  resolve({ code: null, stdout, stderr, timedOut, spawnError: String(e) });
@@ -518,18 +842,18 @@ async function runCommandHook(entry, payload, ctx) {
518
842
  child.stdin?.end(`${JSON.stringify(boundedToolInputPayload(payload))}\n`);
519
843
  });
520
844
  }
521
- async function fireAsyncCommandHook(entry, payload, ctx, event) {
845
+ async function fireAsyncCommandHook(entry, payload, ctx, event, halt) {
522
846
  try {
523
847
  const rewake = entry.asyncRewake === true;
524
848
  const timeoutMs = Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) * 1000;
525
- await resolveHostShell();
526
- const sh = hostShell();
527
- const child = spawn(sh.shell, [...sh.args, entry.command], {
528
- cwd: ctx.cwd,
529
- env: hookEnv(ctx),
530
- stdio: ["pipe", "ignore", rewake ? "pipe" : "ignore"],
531
- detached: true,
532
- });
849
+ const plan = await planCommandSpawn(entry, ctx.cwd, ctx.shellEnv);
850
+ if (plan.kind === "refused")
851
+ throw new Error(plan.error);
852
+ if (plan.kind === "shell")
853
+ await resolveHostShell();
854
+ if (halt.aborted)
855
+ return;
856
+ const child = spawnPlannedCommand(plan, ctx, ["pipe", "ignore", rewake ? "pipe" : "ignore"]);
533
857
  const ticket = adoptHostForegroundChild(child);
534
858
  let killedByTimeout = false;
535
859
  const timer = setTimeout(() => {
@@ -555,7 +879,7 @@ async function fireAsyncCommandHook(entry, payload, ctx, event) {
555
879
  }
556
880
  child.on("error", (e) => {
557
881
  clearTimeout(timer);
558
- ctx.logger.warn("hook_async_failed", { event, reason: "spawn_failed", error: clip(String(e), 300) });
882
+ ctx.logger.warn("hook_async_failed", { event, reason: "spawn_failed", error: echoCallerText(String(e), { maxChars: LOG_ERROR_ECHO_CHARS }) });
559
883
  noteHookFailure(ctx, { event, entry, reason: "spawn_failed", stderr: String(e), toolName: toolNameOf(payload) });
560
884
  });
561
885
  child.on("close", (code) => {
@@ -578,14 +902,14 @@ async function fireAsyncCommandHook(entry, payload, ctx, event) {
578
902
  ctx.logger.info("hook_async_rewake_delivered", { event, chars: text.length });
579
903
  else
580
904
  ctx.logger.warn("hook_async_rewake_dropped", { event, reason: "no_live_stream" });
581
- }, (e) => ctx.logger.warn("hook_async_rewake_dropped", { event, reason: "wake_failed", error: String(e).slice(0, 200) }));
905
+ }, (e) => ctx.logger.warn("hook_async_rewake_dropped", { event, reason: "wake_failed", error: echoCallerText(String(e), { maxChars: LOG_PREVIEW_ECHO_CHARS }) }));
582
906
  });
583
907
  child.stdin?.on("error", () => { });
584
908
  child.stdin?.end(`${JSON.stringify(boundedToolInputPayload(payload))}\n`);
585
909
  child.unref();
586
910
  }
587
911
  catch (e) {
588
- ctx.logger.warn("hook_async_failed", { event, reason: "spawn_failed", error: clip(String(e), 300) });
912
+ ctx.logger.warn("hook_async_failed", { event, reason: "spawn_failed", error: echoCallerText(String(e), { maxChars: LOG_ERROR_ECHO_CHARS }) });
589
913
  noteHookFailure(ctx, { event, entry, reason: "spawn_failed", stderr: String(e), toolName: toolNameOf(payload) });
590
914
  }
591
915
  }
@@ -610,14 +934,19 @@ function noteAsyncOutcome(entry, ctx, event, payload, outcome) {
610
934
  ...(code === 2 ? { detail: "exit 2 on a fire-and-forget async hook: there is no blocking or wake face to honor it (asyncRewake would wake the model)" } : {}),
611
935
  });
612
936
  }
937
+ function llmHookArguments(payload) {
938
+ const bounded = boundedToolInputPayload(payload);
939
+ const json = JSON.stringify(bounded);
940
+ const text = clipDataText(json, MAX_TOOL_INPUT_CHARS);
941
+ return { text, truncated: bounded !== payload || text !== json };
942
+ }
613
943
  async function runLlmHook(entry, payload, ctx, extra) {
614
944
  const call = entry.type === "prompt" ? ctx.hookLlm : ctx.hookAgent;
615
945
  if (!call)
616
946
  return { code: null, stdout: "", stderr: "", timedOut: false, spawnError: `no ${entry.type} hook carrier wired on this deployment` };
617
947
  const defSec = entry.type === "prompt" ? DEFAULT_PROMPT_HOOK_TIMEOUT_SECONDS : DEFAULT_AGENT_HOOK_TIMEOUT_SECONDS;
618
948
  const timeoutMs = Math.min(entry.timeout ?? defSec, MAX_HOOK_TIMEOUT_SECONDS) * 1000;
619
- const args = clip(JSON.stringify(boundedToolInputPayload(payload)), MAX_TOOL_INPUT_CHARS);
620
- const substituted = entry.prompt.split("$ARGUMENTS").join(args);
949
+ const substituted = entry.prompt.split("$ARGUMENTS").join(llmHookArguments(payload).text);
621
950
  const prompt = extra?.transcript !== undefined ? `${extra.transcript}\n\n---\n\n${wrapCondition("Stop", substituted)}` : substituted;
622
951
  let hardTopTimer;
623
952
  const hardTop = new Promise((r) => {
@@ -647,9 +976,9 @@ async function runLlmHook(entry, payload, ctx, extra) {
647
976
  if (!res.ok) {
648
977
  return res.code === "hard_timeout"
649
978
  ? { code: null, stdout: "", stderr: "", timedOut: true, ...(noticed ? { noticed } : {}) }
650
- : { code: null, stdout: "", stderr: "", timedOut: false, spawnError: clip(res.error, 500), ...(noticed ? { noticed } : {}) };
979
+ : { code: null, stdout: "", stderr: "", timedOut: false, spawnError: res.error, ...(noticed ? { noticed } : {}) };
651
980
  }
652
- return { code: 0, stdout: clip(res.text, 1024 * 1024), stderr: "", timedOut: false };
981
+ return { code: 0, stdout: clipDataText(res.text, 1024 * 1024), stderr: "", timedOut: false };
653
982
  }
654
983
  finally {
655
984
  if (hardTopTimer !== undefined)
@@ -665,7 +994,9 @@ async function runHttpHook(entry, payload, ctx, event) {
665
994
  event,
666
995
  url: buildDisplaySafeUrl(entry.url),
667
996
  blankedCount: interpolated.blanked.length,
668
- blanked: interpolated.blanked.slice(0, MAX_BLANKED_REFERENCES_LOGGED).map(({ header, name, reason }) => ({ header, name, reason })),
997
+ blanked: interpolated.blanked
998
+ .slice(0, MAX_BLANKED_REFERENCES_LOGGED)
999
+ .map(({ header, name, reason }) => ({ header: echoCallerText(header, { maxChars: LOG_PREVIEW_ECHO_CHARS }), name: echoCallerText(name, { maxChars: LOG_PREVIEW_ECHO_CHARS }), reason })),
669
1000
  });
670
1001
  }
671
1002
  const headers = { "content-type": "application/json", ...interpolated.headers };
@@ -703,7 +1034,7 @@ async function runHttpHook(entry, payload, ctx, event) {
703
1034
  if (trimmed === "")
704
1035
  return { code: 0, stdout: "{}", stderr: "", timedOut: false };
705
1036
  if (!parseHookStdout(body)) {
706
- ctx.logger.warn("hook_http_non_json_response", { url: buildDisplaySafeUrl(entry.url), preview: clip(trimmed, 200) });
1037
+ ctx.logger.warn("hook_http_non_json_response", { url: buildDisplaySafeUrl(entry.url), preview: echoCallerText(trimmed, { maxChars: LOG_PREVIEW_ECHO_CHARS }) });
707
1038
  noteHookFailure(ctx, {
708
1039
  event: eventOfPayload(payload),
709
1040
  entry,
@@ -712,7 +1043,7 @@ async function runHttpHook(entry, payload, ctx, event) {
712
1043
  stderr: trimmed,
713
1044
  toolName: toolNameOf(payload),
714
1045
  });
715
- return { code: 0, stdout: "", stderr: "", timedOut: false };
1046
+ return { code: 0, stdout: "", stderr: "", timedOut: false, badJson: true };
716
1047
  }
717
1048
  return { code: 0, stdout: body, stderr: "", timedOut: false };
718
1049
  }
@@ -748,14 +1079,16 @@ function boundedToolInputPayload(payload) {
748
1079
  const bounded = boundedInputValue(p.tool_input);
749
1080
  return bounded === p.tool_input ? payload : { ...p, tool_input: bounded };
750
1081
  }
751
- function readHookStdout(run, entry, event, ctx, payload) {
1082
+ function readHookStdout(ran, event, ctx, payload, ledger) {
1083
+ const { run, entry } = ran;
752
1084
  const out = parseHookStdout(run.stdout);
753
1085
  if (out !== undefined)
754
1086
  return out;
755
1087
  const t = run.stdout.trim();
756
1088
  if (t.startsWith("{") || t.startsWith("[")) {
757
- ctx.logger.warn("hook_bad_json_output", { event, type: entry.type, preview: clip(t, 200) });
1089
+ ctx.logger.warn("hook_bad_json_output", { event, type: entry.type, preview: echoCallerText(t, { maxChars: LOG_PREVIEW_ECHO_CHARS }) });
758
1090
  noteHookFailure(ctx, { event, entry, reason: "bad_json", exitCode: run.code, stderr: run.stderr, toolName: toolNameOf(payload) });
1091
+ ledger.unrun(ran.source, entry, "bad_json");
759
1092
  }
760
1093
  return undefined;
761
1094
  }
@@ -771,7 +1104,7 @@ function parseHookStdout(stdout) {
771
1104
  return undefined;
772
1105
  }
773
1106
  }
774
- function runRunnableEntry(entry, payload, ctx, event, llmExtra) {
1107
+ function runRunnableEntry(entry, payload, ctx, event, llmExtra, halt) {
775
1108
  switch (entry.type) {
776
1109
  case "http":
777
1110
  return runHttpHook(entry, payload, ctx, event);
@@ -780,7 +1113,7 @@ function runRunnableEntry(entry, payload, ctx, event, llmExtra) {
780
1113
  case "agent":
781
1114
  return runLlmHook(entry, payload, ctx, { system: ccAgentHookSystemFor(event), event });
782
1115
  case "command":
783
- return runCommandHook(entry, payload, ctx);
1116
+ return runCommandHook(entry, payload, ctx, halt);
784
1117
  default: {
785
1118
  const unreachable = entry;
786
1119
  void unreachable;
@@ -788,79 +1121,162 @@ function runRunnableEntry(entry, payload, ctx, event, llmExtra) {
788
1121
  }
789
1122
  }
790
1123
  }
791
- async function runMatchingEntries(event, groups, matchValue, payload, ctx, onceFired, deadlineAt, keyBase, llmExtra) {
1124
+ function isSyncEntry(entry) {
1125
+ return !(entry.type === "command" && (entry.async === true || entry.asyncRewake === true));
1126
+ }
1127
+ async function runMatchingEntries(event, groups, matchValue, payload, ctx, onceFired, halt, deadlineAt, ledger, llmExtra) {
792
1128
  const results = [];
793
1129
  let llmEntries = 0;
794
- let gi = -1;
795
- for (const group of groups) {
796
- gi++;
797
- const m = hookMatcherMatches(group.matcher, matchValue);
798
- if (m === "invalid") {
799
- ctx.logger.warn("hook_matcher_invalid", { event, matcher: clip(group.matcher ?? "", 200) });
800
- continue;
801
- }
802
- if (!m)
803
- continue;
804
- let ei = -1;
805
- for (const entry of group.hooks) {
806
- ei++;
807
- const onceKey = `${keyBase ?? event}:${gi}:${ei}`;
808
- if (entry.once && onceFired.has(onceKey))
809
- continue;
810
- if (entry.type === "prompt" || entry.type === "agent") {
811
- const carrier = entry.type === "prompt" ? ctx.hookLlm : ctx.hookAgent;
812
- if (!carrier) {
813
- ctx.logger.warn("hook_entries_skipped", { event, type: entry.type, reason: "llm_carrier_unavailable" });
814
- continue;
815
- }
816
- if (llmEntries >= MAX_LLM_ENTRIES_PER_EVENT) {
817
- ctx.logger.warn("hook_entries_skipped", { event, type: entry.type, reason: "llm_entries_per_event_cap", cap: MAX_LLM_ENTRIES_PER_EVENT });
818
- continue;
819
- }
820
- }
821
- if (entry.type === "command" && entry.shell === "powershell") {
822
- ctx.logger.warn("hook_entries_skipped", { event, type: entry.type, reason: "powershell_not_supported" });
823
- continue;
824
- }
825
- if (entry.if !== undefined) {
826
- const iv = hookIfMatches(entry.if, payload);
827
- if (iv !== true) {
828
- ctx.logger.warn("hook_entries_skipped", { event, type: entry.type, reason: iv === "unevaluable" ? "if_unevaluable" : "if_not_matched", if: clip(entry.if, 200) });
829
- continue;
1130
+ const toolName = toolNameOf(payload);
1131
+ const inputTruncated = boundedToolInputPayload(payload) !== payload;
1132
+ const truncatedFor = (entry) => (entry.type === "prompt" || entry.type === "agent" ? llmHookArguments(payload).truncated : inputTruncated);
1133
+ const settle = async (entry, onceKey) => {
1134
+ if (halt.aborted)
1135
+ return { kind: "halted" };
1136
+ if (entry.once && onceFired.has(onceKey))
1137
+ return { kind: "passed" };
1138
+ let ifOverBudget = false;
1139
+ if (entry.if !== undefined) {
1140
+ const iv = hookIfMatches(entry.if, payload);
1141
+ switch (iv) {
1142
+ case true:
1143
+ break;
1144
+ case "over_budget":
1145
+ noteMatcherOverBudget(ctx, { event, scope: "if", expression: entry.if, value: toolName ?? "", entries: [entry], ...(toolName !== undefined ? { toolName } : {}) });
1146
+ ifOverBudget = true;
1147
+ break;
1148
+ case "not_matched":
1149
+ ctx.logger.warn("hook_entries_skipped", { event, type: entry.type, reason: "if_not_matched", if: echoCallerText(entry.if, { maxChars: IF_ECHO_CHARS }) });
1150
+ return { kind: "passed" };
1151
+ case "unevaluable":
1152
+ ctx.logger.warn("hook_entries_skipped", { event, type: entry.type, reason: "if_unevaluable", if: echoCallerText(entry.if, { maxChars: IF_ECHO_CHARS }) });
1153
+ return { kind: "unrun", reason: "if_unevaluable" };
1154
+ default: {
1155
+ const unreachable = iv;
1156
+ return unreachable;
830
1157
  }
831
1158
  }
832
- if (entry.type === "command" && (entry.async || entry.asyncRewake)) {
833
- if (entry.once)
834
- onceFired.add(onceKey);
835
- void fireAsyncCommandHook(entry, payload, ctx, event);
836
- continue;
1159
+ }
1160
+ if (entry.type === "command" && entry.shell === "powershell") {
1161
+ ctx.logger.warn("hook_entries_skipped", { event, type: entry.type, reason: "powershell_not_supported" });
1162
+ return isSyncEntry(entry) ? { kind: "unrun", reason: "powershell_unsupported" } : { kind: "passed" };
1163
+ }
1164
+ if (entry.type === "prompt" || entry.type === "agent") {
1165
+ const carrier = entry.type === "prompt" ? ctx.hookLlm : ctx.hookAgent;
1166
+ if (!carrier) {
1167
+ ctx.logger.warn("hook_entries_skipped", { event, type: entry.type, reason: "llm_carrier_unavailable" });
1168
+ return { kind: "unrun", reason: "llm_carrier_unavailable" };
837
1169
  }
838
- if (Date.now() > deadlineAt) {
839
- ctx.logger.warn("hook_event_deadline_exceeded", { event, budgetSec: MAX_HOOK_EVENT_TOTAL_SECONDS });
840
- return results;
1170
+ if (llmEntries >= MAX_LLM_ENTRIES_PER_EVENT) {
1171
+ ctx.logger.warn("hook_entries_skipped", { event, type: entry.type, reason: "llm_entries_per_event_cap", cap: MAX_LLM_ENTRIES_PER_EVENT });
1172
+ return { kind: "unrun", reason: "llm_cap" };
841
1173
  }
1174
+ }
1175
+ if (entry.type === "command" && (entry.async || entry.asyncRewake)) {
842
1176
  if (entry.once)
843
1177
  onceFired.add(onceKey);
844
- if (entry.type === "prompt" || entry.type === "agent")
845
- llmEntries++;
846
- const run = await runRunnableEntry(entry, payload, ctx, event, llmExtra);
847
- if (run.spawnError) {
848
- ctx.logger.warn(entry.type === "http" ? "hook_http_request_failed" : entry.type === "prompt" || entry.type === "agent" ? "hook_llm_failed" : "hook_command_spawn_failed", { event, type: entry.type, error: clip(run.spawnError, 300) });
849
- if (!run.noticed)
850
- noteHookFailure(ctx, { event, entry, reason: "spawn_failed", exitCode: run.code, stderr: run.spawnError, toolName: toolNameOf(payload) });
851
- continue;
852
- }
853
- if (run.timedOut) {
854
- ctx.logger.warn("hook_command_timeout", { event, timeoutSec: Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) });
855
- if (!run.noticed)
856
- noteHookFailure(ctx, { event, entry, reason: "timeout", exitCode: run.code, stderr: run.stderr, toolName: toolNameOf(payload) });
857
- continue;
1178
+ void fireAsyncCommandHook(entry, payload, ctx, event, halt);
1179
+ return { kind: "passed" };
1180
+ }
1181
+ if (Date.now() > deadlineAt) {
1182
+ ctx.logger.warn("hook_event_deadline_exceeded", { event, budgetSec: MAX_HOOK_EVENT_TOTAL_SECONDS });
1183
+ return { kind: "unrun", reason: "budget_exhausted" };
1184
+ }
1185
+ if (entry.once)
1186
+ onceFired.add(onceKey);
1187
+ if (entry.type === "prompt" || entry.type === "agent")
1188
+ llmEntries++;
1189
+ const run = await runRunnableEntry(entry, payload, ctx, event, llmExtra, halt);
1190
+ if (run.aborted !== undefined) {
1191
+ if (run.aborted.signal !== undefined)
1192
+ ctx.logger.warn("hook_command_killed", { event, type: entry.type, killed: true, signal: run.aborted.signal });
1193
+ return { kind: "halted" };
1194
+ }
1195
+ if (run.spawnError) {
1196
+ ctx.logger.warn(entry.type === "http" ? "hook_http_request_failed" : entry.type === "prompt" || entry.type === "agent" ? "hook_llm_failed" : "hook_command_spawn_failed", { event, type: entry.type, error: echoCallerText(run.spawnError, { maxChars: LOG_ERROR_ECHO_CHARS }) });
1197
+ if (!run.noticed)
1198
+ noteHookFailure(ctx, { event, entry, reason: "spawn_failed", exitCode: run.code, stderr: run.spawnError, ...(toolName !== undefined ? { toolName } : {}) });
1199
+ return { kind: "unrun", reason: "spawn_failed" };
1200
+ }
1201
+ if (run.timedOut) {
1202
+ ctx.logger.warn("hook_command_timeout", { event, timeoutSec: Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) });
1203
+ if (!run.noticed)
1204
+ noteHookFailure(ctx, { event, entry, reason: "timeout", exitCode: run.code, stderr: run.stderr, ...(toolName !== undefined ? { toolName } : {}) });
1205
+ return { kind: "unrun", reason: "timeout" };
1206
+ }
1207
+ if (run.badJson)
1208
+ return { kind: "unrun", reason: "bad_json" };
1209
+ return ifOverBudget ? { kind: "ran", run, overBudget: true } : { kind: "ran", run };
1210
+ };
1211
+ for (const { group, index } of groups) {
1212
+ const selected = matcherGroupSelected(ctx, event, group, matchValue, toolName);
1213
+ if (selected === "invalid") {
1214
+ for (const entry of group.hooks)
1215
+ if (isSyncEntry(entry))
1216
+ ledger.unrun(group.source, entry, "matcher_invalid");
1217
+ continue;
1218
+ }
1219
+ if (selected === false)
1220
+ continue;
1221
+ for (const [ei, entry] of group.hooks.entries()) {
1222
+ const fate = await settle(entry, `${event}:${index}:${ei}`);
1223
+ switch (fate.kind) {
1224
+ case "ran": {
1225
+ const distrust = !PERMISSIVE_VERDICT_EVENTS[event]
1226
+ ? undefined
1227
+ : truncatedFor(entry)
1228
+ ? "input_truncated"
1229
+ : selected === "over_budget" || fate.overBudget === true
1230
+ ? "matcher_over_budget"
1231
+ : undefined;
1232
+ results.push({ entry, run: fate.run, source: group.source, ...(distrust !== undefined ? { distrust } : {}) });
1233
+ if (distrust !== undefined)
1234
+ ledger.untrusted(group.source, entry, distrust);
1235
+ break;
1236
+ }
1237
+ case "unrun":
1238
+ ledger.unrun(group.source, entry, fate.reason);
1239
+ if (fate.reason === "budget_exhausted")
1240
+ return results;
1241
+ break;
1242
+ case "passed":
1243
+ break;
1244
+ case "halted":
1245
+ return results;
1246
+ default: {
1247
+ const unreachable = fate;
1248
+ return unreachable;
1249
+ }
858
1250
  }
859
- results.push({ entry, run });
860
1251
  }
861
1252
  }
862
1253
  return results;
863
1254
  }
1255
+ function createLaneClocks() {
1256
+ const startedAt = {};
1257
+ return (phase) => {
1258
+ const lane = phase === "unmanaged" ? "unmanaged" : "managed";
1259
+ return (startedAt[lane] ??= Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
1260
+ };
1261
+ }
1262
+ function hookLanesOf(groups) {
1263
+ const indexed = groups.map((group, index) => ({ group, index, phase: hookPhaseOf(group.source) }));
1264
+ return HOOK_PHASES.map((phase) => ({ phase, groups: indexed.filter((g) => g.phase === phase).map(({ group, index }) => ({ group, index })) })).filter((l) => l.groups.length > 0);
1265
+ }
1266
+ function trustRank(phase) {
1267
+ return HOOK_PHASES.length - 1 - HOOK_PHASES.indexOf(phase);
1268
+ }
1269
+ async function runEventLanes(event, groups, matchValue, payload, ctx, onceFired, halt, ledger, llmExtra) {
1270
+ const deadlineFor = createLaneClocks();
1271
+ const ran = [];
1272
+ for (const lane of hookLanesOf(groups)) {
1273
+ if (halt.aborted)
1274
+ break;
1275
+ for (const r of await runMatchingEntries(event, lane.groups, matchValue, payload, ctx, onceFired, halt, deadlineFor(lane.phase), ledger, llmExtra))
1276
+ ran.push({ ...r, phase: lane.phase });
1277
+ }
1278
+ return ran.sort((a, b) => trustRank(a.phase) - trustRank(b.phase)).map(({ entry, run, source }) => ({ entry, run, source }));
1279
+ }
864
1280
  function basePayload(ctx, permissionMode) {
865
1281
  return {
866
1282
  session_id: ctx.sessionId,
@@ -872,38 +1288,27 @@ function basePayload(ctx, permissionMode) {
872
1288
  function ignoreMatchers(groups) {
873
1289
  return groups.map((g) => (g.matcher === undefined ? g : { ...g, matcher: undefined }));
874
1290
  }
875
- async function runObserveOnlyEvent(event, groups, matchValue, payload, ctx, onceFired) {
876
- const singles = await runMatchingEntries(event, groups, matchValue, payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
877
- for (const { entry, run } of singles) {
1291
+ async function runObserveOnlyEvent(event, groups, matchValue, payload, ctx, onceFired, halt) {
1292
+ const ledger = createUnrunLedger(ctx, event, toolNameOf(payload));
1293
+ const singles = await runEventLanes(event, groups, matchValue, payload, ctx, onceFired, halt, ledger);
1294
+ for (const ran of singles) {
1295
+ const { run } = ran;
878
1296
  if (run.code !== 0) {
879
- ctx.logger.warn("hook_command_failed", { event, code: run.code, stderr: clip(run.stderr, 300) });
880
- noteHookFailure(ctx, {
881
- event,
882
- entry,
883
- reason: "exit_nonzero",
884
- exitCode: run.code,
885
- stderr: run.stderr,
886
- toolName: toolNameOf(payload),
887
- ...(run.code === 2 ? { detail: `exit 2 on ${event}: this event has no blocking face, so the hook's blocking intent could not be honored` } : {}),
888
- });
1297
+ noteExitNonzero(ctx, ledger, event, ran, toolNameOf(payload), run.code === 2 ? `exit 2 on ${event}: this event has no blocking face, so the hook's blocking intent could not be honored` : undefined);
889
1298
  continue;
890
1299
  }
891
- const out = readHookStdout(run, entry, event, ctx, payload);
1300
+ const out = readHookStdout(ran, event, ctx, payload, ledger);
892
1301
  if (!out)
893
1302
  continue;
894
1303
  if (typeof out.systemMessage === "string")
895
- ctx.logger.info("hook_system_message", { event, message: clip(out.systemMessage, 500) });
1304
+ logHookSystemMessage(ctx, event, out.systemMessage);
896
1305
  if (out.decision !== undefined || out.continue === false || out.hookSpecificOutput !== undefined) {
897
1306
  ctx.logger.warn("hook_result_unsupported", { event });
898
1307
  }
899
1308
  }
900
1309
  }
901
- export function createTaskHooks(config, ctx) {
1310
+ export function createSettingsHooksRegistration(config, ctx, budgetMs) {
902
1311
  const configured = Object.keys(config).filter((e) => (config[e]?.length ?? 0) > 0);
903
- const dark = configured.filter((e) => HOOK_EVENT_OWNER[e] === "engine" && !LIT_EVENTS.includes(e));
904
- if (dark.length > 0) {
905
- ctx.logger.warn("hook_events_not_fired", { events: dark, phase: 2 });
906
- }
907
1312
  const shellOwned = configured.filter((e) => HOOK_EVENT_OWNER[e] === "shell");
908
1313
  if (shellOwned.length > 0) {
909
1314
  ctx.logger.debug("hook_events_shell_owned", { events: shellOwned });
@@ -925,98 +1330,131 @@ export function createTaskHooks(config, ctx) {
925
1330
  return undefined;
926
1331
  }
927
1332
  const onceFired = new Set();
928
- const hooks = {};
1333
+ const translators = {};
929
1334
  if (pre) {
930
- const preToolUseHook = async (toolName, input, tctx) => {
931
- let currentInput = input;
1335
+ translators.PreToolUse = async (e, signal) => {
1336
+ const toolName = e.tool;
1337
+ const ledger = createUnrunLedger(ctx, "PreToolUse", toolName);
1338
+ const deadlineFor = createLaneClocks();
1339
+ let currentInput = e.input;
932
1340
  let rewritten = false;
933
1341
  let asked;
934
1342
  let allowedFor;
935
1343
  let unknownDecision = false;
936
- const contexts = [];
937
- const deadlineAt = Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000;
938
- let gi = -1;
939
- for (const group of pre) {
940
- gi++;
941
- const m = hookMatcherMatches(group.matcher, toolName);
942
- if (m === "invalid") {
943
- ctx.logger.warn("hook_matcher_invalid", { event: "PreToolUse", matcher: clip(group.matcher ?? "", 200) });
944
- continue;
945
- }
946
- if (!m)
947
- continue;
948
- const groupInput = currentInput;
949
- const payload = {
950
- ...basePayload(ctx, tctx.permissionMode),
951
- hook_event_name: "PreToolUse",
952
- tool_name: toolName,
953
- tool_input: groupInput,
954
- tool_use_id: tctx.toolCallId,
955
- };
956
- const singles = await runMatchingEntries("PreToolUse", [{ ...group, matcher: undefined }], toolName, payload, ctx, onceFired, deadlineAt, `PreToolUse:${gi}`);
957
- for (const { entry, run } of singles) {
958
- if (run.code === 2) {
959
- const reason = clip(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS) || "blocked by a PreToolUse hook";
960
- return { action: "deny", message: reason, ...buildHookContextField(contexts) };
961
- }
962
- if (run.code !== 0) {
963
- ctx.logger.warn("hook_command_failed", { event: "PreToolUse", code: run.code, stderr: clip(run.stderr, 300) });
964
- noteHookFailure(ctx, { event: "PreToolUse", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr, toolName });
965
- continue;
966
- }
967
- const out = readHookStdout(run, entry, "PreToolUse", ctx, payload);
968
- if (!out)
969
- continue;
970
- if (typeof out.systemMessage === "string")
971
- ctx.logger.info("hook_system_message", { event: "PreToolUse", message: clip(out.systemMessage, 500) });
972
- if (out.continue === false) {
973
- const reason = clip(typeof out.stopReason === "string" ? out.stopReason : "hook requested stop", MAX_HOOK_FEEDBACK_CHARS);
974
- return { action: "deny", message: reason, ...buildHookContextField(contexts) };
975
- }
976
- if (out.decision === "block") {
977
- const reason = clip(typeof out.reason === "string" ? out.reason : "blocked by a PreToolUse hook", MAX_HOOK_FEEDBACK_CHARS);
978
- return { action: "deny", message: reason, ...buildHookContextField(contexts) };
979
- }
980
- const hso = out.hookSpecificOutput;
981
- if (hso !== null && typeof hso === "object" && hso.hookEventName === "PreToolUse") {
982
- const h = hso;
983
- if (typeof h.additionalContext === "string")
984
- contexts.push(clip(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
985
- const decision = h.permissionDecision;
986
- if (decision === "deny") {
987
- const reason = clip(typeof h.permissionDecisionReason === "string" ? h.permissionDecisionReason : "denied by a PreToolUse hook", MAX_HOOK_FEEDBACK_CHARS);
988
- return { action: "deny", message: reason, ...buildHookContextField(contexts) };
1344
+ const contexts = { unmanaged: [], managedPlugin: [], managedPolicy: [] };
1345
+ const contextField = () => buildHookContextField([...HOOK_PHASES].sort((a, b) => trustRank(a) - trustRank(b)).flatMap((p) => contexts[p]));
1346
+ const runPass = async (phase, groups, once) => {
1347
+ let rewroteBy;
1348
+ for (const laneGroup of groups) {
1349
+ const groupInput = currentInput;
1350
+ const payload = {
1351
+ ...basePayload(ctx, e.permissionMode),
1352
+ hook_event_name: "PreToolUse",
1353
+ tool_name: toolName,
1354
+ tool_input: groupInput,
1355
+ tool_use_id: e.tool_use_id,
1356
+ };
1357
+ const singles = await runMatchingEntries("PreToolUse", [laneGroup], toolName, payload, ctx, once, signal, deadlineFor(phase), ledger);
1358
+ for (const ran of singles) {
1359
+ const { entry, run } = ran;
1360
+ if (run.code === 2) {
1361
+ const reason = clipDataText(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS) || "blocked by a PreToolUse hook";
1362
+ return { deny: { action: "deny", message: reason, ...contextField() } };
989
1363
  }
990
- if (decision === "ask") {
991
- asked = clip(typeof h.permissionDecisionReason === "string" ? h.permissionDecisionReason : "a PreToolUse hook requires approval", MAX_HOOK_FEEDBACK_CHARS);
1364
+ if (run.code !== 0) {
1365
+ noteExitNonzero(ctx, ledger, "PreToolUse", ran, toolName);
1366
+ continue;
992
1367
  }
993
- if (decision === "allow" || decision === undefined) {
994
- const rewrites = h.updatedInput !== null && typeof h.updatedInput === "object";
995
- if (rewrites) {
996
- currentInput = h.updatedInput;
997
- rewritten = true;
998
- }
999
- if (decision === "allow")
1000
- allowedFor = { input: rewrites ? h.updatedInput : groupInput };
1368
+ const out = readHookStdout(ran, "PreToolUse", ctx, payload, ledger);
1369
+ if (!out)
1370
+ continue;
1371
+ if (typeof out.systemMessage === "string")
1372
+ logHookSystemMessage(ctx, "PreToolUse", out.systemMessage);
1373
+ if (out.continue === false) {
1374
+ const reason = clipDataText(typeof out.stopReason === "string" ? out.stopReason : "hook requested stop", MAX_HOOK_FEEDBACK_CHARS);
1375
+ return { deny: { action: "deny", message: reason, ...contextField() } };
1001
1376
  }
1002
- else if (decision !== "deny" && decision !== "ask") {
1003
- unknownDecision = true;
1004
- ctx.logger.warn("hook_permission_decision_unknown", {
1005
- event: "PreToolUse",
1006
- decision: clip(typeof decision === "string" ? decision : typeof decision, 200),
1007
- tool: toolName,
1008
- effect: "hook ruling not honored (closed word set is allow|deny|ask); this call falls back to the engine gate",
1009
- });
1377
+ if (out.decision === "block") {
1378
+ const reason = clipDataText(typeof out.reason === "string" ? out.reason : "blocked by a PreToolUse hook", MAX_HOOK_FEEDBACK_CHARS);
1379
+ return { deny: { action: "deny", message: reason, ...contextField() } };
1380
+ }
1381
+ const hso = out.hookSpecificOutput;
1382
+ if (hso !== null && typeof hso === "object" && hso.hookEventName === "PreToolUse") {
1383
+ const h = hso;
1384
+ if (typeof h.additionalContext === "string")
1385
+ contexts[phase].push(clipDataText(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
1386
+ const decision = h.permissionDecision;
1387
+ if (decision === "deny") {
1388
+ const reason = clipDataText(typeof h.permissionDecisionReason === "string" ? h.permissionDecisionReason : "denied by a PreToolUse hook", MAX_HOOK_FEEDBACK_CHARS);
1389
+ return { deny: { action: "deny", message: reason, ...contextField() } };
1390
+ }
1391
+ if (decision === "ask") {
1392
+ asked = clipDataText(typeof h.permissionDecisionReason === "string" ? h.permissionDecisionReason : "a PreToolUse hook requires approval", MAX_HOOK_FEEDBACK_CHARS);
1393
+ }
1394
+ if (decision === "allow" || decision === undefined) {
1395
+ const proposed = h.updatedInput !== null && typeof h.updatedInput === "object" ? h.updatedInput : undefined;
1396
+ const proposesRewrite = proposed !== undefined && canonicalJson(proposed) !== canonicalJson(groupInput);
1397
+ if (ran.distrust !== undefined && hookPhaseOf(ran.source) === "unmanaged" && (decision === "allow" || proposesRewrite)) {
1398
+ noteVerdictUntrusted(ctx, { event: "PreToolUse", reason: ran.distrust, entry, source: ran.source, toolName });
1399
+ }
1400
+ const offered = ran.distrust === undefined ? proposed : undefined;
1401
+ const rewrites = offered !== undefined && proposesRewrite;
1402
+ if (rewrites) {
1403
+ currentInput = offered;
1404
+ rewritten = true;
1405
+ rewroteBy = ran;
1406
+ }
1407
+ if (decision === "allow" && ran.distrust === undefined)
1408
+ allowedFor = { input: rewrites ? offered : groupInput };
1409
+ }
1410
+ else if (decision !== "deny" && decision !== "ask") {
1411
+ unknownDecision = true;
1412
+ ctx.logger.warn("hook_permission_decision_unknown", {
1413
+ event: "PreToolUse",
1414
+ decision: echoCallerText(typeof decision === "string" ? decision : typeof decision, { maxChars: LOG_PREVIEW_ECHO_CHARS }),
1415
+ tool: toolName,
1416
+ effect: "hook ruling not honored (closed word set is allow|deny|ask); this call falls back to the engine gate",
1417
+ });
1418
+ ledger.unrun(ran.source, entry, "decision_out_of_set");
1419
+ }
1010
1420
  }
1011
1421
  }
1012
1422
  }
1423
+ return { rewroteBy };
1424
+ };
1425
+ for (const lane of hookLanesOf(pre)) {
1426
+ if (lane.phase === "unmanaged") {
1427
+ const pass = await runPass(lane.phase, lane.groups, onceFired);
1428
+ if (pass.deny !== undefined)
1429
+ return pass.deny;
1430
+ continue;
1431
+ }
1432
+ const onceBefore = new Set(onceFired);
1433
+ const first = await runPass(lane.phase, lane.groups, onceFired);
1434
+ if (first.deny !== undefined)
1435
+ return first.deny;
1436
+ if (first.rewroteBy === undefined)
1437
+ continue;
1438
+ contexts[lane.phase].length = 0;
1439
+ const rejudgeOnce = new Set(onceBefore);
1440
+ const second = await runPass(lane.phase, lane.groups, rejudgeOnce);
1441
+ for (const key of rejudgeOnce)
1442
+ onceFired.add(key);
1443
+ if (second.deny !== undefined)
1444
+ return second.deny;
1445
+ if (second.rewroteBy !== undefined) {
1446
+ ledger.unrun(second.rewroteBy.source, second.rewroteBy.entry, "rewrite_unsettled", "the managed hooks of this phase rewrote the tool input again after being re-run on their own rewrite; approval required");
1447
+ }
1013
1448
  }
1014
1449
  if (asked !== undefined) {
1450
+ return { action: "ask", message: asked, ...(rewritten ? { updatedInput: currentInput } : {}), ...contextField() };
1451
+ }
1452
+ if (ledger.managedWithoutVerdict) {
1015
1453
  return {
1016
1454
  action: "ask",
1017
- message: asked,
1455
+ message: "a managed PreToolUse hook did not run to a verdict — approval required",
1018
1456
  ...(rewritten ? { updatedInput: currentInput } : {}),
1019
- ...buildHookContextField(contexts),
1457
+ ...contextField(),
1020
1458
  };
1021
1459
  }
1022
1460
  if (unknownDecision) {
@@ -1024,60 +1462,63 @@ export function createTaskHooks(config, ctx) {
1024
1462
  action: "ask",
1025
1463
  message: "a PreToolUse hook returned an unrecognized permissionDecision — approval required",
1026
1464
  ...(rewritten ? { updatedInput: currentInput } : {}),
1027
- ...buildHookContextField(contexts),
1465
+ ...contextField(),
1028
1466
  };
1029
1467
  }
1030
- const carried = { ...(rewritten ? { updatedInput: currentInput } : {}), ...buildHookContextField(contexts) };
1468
+ const carried = { ...(rewritten ? { updatedInput: currentInput } : {}), ...contextField() };
1031
1469
  if (allowedFor !== undefined && allowedFor.input === currentInput)
1032
1470
  return { action: "allow", ...carried };
1033
- if (rewritten || contexts.length > 0)
1471
+ if (rewritten || HOOK_PHASES.some((p) => contexts[p].length > 0))
1034
1472
  return carried;
1035
1473
  return undefined;
1036
1474
  };
1037
- hooks.preToolUse = preToolUseHook;
1038
1475
  }
1039
1476
  if (post) {
1040
- const postToolUseHook = async (toolName, input, output, tctx) => {
1477
+ translators.PostToolUse = async (e, signal) => {
1478
+ const toolName = e.tool;
1479
+ const input = e.input;
1480
+ const output = e.output;
1041
1481
  const contexts = [];
1042
1482
  const responseText = output.content
1043
1483
  .map((c) => (c && typeof c === "object" && typeof c.text === "string" ? c.text : ""))
1044
1484
  .join("");
1045
1485
  const payload = {
1046
- ...basePayload(ctx, tctx.permissionMode),
1486
+ ...basePayload(ctx, e.permissionMode),
1047
1487
  hook_event_name: "PostToolUse",
1048
1488
  tool_name: toolName,
1049
1489
  tool_input: input,
1050
- tool_response: { isError: output.isError, text: clip(responseText, MAX_TOOL_RESPONSE_CHARS) },
1051
- tool_use_id: tctx.toolCallId,
1490
+ tool_response: { isError: output.isError, text: clipDataText(responseText, MAX_TOOL_RESPONSE_CHARS) },
1491
+ tool_use_id: e.tool_use_id,
1052
1492
  };
1053
- const singles = await runMatchingEntries("PostToolUse", post, toolName, payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
1054
- for (const { entry, run } of singles) {
1493
+ const ledger = createUnrunLedger(ctx, "PostToolUse", toolName);
1494
+ const singles = await runEventLanes("PostToolUse", post, toolName, payload, ctx, onceFired, signal, ledger);
1495
+ for (const ran of singles) {
1496
+ const { entry, run } = ran;
1055
1497
  if (run.code === 2) {
1056
- const fb = clip(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS);
1498
+ const fb = clipDataText(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS);
1057
1499
  if (fb)
1058
1500
  contexts.push(fb);
1059
1501
  continue;
1060
1502
  }
1061
1503
  if (run.code !== 0) {
1062
- ctx.logger.warn("hook_command_failed", { event: "PostToolUse", code: run.code, stderr: clip(run.stderr, 300) });
1063
- noteHookFailure(ctx, { event: "PostToolUse", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr, toolName });
1504
+ noteExitNonzero(ctx, ledger, "PostToolUse", ran, toolName);
1064
1505
  continue;
1065
1506
  }
1066
- const out = readHookStdout(run, entry, "PostToolUse", ctx, payload);
1507
+ const out = readHookStdout(ran, "PostToolUse", ctx, payload, ledger);
1067
1508
  if (!out)
1068
1509
  continue;
1069
1510
  if (typeof out.systemMessage === "string")
1070
- ctx.logger.info("hook_system_message", { event: "PostToolUse", message: clip(out.systemMessage, 500) });
1511
+ logHookSystemMessage(ctx, "PostToolUse", out.systemMessage);
1071
1512
  if (out.continue === false) {
1072
1513
  ctx.logger.warn("hook_continue_false_unsupported", { event: "PostToolUse" });
1073
1514
  }
1074
1515
  if (out.decision === "block" && typeof out.reason === "string")
1075
- contexts.push(clip(out.reason, MAX_HOOK_FEEDBACK_CHARS));
1516
+ contexts.push(clipDataText(out.reason, MAX_HOOK_FEEDBACK_CHARS));
1076
1517
  const hso = out.hookSpecificOutput;
1077
1518
  if (hso !== null && typeof hso === "object" && hso.hookEventName === "PostToolUse") {
1078
1519
  const h = hso;
1079
1520
  if (typeof h.additionalContext === "string")
1080
- contexts.push(clip(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
1521
+ contexts.push(clipDataText(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
1081
1522
  if (h.updatedMCPToolOutput !== undefined) {
1082
1523
  ctx.logger.warn("hook_updated_mcp_output_unsupported", { event: "PostToolUse" });
1083
1524
  }
@@ -1086,139 +1527,140 @@ export function createTaskHooks(config, ctx) {
1086
1527
  const additionalContext = buildHookContext(contexts);
1087
1528
  return additionalContext === undefined ? undefined : { additionalContext };
1088
1529
  };
1089
- hooks.postToolUse = postToolUseHook;
1090
1530
  }
1091
1531
  if (postFailure) {
1092
- const postToolUseFailureHook = async (toolName, input, f, tctx) => {
1532
+ translators.PostToolUseFailure = async (e, signal) => {
1533
+ const toolName = e.tool;
1534
+ const f = e.failure;
1093
1535
  const contexts = [];
1094
1536
  const payload = {
1095
- ...basePayload(ctx, tctx.permissionMode),
1537
+ ...basePayload(ctx, e.permissionMode),
1096
1538
  hook_event_name: "PostToolUseFailure",
1097
1539
  tool_name: toolName,
1098
- tool_input: input,
1099
- tool_use_id: tctx.toolCallId,
1100
- error: clip(f.error, MAX_TOOL_RESPONSE_CHARS),
1540
+ tool_input: e.input,
1541
+ tool_use_id: e.tool_use_id,
1542
+ error: clipDataText(f.error, MAX_TOOL_RESPONSE_CHARS),
1101
1543
  is_interrupt: f.isInterrupt,
1102
1544
  };
1103
- const singles = await runMatchingEntries("PostToolUseFailure", postFailure, toolName, payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
1104
- for (const { entry, run } of singles) {
1545
+ const ledger = createUnrunLedger(ctx, "PostToolUseFailure", toolName);
1546
+ const singles = await runEventLanes("PostToolUseFailure", postFailure, toolName, payload, ctx, onceFired, signal, ledger);
1547
+ for (const ran of singles) {
1548
+ const { entry, run } = ran;
1105
1549
  if (run.code === 2) {
1106
- const fb = clip(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS);
1550
+ const fb = clipDataText(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS);
1107
1551
  if (fb)
1108
1552
  contexts.push(fb);
1109
1553
  continue;
1110
1554
  }
1111
1555
  if (run.code !== 0) {
1112
- ctx.logger.warn("hook_command_failed", { event: "PostToolUseFailure", code: run.code, stderr: clip(run.stderr, 300) });
1113
- noteHookFailure(ctx, { event: "PostToolUseFailure", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr, toolName });
1556
+ noteExitNonzero(ctx, ledger, "PostToolUseFailure", ran, toolName);
1114
1557
  continue;
1115
1558
  }
1116
- const out = readHookStdout(run, entry, "PostToolUseFailure", ctx, payload);
1559
+ const out = readHookStdout(ran, "PostToolUseFailure", ctx, payload, ledger);
1117
1560
  if (!out)
1118
1561
  continue;
1119
1562
  if (typeof out.systemMessage === "string")
1120
- ctx.logger.info("hook_system_message", { event: "PostToolUseFailure", message: clip(out.systemMessage, 500) });
1563
+ logHookSystemMessage(ctx, "PostToolUseFailure", out.systemMessage);
1121
1564
  if (out.continue === false)
1122
1565
  ctx.logger.warn("hook_continue_false_unsupported", { event: "PostToolUseFailure" });
1123
1566
  if (out.decision === "block" && typeof out.reason === "string")
1124
- contexts.push(clip(out.reason, MAX_HOOK_FEEDBACK_CHARS));
1567
+ contexts.push(clipDataText(out.reason, MAX_HOOK_FEEDBACK_CHARS));
1125
1568
  const hso = out.hookSpecificOutput;
1126
1569
  if (hso !== null && typeof hso === "object" && hso.hookEventName === "PostToolUseFailure") {
1127
1570
  const h = hso;
1128
1571
  if (typeof h.additionalContext === "string")
1129
- contexts.push(clip(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
1572
+ contexts.push(clipDataText(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
1130
1573
  }
1131
1574
  }
1132
1575
  const additionalContext = buildHookContext(contexts);
1133
1576
  return additionalContext === undefined ? undefined : { additionalContext };
1134
1577
  };
1135
- hooks.postToolUseFailure = postToolUseFailureHook;
1136
1578
  }
1137
1579
  if (postBatch) {
1138
- const postToolBatchHook = async (calls, _meta, bctx) => {
1580
+ translators.PostToolBatch = async (e, signal) => {
1139
1581
  const contexts = [];
1140
1582
  const payload = {
1141
- ...basePayload(ctx, bctx?.permissionMode),
1583
+ ...basePayload(ctx, e.permissionMode),
1142
1584
  hook_event_name: "PostToolBatch",
1143
- tool_calls: calls.map((c) => ({
1585
+ tool_calls: e.batch.map((c) => ({
1144
1586
  tool_name: c.toolName,
1145
1587
  tool_input: boundedInputValue(c.input),
1146
1588
  tool_use_id: c.toolCallId,
1147
- tool_response: { isError: c.isError, ...(c.response !== undefined ? { text: clip(c.response, MAX_TOOL_RESPONSE_CHARS) } : {}) },
1589
+ tool_response: { isError: c.isError, ...(c.response !== undefined ? { text: clipDataText(c.response, MAX_TOOL_RESPONSE_CHARS) } : {}) },
1148
1590
  })),
1149
1591
  };
1150
- const singles = await runMatchingEntries("PostToolBatch", ignoreMatchers(postBatch), "", payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
1151
- for (const { entry, run } of singles) {
1592
+ const ledger = createUnrunLedger(ctx, "PostToolBatch", undefined);
1593
+ const singles = await runEventLanes("PostToolBatch", ignoreMatchers(postBatch), "", payload, ctx, onceFired, signal, ledger);
1594
+ for (const ran of singles) {
1595
+ const { entry, run } = ran;
1152
1596
  if (run.code === 2) {
1153
- const fb = clip(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS);
1597
+ const fb = clipDataText(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS);
1154
1598
  if (fb)
1155
1599
  contexts.push(fb);
1156
1600
  continue;
1157
1601
  }
1158
1602
  if (run.code !== 0) {
1159
- ctx.logger.warn("hook_command_failed", { event: "PostToolBatch", code: run.code, stderr: clip(run.stderr, 300) });
1160
- noteHookFailure(ctx, { event: "PostToolBatch", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr });
1603
+ noteExitNonzero(ctx, ledger, "PostToolBatch", ran);
1161
1604
  continue;
1162
1605
  }
1163
- const out = readHookStdout(run, entry, "PostToolBatch", ctx, payload);
1606
+ const out = readHookStdout(ran, "PostToolBatch", ctx, payload, ledger);
1164
1607
  if (!out)
1165
1608
  continue;
1166
1609
  if (typeof out.systemMessage === "string")
1167
- ctx.logger.info("hook_system_message", { event: "PostToolBatch", message: clip(out.systemMessage, 500) });
1610
+ logHookSystemMessage(ctx, "PostToolBatch", out.systemMessage);
1168
1611
  if (out.continue === false)
1169
1612
  ctx.logger.warn("hook_continue_false_unsupported", { event: "PostToolBatch" });
1170
1613
  if (out.decision === "block" && typeof out.reason === "string")
1171
- contexts.push(clip(out.reason, MAX_HOOK_FEEDBACK_CHARS));
1614
+ contexts.push(clipDataText(out.reason, MAX_HOOK_FEEDBACK_CHARS));
1172
1615
  if (out.hookSpecificOutput !== undefined)
1173
1616
  ctx.logger.warn("hook_result_unsupported", { event: "PostToolBatch" });
1174
1617
  }
1175
1618
  const additionalContext = buildHookContext(contexts);
1176
1619
  return additionalContext === undefined ? undefined : { additionalContext };
1177
1620
  };
1178
- hooks.postToolBatch = postToolBatchHook;
1179
1621
  }
1180
1622
  if (promptSubmit) {
1181
- const userPromptSubmitHook = async (prompt, uctx) => {
1623
+ translators.UserPromptSubmit = async (e, signal) => {
1182
1624
  const contexts = [];
1183
- const payload = { ...basePayload(ctx, uctx?.permissionMode), hook_event_name: "UserPromptSubmit", prompt: clip(prompt, MAX_TOOL_INPUT_CHARS) };
1184
- const singles = await runMatchingEntries("UserPromptSubmit", ignoreMatchers(promptSubmit), "", payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
1185
- for (const { entry, run } of singles) {
1625
+ const payload = { ...basePayload(ctx, e.permissionMode), hook_event_name: "UserPromptSubmit", prompt: clipDataText(e.prompt, MAX_TOOL_INPUT_CHARS) };
1626
+ const ledger = createUnrunLedger(ctx, "UserPromptSubmit", undefined);
1627
+ const singles = await runEventLanes("UserPromptSubmit", ignoreMatchers(promptSubmit), "", payload, ctx, onceFired, signal, ledger);
1628
+ for (const ran of singles) {
1629
+ const { entry, run } = ran;
1186
1630
  if (run.code === 2) {
1187
- const reason = clip(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS) || "blocked by a UserPromptSubmit hook";
1631
+ const reason = clipDataText(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS) || "blocked by a UserPromptSubmit hook";
1188
1632
  return { block: reason, ...buildHookContextField(contexts) };
1189
1633
  }
1190
1634
  if (run.code !== 0) {
1191
- ctx.logger.warn("hook_command_failed", { event: "UserPromptSubmit", code: run.code, stderr: clip(run.stderr, 300) });
1192
- noteHookFailure(ctx, { event: "UserPromptSubmit", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr });
1635
+ noteExitNonzero(ctx, ledger, "UserPromptSubmit", ran);
1193
1636
  continue;
1194
1637
  }
1195
- const out = readHookStdout(run, entry, "UserPromptSubmit", ctx, payload);
1638
+ const out = readHookStdout(ran, "UserPromptSubmit", ctx, payload, ledger);
1196
1639
  if (!out)
1197
1640
  continue;
1198
1641
  if (typeof out.systemMessage === "string")
1199
- ctx.logger.info("hook_system_message", { event: "UserPromptSubmit", message: clip(out.systemMessage, 500) });
1642
+ logHookSystemMessage(ctx, "UserPromptSubmit", out.systemMessage);
1200
1643
  if (out.continue === false) {
1201
- const reason = clip(typeof out.stopReason === "string" ? out.stopReason : "hook requested stop", MAX_HOOK_FEEDBACK_CHARS);
1644
+ const reason = clipDataText(typeof out.stopReason === "string" ? out.stopReason : "hook requested stop", MAX_HOOK_FEEDBACK_CHARS);
1202
1645
  return { block: reason, ...buildHookContextField(contexts) };
1203
1646
  }
1204
1647
  if (out.decision === "block") {
1205
- const reason = clip(typeof out.reason === "string" ? out.reason : "blocked by a UserPromptSubmit hook", MAX_HOOK_FEEDBACK_CHARS);
1648
+ const reason = clipDataText(typeof out.reason === "string" ? out.reason : "blocked by a UserPromptSubmit hook", MAX_HOOK_FEEDBACK_CHARS);
1206
1649
  return { block: reason, ...buildHookContextField(contexts) };
1207
1650
  }
1208
1651
  const hso = out.hookSpecificOutput;
1209
1652
  if (hso !== null && typeof hso === "object" && hso.hookEventName === "UserPromptSubmit") {
1210
1653
  const h = hso;
1211
1654
  if (typeof h.additionalContext === "string")
1212
- contexts.push(clip(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
1655
+ contexts.push(clipDataText(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
1213
1656
  }
1214
1657
  }
1215
1658
  const additionalContext = buildHookContext(contexts);
1216
1659
  return additionalContext === undefined ? undefined : { additionalContext };
1217
1660
  };
1218
- hooks.userPromptSubmit = userPromptSubmitHook;
1219
1661
  }
1220
1662
  if (stopEntries) {
1221
- const stopHook = async (sctx) => {
1663
+ translators.Stop = async (sctx, signal) => {
1222
1664
  const payload = { ...basePayload(ctx, sctx.permissionMode), hook_event_name: "Stop", stop_hook_active: sctx.stopHookActive };
1223
1665
  let llmExtra;
1224
1666
  const wantsPrompt = stopEntries.some((g) => g.hooks.some((h) => h.type === "prompt"));
@@ -1233,7 +1675,7 @@ export function createTaskHooks(config, ctx) {
1233
1675
  branch = await sctx.getBranch?.();
1234
1676
  }
1235
1677
  catch (e) {
1236
- ctx.logger.warn("hook_stop_branch_failed", { error: e instanceof Error ? e.message : String(e) });
1678
+ ctx.logger.warn("hook_stop_branch_failed", { error: echoCallerText(e instanceof Error ? e.message : String(e), { maxChars: LOG_ERROR_ECHO_CHARS }) });
1237
1679
  }
1238
1680
  const rendered = renderBranchTranscript(branch);
1239
1681
  if (rendered.sufficient) {
@@ -1246,22 +1688,29 @@ export function createTaskHooks(config, ctx) {
1246
1688
  }
1247
1689
  }
1248
1690
  const stopGroups = ignoreMatchers(stopEntries);
1691
+ const ledger = createUnrunLedger(ctx, "Stop", undefined);
1692
+ if (!llmExtra && wantsPrompt) {
1693
+ for (const g of stopGroups)
1694
+ for (const h of g.hooks)
1695
+ if (h.type === "prompt")
1696
+ ledger.unrun(g.source, h, "evidence_unavailable", ctx.stopPromptTranscript === false ? "transcript disabled" : "no transcript evidence");
1697
+ }
1249
1698
  const effectiveGroups = llmExtra || !wantsPrompt
1250
1699
  ? stopGroups
1251
1700
  : stopGroups.map((g) => ({ ...g, hooks: g.hooks.filter((h) => h.type !== "prompt") })).filter((g) => g.hooks.length > 0);
1252
- const singles = await runMatchingEntries("Stop", effectiveGroups, "", payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000, undefined, llmExtra);
1701
+ const singles = await runEventLanes("Stop", effectiveGroups, "", payload, ctx, onceFired, signal, ledger, llmExtra);
1253
1702
  let block;
1254
1703
  let sawContinueFalse = false;
1255
1704
  const contexts = [];
1256
- for (const { entry, run } of singles) {
1705
+ for (const ran of singles) {
1706
+ const { entry, run } = ran;
1257
1707
  if (run.code === 2) {
1258
1708
  if (block === undefined)
1259
- block = clip(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS) || "blocked by a Stop hook";
1709
+ block = clipDataText(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS) || "blocked by a Stop hook";
1260
1710
  continue;
1261
1711
  }
1262
1712
  if (run.code !== 0) {
1263
- ctx.logger.warn("hook_command_failed", { event: "Stop", code: run.code, stderr: clip(run.stderr, 300) });
1264
- noteHookFailure(ctx, { event: "Stop", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr });
1713
+ noteExitNonzero(ctx, ledger, "Stop", ran);
1265
1714
  continue;
1266
1715
  }
1267
1716
  if (llmExtra && entry.type === "prompt" && parseCcVerdict(run.stdout)) {
@@ -1269,33 +1718,34 @@ export function createTaskHooks(config, ctx) {
1269
1718
  if (v.ok)
1270
1719
  continue;
1271
1720
  if (v.impossible) {
1272
- ctx.logger.warn("hook_stop_condition_impossible", { event: "Stop", reason: clip(v.reason?.trim() ?? "", 500) });
1721
+ ctx.logger.warn("hook_stop_condition_impossible", { event: "Stop", reason: echoCallerText(v.reason?.trim() ?? "", { maxChars: LOG_MESSAGE_ECHO_CHARS }) });
1273
1722
  continue;
1274
1723
  }
1275
1724
  if (block === undefined)
1276
- block = clip(v.reason?.trim() || "stop condition not satisfied", MAX_HOOK_FEEDBACK_CHARS);
1725
+ block = clipDataText(v.reason?.trim() || "stop condition not satisfied", MAX_HOOK_FEEDBACK_CHARS);
1277
1726
  continue;
1278
1727
  }
1279
- const out = llmExtra && entry.type === "prompt" ? parseHookStdout(run.stdout) : readHookStdout(run, entry, "Stop", ctx, payload);
1728
+ const out = llmExtra && entry.type === "prompt" ? parseHookStdout(run.stdout) : readHookStdout(ran, "Stop", ctx, payload, ledger);
1280
1729
  if (!out) {
1281
1730
  if (llmExtra && entry.type === "prompt") {
1282
- ctx.logger.warn("hook_llm_unparsed_verdict", { event: "Stop", head: clip(run.stdout.trim(), 200) });
1731
+ ctx.logger.warn("hook_llm_unparsed_verdict", { event: "Stop", head: echoCallerText(run.stdout.trim(), { maxChars: LOG_PREVIEW_ECHO_CHARS }) });
1283
1732
  emitHookNotice(ctx, { kind: "hook_decision_unavailable", event: "Stop", reason: "unparsed" });
1733
+ ledger.unrun(ran.source, entry, "bad_json", "the prompt hook's answer could not be read as a verdict");
1284
1734
  }
1285
1735
  continue;
1286
1736
  }
1287
1737
  if (typeof out.systemMessage === "string")
1288
- ctx.logger.info("hook_system_message", { event: "Stop", message: clip(out.systemMessage, 500) });
1738
+ logHookSystemMessage(ctx, "Stop", out.systemMessage);
1289
1739
  if (out.continue === false)
1290
1740
  sawContinueFalse = true;
1291
1741
  if (out.decision === "block" && block === undefined) {
1292
- block = clip(typeof out.reason === "string" ? out.reason : "blocked by a Stop hook", MAX_HOOK_FEEDBACK_CHARS);
1742
+ block = clipDataText(typeof out.reason === "string" ? out.reason : "blocked by a Stop hook", MAX_HOOK_FEEDBACK_CHARS);
1293
1743
  }
1294
1744
  const hso = out.hookSpecificOutput;
1295
1745
  if (hso !== null && typeof hso === "object" && hso.hookEventName === "Stop") {
1296
1746
  const h = hso;
1297
1747
  if (typeof h.additionalContext === "string")
1298
- contexts.push(clip(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
1748
+ contexts.push(clipDataText(h.additionalContext, MAX_HOOK_FEEDBACK_CHARS));
1299
1749
  }
1300
1750
  }
1301
1751
  if (sawContinueFalse)
@@ -1307,236 +1757,73 @@ export function createTaskHooks(config, ctx) {
1307
1757
  ...buildHookContextField(contexts),
1308
1758
  };
1309
1759
  };
1310
- hooks.stop = stopHook;
1311
1760
  }
1312
1761
  if (stopFailureEntries) {
1313
- const stopFailureHook = async (fctx) => {
1762
+ translators.StopFailure = async (fctx, signal) => {
1314
1763
  const turnError = toAssistantTurnError(fctx.errorKind);
1315
1764
  const payload = {
1316
1765
  ...basePayload(ctx, undefined),
1317
1766
  hook_event_name: "StopFailure",
1318
1767
  error: turnError,
1319
- ...(fctx.error ? { error_details: clip(redactSecrets(fctx.error), MAX_HOOK_FEEDBACK_CHARS) } : {}),
1768
+ ...(fctx.error ? { error_details: clipDataText(redactSecrets(fctx.error), MAX_HOOK_FEEDBACK_CHARS) } : {}),
1320
1769
  };
1321
- await runObserveOnlyEvent("StopFailure", stopFailureEntries, turnError, payload, ctx, onceFired);
1770
+ await runObserveOnlyEvent("StopFailure", stopFailureEntries, turnError, payload, ctx, onceFired, signal);
1322
1771
  };
1323
- hooks.stopFailure = stopFailureHook;
1324
1772
  }
1325
1773
  if (preCompactEntries) {
1326
- const preCompactHook = async (pctx) => {
1774
+ translators.PreCompact = async (pctx, signal) => {
1327
1775
  const payload = {
1328
1776
  ...basePayload(ctx, undefined),
1329
1777
  hook_event_name: "PreCompact",
1330
1778
  trigger: pctx.trigger,
1331
1779
  custom_instructions: pctx.customInstructions ?? null,
1332
1780
  };
1333
- const singles = await runMatchingEntries("PreCompact", preCompactEntries, pctx.trigger, payload, ctx, onceFired, Date.now() + MAX_HOOK_EVENT_TOTAL_SECONDS * 1000);
1334
- for (const { entry, run } of singles) {
1781
+ const ledger = createUnrunLedger(ctx, "PreCompact", undefined);
1782
+ const singles = await runEventLanes("PreCompact", preCompactEntries, pctx.trigger, payload, ctx, onceFired, signal, ledger);
1783
+ for (const ran of singles) {
1784
+ const { entry, run } = ran;
1335
1785
  if (run.code === 2) {
1336
- return { block: clip(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS) || "blocked by a PreCompact hook" };
1786
+ return { block: clipDataText(run.stderr.trim(), MAX_HOOK_FEEDBACK_CHARS) || "blocked by a PreCompact hook" };
1337
1787
  }
1338
1788
  if (run.code !== 0) {
1339
- ctx.logger.warn("hook_command_failed", { event: "PreCompact", code: run.code, stderr: clip(run.stderr, 300) });
1340
- noteHookFailure(ctx, { event: "PreCompact", entry, reason: "exit_nonzero", exitCode: run.code, stderr: run.stderr });
1789
+ noteExitNonzero(ctx, ledger, "PreCompact", ran);
1341
1790
  continue;
1342
1791
  }
1343
- const out = readHookStdout(run, entry, "PreCompact", ctx, payload);
1792
+ const out = readHookStdout(ran, "PreCompact", ctx, payload, ledger);
1344
1793
  if (!out)
1345
1794
  continue;
1346
1795
  if (typeof out.systemMessage === "string")
1347
- ctx.logger.info("hook_system_message", { event: "PreCompact", message: clip(out.systemMessage, 500) });
1796
+ logHookSystemMessage(ctx, "PreCompact", out.systemMessage);
1348
1797
  if (out.continue === false) {
1349
1798
  ctx.logger.warn("hook_continue_false_unsupported", { event: "PreCompact" });
1350
1799
  }
1351
1800
  if (out.decision === "block") {
1352
- return { block: clip(typeof out.reason === "string" ? out.reason : "blocked by a PreCompact hook", MAX_HOOK_FEEDBACK_CHARS) };
1801
+ return { block: clipDataText(typeof out.reason === "string" ? out.reason : "blocked by a PreCompact hook", MAX_HOOK_FEEDBACK_CHARS) };
1353
1802
  }
1354
1803
  }
1355
1804
  return undefined;
1356
1805
  };
1357
- hooks.preCompact = preCompactHook;
1358
1806
  }
1359
1807
  if (postCompactEntries) {
1360
- const postCompactHook = async (cctx) => {
1808
+ translators.PostCompact = async (cctx, signal) => {
1361
1809
  const payload = {
1362
1810
  ...basePayload(ctx, undefined),
1363
1811
  hook_event_name: "PostCompact",
1364
1812
  trigger: cctx.trigger,
1365
- compact_summary: clip(cctx.summary, MAX_TOOL_RESPONSE_CHARS),
1366
- };
1367
- await runObserveOnlyEvent("PostCompact", postCompactEntries, cctx.trigger, payload, ctx, onceFired);
1368
- };
1369
- hooks.postCompact = postCompactHook;
1370
- }
1371
- return hooks;
1372
- }
1373
- const HOOK_SLOT_FOLD = {
1374
- preToolUse: "custom",
1375
- preToolUseObservational: "custom",
1376
- postToolUse: "custom",
1377
- postToolUseFailure: "custom",
1378
- postToolBatch: "custom",
1379
- userPromptSubmit: "custom",
1380
- stop: "custom",
1381
- preCompact: "custom",
1382
- timeoutMs: "scalar",
1383
- postCompact: "observe-void",
1384
- stopFailure: "observe-void",
1385
- permissionDenied: "observe-void",
1386
- };
1387
- const OBSERVE_VOID_HOOK_SLOTS = Object.keys(HOOK_SLOT_FOLD).filter((k) => HOOK_SLOT_FOLD[k] === "observe-void");
1388
- export function composeHooks(deployment, task) {
1389
- if (!deployment)
1390
- return task;
1391
- const d = deployment;
1392
- const t = task;
1393
- const out = {};
1394
- const timeoutMs = d.timeoutMs ?? t.timeoutMs;
1395
- if (timeoutMs !== undefined)
1396
- out.timeoutMs = timeoutMs;
1397
- if (d.preToolUse || t.preToolUse) {
1398
- const bothObservational = (d.preToolUse === undefined || d.preToolUseObservational === true) &&
1399
- (t.preToolUse === undefined || t.preToolUseObservational === true);
1400
- if (bothObservational)
1401
- out.preToolUseObservational = true;
1402
- out.preToolUse = async (toolName, input, ctx) => {
1403
- let current = input;
1404
- const contexts = [];
1405
- let asked;
1406
- let allowedFor;
1407
- for (const slot of [d.preToolUse?.bind(d), t.preToolUse?.bind(t)]) {
1408
- if (!slot)
1409
- continue;
1410
- const r = receiveHookResult(await slot(toolName, current, ctx));
1411
- if (r === undefined || r === null || r === false)
1412
- continue;
1413
- if (isEngineRefusal(r))
1414
- return r;
1415
- if (r.additionalContext)
1416
- contexts.push(r.additionalContext);
1417
- const action = r.action;
1418
- const updatedInput = r.updatedInput;
1419
- switch (action) {
1420
- case "deny":
1421
- return { ...r, ...buildHookContextField(contexts) };
1422
- case "ask":
1423
- asked = r;
1424
- break;
1425
- case "allow":
1426
- if (updatedInput !== undefined)
1427
- current = updatedInput;
1428
- allowedFor = { input: current };
1429
- break;
1430
- case undefined:
1431
- if (updatedInput !== undefined)
1432
- current = updatedInput;
1433
- break;
1434
- default: {
1435
- const unread = action;
1436
- void unread;
1437
- return r;
1438
- }
1439
- }
1440
- }
1441
- if (asked) {
1442
- const askedUpdated = asked.updatedInput;
1443
- return {
1444
- ...asked,
1445
- ...(askedUpdated === undefined && current !== input ? { updatedInput: current } : {}),
1446
- ...buildHookContextField(contexts),
1447
- };
1448
- }
1449
- const carried = { ...(current !== input ? { updatedInput: current } : {}), ...buildHookContextField(contexts) };
1450
- if (allowedFor !== undefined && allowedFor.input === current)
1451
- return { action: "allow", ...carried };
1452
- if (current !== input || contexts.length > 0)
1453
- return carried;
1454
- return undefined;
1455
- };
1456
- }
1457
- if (d.postToolUse || t.postToolUse) {
1458
- out.postToolUse = async (toolName, input, output, ctx) => {
1459
- const rd = d.postToolUse ? await d.postToolUse(toolName, input, output, ctx) : undefined;
1460
- const rt = t.postToolUse ? await t.postToolUse(toolName, input, output, ctx) : undefined;
1461
- if (!rd && !rt)
1462
- return undefined;
1463
- const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
1464
- const updatedOutput = rt?.updatedOutput ?? rd?.updatedOutput;
1465
- return {
1466
- ...(updatedOutput ? { updatedOutput } : {}),
1467
- ...buildHookContextField(contexts),
1468
- };
1469
- };
1470
- }
1471
- if (d.postToolUseFailure || t.postToolUseFailure) {
1472
- out.postToolUseFailure = async (toolName, input, failure, ctx) => {
1473
- const rd = d.postToolUseFailure ? await d.postToolUseFailure(toolName, input, failure, ctx) : undefined;
1474
- const rt = t.postToolUseFailure ? await t.postToolUseFailure(toolName, input, failure, ctx) : undefined;
1475
- const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
1476
- const additionalContext = buildHookContext(contexts);
1477
- return additionalContext === undefined ? undefined : { additionalContext };
1478
- };
1479
- }
1480
- if (d.postToolBatch || t.postToolBatch) {
1481
- out.postToolBatch = async (calls, meta, bctx) => {
1482
- const rd = d.postToolBatch ? await d.postToolBatch(calls, meta, bctx) : undefined;
1483
- const rt = t.postToolBatch ? await t.postToolBatch(calls, meta, bctx) : undefined;
1484
- const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
1485
- const additionalContext = buildHookContext(contexts);
1486
- return additionalContext === undefined ? undefined : { additionalContext };
1487
- };
1488
- }
1489
- if (d.userPromptSubmit || t.userPromptSubmit) {
1490
- out.userPromptSubmit = async (prompt, uctx) => {
1491
- const rd = d.userPromptSubmit ? await d.userPromptSubmit(prompt, uctx) : undefined;
1492
- if (rd?.block)
1493
- return { ...rd, ...buildHookContextField(rd.additionalContext ? [rd.additionalContext] : []) };
1494
- const rt = t.userPromptSubmit ? await t.userPromptSubmit(prompt, uctx) : undefined;
1495
- if (!rd && !rt)
1496
- return undefined;
1497
- const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
1498
- return { ...(rt?.block ? { block: rt.block } : {}), ...buildHookContextField(contexts) };
1499
- };
1500
- }
1501
- if (d.stop || t.stop) {
1502
- out.stop = async (sctx) => {
1503
- const rd = d.stop ? await d.stop(sctx) : undefined;
1504
- if (rd?.block)
1505
- return { ...rd, ...buildHookContextField(rd.additionalContext ? [rd.additionalContext] : []) };
1506
- const rt = t.stop ? await t.stop(sctx) : undefined;
1507
- if (!rd && !rt)
1508
- return undefined;
1509
- const contexts = [rd?.additionalContext, rt?.additionalContext].filter((c) => typeof c === "string" && c.length > 0);
1510
- return { ...(rt?.block ? { block: rt.block } : {}), ...buildHookContextField(contexts) };
1511
- };
1512
- }
1513
- if (d.preCompact || t.preCompact) {
1514
- out.preCompact = async (pctx) => {
1515
- const rd = d.preCompact ? await d.preCompact(pctx) : undefined;
1516
- const rt = t.preCompact ? await t.preCompact(pctx) : undefined;
1517
- if (!rd && !rt)
1518
- return undefined;
1519
- const block = rd?.block ?? rt?.block;
1520
- const instructions = [rd?.additionalInstructions, rt?.additionalInstructions].filter((c) => typeof c === "string" && c.length > 0);
1521
- return {
1522
- ...(block !== undefined ? { block } : {}),
1523
- ...(instructions.length ? { additionalInstructions: instructions.join("\n") } : {}),
1813
+ compact_summary: clipDataText(cctx.summary, MAX_TOOL_RESPONSE_CHARS),
1524
1814
  };
1815
+ await runObserveOnlyEvent("PostCompact", postCompactEntries, cctx.trigger, payload, ctx, onceFired, signal);
1525
1816
  };
1526
1817
  }
1527
- for (const slot of OBSERVE_VOID_HOOK_SLOTS) {
1528
- const ds = d[slot];
1529
- const ts = t[slot];
1530
- if (ds && ts) {
1531
- out[slot] = async (...args) => {
1532
- await ds(...args);
1533
- await ts(...args);
1534
- };
1535
- }
1536
- else if (ds || ts) {
1537
- out[slot] = (ds ?? ts);
1538
- }
1539
- }
1540
- return out;
1818
+ const events = Object.keys(translators);
1819
+ return {
1820
+ name: SETTINGS_HOOKS_REGISTRATION,
1821
+ uses: { events: events.flatMap((event) => SETTINGS_HOOK_STATIONS[event]), calls: [] },
1822
+ budgetMs,
1823
+ register(on) {
1824
+ for (const event of events)
1825
+ hangTranslator(on, event, translators);
1826
+ },
1827
+ };
1541
1828
  }
1542
1829
  //# sourceMappingURL=hook-runner.js.map