mixdog 0.8.1 → 0.9.1

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 (299) hide show
  1. package/README.md +47 -23
  2. package/package.json +34 -28
  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 +205 -33
  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 +1222 -90
  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 -29
  62. package/src/lib/plugin-paths.cjs +1 -7
  63. package/src/lib/rules-builder.cjs +242 -98
  64. package/src/lib/text-utils.cjs +1 -1
  65. package/src/mixdog-session-runtime.mjs +2211 -446
  66. package/src/output-styles/default.md +12 -28
  67. package/src/output-styles/extreme-simple.md +9 -6
  68. package/src/output-styles/simple.md +22 -9
  69. package/src/repl.mjs +108 -59
  70. package/src/rules/agent/00-common.md +15 -0
  71. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  72. package/src/rules/agent/30-explorer.md +22 -0
  73. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  75. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  76. package/src/rules/lead/01-general.md +9 -5
  77. package/src/rules/lead/04-workflow.md +51 -12
  78. package/src/rules/lead/lead-tool.md +6 -0
  79. package/src/rules/shared/01-tool.md +12 -1
  80. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  81. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  82. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  85. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  86. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  87. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  88. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  89. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  90. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  92. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  93. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  94. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  95. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +380 -268
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -93
  97. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  98. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  100. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  101. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  102. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  103. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  104. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  107. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +203 -71
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
  109. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
  110. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  111. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  112. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +289 -11
  113. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  114. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  115. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  116. package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
  117. package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
  118. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  119. package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
  120. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  121. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  122. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  123. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +35 -8
  124. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
  125. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
  126. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  127. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  130. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  133. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  138. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  139. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  140. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  141. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  142. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  143. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  146. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  147. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  148. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  149. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  150. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  151. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  152. package/src/runtime/channels/index.mjs +149 -51
  153. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  154. package/src/runtime/channels/lib/config.mjs +0 -1
  155. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  156. package/src/runtime/channels/lib/executor.mjs +0 -3
  157. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  158. package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
  159. package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
  160. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  161. package/src/runtime/channels/lib/session-discovery.mjs +3 -6
  162. package/src/runtime/channels/lib/tool-format.mjs +0 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
  164. package/src/runtime/channels/lib/webhook.mjs +1 -1
  165. package/src/runtime/channels/tool-defs.mjs +29 -29
  166. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  167. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  168. package/src/runtime/memory/index.mjs +519 -118
  169. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  170. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  171. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  172. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  173. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  174. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  175. package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
  176. package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
  177. package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
  178. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
  179. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  180. package/src/runtime/memory/lib/memory.mjs +88 -9
  181. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  182. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  183. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  184. package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
  185. package/src/runtime/memory/lib/session-ingest.mjs +196 -0
  186. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  187. package/src/runtime/memory/tool-defs.mjs +46 -37
  188. package/src/runtime/search/index.mjs +104 -473
  189. package/src/runtime/search/lib/config.mjs +0 -4
  190. package/src/runtime/search/lib/state.mjs +1 -15
  191. package/src/runtime/search/lib/web-tools.mjs +44 -25
  192. package/src/runtime/search/tool-defs.mjs +16 -23
  193. package/src/runtime/shared/abort-controller.mjs +1 -1
  194. package/src/runtime/shared/atomic-file.mjs +4 -3
  195. package/src/runtime/shared/background-tasks.mjs +122 -11
  196. package/src/runtime/shared/child-spawn-gate.mjs +139 -0
  197. package/src/runtime/shared/config.mjs +7 -4
  198. package/src/runtime/shared/err-text.mjs +131 -4
  199. package/src/runtime/shared/llm/cost.mjs +2 -2
  200. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  201. package/src/runtime/shared/llm/index.mjs +34 -11
  202. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  203. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  204. package/src/runtime/shared/singleton-owner.mjs +104 -0
  205. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  206. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  207. package/src/runtime/shared/tool-surface.mjs +624 -95
  208. package/src/runtime/shared/user-data-guard.mjs +0 -2
  209. package/src/standalone/agent-task-status.mjs +203 -0
  210. package/src/standalone/agent-task-status.test.mjs +76 -0
  211. package/src/standalone/agent-tool.mjs +1913 -0
  212. package/src/standalone/channel-worker.mjs +370 -14
  213. package/src/standalone/explore-tool.mjs +165 -70
  214. package/src/standalone/folder-dialog.mjs +314 -0
  215. package/src/standalone/hook-bus.mjs +898 -22
  216. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  217. package/src/standalone/projects.mjs +226 -0
  218. package/src/standalone/provider-admin.mjs +41 -24
  219. package/src/standalone/seeds.mjs +1 -78
  220. package/src/standalone/usage-dashboard.mjs +96 -8
  221. package/src/tui/App.jsx +4800 -2140
  222. package/src/tui/components/AnsiText.jsx +39 -28
  223. package/src/tui/components/ContextPanel.jsx +14 -4
  224. package/src/tui/components/Markdown.jsx +43 -77
  225. package/src/tui/components/MarkdownTable.jsx +9 -184
  226. package/src/tui/components/Message.jsx +28 -11
  227. package/src/tui/components/Picker.jsx +95 -56
  228. package/src/tui/components/PromptInput.jsx +428 -239
  229. package/src/tui/components/QueuedCommands.jsx +1 -1
  230. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  231. package/src/tui/components/Spinner.jsx +67 -38
  232. package/src/tui/components/StatusLine.jsx +606 -38
  233. package/src/tui/components/TextEntryPanel.jsx +128 -9
  234. package/src/tui/components/ToolExecution.jsx +597 -362
  235. package/src/tui/components/TurnDone.jsx +3 -3
  236. package/src/tui/components/UsagePanel.jsx +3 -5
  237. package/src/tui/components/tool-output-format.mjs +499 -0
  238. package/src/tui/components/tool-output-format.test.mjs +312 -0
  239. package/src/tui/dist/index.mjs +9192 -2399
  240. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  241. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  242. package/src/tui/engine.mjs +1455 -279
  243. package/src/tui/figures.mjs +21 -40
  244. package/src/tui/index.jsx +75 -31
  245. package/src/tui/input-editing.mjs +25 -0
  246. package/src/tui/markdown/format-token.mjs +671 -69
  247. package/src/tui/markdown/format-token.test.mjs +312 -0
  248. package/src/tui/markdown/render-ansi.mjs +94 -0
  249. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  250. package/src/tui/markdown/stream-fence.mjs +34 -0
  251. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  252. package/src/tui/markdown/table-layout.mjs +250 -0
  253. package/src/tui/paste-attachments.mjs +0 -7
  254. package/src/tui/spinner-verbs.mjs +1 -2
  255. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  256. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  257. package/src/tui/theme.mjs +756 -24
  258. package/src/tui/time-format.mjs +1 -1
  259. package/src/tui/transcript-tool-failures.mjs +67 -0
  260. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  261. package/src/ui/ansi.mjs +1 -2
  262. package/src/ui/markdown.mjs +85 -26
  263. package/src/ui/markdown.test.mjs +70 -0
  264. package/src/ui/model-display.mjs +121 -0
  265. package/src/ui/session-stats.mjs +44 -0
  266. package/src/ui/statusline-context-label.test.mjs +15 -0
  267. package/src/ui/statusline.mjs +386 -178
  268. package/src/ui/tool-card.mjs +2 -13
  269. package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
  270. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  271. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  272. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  273. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  274. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  275. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  276. package/src/workflows/default/WORKFLOW.md +12 -5
  277. package/src/workflows/default/workflow.json +0 -1
  278. package/src/workflows/solo/WORKFLOW.md +15 -0
  279. package/src/workflows/solo/workflow.json +7 -0
  280. package/vendor/ink/build/ink.js +54 -8
  281. package/vendor/ink/build/output.js +6 -1
  282. package/src/agents/scheduler-task.md +0 -3
  283. package/src/agents/web-researcher/AGENT.md +0 -3
  284. package/src/agents/web-researcher/agent.json +0 -6
  285. package/src/agents/webhook-handler.md +0 -3
  286. package/src/hooks/lib/permission-rules.cjs +0 -170
  287. package/src/hooks/lib/settings-loader.cjs +0 -112
  288. package/src/lib/hook-pipe-path.cjs +0 -10
  289. package/src/rules/bridge/00-common.md +0 -5
  290. package/src/rules/bridge/30-explorer.md +0 -4
  291. package/src/rules/lead/00-tool-lead.md +0 -5
  292. package/src/rules/shared/00-language.md +0 -3
  293. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  294. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  295. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  296. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  297. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  298. package/src/standalone/bridge-tool.mjs +0 -1414
  299. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -0,0 +1,330 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { createStandaloneHookBus } from '../src/standalone/hook-bus.mjs';
7
+
8
+ function tempRoot() {
9
+ return mkdtempSync(join(tmpdir(), 'mixdog-hook-test-'));
10
+ }
11
+
12
+ function writeJson(file, value) {
13
+ writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
14
+ }
15
+
16
+ test('standard PreToolUse hook can deny and modify tool input', async () => {
17
+ const root = tempRoot();
18
+ const hookScript = join(root, 'pretool.mjs');
19
+ writeFileSync(hookScript, `
20
+ import { readFileSync } from 'node:fs';
21
+ const input = JSON.parse(readFileSync(0, 'utf8'));
22
+ const command = input.tool_input?.command || '';
23
+ if (command.includes('rm -rf')) {
24
+ console.log(JSON.stringify({ hookSpecificOutput: {
25
+ hookEventName: 'PreToolUse',
26
+ permissionDecision: 'deny',
27
+ permissionDecisionReason: 'blocked destructive command'
28
+ }}));
29
+ } else if (command === 'rewrite-me') {
30
+ console.log(JSON.stringify({ hookSpecificOutput: {
31
+ hookEventName: 'PreToolUse',
32
+ updatedInput: { command: 'rewritten' }
33
+ }}));
34
+ }
35
+ `, 'utf8');
36
+ const hooksFile = join(root, 'hooks.json');
37
+ writeJson(hooksFile, {
38
+ hooks: {
39
+ PreToolUse: [{
40
+ matcher: 'shell',
41
+ hooks: [{ type: 'command', command: process.execPath, args: [hookScript] }],
42
+ }],
43
+ },
44
+ });
45
+
46
+ const prev = process.env.MIXDOG_HOOKS_FILE;
47
+ process.env.MIXDOG_HOOKS_FILE = hooksFile;
48
+ try {
49
+ const bus = createStandaloneHookBus({ dataDir: root });
50
+ const denied = await bus.beforeTool({ sessionId: 'sess_test', cwd: root, name: 'shell', args: { command: 'rm -rf build' } });
51
+ assert.equal(denied.action, 'deny');
52
+ assert.match(denied.reason, /blocked destructive/);
53
+
54
+ const modified = await bus.beforeTool({ sessionId: 'sess_test', cwd: root, name: 'shell', args: { command: 'rewrite-me' } });
55
+ assert.equal(modified.action, 'modify');
56
+ assert.deepEqual(modified.args, { command: 'rewritten' });
57
+ } finally {
58
+ if (prev == null) delete process.env.MIXDOG_HOOKS_FILE;
59
+ else process.env.MIXDOG_HOOKS_FILE = prev;
60
+ }
61
+ });
62
+
63
+ test('UserPromptSubmit hook returns additional context', async () => {
64
+ const root = tempRoot();
65
+ const hookScript = join(root, 'prompt.mjs');
66
+ writeFileSync(hookScript, `console.log('plain prompt context');\n`, 'utf8');
67
+ const hooksFile = join(root, 'hooks.json');
68
+ writeJson(hooksFile, {
69
+ hooks: {
70
+ UserPromptSubmit: [{
71
+ hooks: [{ type: 'command', command: process.execPath, args: [hookScript] }],
72
+ }],
73
+ },
74
+ });
75
+
76
+ const prev = process.env.MIXDOG_HOOKS_FILE;
77
+ process.env.MIXDOG_HOOKS_FILE = hooksFile;
78
+ try {
79
+ const bus = createStandaloneHookBus({ dataDir: root });
80
+ const result = await bus.dispatch('UserPromptSubmit', { session_id: 'sess_test', cwd: root, prompt: 'hello' });
81
+ assert.deepEqual(result.additionalContext, ['plain prompt context']);
82
+ } finally {
83
+ if (prev == null) delete process.env.MIXDOG_HOOKS_FILE;
84
+ else process.env.MIXDOG_HOOKS_FILE = prev;
85
+ }
86
+ });
87
+
88
+ test('legacy hook rule ask requests approval instead of allowing silently', async () => {
89
+ const root = tempRoot();
90
+ const rulesFile = join(root, 'hooks.json');
91
+ writeJson(rulesFile, {
92
+ toolBefore: [{
93
+ tool: 'shell',
94
+ action: 'ask',
95
+ reason: 'legacy rule approval',
96
+ enabled: true,
97
+ }],
98
+ });
99
+
100
+ const bus = createStandaloneHookBus({ dataDir: root });
101
+ const asked = await bus.beforeTool({ sessionId: 'sess_test', cwd: root, name: 'shell', args: { command: 'echo ok' } });
102
+ assert.equal(asked.action, 'ask');
103
+ assert.match(asked.reason, /legacy rule approval/);
104
+ });
105
+
106
+ test('hook config ignores Claude settings paths and reads project .mixdog hooks', async () => {
107
+ const root = tempRoot();
108
+ const claudeDir = join(root, '.claude');
109
+ const mixdogDir = join(root, '.mixdog');
110
+ mkdirSync(claudeDir, { recursive: true });
111
+ mkdirSync(mixdogDir, { recursive: true });
112
+
113
+ const denyScript = join(root, 'deny.mjs');
114
+ writeFileSync(denyScript, `
115
+ console.log(JSON.stringify({ hookSpecificOutput: {
116
+ hookEventName: 'PreToolUse',
117
+ permissionDecision: 'deny',
118
+ permissionDecisionReason: 'mixdog hook only'
119
+ }}));
120
+ `, 'utf8');
121
+
122
+ writeJson(join(claudeDir, 'settings.json'), {
123
+ hooks: {
124
+ PreToolUse: [{
125
+ matcher: 'shell',
126
+ hooks: [{ type: 'command', command: process.execPath, args: [denyScript] }],
127
+ }],
128
+ },
129
+ });
130
+
131
+ const busWithoutMixdog = createStandaloneHookBus({ dataDir: join(root, 'data-empty') });
132
+ const ignored = await busWithoutMixdog.beforeTool({ sessionId: 'sess_test', cwd: root, name: 'shell', args: { command: 'echo ok' } });
133
+ assert.equal(ignored, null);
134
+ assert.equal(busWithoutMixdog.status().configSources.some((p) => p.includes(`${join(root, '.claude')}`)), false);
135
+
136
+ writeJson(join(mixdogDir, 'hooks.json'), {
137
+ hooks: {
138
+ PreToolUse: [{
139
+ matcher: 'shell',
140
+ hooks: [{ type: 'command', command: process.execPath, args: [denyScript] }],
141
+ }],
142
+ },
143
+ });
144
+
145
+ const busWithMixdog = createStandaloneHookBus({ dataDir: join(root, 'data-mixdog') });
146
+ const denied = await busWithMixdog.beforeTool({ sessionId: 'sess_test', cwd: root, name: 'shell', args: { command: 'echo ok' } });
147
+ assert.equal(denied.action, 'deny');
148
+ assert.match(denied.reason, /mixdog hook only/);
149
+ });
150
+
151
+ test('registered plugin hooks read from plugin root with plugin env aliases', async () => {
152
+ const root = tempRoot();
153
+ const dataDir = join(root, 'data');
154
+ const pluginRoot = join(root, 'plugin-a');
155
+ const hooksDir = join(pluginRoot, 'hooks');
156
+ mkdirSync(hooksDir, { recursive: true });
157
+ mkdirSync(join(dataDir, 'plugins'), { recursive: true });
158
+
159
+ const envScript = join(pluginRoot, 'env-check.mjs');
160
+ writeFileSync(envScript, `
161
+ import { readFileSync } from 'node:fs';
162
+ JSON.parse(readFileSync(0, 'utf8'));
163
+ if (process.env.CLAUDE_PLUGIN_ROOT === ${JSON.stringify(pluginRoot)}
164
+ && process.env.MIXDOG_PLUGIN_ROOT === ${JSON.stringify(pluginRoot)}
165
+ && /plugin-a/.test(process.env.CLAUDE_PLUGIN_DATA || '')
166
+ && /plugin-a/.test(process.env.MIXDOG_PLUGIN_DATA || '')) {
167
+ console.log(JSON.stringify({ hookSpecificOutput: {
168
+ hookEventName: 'PreToolUse',
169
+ permissionDecision: 'deny',
170
+ permissionDecisionReason: 'plugin root env ok'
171
+ }}));
172
+ }
173
+ `, 'utf8');
174
+
175
+ writeJson(join(dataDir, 'plugins', 'registry.json'), {
176
+ version: 1,
177
+ plugins: [{
178
+ id: 'plugin-a',
179
+ source: pluginRoot,
180
+ sourceType: 'local',
181
+ root: pluginRoot,
182
+ name: 'plugin-a',
183
+ managed: false,
184
+ }],
185
+ });
186
+ writeJson(join(hooksDir, 'hooks.json'), {
187
+ hooks: {
188
+ PreToolUse: [{
189
+ matcher: 'shell',
190
+ hooks: [{ type: 'command', command: process.execPath, args: ['${CLAUDE_PLUGIN_ROOT}/env-check.mjs'] }],
191
+ }],
192
+ },
193
+ });
194
+
195
+ const bus = createStandaloneHookBus({ dataDir });
196
+ const denied = await bus.beforeTool({ sessionId: 'sess_test', cwd: root, name: 'shell', args: { command: 'echo ok' } });
197
+ assert.equal(denied.action, 'deny');
198
+ assert.match(denied.reason, /plugin root env ok/);
199
+ });
200
+
201
+ test('standard PreToolUse ask requests approval instead of allowing silently', async () => {
202
+ const root = tempRoot();
203
+ const hookScript = join(root, 'ask.mjs');
204
+ writeFileSync(hookScript, `
205
+ console.log(JSON.stringify({ hookSpecificOutput: {
206
+ hookEventName: 'PreToolUse',
207
+ permissionDecision: 'ask',
208
+ permissionDecisionReason: 'needs human approval'
209
+ }}));
210
+ `, 'utf8');
211
+ const hooksFile = join(root, 'hooks.json');
212
+ writeJson(hooksFile, {
213
+ hooks: {
214
+ PreToolUse: [{
215
+ matcher: 'shell',
216
+ hooks: [{ type: 'command', command: process.execPath, args: [hookScript] }],
217
+ }],
218
+ },
219
+ });
220
+
221
+ const prev = process.env.MIXDOG_HOOKS_FILE;
222
+ process.env.MIXDOG_HOOKS_FILE = hooksFile;
223
+ try {
224
+ const bus = createStandaloneHookBus({ dataDir: root });
225
+ const asked = await bus.beforeTool({ sessionId: 'sess_test', cwd: root, name: 'shell', args: { command: 'echo ok' } });
226
+ assert.equal(asked.action, 'ask');
227
+ assert.match(asked.reason, /human approval/);
228
+ } finally {
229
+ if (prev == null) delete process.env.MIXDOG_HOOKS_FILE;
230
+ else process.env.MIXDOG_HOOKS_FILE = prev;
231
+ }
232
+ });
233
+
234
+ test('async command hook reports spawn errors without crashing', async () => {
235
+ const root = tempRoot();
236
+ const missingCommand = join(root, 'missing-async-hook-binary');
237
+ const hooksFile = join(root, 'hooks.json');
238
+ writeJson(hooksFile, {
239
+ hooks: {
240
+ Stop: [{
241
+ hooks: [{
242
+ type: 'command',
243
+ command: missingCommand,
244
+ args: [],
245
+ async: true,
246
+ }],
247
+ }],
248
+ },
249
+ });
250
+
251
+ const prev = process.env.MIXDOG_HOOKS_FILE;
252
+ process.env.MIXDOG_HOOKS_FILE = hooksFile;
253
+ try {
254
+ const bus = createStandaloneHookBus({ dataDir: root });
255
+ await bus.dispatch('Stop', { session_id: 'sess_test', cwd: root });
256
+ await new Promise((resolve) => setTimeout(resolve, 50));
257
+ const status = bus.status();
258
+ const spawnError = status.recent.find((entry) => entry.name === 'hook:error'
259
+ && String(entry.payload?.error || '').includes('hook spawn failed'));
260
+ assert.ok(spawnError, 'expected async spawn failure to emit hook:error');
261
+ } finally {
262
+ if (prev == null) delete process.env.MIXDOG_HOOKS_FILE;
263
+ else process.env.MIXDOG_HOOKS_FILE = prev;
264
+ }
265
+ });
266
+
267
+ test('PostToolUse dispatch omits updatedToolOutput when hook does not override', async () => {
268
+ const root = tempRoot();
269
+ const hookScript = join(root, 'posttool-noop.mjs');
270
+ writeFileSync(hookScript, `console.log('{}');\n`, 'utf8');
271
+ const hooksFile = join(root, 'hooks.json');
272
+ writeJson(hooksFile, {
273
+ hooks: {
274
+ PostToolUse: [{
275
+ hooks: [{ type: 'command', command: process.execPath, args: [hookScript] }],
276
+ }],
277
+ },
278
+ });
279
+
280
+ const prev = process.env.MIXDOG_HOOKS_FILE;
281
+ process.env.MIXDOG_HOOKS_FILE = hooksFile;
282
+ try {
283
+ const bus = createStandaloneHookBus({ dataDir: root });
284
+ const result = await bus.dispatch('PostToolUse', {
285
+ session_id: 'sess_test',
286
+ cwd: root,
287
+ tool_name: 'list',
288
+ tool_response: 'entries',
289
+ });
290
+ assert.equal('updatedToolOutput' in result, false);
291
+ } finally {
292
+ if (prev == null) delete process.env.MIXDOG_HOOKS_FILE;
293
+ else process.env.MIXDOG_HOOKS_FILE = prev;
294
+ }
295
+ });
296
+
297
+ test('PostToolUse dispatch preserves empty-string updatedToolOutput override', async () => {
298
+ const root = tempRoot();
299
+ const hookScript = join(root, 'posttool-clear.mjs');
300
+ writeFileSync(hookScript, `
301
+ console.log(JSON.stringify({ hookSpecificOutput: {
302
+ hookEventName: 'PostToolUse',
303
+ updatedToolOutput: ''
304
+ }}));
305
+ `, 'utf8');
306
+ const hooksFile = join(root, 'hooks.json');
307
+ writeJson(hooksFile, {
308
+ hooks: {
309
+ PostToolUse: [{
310
+ hooks: [{ type: 'command', command: process.execPath, args: [hookScript] }],
311
+ }],
312
+ },
313
+ });
314
+
315
+ const prev = process.env.MIXDOG_HOOKS_FILE;
316
+ process.env.MIXDOG_HOOKS_FILE = hooksFile;
317
+ try {
318
+ const bus = createStandaloneHookBus({ dataDir: root });
319
+ const result = await bus.dispatch('PostToolUse', {
320
+ session_id: 'sess_test',
321
+ cwd: root,
322
+ tool_name: 'list',
323
+ tool_response: 'entries',
324
+ });
325
+ assert.equal(result.updatedToolOutput, '');
326
+ } finally {
327
+ if (prev == null) delete process.env.MIXDOG_HOOKS_FILE;
328
+ else process.env.MIXDOG_HOOKS_FILE = prev;
329
+ }
330
+ });
@@ -9,12 +9,6 @@ const args = parseArgs(process.argv.slice(2));
9
9
  const timeoutMs = Number(args.get('timeout-ms') || 480000);
10
10
  const keep = args.get('keep') === true;
11
11
  const selectedScenario = String(args.get('scenario') || 'explicit-parallel');
12
- const RETRIEVAL_SCENARIOS = new Set([
13
- 'broad-locator-explore',
14
- 'known-symbol-direct',
15
- 'memory-recall',
16
- 'web-search',
17
- ]);
18
12
 
19
13
  const SCENARIOS = {
20
14
  'explicit-parallel': {
@@ -28,7 +22,7 @@ ${projectDir}
28
22
 
29
23
  Goal: validate how the lead plans and batches independent work. Do not solve everything directly in the lead.
30
24
 
31
- Please use the bridge tool with async workers for the independent sections below. Spawn distinct tags early so the work can overlap, then wait for completion notifications and make a concise final report.
25
+ Please use the agent tool with async workers for the independent sections below. Spawn distinct tags early so the work can overlap, then wait for completion notifications and make a concise final report.
32
26
 
33
27
  Independent sections:
34
28
  1. worker: update src/math.mjs to export subtract(a, b).
@@ -37,20 +31,20 @@ Independent sections:
37
31
 
38
32
  Use minimal exploration. Prefer grep/code_graph/apply_patch over broad reading. Keep each worker brief: maxLoopIterations 4 and idleTimeoutMs 120000 are enough.
39
33
  After worker results return, run npm run smoke in ${projectDir} if needed, then summarize:
40
- - whether bridge workers were used
41
- - which roles/tags ran
34
+ - whether agent workers were used
35
+ - which agents/tags ran
42
36
  - whether the smoke passed
43
37
  `.trim(),
44
38
  validate: validateMultiFileProject,
45
39
  expect: {
46
40
  minSpawns: 3,
47
- minFirstIterBridgeCalls: 2,
41
+ minFirstIterAgentCalls: 2,
48
42
  requiredRoles: ['worker', 'debugger', 'reviewer'],
49
43
  requireFilesOk: true,
50
44
  },
51
45
  },
52
46
  'natural-multifile': {
53
- description: 'Natural multi-file task with no hard bridge command; should still delegate useful independent work.',
47
+ description: 'Natural multi-file task with no hard agent command; should still delegate useful independent work.',
54
48
  setup: setupMultiFileProject,
55
49
  prompt: (projectDir) => `
56
50
  Work in this temporary project only:
@@ -68,7 +62,7 @@ Keep exploration tight and verify with npm run smoke.
68
62
  validate: validateMultiFileProject,
69
63
  expect: {
70
64
  minSpawns: 2,
71
- minFirstIterBridgeCalls: 1,
65
+ minFirstIterAgentCalls: 1,
72
66
  minPreEditImplementationSpawns: 1,
73
67
  requiredAnyRoles: ['worker', 'debugger', 'reviewer'],
74
68
  requireFilesOk: true,
@@ -315,7 +309,7 @@ function compact(value, max = 700) {
315
309
  return raw.length > max ? `${raw.slice(0, max)}...` : raw;
316
310
  }
317
311
 
318
- function bridgeSummary(call) {
312
+ function agentSummary(call) {
319
313
  const input = call?.input || call?.arguments || call?.args || {};
320
314
  return {
321
315
  id: call?.id || call?.callId || null,
@@ -352,24 +346,24 @@ function evaluate(summary, scenario) {
352
346
  if (Number.isFinite(expect.maxMutations) && summary.tools.mutations > expect.maxMutations) {
353
347
  failures.push(`expected mutations <= ${expect.maxMutations}, got ${summary.tools.mutations}`);
354
348
  }
355
- if (Number.isFinite(expect.minSpawns) && summary.bridge.spawns < expect.minSpawns) {
356
- failures.push(`expected at least ${expect.minSpawns} bridge spawns, got ${summary.bridge.spawns}`);
349
+ if (Number.isFinite(expect.minSpawns) && summary.agent.spawns < expect.minSpawns) {
350
+ failures.push(`expected at least ${expect.minSpawns} agent spawns, got ${summary.agent.spawns}`);
357
351
  }
358
- if (Number.isFinite(expect.maxSpawns) && summary.bridge.spawns > expect.maxSpawns) {
359
- failures.push(`expected at most ${expect.maxSpawns} bridge spawns, got ${summary.bridge.spawns}`);
352
+ if (Number.isFinite(expect.maxSpawns) && summary.agent.spawns > expect.maxSpawns) {
353
+ failures.push(`expected at most ${expect.maxSpawns} agent spawns, got ${summary.agent.spawns}`);
360
354
  }
361
- if (Number.isFinite(expect.minFirstIterBridgeCalls) && summary.bridge.firstIterBridgeCount < expect.minFirstIterBridgeCalls) {
362
- failures.push(`expected at least ${expect.minFirstIterBridgeCalls} bridge calls in first bridge iteration, got ${summary.bridge.firstIterBridgeCount}`);
355
+ if (Number.isFinite(expect.minFirstIterAgentCalls) && summary.agent.firstIterAgentCount < expect.minFirstIterAgentCalls) {
356
+ failures.push(`expected at least ${expect.minFirstIterAgentCalls} agent calls in first agent iteration, got ${summary.agent.firstIterAgentCount}`);
363
357
  }
364
- if (Number.isFinite(expect.minPreEditImplementationSpawns) && summary.bridge.preEditImplementationSpawns < expect.minPreEditImplementationSpawns) {
365
- failures.push(`expected at least ${expect.minPreEditImplementationSpawns} pre-edit implementation/debug bridge spawns, got ${summary.bridge.preEditImplementationSpawns}`);
358
+ if (Number.isFinite(expect.minPreEditImplementationSpawns) && summary.agent.preEditImplementationSpawns < expect.minPreEditImplementationSpawns) {
359
+ failures.push(`expected at least ${expect.minPreEditImplementationSpawns} pre-edit implementation/debug agent spawns, got ${summary.agent.preEditImplementationSpawns}`);
366
360
  }
367
361
  for (const role of expect.requiredRoles || []) {
368
- if (!summary.bridge.roles.includes(role)) failures.push(`missing bridge role ${role}`);
362
+ if (!summary.agent.roles.includes(role)) failures.push(`missing agent role ${role}`);
369
363
  }
370
364
  if (Array.isArray(expect.requiredAnyRoles) && expect.requiredAnyRoles.length > 0) {
371
- const hasAny = expect.requiredAnyRoles.some((role) => summary.bridge.roles.includes(role));
372
- if (!hasAny) failures.push(`missing any bridge role from ${expect.requiredAnyRoles.join(', ')}`);
365
+ const hasAny = expect.requiredAnyRoles.some((role) => summary.agent.roles.includes(role));
366
+ if (!hasAny) failures.push(`missing any agent role from ${expect.requiredAnyRoles.join(', ')}`);
373
367
  }
374
368
  if (expect.requireFilesOk && !Object.values(summary.files || {}).every(Boolean)) {
375
369
  failures.push(`file validation failed: ${JSON.stringify(summary.files)}`);
@@ -454,7 +448,7 @@ async function runScenario(name) {
454
448
  const { result } = await runtime.ask(scenario.prompt(projectDir), {
455
449
  onToolCall(iter, calls) {
456
450
  for (const call of calls || []) {
457
- const summary = bridgeSummary(call);
451
+ const summary = agentSummary(call);
458
452
  toolCalls.push({ iter, ...summary });
459
453
  record('toolCall', { iter, ...summary });
460
454
  }
@@ -484,13 +478,13 @@ async function runScenario(name) {
484
478
  }
485
479
 
486
480
  const elapsedMs = Math.round(performance.now() - startedAt);
487
- const bridgeCalls = toolCalls.filter((call) => call.name === 'bridge');
481
+ const agentCalls = toolCalls.filter((call) => call.name === 'agent');
488
482
  const toolCounts = {};
489
483
  for (const call of toolCalls) {
490
484
  toolCounts[call.name] = (toolCounts[call.name] || 0) + 1;
491
485
  }
492
486
  const mutationCount = toolCalls.filter((call) => call.name === 'apply_patch').length;
493
- const spawnCalls = bridgeCalls.filter((call) => call.type === 'spawn' || call.type === null);
487
+ const spawnCalls = agentCalls.filter((call) => call.type === 'spawn' || call.type === null);
494
488
  const mutationIters = toolCalls
495
489
  .filter((call) => call.name === 'apply_patch')
496
490
  .map((call) => Number(call.iter))
@@ -505,11 +499,11 @@ async function runScenario(name) {
505
499
  const preEditImplementationSpawns = firstMutationIter == null
506
500
  ? implementationSpawnCalls.length
507
501
  : implementationSpawnCalls.filter((call) => Number(call.iter) < firstMutationIter).length;
508
- const distinctRoles = [...new Set(bridgeCalls.map((call) => call.role).filter(Boolean))];
509
- const distinctTags = [...new Set(bridgeCalls.map((call) => call.tag).filter(Boolean))];
510
- const firstBridgeIter = Math.min(...bridgeCalls.map((call) => Number(call.iter)).filter(Number.isFinite));
511
- const firstIterBridgeCount = Number.isFinite(firstBridgeIter)
512
- ? bridgeCalls.filter((call) => Number(call.iter) === firstBridgeIter).length
502
+ const distinctRoles = [...new Set(agentCalls.map((call) => call.role).filter(Boolean))];
503
+ const distinctTags = [...new Set(agentCalls.map((call) => call.tag).filter(Boolean))];
504
+ const firstAgentIter = Math.min(...agentCalls.map((call) => Number(call.iter)).filter(Number.isFinite));
505
+ const firstIterAgentCount = Number.isFinite(firstAgentIter)
506
+ ? agentCalls.filter((call) => Number(call.iter) === firstAgentIter).length
513
507
  : 0;
514
508
 
515
509
  const summary = {
@@ -522,11 +516,11 @@ async function runScenario(name) {
522
516
  projectDir,
523
517
  route: events.find((e) => e.kind === 'runtime')?.payload || null,
524
518
  prepareResults,
525
- bridge: {
526
- calls: bridgeCalls.length,
519
+ agent: {
520
+ calls: agentCalls.length,
527
521
  spawns: spawnCalls.length,
528
- firstBridgeIter: Number.isFinite(firstBridgeIter) ? firstBridgeIter : null,
529
- firstIterBridgeCount,
522
+ firstAgentIter: Number.isFinite(firstAgentIter) ? firstAgentIter : null,
523
+ firstIterAgentCount,
530
524
  firstMutationIter,
531
525
  implementationSpawns: implementationSpawnCalls.length,
532
526
  preEditImplementationSpawns,
@@ -578,9 +572,9 @@ async function main() {
578
572
  scenario: summary.scenario,
579
573
  passed: summary.passed,
580
574
  elapsedMs: summary.elapsedMs,
581
- spawns: summary.bridge.spawns,
582
- firstIterBridgeCount: summary.bridge.firstIterBridgeCount,
583
- roles: summary.bridge.roles,
575
+ spawns: summary.agent.spawns,
576
+ firstIterAgentCount: summary.agent.firstIterAgentCount,
577
+ roles: summary.agent.roles,
584
578
  failures: summary.failures,
585
579
  })),
586
580
  }, null, 2));