mixdog 0.8.0 → 0.9.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 (285) hide show
  1. package/README.md +47 -23
  2. package/package.json +33 -27
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +344 -0
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1223 -95
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -7
  62. package/src/lib/rules-builder.cjs +240 -96
  63. package/src/lib/text-utils.cjs +1 -1
  64. package/src/mixdog-session-runtime.mjs +2325 -450
  65. package/src/output-styles/default.md +12 -28
  66. package/src/output-styles/extreme-simple.md +9 -6
  67. package/src/output-styles/simple.md +22 -9
  68. package/src/repl.mjs +118 -59
  69. package/src/rules/agent/00-common.md +15 -0
  70. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  71. package/src/rules/agent/30-explorer.md +22 -0
  72. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  73. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  75. package/src/rules/lead/01-general.md +9 -5
  76. package/src/rules/lead/04-workflow.md +51 -12
  77. package/src/rules/lead/lead-tool.md +6 -0
  78. package/src/rules/shared/01-tool.md +12 -1
  79. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  80. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  81. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  82. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  83. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  85. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  86. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  87. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  88. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  89. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  91. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  92. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  93. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
  95. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
  96. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  97. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  99. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  100. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  101. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  102. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  103. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  106. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
  107. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
  109. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  110. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  111. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
  112. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  113. package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
  114. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
  115. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  116. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  117. package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
  118. package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
  119. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  120. package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
  121. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  122. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  123. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  124. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
  125. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
  126. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
  127. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
  128. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  129. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  130. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  131. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  132. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  133. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  138. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  139. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  140. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  141. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  142. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  143. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  144. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
  145. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  146. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  147. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  148. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  149. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  150. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  151. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  152. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  153. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  154. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  155. package/src/runtime/channels/index.mjs +152 -24
  156. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  157. package/src/runtime/channels/lib/session-discovery.mjs +3 -2
  158. package/src/runtime/channels/lib/tool-format.mjs +0 -2
  159. package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
  160. package/src/runtime/channels/lib/webhook.mjs +1 -1
  161. package/src/runtime/channels/tool-defs.mjs +29 -29
  162. package/src/runtime/memory/index.mjs +635 -107
  163. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  164. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  165. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  166. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  167. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  168. package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
  169. package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
  170. package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
  171. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
  172. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  173. package/src/runtime/memory/lib/memory.mjs +88 -9
  174. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  175. package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  177. package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
  178. package/src/runtime/memory/lib/session-ingest.mjs +194 -0
  179. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  180. package/src/runtime/memory/tool-defs.mjs +46 -37
  181. package/src/runtime/search/index.mjs +102 -466
  182. package/src/runtime/search/lib/web-tools.mjs +45 -25
  183. package/src/runtime/search/tool-defs.mjs +16 -23
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/atomic-file.mjs +4 -3
  186. package/src/runtime/shared/background-tasks.mjs +122 -11
  187. package/src/runtime/shared/child-spawn-gate.mjs +145 -0
  188. package/src/runtime/shared/config.mjs +7 -4
  189. package/src/runtime/shared/err-text.mjs +131 -4
  190. package/src/runtime/shared/llm/cost.mjs +2 -2
  191. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  192. package/src/runtime/shared/llm/index.mjs +34 -11
  193. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  194. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  195. package/src/runtime/shared/singleton-owner.mjs +104 -0
  196. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  197. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  198. package/src/runtime/shared/tool-surface.mjs +624 -98
  199. package/src/runtime/shared/user-data-guard.mjs +0 -2
  200. package/src/standalone/agent-task-status.mjs +203 -0
  201. package/src/standalone/agent-task-status.test.mjs +76 -0
  202. package/src/standalone/agent-tool.mjs +1913 -0
  203. package/src/standalone/channel-worker.mjs +370 -14
  204. package/src/standalone/explore-tool.mjs +165 -70
  205. package/src/standalone/folder-dialog.mjs +314 -0
  206. package/src/standalone/hook-bus.mjs +898 -22
  207. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  208. package/src/standalone/projects.mjs +226 -0
  209. package/src/standalone/provider-admin.mjs +41 -24
  210. package/src/standalone/seeds.mjs +2 -69
  211. package/src/standalone/usage-dashboard.mjs +96 -8
  212. package/src/tui/App.jsx +4798 -2153
  213. package/src/tui/components/AnsiText.jsx +39 -28
  214. package/src/tui/components/ContextPanel.jsx +87 -29
  215. package/src/tui/components/Markdown.jsx +43 -77
  216. package/src/tui/components/MarkdownTable.jsx +9 -184
  217. package/src/tui/components/Message.jsx +28 -11
  218. package/src/tui/components/Picker.jsx +95 -56
  219. package/src/tui/components/PromptInput.jsx +367 -239
  220. package/src/tui/components/QueuedCommands.jsx +1 -1
  221. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  222. package/src/tui/components/Spinner.jsx +67 -38
  223. package/src/tui/components/StatusLine.jsx +606 -38
  224. package/src/tui/components/TextEntryPanel.jsx +128 -9
  225. package/src/tui/components/ToolExecution.jsx +617 -368
  226. package/src/tui/components/TurnDone.jsx +3 -3
  227. package/src/tui/components/UsagePanel.jsx +3 -5
  228. package/src/tui/components/tool-output-format.mjs +365 -0
  229. package/src/tui/components/tool-output-format.test.mjs +220 -0
  230. package/src/tui/dist/index.mjs +8915 -2418
  231. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  232. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  233. package/src/tui/engine.mjs +1455 -279
  234. package/src/tui/figures.mjs +21 -40
  235. package/src/tui/index.jsx +75 -31
  236. package/src/tui/input-editing.mjs +25 -0
  237. package/src/tui/markdown/format-token.mjs +511 -68
  238. package/src/tui/markdown/format-token.test.mjs +216 -0
  239. package/src/tui/markdown/render-ansi.mjs +94 -0
  240. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  241. package/src/tui/markdown/stream-fence.mjs +34 -0
  242. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  243. package/src/tui/markdown/table-layout.mjs +250 -0
  244. package/src/tui/paste-attachments.mjs +0 -7
  245. package/src/tui/spinner-verbs.mjs +1 -2
  246. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  247. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  248. package/src/tui/theme.mjs +746 -24
  249. package/src/tui/time-format.mjs +1 -1
  250. package/src/tui/transcript-tool-failures.mjs +67 -0
  251. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  252. package/src/ui/ansi.mjs +1 -2
  253. package/src/ui/markdown.mjs +85 -26
  254. package/src/ui/markdown.test.mjs +70 -0
  255. package/src/ui/model-display.mjs +121 -0
  256. package/src/ui/session-stats.mjs +44 -0
  257. package/src/ui/statusline-context-label.test.mjs +15 -0
  258. package/src/ui/statusline.mjs +386 -178
  259. package/src/ui/tool-card.mjs +3 -16
  260. package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
  261. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  262. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  263. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  264. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  265. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  266. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  267. package/src/workflows/default/WORKFLOW.md +12 -5
  268. package/src/workflows/default/workflow.json +0 -1
  269. package/src/workflows/solo/WORKFLOW.md +15 -0
  270. package/src/workflows/solo/workflow.json +7 -0
  271. package/vendor/ink/build/output.js +6 -1
  272. package/src/agents/scheduler-task.md +0 -3
  273. package/src/agents/web-researcher/AGENT.md +0 -3
  274. package/src/agents/web-researcher/agent.json +0 -6
  275. package/src/agents/webhook-handler.md +0 -3
  276. package/src/rules/bridge/00-common.md +0 -5
  277. package/src/rules/bridge/30-explorer.md +0 -4
  278. package/src/rules/lead/00-tool-lead.md +0 -5
  279. package/src/rules/shared/00-language.md +0 -3
  280. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  281. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  282. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  283. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  284. package/src/standalone/bridge-tool.mjs +0 -1414
  285. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -1,5 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, join, resolve } from 'node:path';
3
+ import { spawn } from 'node:child_process';
4
+ import { listRegisteredPlugins } from './plugin-admin.mjs';
3
5
 
4
6
  const DEFAULT_EVENTS = Object.freeze([
5
7
  'runtime:start',
@@ -9,11 +11,83 @@ const DEFAULT_EVENTS = Object.freeze([
9
11
  'turn:error',
10
12
  'tool:planned',
11
13
  'tool:before',
14
+ 'tool:ask',
12
15
  'tool:deny',
13
16
  'tool:modify',
14
17
  'hook:error',
18
+ 'SessionStart',
19
+ 'UserPromptSubmit',
20
+ 'PreToolUse',
21
+ 'PostToolUse',
22
+ 'Stop',
15
23
  ]);
16
24
 
25
+ const DEFAULT_COMMAND_TIMEOUT_S = 600;
26
+ const USER_PROMPT_TIMEOUT_S = 30;
27
+ const MESSAGE_DISPLAY_TIMEOUT_S = 10;
28
+ const DEFAULT_PROMPT_TIMEOUT_S = 30;
29
+ const DEFAULT_AGENT_TIMEOUT_S = 60;
30
+ const MAX_OUTPUT_CHARS = 10_000;
31
+ const MAX_BUFFER_BYTES = 10 * 1024 * 1024;
32
+
33
+ const TOOL_IF_EVENTS = new Set([
34
+ 'PreToolUse',
35
+ 'PostToolUse',
36
+ 'PostToolUseFailure',
37
+ 'PermissionRequest',
38
+ 'PermissionDenied',
39
+ ]);
40
+
41
+ const NO_MATCHER_EVENTS = new Set([
42
+ 'UserPromptSubmit',
43
+ 'PostToolBatch',
44
+ 'Stop',
45
+ 'TeammateIdle',
46
+ 'TaskCreated',
47
+ 'TaskCompleted',
48
+ 'WorktreeCreate',
49
+ 'WorktreeRemove',
50
+ 'CwdChanged',
51
+ 'MessageDisplay',
52
+ ]);
53
+
54
+ const EXIT2_BLOCK_EVENTS = new Set([
55
+ 'PreToolUse',
56
+ 'PermissionRequest',
57
+ 'UserPromptSubmit',
58
+ 'UserPromptExpansion',
59
+ 'Stop',
60
+ 'SubagentStop',
61
+ 'TeammateIdle',
62
+ 'TaskCreated',
63
+ 'TaskCompleted',
64
+ 'ConfigChange',
65
+ 'PostToolBatch',
66
+ 'PreCompact',
67
+ 'Elicitation',
68
+ 'ElicitationResult',
69
+ ]);
70
+
71
+ const TOP_LEVEL_DECISION_EVENTS = new Set([
72
+ 'UserPromptSubmit',
73
+ 'UserPromptExpansion',
74
+ 'PostToolUse',
75
+ 'PostToolUseFailure',
76
+ 'PostToolBatch',
77
+ 'Stop',
78
+ 'SubagentStop',
79
+ 'ConfigChange',
80
+ 'PreCompact',
81
+ ]);
82
+
83
+ const PLAIN_STDOUT_CONTEXT_EVENTS = new Set([
84
+ 'SessionStart',
85
+ 'UserPromptSubmit',
86
+ 'UserPromptExpansion',
87
+ ]);
88
+
89
+ const SUPPORTED_HANDLER_TYPES = new Set(['command', 'http']);
90
+
17
91
  function compactValue(value) {
18
92
  if (value == null) return value;
19
93
  if (typeof value === 'string') return value.length > 180 ? `${value.slice(0, 180)}...` : value;
@@ -36,8 +110,8 @@ function compactValue(value) {
36
110
  function summarizePayload(payload = {}) {
37
111
  if (!payload || typeof payload !== 'object') return String(payload ?? '');
38
112
  const parts = [];
39
- if (payload.sessionId) parts.push(`session=${payload.sessionId}`);
40
- if (payload.name) parts.push(`name=${payload.name}`);
113
+ if (payload.sessionId || payload.session_id) parts.push(`session=${payload.sessionId || payload.session_id}`);
114
+ if (payload.name || payload.tool_name) parts.push(`name=${payload.name || payload.tool_name}`);
41
115
  if (payload.provider || payload.model) parts.push([payload.provider, payload.model].filter(Boolean).join('/'));
42
116
  if (payload.prompt) parts.push(`prompt=${String(payload.prompt).slice(0, 60).replace(/\s+/g, ' ')}`);
43
117
  if (payload.reason) parts.push(`reason=${String(payload.reason).slice(0, 120)}`);
@@ -59,6 +133,543 @@ function normalizeRules(raw) {
59
133
  return [];
60
134
  }
61
135
 
136
+ function uniqueHookEntries(entries) {
137
+ const seen = new Set();
138
+ const out = [];
139
+ for (const entry of entries) {
140
+ if (!entry?.path) continue;
141
+ const resolved = resolve(entry.path);
142
+ const key = process.platform === 'win32' ? resolved.toLowerCase() : resolved;
143
+ if (seen.has(key)) continue;
144
+ seen.add(key);
145
+ out.push({ ...entry, path: resolved });
146
+ }
147
+ return out;
148
+ }
149
+
150
+ function cleanHookId(value) {
151
+ return String(value ?? '').trim()
152
+ .toLowerCase()
153
+ .replace(/[^a-z0-9_.-]+/g, '-')
154
+ .replace(/^-+|-+$/g, '');
155
+ }
156
+
157
+ function pluginHookConfigEntries(dataDir) {
158
+ if (!dataDir) return [];
159
+ let plugins = [];
160
+ try {
161
+ plugins = listRegisteredPlugins({ dataDir });
162
+ } catch {
163
+ return [];
164
+ }
165
+ const entries = [];
166
+ for (const plugin of plugins || []) {
167
+ const root = String(plugin?.root || '').trim();
168
+ if (!root || !existsSync(root)) continue;
169
+ const id = cleanHookId(plugin.id || plugin.name || plugin.title || root.split(/[\\/]/).pop());
170
+ const pluginData = join(dataDir, 'plugins', 'data', id || 'plugin');
171
+ entries.push({
172
+ path: join(root, 'hooks', 'hooks.json'),
173
+ pluginRoot: root,
174
+ pluginData,
175
+ sourceType: 'plugin',
176
+ });
177
+ entries.push({
178
+ path: join(root, '.mixdog', 'hooks.json'),
179
+ pluginRoot: root,
180
+ pluginData,
181
+ sourceType: 'plugin',
182
+ });
183
+ }
184
+ return entries;
185
+ }
186
+
187
+ function hookConfigEntries(dataDir, cwd) {
188
+ if (process.env.MIXDOG_HOOKS_FILE) {
189
+ return uniqueHookEntries([{ path: process.env.MIXDOG_HOOKS_FILE, sourceType: 'env' }]);
190
+ }
191
+ const projectDir = cwd ? resolve(cwd) : process.cwd();
192
+ return uniqueHookEntries([
193
+ projectDir ? { path: join(projectDir, '.mixdog', 'hooks.json'), sourceType: 'project' } : null,
194
+ projectDir ? { path: join(projectDir, '.mixdog', 'hooks', 'hooks.json'), sourceType: 'project' } : null,
195
+ dataDir ? { path: join(dataDir, 'hooks.json'), sourceType: 'data' } : null,
196
+ dataDir ? { path: join(dataDir, 'hooks', 'hooks.json'), sourceType: 'data' } : null,
197
+ ...pluginHookConfigEntries(dataDir),
198
+ ]);
199
+ }
200
+
201
+ function isStandardConfig(parsed) {
202
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false;
203
+ const hooks = parsed.hooks;
204
+ if (!hooks || typeof hooks !== 'object' || Array.isArray(hooks)) return false;
205
+ const values = Object.values(hooks);
206
+ if (values.length === 0) return false;
207
+ return values.every((groups) => (
208
+ Array.isArray(groups)
209
+ && groups.every((g) => g && typeof g === 'object' && Array.isArray(g.hooks))
210
+ ));
211
+ }
212
+
213
+ const SIMPLE_MATCHER_RE = /^[A-Za-z0-9_ ,|]*$/;
214
+ function matcherFires(matcher, field) {
215
+ if (matcher == null) return true;
216
+ const text = String(matcher).trim();
217
+ if (text === '' || text === '*') return true;
218
+ const value = String(field ?? '');
219
+ if (SIMPLE_MATCHER_RE.test(text)) {
220
+ const items = text.split(/[|,]/).map((s) => s.trim()).filter(Boolean);
221
+ if (items.length === 0) return true;
222
+ return items.includes(value);
223
+ }
224
+ try {
225
+ return new RegExp(text).test(value);
226
+ } catch {
227
+ return true;
228
+ }
229
+ }
230
+
231
+ function globToRegExp(glob) {
232
+ let out = '^';
233
+ for (const ch of String(glob || '')) {
234
+ if (ch === '*') out += '.*';
235
+ else if (ch === '?') out += '.';
236
+ else out += ch.replace(/[.+^${}()|[\]\\]/g, '\\$&');
237
+ }
238
+ return new RegExp(`${out}$`);
239
+ }
240
+
241
+ function primaryArgFor(toolName, toolInput) {
242
+ const input = toolInput && typeof toolInput === 'object' ? toolInput : {};
243
+ if (toolName === 'Bash' || toolName === 'bash' || toolName === 'shell') return input.command ?? '';
244
+ if (input.file_path != null) return input.file_path;
245
+ if (input.path != null) return input.path;
246
+ if (input.command != null) return input.command;
247
+ if (input.file != null) return input.file;
248
+ return '';
249
+ }
250
+
251
+ function ifConditionPasses(ifExpr, eventName, toolName, toolInput) {
252
+ if (ifExpr == null || String(ifExpr).trim() === '') return true;
253
+ if (!TOOL_IF_EVENTS.has(eventName)) return false;
254
+ const m = /^\s*([A-Za-z0-9_*]+)\s*\(([^)]*)\)\s*$/.exec(String(ifExpr));
255
+ if (!m) return true;
256
+ const [, tool, pattern] = m;
257
+ if (tool !== '*' && tool !== toolName) return false;
258
+ try {
259
+ return globToRegExp(pattern.trim()).test(String(primaryArgFor(toolName, toolInput)));
260
+ } catch {
261
+ return true;
262
+ }
263
+ }
264
+
265
+ function resolvePlaceholders(str, projectDir, pluginData, pluginRoot = null) {
266
+ if (typeof str !== 'string') return str;
267
+ const resolvedProject = projectDir || process.cwd();
268
+ const resolvedPluginRoot = pluginRoot || resolvedProject;
269
+ const resolvedPluginData = pluginData || resolvedProject;
270
+ return str
271
+ .replace(/\$\{CLAUDE_PROJECT_DIR\}/g, resolvedProject)
272
+ .replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, resolvedPluginRoot)
273
+ .replace(/\$\{CLAUDE_PLUGIN_DATA\}/g, resolvedPluginData)
274
+ .replace(/\$\{MIXDOG_PROJECT_DIR\}/g, resolvedProject)
275
+ .replace(/\$\{MIXDOG_PLUGIN_ROOT\}/g, resolvedPluginRoot)
276
+ .replace(/\$\{MIXDOG_PLUGIN_DATA\}/g, resolvedPluginData);
277
+ }
278
+
279
+ function parseStandardConfig(parsed, source, meta = {}) {
280
+ const events = {};
281
+ const hooks = parsed.hooks || {};
282
+ for (const [eventName, groups] of Object.entries(hooks)) {
283
+ if (!Array.isArray(groups)) continue;
284
+ const cleanGroups = [];
285
+ for (const group of groups) {
286
+ if (!group || typeof group !== 'object') continue;
287
+ const handlers = Array.isArray(group.hooks)
288
+ ? group.hooks
289
+ .filter((h) => h && typeof h === 'object' && h.type)
290
+ .map((h) => ({
291
+ ...h,
292
+ _source: source,
293
+ ...(meta.pluginRoot ? { _pluginRoot: meta.pluginRoot } : {}),
294
+ ...(meta.pluginData ? { _pluginData: meta.pluginData } : {}),
295
+ }))
296
+ : [];
297
+ if (handlers.length === 0) continue;
298
+ cleanGroups.push({ matcher: group.matcher, hooks: handlers, _source: source });
299
+ }
300
+ if (cleanGroups.length > 0) events[eventName] = cleanGroups;
301
+ }
302
+ return events;
303
+ }
304
+
305
+ function mergeEvents(target, source) {
306
+ for (const [eventName, groups] of Object.entries(source || {})) {
307
+ if (!Array.isArray(target[eventName])) target[eventName] = [];
308
+ target[eventName].push(...groups);
309
+ }
310
+ }
311
+
312
+ function buildEventPayload(eventName, input = {}) {
313
+ const payload = {
314
+ session_id: input.sessionId ?? input.session_id ?? null,
315
+ cwd: input.cwd ?? null,
316
+ hook_event_name: eventName,
317
+ };
318
+ if (input.transcriptPath != null || input.transcript_path != null) {
319
+ payload.transcript_path = input.transcriptPath ?? input.transcript_path;
320
+ }
321
+ if (input.permissionMode != null || input.permission_mode != null) {
322
+ payload.permission_mode = input.permissionMode ?? input.permission_mode;
323
+ }
324
+ if (input.effort != null) payload.effort = input.effort;
325
+ const toolName = input.name ?? input.tool_name;
326
+ if (toolName != null) payload.tool_name = toolName;
327
+ const toolInput = input.args ?? input.tool_input;
328
+ if (toolInput != null) payload.tool_input = toolInput;
329
+ if (input.toolCallId != null || input.tool_use_id != null) {
330
+ payload.tool_use_id = input.toolCallId ?? input.tool_use_id;
331
+ }
332
+ if (input.toolResponse != null || input.tool_response != null || input.result != null) {
333
+ payload.tool_response = input.toolResponse ?? input.tool_response ?? input.result;
334
+ }
335
+ for (const key of [
336
+ 'prompt',
337
+ 'source',
338
+ 'model',
339
+ 'session_title',
340
+ 'message',
341
+ 'notification_type',
342
+ 'trigger',
343
+ 'load_reason',
344
+ 'file_path',
345
+ 'memory_type',
346
+ 'command_name',
347
+ 'command_args',
348
+ 'command_source',
349
+ 'expansion_type',
350
+ 'stop_reason',
351
+ 'error_type',
352
+ ]) {
353
+ if (input[key] != null) payload[key] = input[key];
354
+ }
355
+ return payload;
356
+ }
357
+
358
+ function matchFieldFor(eventName, payload) {
359
+ if (NO_MATCHER_EVENTS.has(eventName)) return null;
360
+ if (eventName === 'SessionStart') return payload.source ?? '';
361
+ if (eventName === 'Setup') return payload.trigger ?? '';
362
+ if (eventName === 'SessionEnd') return payload.reason ?? '';
363
+ if (eventName === 'Notification') return payload.notification_type ?? payload.type ?? '';
364
+ if (eventName === 'SubagentStart' || eventName === 'SubagentStop') return payload.agent_type ?? '';
365
+ if (eventName === 'PreCompact' || eventName === 'PostCompact') return payload.trigger ?? '';
366
+ if (eventName === 'ConfigChange') return payload.source ?? '';
367
+ if (eventName === 'InstructionsLoaded') return payload.load_reason ?? '';
368
+ if (eventName === 'UserPromptExpansion') return payload.command_name ?? '';
369
+ if (eventName === 'StopFailure') return payload.error_type ?? '';
370
+ return payload.tool_name ?? '';
371
+ }
372
+
373
+ function handlerTimeoutS(handler, eventName) {
374
+ if (Number.isFinite(handler.timeout) && handler.timeout > 0) return handler.timeout;
375
+ if (handler.type === 'prompt') return DEFAULT_PROMPT_TIMEOUT_S;
376
+ if (handler.type === 'agent') return DEFAULT_AGENT_TIMEOUT_S;
377
+ if (eventName === 'UserPromptSubmit') return USER_PROMPT_TIMEOUT_S;
378
+ if (eventName === 'MessageDisplay') return MESSAGE_DISPLAY_TIMEOUT_S;
379
+ return DEFAULT_COMMAND_TIMEOUT_S;
380
+ }
381
+
382
+ function defaultShellKind() {
383
+ return process.platform === 'win32' ? 'powershell' : 'bash';
384
+ }
385
+
386
+ function commandSpawnSpec(handler, projectDir, pluginData) {
387
+ const command = resolvePlaceholders(handler.command, projectDir, pluginData, handler._pluginRoot || null);
388
+ if (Array.isArray(handler.args)) {
389
+ return {
390
+ command,
391
+ args: handler.args.map((a) => resolvePlaceholders(String(a), projectDir, pluginData, handler._pluginRoot || null)),
392
+ shellKind: 'exec',
393
+ };
394
+ }
395
+ const shellKind = handler.shell === 'powershell' || handler.shell === 'bash'
396
+ ? handler.shell
397
+ : defaultShellKind();
398
+ if (shellKind === 'powershell') {
399
+ return {
400
+ command: process.platform === 'win32' ? 'powershell.exe' : 'pwsh',
401
+ args: ['-NoProfile', '-NonInteractive', '-Command', command],
402
+ shellKind,
403
+ };
404
+ }
405
+ return {
406
+ command: process.platform === 'win32' ? 'bash.exe' : 'bash',
407
+ args: ['-lc', command],
408
+ shellKind,
409
+ };
410
+ }
411
+
412
+ function hookEnv(projectDir, pluginData, payload, pluginRoot = null) {
413
+ const resolvedProject = projectDir || process.cwd();
414
+ const resolvedPluginRoot = pluginRoot || resolvedProject;
415
+ const resolvedPluginData = pluginData || resolvedProject;
416
+ const env = {
417
+ ...process.env,
418
+ MIXDOG_PROJECT_DIR: resolvedProject,
419
+ MIXDOG_PLUGIN_ROOT: resolvedPluginRoot,
420
+ MIXDOG_PLUGIN_DATA: resolvedPluginData,
421
+ CLAUDE_PROJECT_DIR: resolvedProject,
422
+ CLAUDE_PLUGIN_ROOT: resolvedPluginRoot,
423
+ CLAUDE_PLUGIN_DATA: resolvedPluginData,
424
+ };
425
+ const effortLevel = payload?.effort?.level || payload?.effort;
426
+ if (effortLevel) env.CLAUDE_EFFORT = String(effortLevel);
427
+ return env;
428
+ }
429
+
430
+ function runCommandHandler(handler, payload, eventName, pluginData, onSpawnError = null) {
431
+ const projectDir = payload.cwd || process.cwd();
432
+ const effectivePluginData = handler._pluginData || pluginData || null;
433
+ const stdin = JSON.stringify(payload);
434
+ const timeoutMs = Math.round(handlerTimeoutS(handler, eventName) * 1000);
435
+ const spec = commandSpawnSpec(handler, projectDir, effectivePluginData);
436
+ const baseOpts = {
437
+ cwd: existsSync(projectDir) ? projectDir : undefined,
438
+ env: hookEnv(projectDir, effectivePluginData, payload, handler._pluginRoot || null),
439
+ windowsHide: true,
440
+ stdio: ['pipe', 'pipe', 'pipe'],
441
+ };
442
+
443
+ if (handler.async === true || handler.asyncRewake === true) {
444
+ try {
445
+ const child = spawn(spec.command, spec.args, {
446
+ ...baseOpts,
447
+ detached: true,
448
+ stdio: ['pipe', 'ignore', 'ignore'],
449
+ });
450
+ child.on('error', (error) => {
451
+ if (typeof onSpawnError === 'function') onSpawnError(error);
452
+ });
453
+ child.stdin?.end(stdin);
454
+ child.unref?.();
455
+ return Promise.resolve({ exitCode: 0, stdout: '', stderr: '', async: true });
456
+ } catch (error) {
457
+ return Promise.resolve({
458
+ exitCode: -1,
459
+ stdout: '',
460
+ stderr: error?.message || String(error),
461
+ timedOut: false,
462
+ spawnError: error,
463
+ });
464
+ }
465
+ }
466
+
467
+ return new Promise((resolveRun) => {
468
+ let child;
469
+ let settled = false;
470
+ let stdout = '';
471
+ let stderr = '';
472
+ let stdoutBytes = 0;
473
+ let stderrBytes = 0;
474
+ let timedOut = false;
475
+ let timer = null;
476
+ const finish = (result) => {
477
+ if (settled) return;
478
+ settled = true;
479
+ if (timer) clearTimeout(timer);
480
+ resolveRun(result);
481
+ };
482
+ try {
483
+ child = spawn(spec.command, spec.args, baseOpts);
484
+ } catch (error) {
485
+ finish({
486
+ exitCode: -1,
487
+ stdout: '',
488
+ stderr: error?.message || String(error),
489
+ timedOut: false,
490
+ spawnError: error,
491
+ });
492
+ return;
493
+ }
494
+ timer = setTimeout(() => {
495
+ timedOut = true;
496
+ try { child.kill('SIGTERM'); } catch {}
497
+ finish({
498
+ exitCode: -1,
499
+ stdout,
500
+ stderr: stderr || `hook command timed out after ${timeoutMs}ms`,
501
+ timedOut,
502
+ spawnError: null,
503
+ });
504
+ }, timeoutMs);
505
+ timer.unref?.();
506
+ child.stdout?.on('data', (chunk) => {
507
+ stdoutBytes += chunk.length;
508
+ if (stdoutBytes <= MAX_BUFFER_BYTES) stdout += chunk.toString('utf8');
509
+ });
510
+ child.stderr?.on('data', (chunk) => {
511
+ stderrBytes += chunk.length;
512
+ if (stderrBytes <= MAX_BUFFER_BYTES) stderr += chunk.toString('utf8');
513
+ });
514
+ child.on('error', (error) => {
515
+ finish({
516
+ exitCode: -1,
517
+ stdout,
518
+ stderr: stderr || error?.message || String(error),
519
+ timedOut: false,
520
+ spawnError: error,
521
+ });
522
+ });
523
+ child.on('close', (code) => {
524
+ finish({
525
+ exitCode: timedOut ? -1 : (typeof code === 'number' ? code : 0),
526
+ stdout,
527
+ stderr,
528
+ timedOut,
529
+ spawnError: null,
530
+ });
531
+ });
532
+ try {
533
+ child.stdin?.end(stdin);
534
+ } catch {}
535
+ });
536
+ }
537
+
538
+ function resolveHeaderValue(value, allowed) {
539
+ if (typeof value !== 'string') return String(value ?? '');
540
+ return value.replace(/\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g, (_m, braced, bare) => {
541
+ const name = braced || bare;
542
+ return allowed.has(name) ? (process.env[name] || '') : '';
543
+ });
544
+ }
545
+
546
+ async function runHttpHandler(handler, payload, eventName) {
547
+ if (typeof fetch !== 'function') {
548
+ return { exitCode: -1, stdout: '', stderr: 'fetch is not available', timedOut: false, spawnError: new Error('fetch is not available') };
549
+ }
550
+ const timeoutMs = Math.round(handlerTimeoutS(handler, eventName) * 1000);
551
+ const controller = new AbortController();
552
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
553
+ timer.unref?.();
554
+ try {
555
+ const allowed = new Set(Array.isArray(handler.allowedEnvVars) ? handler.allowedEnvVars.map(String) : []);
556
+ const headers = { 'Content-Type': 'application/json' };
557
+ if (handler.headers && typeof handler.headers === 'object' && !Array.isArray(handler.headers)) {
558
+ for (const [key, value] of Object.entries(handler.headers)) {
559
+ headers[key] = resolveHeaderValue(value, allowed);
560
+ }
561
+ }
562
+ const response = await fetch(handler.url, {
563
+ method: 'POST',
564
+ headers,
565
+ body: JSON.stringify(payload),
566
+ signal: controller.signal,
567
+ });
568
+ const text = await response.text();
569
+ if (!response.ok) {
570
+ return { exitCode: 1, stdout: text, stderr: `HTTP ${response.status} ${response.statusText}`.trim(), timedOut: false, spawnError: null };
571
+ }
572
+ return { exitCode: 0, stdout: text, stderr: '', timedOut: false, spawnError: null };
573
+ } catch (error) {
574
+ const aborted = error?.name === 'AbortError';
575
+ return {
576
+ exitCode: -1,
577
+ stdout: '',
578
+ stderr: aborted ? `HTTP hook timed out: ${handler.url}` : (error?.message || String(error)),
579
+ timedOut: aborted,
580
+ spawnError: aborted ? null : error,
581
+ };
582
+ } finally {
583
+ clearTimeout(timer);
584
+ }
585
+ }
586
+
587
+ function limitText(text) {
588
+ const value = String(text || '');
589
+ if (value.length <= MAX_OUTPUT_CHARS) return value;
590
+ return `${value.slice(0, MAX_OUTPUT_CHARS)}\n... [hook output truncated; original ${value.length} chars]`;
591
+ }
592
+
593
+ function parseHandlerOutput(run, eventName) {
594
+ const out = {
595
+ block: false,
596
+ reason: null,
597
+ permissionDecision: null,
598
+ updatedInput: null,
599
+ updatedToolOutput: null,
600
+ additionalContext: null,
601
+ systemMessage: null,
602
+ suppressOutput: false,
603
+ continueFlag: undefined,
604
+ askReason: null,
605
+ };
606
+ if (run.timedOut || run.spawnError || run.async) return out;
607
+ if (run.exitCode === 2) {
608
+ if (EXIT2_BLOCK_EVENTS.has(eventName)) {
609
+ out.block = true;
610
+ out.reason = limitText((run.stderr || '').trim()) || `blocked by ${eventName} hook`;
611
+ }
612
+ return out;
613
+ }
614
+ if (run.exitCode !== 0) return out;
615
+ const rawText = (run.stdout || '').trim();
616
+ if (!rawText) return out;
617
+ if (!(rawText.startsWith('{') || rawText.startsWith('['))) {
618
+ if (PLAIN_STDOUT_CONTEXT_EVENTS.has(eventName)) out.additionalContext = limitText(rawText);
619
+ return out;
620
+ }
621
+ try {
622
+ const json = JSON.parse(rawText);
623
+ if (!json || typeof json !== 'object' || Array.isArray(json)) return out;
624
+ if (json.continue === false) {
625
+ out.continueFlag = false;
626
+ out.block = true;
627
+ out.reason = limitText(json.stopReason || json.reason || `stopped by ${eventName} hook`);
628
+ }
629
+ if (json.decision === 'block' && TOP_LEVEL_DECISION_EVENTS.has(eventName)) {
630
+ out.block = true;
631
+ out.reason = limitText(json.reason || out.reason || `blocked by ${eventName} hook`);
632
+ }
633
+ if (json.suppressOutput) out.suppressOutput = true;
634
+ if (typeof json.systemMessage === 'string') out.systemMessage = limitText(json.systemMessage);
635
+ if (typeof json.additionalContext === 'string') out.additionalContext = limitText(json.additionalContext);
636
+
637
+ const hso = json.hookSpecificOutput;
638
+ const hsoMatches = hso && typeof hso === 'object'
639
+ && (!hso.hookEventName || hso.hookEventName === eventName);
640
+ if (hsoMatches) {
641
+ if (typeof hso.additionalContext === 'string') out.additionalContext = limitText(hso.additionalContext);
642
+ if (hso.updatedInput && typeof hso.updatedInput === 'object' && !Array.isArray(hso.updatedInput)) {
643
+ out.updatedInput = hso.updatedInput;
644
+ }
645
+ if (hso.updatedToolOutput != null) out.updatedToolOutput = hso.updatedToolOutput;
646
+ if (hso.permissionDecision) out.permissionDecision = String(hso.permissionDecision).toLowerCase();
647
+ if (hso.permissionDecisionReason) out.reason = out.reason || limitText(hso.permissionDecisionReason);
648
+ if (eventName === 'PermissionRequest' && hso.decision && typeof hso.decision === 'object') {
649
+ const behavior = String(hso.decision.behavior || '').toLowerCase();
650
+ if (behavior === 'deny') {
651
+ out.block = true;
652
+ out.reason = out.reason || limitText(hso.decision.reason || `denied by ${eventName} hook`);
653
+ }
654
+ if (hso.decision.updatedInput && typeof hso.decision.updatedInput === 'object' && !Array.isArray(hso.decision.updatedInput)) {
655
+ out.updatedInput = hso.decision.updatedInput;
656
+ }
657
+ }
658
+ }
659
+ if (eventName === 'PreToolUse') {
660
+ if (out.permissionDecision === 'deny') {
661
+ out.block = true;
662
+ out.reason = out.reason || `denied by ${eventName} hook`;
663
+ } else if (out.permissionDecision === 'ask') {
664
+ out.askReason = out.reason || `ask requested by ${eventName} hook`;
665
+ }
666
+ }
667
+ } catch {
668
+ if (PLAIN_STDOUT_CONTEXT_EVENTS.has(eventName)) out.additionalContext = limitText(rawText);
669
+ }
670
+ return out;
671
+ }
672
+
62
673
  function matchesList(pattern, value) {
63
674
  if (pattern == null || pattern === '*' || pattern === 'all') return true;
64
675
  const items = Array.isArray(pattern) ? pattern : [pattern];
@@ -95,6 +706,9 @@ function decisionFromRule(rule, input) {
95
706
  : { ...(input.args || {}), ...(rule.patch || {}) };
96
707
  return { action: 'modify', args: nextArgs, reason: rule.reason || rule.message || `modified by hook rule for ${input.name}` };
97
708
  }
709
+ if (action === 'ask') {
710
+ return { action: 'ask', reason: rule.reason || rule.message || `approval requested by hook rule for ${input.name}` };
711
+ }
98
712
  return { action: 'allow', reason: rule.reason || rule.message || null };
99
713
  }
100
714
 
@@ -111,15 +725,44 @@ function summarizeRule(rule, index) {
111
725
  };
112
726
  }
113
727
 
728
+ function handlerDedupeKey(handler) {
729
+ if (!handler || typeof handler !== 'object') return '';
730
+ if (handler.type === 'command') {
731
+ return `command:${handler._pluginRoot || ''}:${handler.command || ''}:${JSON.stringify(handler.args || null)}`;
732
+ }
733
+ if (handler.type === 'http') return `http:${handler.url || ''}`;
734
+ return `${handler.type || 'unknown'}:${JSON.stringify(handler)}`;
735
+ }
736
+
737
+ function shellCountFor(handler) {
738
+ if (handler?.type === 'command') {
739
+ if (handler.args) return 'exec';
740
+ return handler.shell === 'powershell' || handler.shell === 'bash' ? handler.shell : defaultShellKind();
741
+ }
742
+ return handler?.type || 'unknown';
743
+ }
744
+
114
745
  export function createStandaloneHookBus({ maxEvents = 80, dataDir = null } = {}) {
115
746
  const recent = [];
116
747
  const counts = new Map(DEFAULT_EVENTS.map((name) => [name, 0]));
117
748
  const rulesPath = hookRulesPath(dataDir);
749
+ const pluginData = dataDir || null;
118
750
  let rulesCache = { mtimeMs: -1, rules: [] };
751
+ let configCache = {
752
+ key: '',
753
+ standard: false,
754
+ disabled: false,
755
+ events: {},
756
+ legacyRules: [],
757
+ sources: [],
758
+ errors: [],
759
+ };
760
+ let lastCwd = process.cwd();
119
761
 
120
762
  function emit(name, payload = {}) {
121
763
  const eventName = String(name || '').trim();
122
764
  if (!eventName) return null;
765
+ if (payload?.cwd) lastCwd = payload.cwd;
123
766
  counts.set(eventName, (counts.get(eventName) || 0) + 1);
124
767
  const entry = {
125
768
  ts: new Date().toISOString(),
@@ -147,6 +790,161 @@ export function createStandaloneHookBus({ maxEvents = 80, dataDir = null } = {})
147
790
  return rulesCache.rules;
148
791
  }
149
792
 
793
+ function loadConfig(cwd = lastCwd) {
794
+ const entries = hookConfigEntries(dataDir, cwd);
795
+ const parts = [];
796
+ for (const { path: p } of entries) {
797
+ try {
798
+ const st = existsSync(p) ? statSync(p) : null;
799
+ parts.push(`${p}:${st ? st.mtimeMs : 'absent'}`);
800
+ } catch {
801
+ parts.push(`${p}:error`);
802
+ }
803
+ }
804
+ const key = parts.join('|');
805
+ if (configCache.key === key) return configCache;
806
+
807
+ const events = {};
808
+ const legacyRules = [];
809
+ const sources = [];
810
+ const errors = [];
811
+ let disabled = false;
812
+ let disableSeen = false;
813
+ for (const entry of entries) {
814
+ const filePath = entry.path;
815
+ if (!existsSync(filePath)) continue;
816
+ let parsed = null;
817
+ try {
818
+ parsed = JSON.parse(readFileSync(filePath, 'utf8'));
819
+ } catch (error) {
820
+ errors.push({ file: filePath, error: error?.message || String(error) });
821
+ continue;
822
+ }
823
+ sources.push(filePath);
824
+ if (Object.prototype.hasOwnProperty.call(parsed || {}, 'disableAllHooks')) {
825
+ disabled = parsed.disableAllHooks === true;
826
+ disableSeen = true;
827
+ }
828
+ if (isStandardConfig(parsed)) {
829
+ mergeEvents(events, parseStandardConfig(parsed, filePath, entry));
830
+ } else {
831
+ legacyRules.push(...normalizeRules(parsed).filter((rule) => rule && typeof rule === 'object'));
832
+ }
833
+ }
834
+ configCache = {
835
+ key,
836
+ standard: Object.keys(events).length > 0,
837
+ disabled: disableSeen ? disabled : false,
838
+ events,
839
+ legacyRules,
840
+ sources,
841
+ errors,
842
+ };
843
+ for (const err of errors) {
844
+ emit('hook:error', { error: `failed to parse hooks file ${err.file}: ${err.error}` });
845
+ }
846
+ return configCache;
847
+ }
848
+
849
+ function selectHandlers(eventName, payload) {
850
+ const cfg = loadConfig(payload.cwd || lastCwd);
851
+ if (cfg.disabled || !cfg.standard) return [];
852
+ const groups = cfg.events[eventName];
853
+ if (!Array.isArray(groups)) return [];
854
+ const field = matchFieldFor(eventName, payload);
855
+ const handlers = [];
856
+ const seen = new Set();
857
+ for (const group of groups) {
858
+ if (NO_MATCHER_EVENTS.has(eventName) || matcherFires(group.matcher, field)) {
859
+ for (const handler of group.hooks) {
860
+ const key = handlerDedupeKey(handler);
861
+ if (seen.has(key)) continue;
862
+ seen.add(key);
863
+ handlers.push(handler);
864
+ }
865
+ }
866
+ }
867
+ return handlers;
868
+ }
869
+
870
+ async function runOneHandler(handler, eventName, payload) {
871
+ if (!handler || typeof handler !== 'object') return null;
872
+ if (!ifConditionPasses(handler.if, eventName, payload.tool_name, payload.tool_input)) return null;
873
+ const type = String(handler.type || '').trim();
874
+ if (!SUPPORTED_HANDLER_TYPES.has(type)) {
875
+ emit('hook:error', { name: payload.tool_name || eventName, error: `unsupported hook type: ${type || '(missing)'}` });
876
+ return null;
877
+ }
878
+ if (type === 'command') {
879
+ if (!handler.command) return null;
880
+ const reportSpawnError = (error) => {
881
+ emit('hook:error', {
882
+ name: payload.tool_name || eventName,
883
+ error: `hook spawn failed: ${error?.message || error}`,
884
+ });
885
+ };
886
+ return await runCommandHandler(handler, payload, eventName, pluginData, reportSpawnError);
887
+ }
888
+ if (type === 'http') {
889
+ if (!handler.url) return null;
890
+ return await runHttpHandler(handler, payload, eventName);
891
+ }
892
+ return null;
893
+ }
894
+
895
+ async function runEventHandlers(eventName, payload) {
896
+ const handlers = selectHandlers(eventName, payload);
897
+ const agg = {
898
+ blocked: false,
899
+ reason: null,
900
+ updatedInput: null,
901
+ updatedToolOutput: null,
902
+ additionalContext: [],
903
+ systemMessage: null,
904
+ ask: false,
905
+ askReason: null,
906
+ handlersRun: handlers.length,
907
+ };
908
+ const results = await Promise.all(handlers.map(async (handler) => {
909
+ try {
910
+ return { handler, run: await runOneHandler(handler, eventName, payload) };
911
+ } catch (error) {
912
+ emit('hook:error', { name: payload.tool_name || eventName, error: error?.message || String(error) });
913
+ return { handler, run: null };
914
+ }
915
+ }));
916
+
917
+ for (const { handler, run } of results) {
918
+ if (!run) continue;
919
+ if (run.timedOut) {
920
+ emit('hook:error', { name: payload.tool_name || eventName, error: `hook ${shellCountFor(handler)} timed out: ${handler.command || handler.url || handler.type}` });
921
+ continue;
922
+ }
923
+ if (run.spawnError) {
924
+ emit('hook:error', { name: payload.tool_name || eventName, error: `hook spawn failed: ${run.spawnError.message || run.spawnError}` });
925
+ continue;
926
+ }
927
+ if (run.exitCode && run.exitCode !== 0 && run.exitCode !== 2) {
928
+ emit('hook:error', { name: payload.tool_name || eventName, error: (run.stderr || '').trim() || `hook exited ${run.exitCode}` });
929
+ continue;
930
+ }
931
+ const parsed = parseHandlerOutput(run, eventName);
932
+ if (parsed.additionalContext) agg.additionalContext.push(parsed.additionalContext);
933
+ if (parsed.systemMessage && !agg.systemMessage) agg.systemMessage = parsed.systemMessage;
934
+ if (parsed.updatedInput && !agg.updatedInput) agg.updatedInput = parsed.updatedInput;
935
+ if (parsed.updatedToolOutput != null && agg.updatedToolOutput == null) agg.updatedToolOutput = parsed.updatedToolOutput;
936
+ if (parsed.askReason && !agg.ask && !agg.blocked) {
937
+ agg.ask = true;
938
+ agg.askReason = parsed.askReason;
939
+ }
940
+ if (parsed.block && !agg.blocked) {
941
+ agg.blocked = true;
942
+ agg.reason = parsed.reason;
943
+ }
944
+ }
945
+ return agg;
946
+ }
947
+
150
948
  function saveRules(rules) {
151
949
  if (!rulesPath) throw new Error('hooks rules path is not configured');
152
950
  mkdirSync(dirname(rulesPath), { recursive: true });
@@ -154,6 +952,7 @@ export function createStandaloneHookBus({ maxEvents = 80, dataDir = null } = {})
154
952
  writeFileSync(rulesPath, `${JSON.stringify({ toolBefore: cleanRules }, null, 2)}\n`, 'utf8');
155
953
  const stat = statSync(rulesPath);
156
954
  rulesCache = { mtimeMs: stat.mtimeMs, rules: cleanRules };
955
+ configCache.key = '';
157
956
  return listRules();
158
957
  }
159
958
 
@@ -163,8 +962,8 @@ export function createStandaloneHookBus({ maxEvents = 80, dataDir = null } = {})
163
962
 
164
963
  function addRule(rule = {}) {
165
964
  const action = String(rule.action || rule.decision || '').trim().toLowerCase();
166
- if (!action || !['allow', 'deny', 'block', 'modify', 'rewrite'].includes(action)) {
167
- throw new Error('hook rule action must be allow, deny, block, modify, or rewrite');
965
+ if (!action || !['allow', 'deny', 'block', 'modify', 'rewrite', 'ask'].includes(action)) {
966
+ throw new Error('hook rule action must be allow, deny, block, modify, rewrite, or ask');
168
967
  }
169
968
  const next = {
170
969
  tool: rule.tool || rule.name || '*',
@@ -195,31 +994,99 @@ export function createStandaloneHookBus({ maxEvents = 80, dataDir = null } = {})
195
994
  }
196
995
 
197
996
  async function beforeTool(input = {}) {
997
+ if (input?.cwd) lastCwd = input.cwd;
198
998
  emit('tool:before', {
199
- sessionId: input.sessionId || null,
200
- name: input.name || 'tool',
201
- callId: input.toolCallId || input.callId || null,
202
- args: input.args || null,
999
+ sessionId: input.sessionId || input.session_id || null,
1000
+ name: input.name || input.tool_name || 'tool',
1001
+ callId: input.toolCallId || input.callId || input.tool_use_id || null,
1002
+ args: input.args || input.tool_input || null,
203
1003
  });
204
1004
  try {
205
- const rules = loadRules();
206
- const rule = rules.find((candidate) => ruleMatches(candidate, input));
1005
+ const cfg = loadConfig(input.cwd || lastCwd);
1006
+ if (cfg.disabled) return null;
1007
+ if (!cfg.disabled) {
1008
+ const payload = buildEventPayload('PreToolUse', input);
1009
+ const agg = await runEventHandlers('PreToolUse', payload);
1010
+ if (agg.blocked) {
1011
+ emit('tool:deny', { sessionId: input.sessionId || input.session_id || null, name: input.name || input.tool_name || 'tool', reason: agg.reason });
1012
+ return { action: 'deny', reason: agg.reason };
1013
+ }
1014
+ if (agg.updatedInput) {
1015
+ emit('tool:modify', { sessionId: input.sessionId || input.session_id || null, name: input.name || input.tool_name || 'tool', reason: agg.reason });
1016
+ return { action: 'modify', args: agg.updatedInput, reason: agg.reason };
1017
+ }
1018
+ if (agg.ask) {
1019
+ emit('tool:ask', { sessionId: input.sessionId || input.session_id || null, name: input.name || input.tool_name || 'tool', reason: agg.askReason });
1020
+ return { action: 'ask', reason: agg.askReason };
1021
+ }
1022
+ }
1023
+
1024
+ const rules = Array.isArray(cfg.legacyRules) && cfg.legacyRules.length
1025
+ ? cfg.legacyRules
1026
+ : loadRules();
1027
+ const rule = rules.find((candidate) => ruleMatches(candidate, {
1028
+ name: input.name || input.tool_name,
1029
+ args: input.args || input.tool_input,
1030
+ cwd: input.cwd,
1031
+ }));
207
1032
  if (!rule) return null;
208
- const decision = decisionFromRule(rule, input);
1033
+ const decision = decisionFromRule(rule, {
1034
+ name: input.name || input.tool_name,
1035
+ args: input.args || input.tool_input,
1036
+ cwd: input.cwd,
1037
+ });
209
1038
  if (decision.action === 'deny') {
210
- emit('tool:deny', { sessionId: input.sessionId || null, name: input.name || 'tool', reason: decision.reason });
1039
+ emit('tool:deny', { sessionId: input.sessionId || input.session_id || null, name: input.name || input.tool_name || 'tool', reason: decision.reason });
211
1040
  } else if (decision.action === 'modify') {
212
- emit('tool:modify', { sessionId: input.sessionId || null, name: input.name || 'tool', reason: decision.reason });
1041
+ emit('tool:modify', { sessionId: input.sessionId || input.session_id || null, name: input.name || input.tool_name || 'tool', reason: decision.reason });
1042
+ } else if (decision.action === 'ask') {
1043
+ emit('tool:ask', { sessionId: input.sessionId || input.session_id || null, name: input.name || input.tool_name || 'tool', reason: decision.reason });
213
1044
  }
214
1045
  return decision;
215
1046
  } catch (error) {
216
- emit('hook:error', { name: input.name || 'tool', error: error?.message || String(error) });
1047
+ emit('hook:error', { name: input.name || input.tool_name || 'tool', error: error?.message || String(error) });
217
1048
  return null;
218
1049
  }
219
1050
  }
220
1051
 
1052
+ async function dispatch(eventName, payload = {}) {
1053
+ const name = String(eventName || '').trim();
1054
+ if (!name) return {};
1055
+ if (payload?.cwd) lastCwd = payload.cwd;
1056
+ emit(name, payload);
1057
+ try {
1058
+ const std = buildEventPayload(name, payload);
1059
+ const agg = await runEventHandlers(name, std);
1060
+ const out = {
1061
+ blocked: agg.blocked || undefined,
1062
+ reason: agg.reason || undefined,
1063
+ additionalContext: agg.additionalContext.length ? agg.additionalContext : undefined,
1064
+ systemMessage: agg.systemMessage || undefined,
1065
+ updatedInput: agg.updatedInput || undefined,
1066
+ handlersRun: agg.handlersRun || undefined,
1067
+ };
1068
+ if (agg.updatedToolOutput != null) out.updatedToolOutput = agg.updatedToolOutput;
1069
+ return out;
1070
+ } catch (error) {
1071
+ emit('hook:error', { name, error: error?.message || String(error) });
1072
+ return {};
1073
+ }
1074
+ }
1075
+
221
1076
  function status() {
222
- let ruleCount = rulesCache.rules.length;
1077
+ let cfg = {
1078
+ standard: false,
1079
+ disabled: false,
1080
+ events: {},
1081
+ sources: [],
1082
+ errors: [],
1083
+ };
1084
+ try {
1085
+ cfg = loadConfig(lastCwd);
1086
+ } catch (error) {
1087
+ emit('hook:error', { error: error?.message || String(error) });
1088
+ }
1089
+ let ruleCount = 0;
223
1090
  let rules = [];
224
1091
  try {
225
1092
  rules = listRules();
@@ -227,20 +1094,29 @@ export function createStandaloneHookBus({ maxEvents = 80, dataDir = null } = {})
227
1094
  } catch (error) {
228
1095
  emit('hook:error', { error: error?.message || String(error) });
229
1096
  }
1097
+ const configuredEvents = Object.keys(cfg.events || {});
230
1098
  return {
231
- enabled: true,
232
- mode: 'standalone-executable',
1099
+ enabled: cfg.disabled !== true,
1100
+ mode: 'standalone-standard',
1101
+ configMode: cfg.standard ? 'standard' : 'legacy',
233
1102
  rulesPath,
1103
+ configSources: cfg.sources || [],
234
1104
  ruleCount,
235
1105
  rules,
236
- events: [...new Set([...DEFAULT_EVENTS, ...counts.keys()])],
1106
+ configuredEvents,
1107
+ events: [...new Set([...DEFAULT_EVENTS, ...counts.keys(), ...configuredEvents])],
237
1108
  counts: Object.fromEntries([...counts.entries()].sort((a, b) => a[0].localeCompare(b[0]))),
238
1109
  recent: [...recent].reverse(),
239
- note: ruleCount > 0
240
- ? 'Before-tool hook rules are active. Rules may allow, deny, or modify tool arguments.'
241
- : 'No hook rules configured; lifecycle and tool events are recorded in observer mode.',
1110
+ errors: cfg.errors || [],
1111
+ note: cfg.disabled
1112
+ ? 'Hooks are disabled by disableAllHooks.'
1113
+ : (cfg.standard
1114
+ ? `Standard Mixdog hooks active for events: ${configuredEvents.join(', ') || '(none)'}.`
1115
+ : (ruleCount > 0
1116
+ ? 'Legacy before-tool hook rules are active. Rules may allow, deny, or modify tool arguments.'
1117
+ : 'No hook rules configured; lifecycle and tool events are recorded in observer mode.')),
242
1118
  };
243
1119
  }
244
1120
 
245
- return { addRule, beforeTool, deleteRule, emit, listRules, setRuleEnabled, status };
1121
+ return { addRule, beforeTool, deleteRule, dispatch, emit, listRules, setRuleEnabled, status };
246
1122
  }