@cohortapp/agent-sdk 2.17.0 → 2.18.5

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 (531) hide show
  1. package/.claude/settings.json +18 -0
  2. package/.env.example +18 -5
  3. package/README.md +1 -0
  4. package/bin/maestro.mjs +62 -0
  5. package/docs/guides/billing-console-keys.md +60 -0
  6. package/docs/guides/front-door-session.md +54 -9
  7. package/docs/guides/mac-mini.md +20 -25
  8. package/docs/guides/setup-wizard.md +1 -1
  9. package/docs/runbooks/fleet-rollout.md +156 -0
  10. package/docs/runbooks/mac-mini-bootstrap.md +12 -14
  11. package/lib/action-executor.js +19 -3
  12. package/lib/budget-guard.mjs +279 -3
  13. package/lib/channels/base-adapter.mjs +3 -1
  14. package/lib/channels/contract.mjs +2 -1
  15. package/lib/channels/inbox-item.mjs +8 -0
  16. package/lib/claude-bin.mjs +5 -6
  17. package/lib/cli/doctor-checks.mjs +141 -10
  18. package/lib/cli/global-setup-extras.mjs +5 -1
  19. package/lib/cli/inbox.mjs +100 -15
  20. package/lib/cli/seat-auth.mjs +463 -0
  21. package/lib/cli/session.mjs +80 -12
  22. package/lib/collective/capture-slots.mjs +234 -0
  23. package/lib/collective/capture.mjs +8 -6
  24. package/lib/collective/config.mjs +2 -0
  25. package/lib/collective/global-config.mjs +63 -1
  26. package/lib/collective/loop-guard.mjs +155 -0
  27. package/lib/collective/presence.mjs +142 -5
  28. package/lib/comms/send-gate.mjs +559 -1
  29. package/lib/diagnostics/alerts.mjs +49 -0
  30. package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
  31. package/lib/engine/agents/definitions.mjs +343 -0
  32. package/lib/engine/agents/persist.mjs +275 -0
  33. package/lib/engine/agents/runtime.mjs +748 -0
  34. package/lib/engine/agents/usage.mjs +95 -0
  35. package/lib/engine/auth-status.mjs +139 -0
  36. package/lib/engine/budget.mjs +194 -0
  37. package/lib/engine/cli.mjs +1204 -0
  38. package/lib/engine/commands/index.mjs +269 -0
  39. package/lib/engine/context/budget.mjs +219 -0
  40. package/lib/engine/context/cache.mjs +125 -0
  41. package/lib/engine/context/child-env.mjs +215 -0
  42. package/lib/engine/context/compaction.mjs +342 -0
  43. package/lib/engine/context/images.mjs +90 -0
  44. package/lib/engine/context/instructions.mjs +327 -0
  45. package/lib/engine/context/lazy-instructions.mjs +169 -0
  46. package/lib/engine/context/manager.mjs +182 -0
  47. package/lib/engine/context/real-path.mjs +91 -0
  48. package/lib/engine/context/secret-values.mjs +163 -0
  49. package/lib/engine/context/settings.mjs +274 -0
  50. package/lib/engine/context/stream-input.mjs +159 -0
  51. package/lib/engine/guard.mjs +152 -0
  52. package/lib/engine/hooks.mjs +713 -0
  53. package/lib/engine/loop.mjs +560 -0
  54. package/lib/engine/mcp/client.mjs +254 -0
  55. package/lib/engine/mcp/config.mjs +301 -0
  56. package/lib/engine/mcp/http.mjs +201 -0
  57. package/lib/engine/mcp/index.mjs +146 -0
  58. package/lib/engine/mcp/jsonrpc.mjs +147 -0
  59. package/lib/engine/mcp/naming.mjs +66 -0
  60. package/lib/engine/mcp/resources.mjs +89 -0
  61. package/lib/engine/mcp/results.mjs +133 -0
  62. package/lib/engine/mcp/stdio.mjs +137 -0
  63. package/lib/engine/mcp/supervisor.mjs +116 -0
  64. package/lib/engine/messages.mjs +104 -0
  65. package/lib/engine/output/json.mjs +164 -0
  66. package/lib/engine/output/stream-json.mjs +266 -0
  67. package/lib/engine/permissions.mjs +845 -0
  68. package/lib/engine/process-identity.mjs +164 -0
  69. package/lib/engine/process-tree.mjs +551 -0
  70. package/lib/engine/prompt.mjs +60 -0
  71. package/lib/engine/session/store.mjs +299 -0
  72. package/lib/engine/session-runtime/args.mjs +97 -0
  73. package/lib/engine/session-runtime/host.mjs +143 -0
  74. package/lib/engine/session-runtime/inbox.mjs +122 -0
  75. package/lib/engine/session-runtime/notifications.mjs +129 -0
  76. package/lib/engine/session-runtime/registry.mjs +328 -0
  77. package/lib/engine/session-runtime/runner.mjs +344 -0
  78. package/lib/engine/session-runtime/socket.mjs +212 -0
  79. package/lib/engine/session-runtime/wakeup.mjs +115 -0
  80. package/lib/engine/skills/index.mjs +321 -0
  81. package/lib/engine/tools/bash-background.mjs +533 -0
  82. package/lib/engine/tools/bash.mjs +216 -0
  83. package/lib/engine/tools/edit.mjs +97 -0
  84. package/lib/engine/tools/glob.mjs +81 -0
  85. package/lib/engine/tools/grep.mjs +224 -0
  86. package/lib/engine/tools/index.mjs +84 -0
  87. package/lib/engine/tools/list-agents.mjs +32 -0
  88. package/lib/engine/tools/ls.mjs +127 -0
  89. package/lib/engine/tools/monitor.mjs +82 -0
  90. package/lib/engine/tools/notebook-edit.mjs +218 -0
  91. package/lib/engine/tools/read.mjs +103 -0
  92. package/lib/engine/tools/schedule-wakeup.mjs +45 -0
  93. package/lib/engine/tools/schema.mjs +144 -0
  94. package/lib/engine/tools/send-message.mjs +77 -0
  95. package/lib/engine/tools/session.mjs +70 -0
  96. package/lib/engine/tools/todo.mjs +144 -0
  97. package/lib/engine/tools/toolsearch.mjs +217 -0
  98. package/lib/engine/tools/walk.mjs +193 -0
  99. package/lib/engine/tools/web-switch.mjs +31 -0
  100. package/lib/engine/tools/webfetch-html.mjs +387 -0
  101. package/lib/engine/tools/webfetch-net.mjs +340 -0
  102. package/lib/engine/tools/webfetch.mjs +198 -0
  103. package/lib/engine/tools/websearch.mjs +91 -0
  104. package/lib/engine/tools/workflow.mjs +95 -0
  105. package/lib/engine/tools/write.mjs +76 -0
  106. package/lib/engine/tui/line-editor.mjs +137 -0
  107. package/lib/engine/tui/render.mjs +86 -0
  108. package/lib/engine/tui/tui.mjs +274 -0
  109. package/lib/engine/wire/anthropic-messages.mjs +263 -0
  110. package/lib/engine/wire/effort.mjs +36 -0
  111. package/lib/engine/wire/errors.mjs +496 -0
  112. package/lib/engine/wire/http.mjs +441 -0
  113. package/lib/engine/wire/index.mjs +76 -0
  114. package/lib/engine/wire/openai-chat.mjs +332 -0
  115. package/lib/engine/wire/prompt-cache.mjs +79 -0
  116. package/lib/engine/wire/search.mjs +140 -0
  117. package/lib/engine/wire/sse.mjs +114 -0
  118. package/lib/engine/wire/stall.mjs +349 -0
  119. package/lib/engine/wire/token-provider.mjs +175 -0
  120. package/lib/engine/wire/usage.mjs +192 -0
  121. package/lib/engine/workflow/host.mjs +524 -0
  122. package/lib/engine/workflow/journal.mjs +188 -0
  123. package/lib/engine/workflow/json-schema.mjs +171 -0
  124. package/lib/engine/workflow/meta.mjs +329 -0
  125. package/lib/engine/workflow/notifications.mjs +52 -0
  126. package/lib/engine/workflow/runtime.mjs +447 -0
  127. package/lib/engine/workflow/sandbox.mjs +534 -0
  128. package/lib/engine/workflow/worker.mjs +141 -0
  129. package/lib/engine/workflow/worktree.mjs +74 -0
  130. package/lib/execution/disposition.mjs +1 -1
  131. package/lib/execution/intake.mjs +10 -0
  132. package/lib/execution/surface-policy.mjs +15 -0
  133. package/lib/learning/curator.mjs +8 -6
  134. package/lib/learning/reflect.mjs +8 -6
  135. package/lib/model-router/catalog/cohort.yaml +137 -0
  136. package/lib/model-router/catalog.mjs +118 -1
  137. package/lib/model-router/failover.mjs +67 -16
  138. package/lib/model-router/llm-task.mjs +39 -3
  139. package/lib/model-router/resolve.mjs +89 -3
  140. package/lib/model-router/spawn.mjs +46 -47
  141. package/lib/model-router/taxonomy.mjs +126 -4
  142. package/lib/org/cost-sync.mjs +141 -11
  143. package/lib/org/inbound/broadcast.mjs +289 -0
  144. package/lib/org/inbound/collective.mjs +375 -0
  145. package/lib/org/inbound/directedness.mjs +96 -8
  146. package/lib/org/inbound/facts.mjs +78 -2
  147. package/lib/org/inbound/project.mjs +22 -0
  148. package/lib/org/inbound/surfaces.mjs +14 -0
  149. package/lib/org/llm-token.mjs +879 -0
  150. package/lib/org/mesh.mjs +61 -0
  151. package/lib/org/messaging.mjs +3 -1
  152. package/lib/org/protocol.checksum +1 -1
  153. package/lib/org/protocol.mjs +15 -0
  154. package/lib/org/quota.mjs +520 -0
  155. package/lib/org/tool-surface.mjs +104 -16
  156. package/lib/org/ui-parity.mjs +16 -1
  157. package/lib/org/work-ledger.mjs +37 -6
  158. package/lib/rate-guard.mjs +114 -1
  159. package/lib/resource-governor.mjs +41 -6
  160. package/lib/runtime/adapter.mjs +833 -0
  161. package/lib/runtime/child-env.mjs +191 -0
  162. package/lib/runtime/legacy-shell-guard.mjs +97 -0
  163. package/lib/runtime/seat-engine.mjs +162 -0
  164. package/lib/session/ask-ledger.mjs +271 -0
  165. package/lib/session/current-work.mjs +676 -0
  166. package/lib/session/feed-core.mjs +40 -3
  167. package/lib/session/launch-args.mjs +56 -4
  168. package/lib/session/status-summary.mjs +26 -9
  169. package/lib/session/upgrade-notice.mjs +42 -0
  170. package/lib/setup/claude-probe.mjs +117 -13
  171. package/lib/setup/enrich.mjs +13 -10
  172. package/lib/setup/sections/model.mjs +39 -13
  173. package/lib/telemetry/collect.mjs +229 -11
  174. package/lib/upgrade/ignored-drift.mjs +105 -0
  175. package/lib/voice/post-call-brief.mjs +30 -17
  176. package/package.json +13 -3
  177. package/plugins/maestro-skills/skills/board-work.md +5 -0
  178. package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
  179. package/plugins/maestro-skills/skills/main-session.md +18 -7
  180. package/scaffold/config/collective.yaml +7 -0
  181. package/scripts/ci/check-durable-write-seam.mjs +3 -1
  182. package/scripts/ci/check-tarball-fidelity.mjs +126 -2
  183. package/scripts/ci/run-tests.mjs +47 -19
  184. package/scripts/cohort-llm/api-key-helper.mjs +92 -0
  185. package/scripts/collective/hook-runner.mjs +142 -19
  186. package/scripts/continuous-monitor.sh +13 -0
  187. package/scripts/cost/track-claude-usage.mjs +15 -0
  188. package/scripts/daemon/agent-daemon.mjs +408 -20
  189. package/scripts/daemon/assurance.mjs +48 -12
  190. package/scripts/daemon/cadence-consumer.mjs +218 -68
  191. package/scripts/daemon/cadence-handlers.mjs +73 -4
  192. package/scripts/daemon/classifier.mjs +75 -26
  193. package/scripts/daemon/context-compiler.mjs +51 -37
  194. package/scripts/daemon/deliver.mjs +30 -1
  195. package/scripts/daemon/dispatcher.mjs +595 -149
  196. package/scripts/daemon/health.mjs +14 -1
  197. package/scripts/daemon/maestro-daemon.mjs +11 -0
  198. package/scripts/daemon/prompt-builder.mjs +24 -0
  199. package/scripts/daemon/responder.mjs +246 -79
  200. package/scripts/daemon/sdk-version.mjs +98 -16
  201. package/scripts/eval/probe-gateway.mjs +635 -0
  202. package/scripts/eval/replay/extract.mjs +270 -0
  203. package/scripts/eval/replay/grade.mjs +260 -0
  204. package/scripts/eval/replay/lib/config.mjs +50 -0
  205. package/scripts/eval/replay/lib/effects.mjs +65 -0
  206. package/scripts/eval/replay/lib/fixture.mjs +188 -0
  207. package/scripts/eval/replay/lib/judge.mjs +72 -0
  208. package/scripts/eval/replay/lib/redact.mjs +136 -0
  209. package/scripts/eval/replay/lib/sandbox.mjs +170 -0
  210. package/scripts/eval/replay/lib/schema-check.mjs +63 -0
  211. package/scripts/eval/replay/lib/transcript.mjs +76 -0
  212. package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
  213. package/scripts/eval/replay/report.mjs +185 -0
  214. package/scripts/eval/replay/run.mjs +404 -0
  215. package/scripts/fleet/rollout.mjs +1151 -0
  216. package/scripts/hooks/pre-send-audit.sh +36 -245
  217. package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
  218. package/scripts/hooks/validate-state-yaml.sh +190 -0
  219. package/scripts/huddle/huddle-llm.mjs +361 -0
  220. package/scripts/huddle/huddle-server.mjs +46 -121
  221. package/scripts/local-triggers/autoupdate.sh +465 -81
  222. package/scripts/local-triggers/run-trigger.sh +13 -0
  223. package/scripts/maintenance/pin-integrity.mjs +364 -0
  224. package/scripts/poll-slack-events.sh +41 -9
  225. package/scripts/poller/slack-socket-mode.mjs +28 -3
  226. package/scripts/session/supervisor.mjs +80 -13
  227. package/scripts/spawn-session.sh +13 -0
  228. package/bin/maestro.test.mjs +0 -1574
  229. package/lib/action-executor.test.mjs +0 -871
  230. package/lib/archetype.test.mjs +0 -132
  231. package/lib/assurance/plan-note.test.mjs +0 -234
  232. package/lib/assurance/room-budget.test.mjs +0 -486
  233. package/lib/assurance/tier.test.mjs +0 -174
  234. package/lib/autonomy.test.mjs +0 -66
  235. package/lib/backlog.test.mjs +0 -302
  236. package/lib/backup/policy.test.mjs +0 -305
  237. package/lib/budget-escalate.test.mjs +0 -232
  238. package/lib/budget-guard.envelope.test.mjs +0 -476
  239. package/lib/budget-guard.test.mjs +0 -427
  240. package/lib/cadence-bus-requeue.test.mjs +0 -83
  241. package/lib/cadence-bus-schedule.test.mjs +0 -194
  242. package/lib/cadence-bus.test.mjs +0 -720
  243. package/lib/cadences.test.mjs +0 -230
  244. package/lib/capability/inventory.test.mjs +0 -232
  245. package/lib/capability.test.mjs +0 -78
  246. package/lib/channels/base-adapter.test.mjs +0 -590
  247. package/lib/channels/channels.test.mjs +0 -371
  248. package/lib/channels/contract.test.mjs +0 -162
  249. package/lib/channels/inbox-item.test.mjs +0 -368
  250. package/lib/channels/orgmail/adapter.test.mjs +0 -448
  251. package/lib/channels/pairing.test.mjs +0 -270
  252. package/lib/channels/repeat-suppressor.test.mjs +0 -134
  253. package/lib/channels/slack-adapter.test.mjs +0 -212
  254. package/lib/channels/telegram-adapter.test.mjs +0 -306
  255. package/lib/channels/voice/adapter.test.mjs +0 -278
  256. package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
  257. package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
  258. package/lib/charter.test.mjs +0 -89
  259. package/lib/claude-bin.test.mjs +0 -131
  260. package/lib/cli/board.test.mjs +0 -227
  261. package/lib/cli/design.test.mjs +0 -270
  262. package/lib/cli/doctor-checks.test.mjs +0 -336
  263. package/lib/cli/global-setup-extras.test.mjs +0 -462
  264. package/lib/cli/inbox.test.mjs +0 -230
  265. package/lib/cli/session-ack.test.mjs +0 -63
  266. package/lib/cli/session.test.mjs +0 -613
  267. package/lib/collective/capture.test.mjs +0 -121
  268. package/lib/collective/cards.test.mjs +0 -114
  269. package/lib/collective/config.test.mjs +0 -123
  270. package/lib/collective/global-config.test.mjs +0 -220
  271. package/lib/collective/global-skills.test.mjs +0 -126
  272. package/lib/collective/presence.test.mjs +0 -95
  273. package/lib/collective/recall.test.mjs +0 -116
  274. package/lib/collective/vendor-skills.test.mjs +0 -306
  275. package/lib/comms/send-gate.test.mjs +0 -770
  276. package/lib/comms.test.mjs +0 -41
  277. package/lib/context/budget.test.mjs +0 -252
  278. package/lib/context/history-scope.test.mjs +0 -79
  279. package/lib/cost/ledger-row.test.mjs +0 -183
  280. package/lib/design/design-md.test.mjs +0 -318
  281. package/lib/design/fixtures/DESIGN.golden.md +0 -238
  282. package/lib/design/fixtures/PRODUCT.golden.md +0 -67
  283. package/lib/design/fixtures/foundation.json +0 -133
  284. package/lib/design/refresh-gate.test.mjs +0 -144
  285. package/lib/design/write.test.mjs +0 -241
  286. package/lib/diagnostics/alerts.test.mjs +0 -318
  287. package/lib/diagnostics/backup-freshness.test.mjs +0 -185
  288. package/lib/diagnostics/counters.test.mjs +0 -206
  289. package/lib/diagnostics/events.test.mjs +0 -290
  290. package/lib/diagnostics/otel.test.mjs +0 -196
  291. package/lib/diagnostics/trace.test.mjs +0 -251
  292. package/lib/env-compat.test.mjs +0 -104
  293. package/lib/execution/disposition.test.mjs +0 -553
  294. package/lib/execution/drive.test.mjs +0 -270
  295. package/lib/execution/effects.test.mjs +0 -344
  296. package/lib/execution/intake.test.mjs +0 -389
  297. package/lib/execution/journal.test.mjs +0 -261
  298. package/lib/execution/match.test.mjs +0 -235
  299. package/lib/execution/pipeline.test.mjs +0 -392
  300. package/lib/execution/route.test.mjs +0 -186
  301. package/lib/execution/surface-policy.test.mjs +0 -162
  302. package/lib/fs-atomic.test.mjs +0 -72
  303. package/lib/fs-ownership.test.mjs +0 -158
  304. package/lib/goals/admission.test.mjs +0 -164
  305. package/lib/goals/classify.test.mjs +0 -167
  306. package/lib/goals/collaborate.test.mjs +0 -336
  307. package/lib/goals/gaps.test.mjs +0 -284
  308. package/lib/goals/loop.test.mjs +0 -845
  309. package/lib/hooks/bus.test.mjs +0 -387
  310. package/lib/identity/persona.test.mjs +0 -142
  311. package/lib/kpi-sensors.test.mjs +0 -278
  312. package/lib/kpi.test.mjs +0 -244
  313. package/lib/learning/config.test.mjs +0 -75
  314. package/lib/learning/counters.test.mjs +0 -69
  315. package/lib/learning/curator-consolidate.test.mjs +0 -238
  316. package/lib/learning/curator.test.mjs +0 -106
  317. package/lib/learning/reflect.test.mjs +0 -0
  318. package/lib/learning/session-index.test.mjs +0 -125
  319. package/lib/learning/skill-writer.test.mjs +0 -210
  320. package/lib/mandate/audit.test.mjs +0 -195
  321. package/lib/mandate/contract.test.mjs +0 -185
  322. package/lib/mandate/derive.test.mjs +0 -274
  323. package/lib/mandate/model.test.mjs +0 -164
  324. package/lib/mandate/refresh.test.mjs +0 -389
  325. package/lib/mcp/server.test.mjs +0 -426
  326. package/lib/model-router/auth-profiles.test.mjs +0 -580
  327. package/lib/model-router/catalog.test.mjs +0 -385
  328. package/lib/model-router/economics.test.mjs +0 -438
  329. package/lib/model-router/failover.test.mjs +0 -439
  330. package/lib/model-router/health.test.mjs +0 -338
  331. package/lib/model-router/integration-coverage.test.mjs +0 -831
  332. package/lib/model-router/integration.test.mjs +0 -564
  333. package/lib/model-router/ledger.test.mjs +0 -415
  334. package/lib/model-router/llm-task.test.mjs +0 -392
  335. package/lib/model-router/org-credentials.test.mjs +0 -265
  336. package/lib/model-router/pricing-refresh.test.mjs +0 -286
  337. package/lib/model-router/reconcile.test.mjs +0 -316
  338. package/lib/model-router/repair.test.mjs +0 -180
  339. package/lib/model-router/spawn.test.mjs +0 -446
  340. package/lib/model-router/taxonomy.test.mjs +0 -410
  341. package/lib/model-router.test.mjs +0 -1207
  342. package/lib/org/activity.test.mjs +0 -134
  343. package/lib/org/approvals.test.mjs +0 -216
  344. package/lib/org/awareness.test.mjs +0 -159
  345. package/lib/org/board-mine-cache.test.mjs +0 -53
  346. package/lib/org/board.test.mjs +0 -187
  347. package/lib/org/bootstrap-context.test.mjs +0 -153
  348. package/lib/org/client.test.mjs +0 -1206
  349. package/lib/org/cohort-client.test.mjs +0 -126
  350. package/lib/org/cost-sync.test.mjs +0 -153
  351. package/lib/org/doctor.test.mjs +0 -346
  352. package/lib/org/engagement-ledger.test.mjs +0 -112
  353. package/lib/org/engagement.test.mjs +0 -739
  354. package/lib/org/handoff.test.mjs +0 -269
  355. package/lib/org/inbound/directedness.test.mjs +0 -668
  356. package/lib/org/inbound/facts.test.mjs +0 -471
  357. package/lib/org/inbound/hydrate.test.mjs +0 -908
  358. package/lib/org/inbound/index.test.mjs +0 -429
  359. package/lib/org/inbound/project.test.mjs +0 -287
  360. package/lib/org/integration-tools.test.mjs +0 -160
  361. package/lib/org/keys.test.mjs +0 -92
  362. package/lib/org/knowledge.test.mjs +0 -326
  363. package/lib/org/leases.test.mjs +0 -235
  364. package/lib/org/mesh-directives.test.mjs +0 -110
  365. package/lib/org/mesh-integration.test.mjs +0 -127
  366. package/lib/org/mesh.test.mjs +0 -400
  367. package/lib/org/messaging.test.mjs +0 -471
  368. package/lib/org/param-contract.test.mjs +0 -477
  369. package/lib/org/policy.test.mjs +0 -237
  370. package/lib/org/protocol.checksum.test.mjs +0 -90
  371. package/lib/org/protocol.test.mjs +0 -323
  372. package/lib/org/push.test.mjs +0 -792
  373. package/lib/org/registry.test.mjs +0 -100
  374. package/lib/org/resource-tools.test.mjs +0 -361
  375. package/lib/org/tool-access.test.mjs +0 -144
  376. package/lib/org/tool-surface-integration.test.mjs +0 -120
  377. package/lib/org/tool-surface.test.mjs +0 -1268
  378. package/lib/org/typing.test.mjs +0 -291
  379. package/lib/org/ui-parity.test.mjs +0 -560
  380. package/lib/org/verify.test.mjs +0 -194
  381. package/lib/org/work-ledger.test.mjs +0 -273
  382. package/lib/plan/adoption-e2e.test.mjs +0 -366
  383. package/lib/plan/budget-enforcement.test.mjs +0 -400
  384. package/lib/plan/compile.test.mjs +0 -382
  385. package/lib/plan/emit.test.mjs +0 -269
  386. package/lib/plan/explain.test.mjs +0 -188
  387. package/lib/prompts/parallelism.test.mjs +0 -177
  388. package/lib/rag/rag.test.mjs +0 -505
  389. package/lib/rate-guard.test.mjs +0 -272
  390. package/lib/reactive-gate.test.mjs +0 -57
  391. package/lib/render.test.mjs +0 -68
  392. package/lib/resource-governor.test.mjs +0 -488
  393. package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
  394. package/lib/scheduling/jitter.test.mjs +0 -140
  395. package/lib/secrets/broker.test.mjs +0 -280
  396. package/lib/secrets/providers.test.mjs +0 -274
  397. package/lib/security/audit-engine.test.mjs +0 -424
  398. package/lib/security/coerce-args.test.mjs +0 -281
  399. package/lib/security/dangerous-tools.test.mjs +0 -68
  400. package/lib/security/external-content.test.mjs +0 -84
  401. package/lib/security/redact.test.mjs +0 -441
  402. package/lib/security/secret-equal.test.mjs +0 -55
  403. package/lib/session/config.test.mjs +0 -92
  404. package/lib/session/feed-core.test.mjs +0 -198
  405. package/lib/session/first-run.test.mjs +0 -121
  406. package/lib/session/frontdoor.test.mjs +0 -205
  407. package/lib/session/handoffs.test.mjs +0 -183
  408. package/lib/session/identity.test.mjs +0 -180
  409. package/lib/session/inbox-claims.test.mjs +0 -286
  410. package/lib/session/launch-args.test.mjs +0 -157
  411. package/lib/session/liveness.test.mjs +0 -100
  412. package/lib/session/status-summary.test.mjs +0 -118
  413. package/lib/session-permissions.test.mjs +0 -120
  414. package/lib/setup/claude-probe.test.mjs +0 -187
  415. package/lib/setup/completeness.test.mjs +0 -110
  416. package/lib/setup/context-pack.test.mjs +0 -89
  417. package/lib/setup/enrich.test.mjs +0 -115
  418. package/lib/setup/enroll-from-cohort.test.mjs +0 -300
  419. package/lib/setup/integration.test.mjs +0 -162
  420. package/lib/setup/io.test.mjs +0 -77
  421. package/lib/setup/runner.test.mjs +0 -132
  422. package/lib/setup/sections/identity.test.mjs +0 -234
  423. package/lib/setup/sections/inventory.test.mjs +0 -198
  424. package/lib/setup/sections/learning.test.mjs +0 -81
  425. package/lib/setup/sections/mandate.test.mjs +0 -388
  426. package/lib/setup/sections/messaging.test.mjs +0 -127
  427. package/lib/setup/sections/model.test.mjs +0 -240
  428. package/lib/setup/sections/org.test.mjs +0 -346
  429. package/lib/setup/sections/orgmail.test.mjs +0 -118
  430. package/lib/setup/sections/recovery.test.mjs +0 -98
  431. package/lib/setup/sections/subagents.test.mjs +0 -429
  432. package/lib/setup/sections/verify.test.mjs +0 -175
  433. package/lib/setup/sot.test.mjs +0 -81
  434. package/lib/setup/state.test.mjs +0 -115
  435. package/lib/singleton.test.mjs +0 -151
  436. package/lib/subagents/cli.test.mjs +0 -389
  437. package/lib/subagents/client.test.mjs +0 -309
  438. package/lib/subagents/gap.test.mjs +0 -234
  439. package/lib/subagents/lock.test.mjs +0 -248
  440. package/lib/subagents/manifest.test.mjs +0 -175
  441. package/lib/subagents/refs.test.mjs +0 -204
  442. package/lib/subagents/resolve.test.mjs +0 -422
  443. package/lib/subagents/schema.test.mjs +0 -328
  444. package/lib/telemetry/alerts.test.mjs +0 -109
  445. package/lib/telemetry/collect.test.mjs +0 -1274
  446. package/lib/tool-definitions-integration.test.mjs +0 -83
  447. package/lib/tool-definitions.test.mjs +0 -437
  448. package/lib/upgrade/global-refresh.test.mjs +0 -65
  449. package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
  450. package/lib/upgrade/post-steps.test.mjs +0 -200
  451. package/lib/upgrade/verify.test.mjs +0 -164
  452. package/lib/util/fetch-timeout.test.mjs +0 -202
  453. package/lib/util/reconnect.test.mjs +0 -369
  454. package/lib/util/unhandled.test.mjs +0 -216
  455. package/lib/voice/outbound.test.mjs +0 -69
  456. package/lib/voice/session-rotation.test.mjs +0 -114
  457. package/lib/voice/stt.test.mjs +0 -226
  458. package/lib/voice/voice.test.mjs +0 -990
  459. package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
  460. package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
  461. package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
  462. package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
  463. package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
  464. package/scripts/ci/check-skill-packs.test.mjs +0 -495
  465. package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
  466. package/scripts/ci/check.test.mjs +0 -194
  467. package/scripts/ci/conformance-org-api.test.mjs +0 -425
  468. package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
  469. package/scripts/collective/hook-runner.test.mjs +0 -173
  470. package/scripts/cost/fleet-digest.test.mjs +0 -207
  471. package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
  472. package/scripts/cost/track-claude-usage.test.mjs +0 -148
  473. package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
  474. package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
  475. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
  476. package/scripts/daemon/agent-daemon.test.mjs +0 -995
  477. package/scripts/daemon/assurance-e2e.test.mjs +0 -613
  478. package/scripts/daemon/assurance.test.mjs +0 -1791
  479. package/scripts/daemon/board-mirror.test.mjs +0 -165
  480. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
  481. package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
  482. package/scripts/daemon/cadence-consumer.test.mjs +0 -776
  483. package/scripts/daemon/cadence-handlers.test.mjs +0 -837
  484. package/scripts/daemon/classifier-identity.test.mjs +0 -137
  485. package/scripts/daemon/classifier.test.mjs +0 -266
  486. package/scripts/daemon/classify-kind.test.mjs +0 -40
  487. package/scripts/daemon/context-compiler.test.mjs +0 -406
  488. package/scripts/daemon/deliver.test.mjs +0 -564
  489. package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
  490. package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
  491. package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
  492. package/scripts/daemon/dispatcher-session-continuity.test.mjs +0 -365
  493. package/scripts/daemon/execution-ladder.test.mjs +0 -470
  494. package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
  495. package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
  496. package/scripts/daemon/inbox-deferral.test.mjs +0 -336
  497. package/scripts/daemon/inbox-wake.test.mjs +0 -199
  498. package/scripts/daemon/integration.test.mjs +0 -149
  499. package/scripts/daemon/lib/self-echo.test.mjs +0 -153
  500. package/scripts/daemon/lib/session-router.test.mjs +0 -554
  501. package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
  502. package/scripts/daemon/prompt-builder.test.mjs +0 -556
  503. package/scripts/daemon/responder-cost.test.mjs +0 -68
  504. package/scripts/daemon/responder-history.test.mjs +0 -221
  505. package/scripts/daemon/sdk-version.test.mjs +0 -31
  506. package/scripts/daemon/session-lock.test.mjs +0 -252
  507. package/scripts/daemon/session-outcomes.test.mjs +0 -533
  508. package/scripts/daemon/typing-registry.test.mjs +0 -102
  509. package/scripts/hooks/pre-send-audit.test.mjs +0 -354
  510. package/scripts/huddle/huddle-prompt.test.mjs +0 -176
  511. package/scripts/local-triggers/autoupdate.test.mjs +0 -518
  512. package/scripts/local-triggers/generate-plists.test.mjs +0 -456
  513. package/scripts/media-generation/brand-clause.test.mjs +0 -135
  514. package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
  515. package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
  516. package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
  517. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
  518. package/scripts/poller/slack-socket-mode.test.mjs +0 -805
  519. package/scripts/poller-launchd/install.test.mjs +0 -243
  520. package/scripts/restore-from-backup.test.mjs +0 -181
  521. package/scripts/session/feed.test.mjs +0 -196
  522. package/scripts/session/supervisor-sh.test.mjs +0 -218
  523. package/scripts/session/supervisor.test.mjs +0 -482
  524. package/scripts/setup/configure-macos.test.mjs +0 -306
  525. package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
  526. package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
  527. package/scripts/setup/generate-capability.test.mjs +0 -134
  528. package/scripts/setup/init-agent.test.mjs +0 -370
  529. package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
  530. package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
  531. package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
@@ -0,0 +1,713 @@
1
+ /**
2
+ * lib/engine/hooks.mjs — lifecycle hooks and in-process gates.
3
+ *
4
+ * Command hooks (settings.json `hooks`, every settings layer):
5
+ *
6
+ * "hooks": { "PreToolUse": [ { "matcher": "Bash|Write",
7
+ * "hooks": [ { "type": "command", "command": "…", "timeout": 30 } ] } ] }
8
+ *
9
+ * Events: SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop,
10
+ * SubagentStop, PreCompact, Notification. The matcher is a regular expression
11
+ * that must match the WHOLE target (the tool name for tool events; the source
12
+ * for SessionStart, the trigger for PreCompact, the notification type for
13
+ * Notification); "" or "*" matches everything; a matcher that is not a valid
14
+ * expression matches only itself. Matching hooks run in parallel; identical
15
+ * commands run once.
16
+ *
17
+ * Each command runs through /bin/sh in the working directory, with the run's
18
+ * environment minus every credential-shaped variable, the org credential
19
+ * included (context/child-env.mjs, CF-22) — a hook object's own
20
+ * `"inheritEnv": ["NAME", …]` passes the names it lists (not from project or
21
+ * local settings, for a credential) — and a JSON object on stdin:
22
+ * { session_id, transcript_path, cwd, hook_event_name, permission_mode,
23
+ * tool_name, tool_input, tool_use_id, tool_response, prompt, source,
24
+ * stop_hook_active, trigger, message, … } (the fields the event has)
25
+ *
26
+ * Exit status:
27
+ * 0 continue. Stdout that is a JSON object may carry
28
+ * { continue:false, stopReason } — end the run;
29
+ * { decision:"block"|"approve", reason };
30
+ * { hookSpecificOutput: { permissionDecision:"allow"|"deny"|"ask",
31
+ * permissionDecisionReason, updatedInput, additionalContext } }.
32
+ * Plain stdout from SessionStart / UserPromptSubmit is context.
33
+ * 2 BLOCK, with stderr as the reason: PreToolUse refuses the call and
34
+ * the model reads why; UserPromptSubmit refuses the prompt;
35
+ * Stop / SubagentStop make the run continue with the reason as the
36
+ * next user message; PostToolUse shows the reason to the model;
37
+ * PreCompact limits that compaction to clearing old tool results
38
+ * (no summary). For SessionStart and Notification it is reported only.
39
+ * other a non-blocking error, logged; the run goes on.
40
+ * A hook that exceeds its timeout (default 60 s; `timeout` is in seconds) is
41
+ * killed — its process group and every process it started that the tracker
42
+ * recorded, setsid descendants included (CF-119, `runCommandHook`) — and
43
+ * treated as a non-blocking error. The environment scrub also withholds a
44
+ * variable whose value looks like a credential (CF-118). Anything a hook contributes to
45
+ * the model's context is capped at 10,000 characters.
46
+ *
47
+ * In-process gates run BEFORE any command hook, for every PreToolUse, in every
48
+ * permission mode, whether or not a settings file exists. A gate that throws
49
+ * denies (fail closed). A gate's verdict may carry a `commit` — a side effect
50
+ * (the send counter) the guard runs only once the call is finally allowed,
51
+ * just before the tool runs; a commit that fails refuses the call.
52
+ * `createDefaultGates` registers the outbound send gate (lib/comms/send-gate.mjs,
53
+ * the same verdict the pre-send-audit hook gives) for the five outbound tool
54
+ * names on ANY MCP server: which server is the org server is not something the
55
+ * config key or launcher reliably says, and a clean message costs nothing.
56
+ *
57
+ * The verdict logic (`matcherMatches`, `selectHooks`, `interpretHookRun`,
58
+ * `combinePreToolUse`) is pure; `runCommandHook` is the process edge.
59
+ *
60
+ * @module lib/engine/hooks
61
+ */
62
+
63
+ import { spawn } from "node:child_process";
64
+ import path from "node:path";
65
+ import { preparePreSendHook } from "../comms/send-gate.mjs";
66
+ import { configuredCommandEnv, inheritEnvFor, PROJECT_SCOPES } from "./context/child-env.mjs";
67
+ import { createProcessTracker, newProcessTag, PROCESS_TAG_ENV } from "./process-tree.mjs";
68
+
69
+ export const HOOK_OUTPUT_CAP = 10_000;
70
+ export const DEFAULT_HOOK_TIMEOUT_MS = 60_000;
71
+ const RAW_OUTPUT_LIMIT = 1_000_000;
72
+
73
+ /** Events that name a tool, so their matcher is tested against the tool name. */
74
+ const TOOL_EVENTS = new Set(["PreToolUse", "PostToolUse"]);
75
+
76
+ /** @param {string} s @param {number} [cap] */
77
+ export function capText(s, cap = HOOK_OUTPUT_CAP) {
78
+ const t = String(s ?? "");
79
+ return t.length <= cap ? t : `${t.slice(0, cap)}\n… [hook output truncated at ${cap} characters]`;
80
+ }
81
+
82
+ /**
83
+ * @param {string|undefined} matcher
84
+ * @param {string} target
85
+ */
86
+ export function matcherMatches(matcher, target) {
87
+ if (matcher === undefined || matcher === "" || matcher === "*") return true;
88
+ try {
89
+ return new RegExp(`^(?:${matcher})$`).test(target);
90
+ } catch {
91
+ return matcher === target;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * @typedef {{command:string, timeoutMs:number, source:string, baseDir:string, scope?:string, inheritEnv?:unknown}} SelectedHook
97
+ * `inheritEnv`: the hook object's own list, as configured (validated by the runner, CF-22)
98
+ */
99
+
100
+ /**
101
+ * @param {Record<string, import('./context/settings.mjs').HookEntry[]>} config
102
+ * @param {string} event
103
+ * @param {string} target tool name, source, trigger or notification type ("" when the event has none)
104
+ * @returns {SelectedHook[]}
105
+ */
106
+ export function selectHooks(config, event, target) {
107
+ const seen = new Set();
108
+ /** @type {SelectedHook[]} */
109
+ const out = [];
110
+ for (const entry of config?.[event] ?? []) {
111
+ const applies = TOOL_EVENTS.has(event) || event === "SessionStart" || event === "PreCompact" || event === "Notification" ? matcherMatches(entry.matcher, target) : true;
112
+ if (!applies) continue;
113
+ for (const h of entry.hooks) {
114
+ if (!h || h.type !== "command" || typeof h.command !== "string" || h.command.trim() === "") continue;
115
+ if (seen.has(h.command)) continue;
116
+ seen.add(h.command);
117
+ const secs = Number(h.timeout);
118
+ /** @type {SelectedHook} */
119
+ const selected = { command: h.command, timeoutMs: Number.isFinite(secs) && secs > 0 ? secs * 1000 : DEFAULT_HOOK_TIMEOUT_MS, source: entry.source, baseDir: entry.baseDir };
120
+ if (entry.scope !== undefined) selected.scope = entry.scope;
121
+ if (/** @type {any} */ (h).inheritEnv !== undefined) selected.inheritEnv = /** @type {any} */ (h).inheritEnv;
122
+ out.push(selected);
123
+ }
124
+ }
125
+ return out;
126
+ }
127
+
128
+ /**
129
+ * @typedef {{kind:'exited', code:number|null, stdout:string, stderr:string}
130
+ * | {kind:'timeout', stdout:string, stderr:string, timeoutMs:number}
131
+ * | {kind:'spawn_error', message:string}} HookRun
132
+ *
133
+ * @typedef {Object} HookVerdict
134
+ * @property {'success'|'blocking'|'non_blocking_error'} outcome
135
+ * @property {string} [blockReason] exit 2 or decision "block"
136
+ * @property {'allow'|'deny'|'ask'} [permissionDecision]
137
+ * @property {string} [permissionDecisionReason]
138
+ * @property {Record<string,unknown>} [updatedInput]
139
+ * @property {string} [additionalContext]
140
+ * @property {string} [stopReason] continue:false
141
+ * @property {string} [error]
142
+ */
143
+
144
+ /**
145
+ * @param {string} event
146
+ * @param {HookRun} run
147
+ * @returns {HookVerdict}
148
+ */
149
+ export function interpretHookRun(event, run) {
150
+ if (run.kind === "spawn_error") return { outcome: "non_blocking_error", error: `could not run: ${run.message}` };
151
+ if (run.kind === "timeout") return { outcome: "non_blocking_error", error: `timed out after ${run.timeoutMs} ms` };
152
+ if (run.code === 2) {
153
+ const reason = capText(run.stderr.trim() || "blocked by a hook (no reason given)");
154
+ return { outcome: "blocking", blockReason: reason };
155
+ }
156
+ if (run.code !== 0) {
157
+ return { outcome: "non_blocking_error", error: `exited with code ${run.code}${run.stderr.trim() ? `: ${capText(run.stderr.trim(), 500)}` : ""}` };
158
+ }
159
+
160
+ const out = run.stdout.trim();
161
+ /** @type {any} */
162
+ let json = null;
163
+ if (out.startsWith("{")) {
164
+ try {
165
+ json = JSON.parse(out);
166
+ } catch {
167
+ json = null; // not JSON after all: treated as plain output below
168
+ }
169
+ }
170
+ if (!json || typeof json !== "object" || Array.isArray(json)) {
171
+ const plainIsContext = event === "SessionStart" || event === "UserPromptSubmit";
172
+ return plainIsContext && out !== "" ? { outcome: "success", additionalContext: capText(out) } : { outcome: "success" };
173
+ }
174
+
175
+ /** @type {HookVerdict} */
176
+ const v = { outcome: "success" };
177
+ if (json.continue === false) v.stopReason = capText(typeof json.stopReason === "string" ? json.stopReason : "a hook ended the run");
178
+ const hso = json.hookSpecificOutput && typeof json.hookSpecificOutput === "object" ? json.hookSpecificOutput : {};
179
+ if (typeof hso.additionalContext === "string" && hso.additionalContext !== "") v.additionalContext = capText(hso.additionalContext);
180
+ const reason = typeof json.reason === "string" ? json.reason : "";
181
+ if (event === "PreToolUse") {
182
+ const pd = hso.permissionDecision;
183
+ if (pd === "allow" || pd === "deny" || pd === "ask") {
184
+ v.permissionDecision = pd;
185
+ v.permissionDecisionReason = capText(typeof hso.permissionDecisionReason === "string" ? hso.permissionDecisionReason : reason);
186
+ } else if (json.decision === "block") {
187
+ v.permissionDecision = "deny";
188
+ v.permissionDecisionReason = capText(reason || "blocked by a hook");
189
+ } else if (json.decision === "approve") {
190
+ v.permissionDecision = "allow";
191
+ v.permissionDecisionReason = capText(reason);
192
+ }
193
+ if (hso.updatedInput && typeof hso.updatedInput === "object" && !Array.isArray(hso.updatedInput)) v.updatedInput = hso.updatedInput;
194
+ if (v.permissionDecision === "deny") {
195
+ v.outcome = "blocking";
196
+ v.blockReason = v.permissionDecisionReason || "blocked by a hook";
197
+ }
198
+ } else if (json.decision === "block") {
199
+ v.outcome = "blocking";
200
+ v.blockReason = capText(reason || "blocked by a hook (no reason given)");
201
+ }
202
+ return v;
203
+ }
204
+
205
+ /**
206
+ * Fold PreToolUse verdicts: any block wins, then ask, then allow.
207
+ * @param {HookVerdict[]} verdicts
208
+ * @returns {{decision:'block'|'ask'|'allow'|'none', reason:string, updatedInput:Record<string,unknown>|null, additionalContext:string|null, stopReason:string|null}}
209
+ */
210
+ export function combinePreToolUse(verdicts) {
211
+ const blocks = verdicts.filter((v) => v.outcome === "blocking").map((v) => v.blockReason || "blocked");
212
+ const asks = verdicts.filter((v) => v.permissionDecision === "ask");
213
+ const allows = verdicts.filter((v) => v.permissionDecision === "allow");
214
+ const updated = verdicts.filter((v) => v.updatedInput).map((v) => v.updatedInput);
215
+ const context = verdicts.map((v) => v.additionalContext).filter(Boolean);
216
+ const stop = verdicts.find((v) => v.stopReason);
217
+ const decision = blocks.length ? "block" : asks.length ? "ask" : allows.length ? "allow" : "none";
218
+ const reason = blocks.length ? blocks.join("\n") : asks.length ? asks.map((v) => v.permissionDecisionReason || "").filter(Boolean).join("\n") : allows.map((v) => v.permissionDecisionReason || "").filter(Boolean).join("\n");
219
+ return {
220
+ decision,
221
+ reason: capText(reason),
222
+ updatedInput: updated.length ? /** @type any */ (updated[updated.length - 1]) : null,
223
+ additionalContext: context.length ? capText(context.join("\n\n")) : null,
224
+ stopReason: stop?.stopReason ?? null,
225
+ };
226
+ }
227
+
228
+ /** After a killed hook, how long its pipes may take to close before they are destroyed (CF-119). */
229
+ export const HOOK_PIPE_SETTLE_MS = 500;
230
+
231
+ /**
232
+ * Run one command hook. Never rejects.
233
+ *
234
+ * CF-119 (W5-B): a hook that times out or is aborted is stopped by process
235
+ * TREE, not only by process group — the same tracker as background shells
236
+ * (process-tree.mjs): its environment carries COHORT_ENGINE_PROCESS_TAG, its
237
+ * descendants are sampled (parent links and the tag) 100 ms and 500 ms after it
238
+ * starts and every 2 s while it runs, and again at the kill; every recorded
239
+ * process whose start identity still matches is SIGKILLed with the group. A
240
+ * process no walk found that still holds the hook's pipes cannot hold the run:
241
+ * the pipes are destroyed {@link HOOK_PIPE_SETTLE_MS} after the kill.
242
+ * @param {{command:string, input:object, timeoutMs:number, cwd:string, env:Record<string,string|undefined>, signal?:AbortSignal,
243
+ * processTracker?:ReturnType<typeof createProcessTracker>}} p `processTracker`: injected by tests
244
+ * @returns {Promise<HookRun>}
245
+ */
246
+ export function runCommandHook({ command, input, timeoutMs, cwd, env, signal, processTracker }) {
247
+ return new Promise((resolve) => {
248
+ const tag = newProcessTag();
249
+ const rootId = `hook_${tag}`;
250
+ const tracker = processTracker ?? createProcessTracker();
251
+ let child;
252
+ try {
253
+ child = spawn("/bin/sh", ["-c", command], { cwd, env: { ...env, [PROCESS_TAG_ENV]: tag }, stdio: ["pipe", "pipe", "pipe"], detached: true });
254
+ } catch (e) {
255
+ if (!processTracker) tracker.dispose();
256
+ resolve({ kind: "spawn_error", message: e instanceof Error ? e.message : String(e) });
257
+ return;
258
+ }
259
+ if (child.pid) tracker.track({ id: rootId, pid: child.pid, tag });
260
+ let stdout = "";
261
+ let stderr = "";
262
+ let timedOut = false;
263
+ let settled = false;
264
+ /** @type {NodeJS.Timeout|null} */
265
+ let settleTimer = null;
266
+ const kill = () => {
267
+ if (settleTimer !== null || settled) return;
268
+ // Record the tree (and tagged processes) while the parents still live, then signal group and tree.
269
+ try {
270
+ tracker.capture([rootId], { tags: true });
271
+ } catch {
272
+ /* the group signal still goes out */
273
+ }
274
+ try {
275
+ process.kill(-(/** @type number */ (child.pid)), "SIGKILL");
276
+ } catch {
277
+ /* the group already exited */
278
+ }
279
+ try {
280
+ tracker.signal([rootId], "SIGKILL");
281
+ } catch {
282
+ /* best effort: identity-checked pids only */
283
+ }
284
+ settleTimer = setTimeout(() => {
285
+ child.stdout?.destroy();
286
+ child.stderr?.destroy();
287
+ child.unref();
288
+ finish(timedOut ? { kind: "timeout", stdout, stderr, timeoutMs } : { kind: "exited", code: child.exitCode, stdout, stderr });
289
+ }, HOOK_PIPE_SETTLE_MS);
290
+ };
291
+ const timer = setTimeout(() => {
292
+ timedOut = true;
293
+ kill();
294
+ }, timeoutMs);
295
+ const onAbort = () => kill();
296
+ signal?.addEventListener("abort", onAbort, { once: true });
297
+ child.stdout.setEncoding("utf8");
298
+ child.stderr.setEncoding("utf8");
299
+ child.stdout.on("data", (d) => {
300
+ if (stdout.length < RAW_OUTPUT_LIMIT) stdout += d;
301
+ });
302
+ child.stderr.on("data", (d) => {
303
+ if (stderr.length < RAW_OUTPUT_LIMIT) stderr += d;
304
+ });
305
+ child.stdin.on("error", () => {
306
+ /* a hook that exits without reading stdin: EPIPE is not a failure */
307
+ });
308
+ const finish = (/** @type HookRun */ r) => {
309
+ if (settled) return;
310
+ settled = true;
311
+ clearTimeout(timer);
312
+ if (settleTimer !== null) clearTimeout(settleTimer);
313
+ signal?.removeEventListener("abort", onAbort);
314
+ tracker.rootExited(rootId);
315
+ tracker.forget(rootId);
316
+ if (!processTracker) tracker.dispose();
317
+ resolve(r);
318
+ };
319
+ child.on("error", (e) => finish({ kind: "spawn_error", message: e.message }));
320
+ child.on("close", (code) => finish(timedOut ? { kind: "timeout", stdout, stderr, timeoutMs } : { kind: "exited", code, stdout, stderr }));
321
+ child.stdin.end(JSON.stringify(input));
322
+ });
323
+ }
324
+
325
+ /* ────────────────────────────── in-process gates ─────────────────────────── */
326
+
327
+ /**
328
+ * @typedef {{toolName:string, toolInput:Record<string,unknown>, toolUseId?:string, mcp?:{server:string, tool:string, command?:string|null, args?:string[]}|null}} GateCall
329
+ * @typedef {() => ({ok:true}|{ok:false, reason:string})} GateCommit
330
+ * @typedef {{allow:true, commit?:GateCommit}|{allow:false, reason:string}} GateVerdict
331
+ * @typedef {{name:string, applies:(call:GateCall)=>boolean, check:(call:GateCall)=>(GateVerdict|Promise<GateVerdict>)}} Gate
332
+ */
333
+
334
+ export function createGateRegistry() {
335
+ /** @type {Gate[]} */
336
+ const gates = [];
337
+ return {
338
+ /** @param {Gate} gate */
339
+ register(gate) {
340
+ gates.push(gate);
341
+ },
342
+ list: () => gates.map((g) => g.name),
343
+ /**
344
+ * Every applicable gate, in registration order; the first refusal wins.
345
+ * An allow lists the commits of the gates that returned one.
346
+ * @param {GateCall} call
347
+ * @returns {Promise<{allow:true, commits?:Array<{gate:string, commit:GateCommit}>}|{allow:false, gate:string, reason:string}>}
348
+ */
349
+ async evaluate(call) {
350
+ /** @type {Array<{gate:string, commit:GateCommit}>} */
351
+ const commits = [];
352
+ for (const g of gates) {
353
+ let applies;
354
+ try {
355
+ applies = g.applies(call);
356
+ } catch (e) {
357
+ return { allow: false, gate: g.name, reason: `the ${g.name} gate failed to evaluate (${e instanceof Error ? e.message : e}); failing closed` };
358
+ }
359
+ if (!applies) continue;
360
+ try {
361
+ const v = await g.check(call);
362
+ if (!v || v.allow !== true) return { allow: false, gate: g.name, reason: (v && "reason" in v && v.reason) || "refused" };
363
+ if (typeof v.commit === "function") commits.push({ gate: g.name, commit: v.commit });
364
+ } catch (e) {
365
+ return { allow: false, gate: g.name, reason: `the ${g.name} gate failed (${e instanceof Error ? e.message : e}); failing closed` };
366
+ }
367
+ }
368
+ return commits.length ? { allow: true, commits } : { allow: true };
369
+ },
370
+ };
371
+ }
372
+
373
+ /** The outbound tool names the send gate screens, on any MCP server. */
374
+ export const SEND_GATE_TOOLS = Object.freeze(["messaging_send", "messaging_send_voice_note", "email_send", "email_draft_send", "org_rpc"]);
375
+ /** Any server, any case: `mcp__<server>__<one of SEND_GATE_TOOLS>`. */
376
+ export const SEND_GATE_MATCHER = /^mcp__.+__(messaging_send|messaging_send_voice_note|email_send|email_draft_send|org_rpc)$/i;
377
+
378
+ /**
379
+ * Is this MCP server the Cohort org server, whatever key the config gave it?
380
+ * Informational only (the send gate no longer depends on it): a config key,
381
+ * an HTTP endpoint or a launcher like `maestro mcp` can all hide it.
382
+ * @param {{server:string, command?:string|null, args?:string[]}} mcp
383
+ */
384
+ export function isCohortOrgServer(mcp) {
385
+ const key = String(mcp.server).toLowerCase();
386
+ if (key === "cohort" || key === "neolith") return true;
387
+ const bin = mcp.command ? path.basename(mcp.command) : "";
388
+ if (bin === "cohort-mcp" || bin === "neolith-mcp") return true;
389
+ return (mcp.args ?? []).some((a) => /(^|\/)(cohort|neolith)-mcp(\.mjs)?$/.test(String(a)));
390
+ }
391
+
392
+ /** The send gate's registry name. */
393
+ export const SEND_GATE_NAME = "outbound send";
394
+
395
+ /** The seat's shell pre-send hook (lib/collective/global-config.mjs PRE_SEND_HOOK_MARKER). */
396
+ export const PRE_SEND_AUDIT_SCRIPT = "scripts/hooks/pre-send-audit.sh";
397
+
398
+ /** Shells whose `-c` script is looked into by {@link isPreSendAuditCommand}. */
399
+ const SHELL_WORDS = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
400
+ const MAX_SHELL_NESTING = 3;
401
+
402
+ /** Words that precede a command without being it. */
403
+ const COMMAND_PREFIXES = new Set(["exec", "command", "nohup", "time", "!", "{", "(", "if", "then", "else", "elif", "do", "while", "until"]);
404
+ /** Commands that run a script file named as their first operand. */
405
+ const SOURCE_WORDS = new Set([".", "source"]);
406
+
407
+ /**
408
+ * Split a command into simple commands, each a list of words. Quoted parts
409
+ * stay inside their word (and keep their quotes, so an unquoted word can be
410
+ * told from a quoted one); `;`, `&&`, `||`, `|`, `&`, `(`, `)` and newlines
411
+ * outside quotes separate commands. Not a shell parser — enough to find the
412
+ * command word of each simple command.
413
+ * @param {string} command
414
+ * @returns {string[][]}
415
+ */
416
+ function simpleCommands(command) {
417
+ const re = /(&&|\|\||[;|&()\n])|((?:"(?:[^"\\]|\\.)*"?|'[^']*'?|[^\s;|&()"'])+)/g;
418
+ /** @type {string[][]} */
419
+ const out = [[]];
420
+ let m;
421
+ while ((m = re.exec(String(command ?? ""))) !== null) {
422
+ if (m[1] !== undefined) {
423
+ if (out[out.length - 1].length > 0) out.push([]);
424
+ } else out[out.length - 1].push(m[2]);
425
+ }
426
+ return out.filter((c) => c.length > 0);
427
+ }
428
+
429
+ /** A word with its quotes removed (`"a b"/c` → `a b/c`). @param {string} w */
430
+ function unquote(w) {
431
+ return w.replace(/"((?:[^"\\]|\\.)*)"?|'([^']*)'?/g, (_, dq, sq) => (dq !== undefined ? dq.replace(/\\(["\\$`])/g, "$1") : sq));
432
+ }
433
+
434
+ /**
435
+ * Commands that can sit beside the script in one hook command without doing
436
+ * any screening or side work of their own (`cd /agent && …`, `test -f x; …`).
437
+ */
438
+ const NEUTRAL_COMMANDS = new Set(["cd", "true", ":", "test", "[", "[[", "export", "set", "umask", "fi", "done", "}"]);
439
+
440
+ /**
441
+ * Does this word, as the shell reads it, run a command of its own? True for a
442
+ * command substitution outside single quotes (`$(…)`, backticks, `<(…)`,
443
+ * `>(…)`) and for an assignment to BASH_ENV or ENV (a file the next shell
444
+ * sources). Conservative: a `$(` inside double quotes counts. Pure.
445
+ * @param {string} word
446
+ */
447
+ function runsSomethingItself(word) {
448
+ if (/^(?:BASH_ENV|ENV)=/.test(unquote(word))) return true;
449
+ let single = false;
450
+ for (let i = 0; i < word.length; i++) {
451
+ const ch = word[i];
452
+ if (ch === "\\" && !single) {
453
+ i++;
454
+ continue;
455
+ }
456
+ if (ch === "'") single = !single;
457
+ else if (!single && (ch === "`" || ((ch === "$" || ch === "<" || ch === ">") && word[i + 1] === "("))) return true;
458
+ }
459
+ return false;
460
+ }
461
+
462
+ /**
463
+ * What one simple command does, for {@link isPreSendAuditCommand}:
464
+ * `audit` runs the script, `neutral` is a {@link NEUTRAL_COMMANDS} word (or only
465
+ * prefixes), `other` runs anything else.
466
+ * @param {string[]} words @param {number} depth
467
+ * @returns {'audit'|'neutral'|'other'}
468
+ */
469
+ function classifySimpleCommand(words, depth) {
470
+ const isScript = (/** @type string */ p) => p === PRE_SEND_AUDIT_SCRIPT || p.endsWith(`/${PRE_SEND_AUDIT_SCRIPT}`);
471
+ const isOption = (/** @type string */ w) => /^-[A-Za-z-]*$/.test(w);
472
+ // CF-53: a word that runs something of its own — a command substitution (`"$(…)"`, backticks,
473
+ // `<(…)`) or a shell start-up file (BASH_ENV=…, ENV=…) — makes the whole command other work, even
474
+ // beside a neutral `cd`/`export` or as the script's own argument: skipping it would skip that work.
475
+ if (words.some((w) => runsSomethingItself(w))) return "other";
476
+ let k = 0;
477
+ // Assignments, prefixes and `env` (with its options) come before the command word.
478
+ while (k < words.length) {
479
+ const w = unquote(words[k]);
480
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words[k]) || COMMAND_PREFIXES.has(w)) k++;
481
+ else if ((w.split("/").pop() ?? "") === "env") {
482
+ k++;
483
+ while (k < words.length && isOption(words[k])) k += words[k] === "-u" ? 2 : 1;
484
+ } else break;
485
+ }
486
+ if (k >= words.length) return "neutral";
487
+ const head = unquote(words[k]);
488
+ if (isScript(head)) return "audit";
489
+ if (NEUTRAL_COMMANDS.has(head)) return "neutral";
490
+ if (SOURCE_WORDS.has(head)) return k + 1 < words.length && isScript(unquote(words[k + 1])) ? "audit" : "other";
491
+ if (!SHELL_WORDS.has(head.split("/").pop() ?? "")) return "other";
492
+ // A shell: options up to one carrying `c` (-c, -lc, -ec) hand it a script; otherwise the first operand is a script file.
493
+ let j = k + 1;
494
+ while (j < words.length && isOption(words[j]) && !/^-[A-Za-z]*c[A-Za-z]*$/.test(words[j])) j++;
495
+ if (j >= words.length) return "other";
496
+ if (/^-[A-Za-z]*c[A-Za-z]*$/.test(words[j])) {
497
+ return depth < MAX_SHELL_NESTING && j + 1 < words.length && isPreSendAuditCommand(unquote(words[j + 1]), depth + 1) ? "audit" : "other";
498
+ }
499
+ return isScript(unquote(words[j])) ? "audit" : "other";
500
+ }
501
+
502
+ /**
503
+ * Does a hook command run the seat's pre-send-audit.sh — and nothing else that
504
+ * skipping it would also skip? True when some simple command of it runs the
505
+ * script's path — relative (`./scripts/hooks/pre-send-audit.sh gmail`) or
506
+ * absolute, quoted or not:
507
+ * · as the command word itself, after assignments and wrappers
508
+ * (`A=1 …`, `env -i A=1 …`, `exec …`, `nohup …`);
509
+ * · as the script operand of a shell or `.`/`source`
510
+ * (`'bash' '/agent/scripts/hooks/pre-send-audit.sh' cohort`, the form
511
+ * `maestro setup` writes);
512
+ * · inside the script a shell is handed with `-c` (`bash -c "…"`,
513
+ * `sh -lc '…'`, `/usr/bin/env bash -c "…"`), by the same rules, to a
514
+ * nesting depth of 3 (CF-46, W3-G2);
515
+ * and every other simple command beside it is neutral (`cd`, `test`, `true`,
516
+ * `export`, …). A command that also runs something else
517
+ * (`bash -c "…/pre-send-audit.sh cohort && ./other.sh"`, W3-G1) is NOT
518
+ * recognised: skipping it would skip the other script too, so it runs and the
519
+ * send is counted twice. The path as an argument to anything else
520
+ * (`echo …/pre-send-audit.sh`, `bash -c "echo …/pre-send-audit.sh"`) is not a
521
+ * run of it. A form this misses still runs: the miss costs a double count,
522
+ * never a skipped screen.
523
+ *
524
+ * CF-53 (W5-B): a word that runs a command of its own makes its simple command
525
+ * `other`, wherever it sits — `cd "$(./x.sh)" && …/pre-send-audit.sh`,
526
+ * `` cd `./x.sh`; … ``, `…/pre-send-audit.sh "$(./x.sh)"`,
527
+ * `BASH_ENV=./x.sh bash -c "…/pre-send-audit.sh"` all RUN. The double count
528
+ * is the accepted cost of every such chained hook: the in-process gate counts
529
+ * the send once, and the shell script, which runs too, counts it again — a
530
+ * seat that chains the audit script with its own commands should split them
531
+ * into two hook objects so the audit one alone is skipped.
532
+ * @param {string} command
533
+ * @param {number} [depth]
534
+ */
535
+ export function isPreSendAuditCommand(command, depth = 0) {
536
+ let runs = false;
537
+ for (const words of simpleCommands(command)) {
538
+ const kind = classifySimpleCommand(words, depth);
539
+ if (kind === "other") return false;
540
+ if (kind === "audit") runs = true;
541
+ }
542
+ return runs;
543
+ }
544
+
545
+ /**
546
+ * CF-20: is this command hook superseded by the in-process send gate? The seat's
547
+ * global ~/.claude/settings.json mounts pre-send-audit.sh on the outbound
548
+ * `mcp__cohort__*` tools for the claude binary. The engine loads that file too,
549
+ * and its send gate already screens AND counts the same call, so running the
550
+ * shell hook as well would count one allowed send twice against the caps.
551
+ * Only a PreToolUse hook running pre-send-audit.sh on a tool the gate screens
552
+ * is superseded; the same script on any other tool (a gmail draft) still runs.
553
+ * @param {string} event @param {string} toolName @param {string} command
554
+ */
555
+ export function supersededBySendGate(event, toolName, command) {
556
+ return event === "PreToolUse" && SEND_GATE_MATCHER.test(String(toolName)) && isPreSendAuditCommand(command);
557
+ }
558
+
559
+ /** @param {GateCall} call */
560
+ export function sendGateApplies(call) {
561
+ if (SEND_GATE_MATCHER.test(call.toolName)) return true;
562
+ return Boolean(call.mcp && SEND_GATE_TOOLS.includes(String(call.mcp.tool).toLowerCase()));
563
+ }
564
+
565
+ /**
566
+ * The outbound send gate: the pre-send-audit verdict, in process. The screen
567
+ * and the cap check run at `check`; the counter is written by the verdict's
568
+ * `commit`, which the guard runs only for a call that is finally allowed.
569
+ * @param {{env:Record<string,string|undefined>, now?:()=>number, sessionId?:string, cwd?:string, prepareHook?:typeof preparePreSendHook}} deps
570
+ * @returns {Gate}
571
+ */
572
+ export function createSendGate({ env, now = Date.now, sessionId = "", cwd = "", prepareHook = preparePreSendHook }) {
573
+ const unblock = (/** @type string */ stderr) => String(stderr ?? "").trim().replace(/^BLOCKED:\s*/, "") || "blocked by the outbound send gate";
574
+ return {
575
+ name: SEND_GATE_NAME,
576
+ applies: sendGateApplies,
577
+ check(call) {
578
+ const payload = { session_id: sessionId, cwd, hook_event_name: "PreToolUse", tool_name: call.toolName, tool_input: call.toolInput };
579
+ const prepared = prepareHook({ channel: "cohort", stdin: JSON.stringify(payload), env, now: new Date(now()) });
580
+ if (!prepared.ok) return { allow: false, reason: unblock(prepared.result.stderr) };
581
+ return {
582
+ allow: true,
583
+ commit: () => {
584
+ const res = prepared.commit();
585
+ return res.code === 0 ? { ok: true } : { ok: false, reason: unblock(res.stderr) };
586
+ },
587
+ };
588
+ },
589
+ };
590
+ }
591
+
592
+ /** @param {Parameters<typeof createSendGate>[0]} deps */
593
+ export function createDefaultGates(deps) {
594
+ const registry = createGateRegistry();
595
+ registry.register(createSendGate(deps));
596
+ return registry;
597
+ }
598
+
599
+ /* ─────────────────────────────── the runner ──────────────────────────────── */
600
+
601
+ /**
602
+ * @param {object} p
603
+ * @param {Record<string, import('./context/settings.mjs').HookEntry[]>} p.hooks
604
+ * @param {string} p.sessionId
605
+ * @param {string} p.transcriptPath
606
+ * @param {string} p.cwd
607
+ * @param {string} [p.permissionMode]
608
+ * @param {Record<string,string|undefined>} p.env
609
+ * @param {typeof runCommandHook} [p.runCommand]
610
+ * @param {(line:string)=>void} [p.log] where non-blocking hook errors are reported
611
+ * @param {boolean} [p.sendGateInProcess] the run's gate registry holds the send
612
+ * gate, so a pre-send-audit.sh PreToolUse hook on a gated tool is skipped
613
+ * ({@link supersededBySendGate}). Off by default: a runner without the gate
614
+ * must keep the shell screen.
615
+ */
616
+ export function createHookRunner({ hooks, sessionId, transcriptPath, cwd, permissionMode = "default", env, runCommand = runCommandHook, log = () => {}, sendGateInProcess = false, envPassthrough = [] }) {
617
+ // CF-22: no credential reaches a hook command unless its own `inheritEnv` names it
618
+ // (a project-scoped hook may not name one), or `envPassthrough` names an extended-shape one.
619
+ const extra = { CLAUDE_PROJECT_DIR: cwd, COHORT_PROJECT_DIR: cwd };
620
+ /** @type {Map<string, Record<string,string>>} */
621
+ const envByInherit = new Map();
622
+ const hookEnvFor = (/** @type {string[]} */ inheritEnv) => {
623
+ const key = inheritEnv.join("\n");
624
+ if (!envByInherit.has(key)) envByInherit.set(key, { ...configuredCommandEnv(env, { inheritEnv, passthrough: envPassthrough }), ...extra });
625
+ return /** @type {Record<string,string>} */ (envByInherit.get(key));
626
+ };
627
+ /** Superseded commands and refused inheritEnv names already reported, so the log names each once. */
628
+ const reportedSkips = new Set();
629
+
630
+ /**
631
+ * @param {string} event @param {string} target @param {object} fields @param {AbortSignal} [signal]
632
+ * @returns {Promise<HookVerdict[]>}
633
+ */
634
+ async function fire(event, target, fields, signal) {
635
+ let selected = selectHooks(hooks, event, target);
636
+ if (sendGateInProcess) {
637
+ selected = selected.filter((h) => {
638
+ if (!supersededBySendGate(event, target, h.command)) return true;
639
+ if (!reportedSkips.has(h.command)) {
640
+ reportedSkips.add(h.command);
641
+ log(`${event} hook "${h.command}" (${h.source}) skipped for ${target}: the in-process send gate already screens and counts outbound sends`);
642
+ }
643
+ return false;
644
+ });
645
+ }
646
+ if (selected.length === 0) return [];
647
+ const input = { session_id: sessionId, transcript_path: transcriptPath, cwd, hook_event_name: event, permission_mode: permissionMode, ...fields };
648
+ const envs = selected.map((h) => {
649
+ const inherit = inheritEnvFor(h.inheritEnv, { projectScoped: PROJECT_SCOPES.has(String(h.scope ?? "")), env });
650
+ const note = inherit.error ?? (inherit.refused.length ? `inheritEnv ${inherit.refused.join(", ")} not passed: a ${h.scope} settings hook may not inherit a credential` : null);
651
+ if (note && !reportedSkips.has(`${h.command}\n${note}`)) {
652
+ reportedSkips.add(`${h.command}\n${note}`);
653
+ log(`${event} hook "${h.command}" (${h.source}): ${note}`);
654
+ }
655
+ return hookEnvFor(inherit.names);
656
+ });
657
+ const runs = await Promise.all(selected.map((h, i) => runCommand({ command: h.command, input, timeoutMs: h.timeoutMs, cwd, env: envs[i], signal })));
658
+ return runs.map((run, i) => {
659
+ const v = interpretHookRun(event, run);
660
+ if (v.outcome === "non_blocking_error") log(`${event} hook "${selected[i].command}" (${selected[i].source}): ${v.error}`);
661
+ else if (v.outcome === "blocking" && !BLOCKABLE.has(event)) log(`${event} hook "${selected[i].command}" reported: ${v.blockReason}`);
662
+ return v;
663
+ });
664
+ }
665
+
666
+ const contextOf = (/** @type HookVerdict[] */ vs) => {
667
+ const parts = vs.map((v) => v.additionalContext).filter(Boolean);
668
+ return parts.length ? capText(parts.join("\n\n")) : null;
669
+ };
670
+ const stopOf = (/** @type HookVerdict[] */ vs) => vs.find((v) => v.stopReason)?.stopReason ?? null;
671
+
672
+ return {
673
+ /** @param {{source:'startup'|'resume'|'clear'|'compact'}} p */
674
+ async sessionStart({ source }) {
675
+ const vs = await fire("SessionStart", source, { source });
676
+ return { additionalContext: contextOf(vs), stopReason: stopOf(vs) };
677
+ },
678
+ /** @param {{prompt:string}} p */
679
+ async userPromptSubmit({ prompt }) {
680
+ const vs = await fire("UserPromptSubmit", "", { prompt });
681
+ const blocked = vs.filter((v) => v.outcome === "blocking").map((v) => v.blockReason);
682
+ return { blockReason: blocked.length ? capText(blocked.join("\n")) : null, additionalContext: contextOf(vs), stopReason: stopOf(vs) };
683
+ },
684
+ /** @param {{toolName:string, toolInput:object, toolUseId:string, signal?:AbortSignal}} p */
685
+ async preToolUse({ toolName, toolInput, toolUseId, signal }) {
686
+ return combinePreToolUse(await fire("PreToolUse", toolName, { tool_name: toolName, tool_input: toolInput, tool_use_id: toolUseId }, signal));
687
+ },
688
+ /** @param {{toolName:string, toolInput:object, toolUseId:string, toolResponse:unknown, signal?:AbortSignal}} p */
689
+ async postToolUse({ toolName, toolInput, toolUseId, toolResponse, signal }) {
690
+ const vs = await fire("PostToolUse", toolName, { tool_name: toolName, tool_input: toolInput, tool_use_id: toolUseId, tool_response: toolResponse }, signal);
691
+ const feedback = vs.filter((v) => v.outcome === "blocking").map((v) => v.blockReason);
692
+ return { feedback: feedback.length ? capText(feedback.join("\n")) : null, additionalContext: contextOf(vs), stopReason: stopOf(vs) };
693
+ },
694
+ /** @param {{stopHookActive:boolean, event?:'Stop'|'SubagentStop', fields?:Record<string, unknown>}} p `fields`: SubagentStop's agent_id, agent_type */
695
+ async stop({ stopHookActive, event = "Stop", fields = {} }) {
696
+ const vs = await fire(event, "", { stop_hook_active: stopHookActive, ...fields });
697
+ const cont = vs.filter((v) => v.outcome === "blocking").map((v) => v.blockReason);
698
+ return { continueWith: cont.length ? capText(cont.join("\n")) : null, stopReason: stopOf(vs) };
699
+ },
700
+ /** @param {{trigger:'manual'|'auto', customInstructions?:string}} p */
701
+ async preCompact({ trigger, customInstructions = "" }) {
702
+ const vs = await fire("PreCompact", trigger, { trigger, custom_instructions: customInstructions });
703
+ const blocked = vs.filter((v) => v.outcome === "blocking").map((v) => v.blockReason);
704
+ return { blockReason: blocked.length ? capText(blocked.join("\n")) : null, stopReason: stopOf(vs) };
705
+ },
706
+ /** @param {{message:string, notificationType:string}} p */
707
+ async notification({ message, notificationType }) {
708
+ await fire("Notification", notificationType, { message, notification_type: notificationType });
709
+ },
710
+ };
711
+ }
712
+
713
+ const BLOCKABLE = new Set(["UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop", "SubagentStop", "PreCompact"]);