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
@@ -1,671 +0,0 @@
1
- // Hook IPC daemon — Windows named-pipe server consumed by mixdog-shim.exe.
2
- //
3
- // Replaces the per-spawn cold-start cost of `bun hooks/*.cjs` (≈86ms) with a
4
- // single long-lived listener inside the channels worker. The shim is a tiny
5
- // Rust .exe (~111KB, ~5-10ms cold) that connects, writes one JSON line, reads
6
- // one JSON line back, and exits.
7
- //
8
- // Protocol (line-delimited JSON):
9
- // client → server : <Mixdog hook payload>\n
10
- // server → client : <decision-json or "null">\n
11
- //
12
- // Each connection is handled independently. Long-running handlers (Discord
13
- // permission polling, up to 2 minutes) do not block other connections.
14
- //
15
- // Failure model: dispatch errors emit "null" (fail-open). The shim itself
16
- // also fails open when the pipe is unreachable.
17
-
18
- import { createServer, createConnection } from 'node:net'
19
- import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync, readFileSync } from 'node:fs'
20
- import { appendFile } from 'node:fs/promises'
21
- import { join, resolve as pathResolve } from 'node:path'
22
- import { tmpdir } from 'node:os'
23
- import { randomBytes } from 'node:crypto'
24
- import { request as httpsRequest } from 'node:https'
25
- import { createRequire } from 'node:module'
26
- import { resolvePluginData } from '../../shared/plugin-paths.mjs'
27
-
28
- const moduleRequire = createRequire(import.meta.url)
29
- const {
30
- isMixdogDebugEnabled,
31
- pruneStalePluginDataLogSiblings,
32
- DEFAULT_STALE_LOG_SIBLING_MAX,
33
- } = moduleRequire('../../../lib/mixdog-debug.cjs')
34
-
35
- // IPC transport path. Windows uses a named pipe (`\\.\pipe\…`); Unix uses a
36
- // Unix domain socket under XDG_RUNTIME_DIR (or /tmp as fallback). Node's
37
- // net.createServer().listen() accepts both transparently.
38
- const PIPE_PATH = moduleRequire('../../../lib/hook-pipe-path.cjs')()
39
-
40
- // Honor MIXDOG_RUNTIME_ROOT consistently with runtime-paths.mjs (the consumer
41
- // of these tool-exec signals): when the override is set, the signal PRODUCER
42
- // here must write into the same root the channels worker watches, or signals
43
- // are silently dropped. Default stays tmpdir()/mixdog so non-override installs
44
- // are unchanged.
45
- const RUNTIME_ROOT = process.env.MIXDOG_RUNTIME_ROOT
46
- ? pathResolve(process.env.MIXDOG_RUNTIME_ROOT)
47
- : join(tmpdir(), 'mixdog')
48
- const SIGNAL_CONSUMER_MARKER = join(RUNTIME_ROOT, '.tool-exec-consumer')
49
- const SUBAGENT_SIGNAL_CONSUMER_MARKER = join(RUNTIME_ROOT, '.tool-exec-subagent-consumer')
50
- const SIGNAL_RE_GENERIC = /^tool-exec-\d+-[0-9a-f]+\.signal$/
51
- const SIGNAL_RE_CAPTURE = /^tool-exec-(\d+)-[0-9a-f]+\.signal$/
52
- const SWEEP_MARKER = join(RUNTIME_ROOT, '.tool-exec-sweep')
53
- const SWEEP_INTERVAL_MS = 30_000
54
- const SIGNAL_TTL_MS = 60_000
55
- // Marketplace installs use two naming shapes for the MCP server name —
56
- // `plugin_mixdog_mixdog__` (legacy / mixdog marketplace) and
57
- // `plugin_mixdog_trib-plugin__` (trib-plugin marketplace). PreToolUse
58
- // sandbox checks must recognise both or sandbox evaluation silently
59
- // misses MCP tool names from the other install layout.
60
- const MCP_PREFIXES = [
61
- 'mcp__plugin_mixdog_mixdog__',
62
- 'mcp__plugin_mixdog_trib-plugin__',
63
- ]
64
- const NATIVE_FILE_LOOKUP_TOOLS = new Set(['Read', 'Grep', 'Glob', 'Search', 'LS'])
65
- function isMcpToolName(name) {
66
- if (!name) return false
67
- return MCP_PREFIXES.some(p => name.startsWith(p))
68
- }
69
-
70
- const POLL_INTERVAL_MS = 2000
71
- const SUBAGENT_TIMEOUT_MS = 120_000
72
- const DEFAULT_DISPATCH_TIMEOUT_MS = 15_000
73
- const SESSION_START_MEMORY_DISPATCH_TIMEOUT_MS = 125_000
74
- const MIXDOG_DEBUG_ENABLED = isMixdogDebugEnabled()
75
- let _hookPipeLogsPruned = false
76
-
77
- function hookPipeDebugStderr(line) {
78
- if (!MIXDOG_DEBUG_ENABLED) return
79
- try { process.stderr.write(line) } catch {}
80
- }
81
-
82
- let _started = false
83
- let _server = null
84
- let _subagentSignalConsumers = 0
85
-
86
- function refreshSubagentSignalConsumerMarker() {
87
- try {
88
- if (_subagentSignalConsumers > 0) {
89
- try { mkdirSync(RUNTIME_ROOT, { recursive: true }) } catch {}
90
- writeFileSync(SUBAGENT_SIGNAL_CONSUMER_MARKER, String(Date.now()))
91
- } else {
92
- try { unlinkSync(SUBAGENT_SIGNAL_CONSUMER_MARKER) } catch {}
93
- }
94
- } catch {}
95
- }
96
-
97
- function formatError(err) {
98
- const msg = (err && (err.stack || err.message)) || err
99
- return String(msg || 'unknown').replace(/\s+/g, ' ').slice(0, 2000)
100
- }
101
-
102
- function traceSessionStart(message) {
103
- if (!MIXDOG_DEBUG_ENABLED) return
104
- const line = `[${new Date().toISOString()}] [hook-pipe][session-start] ${message}\n`
105
- try { process.stderr.write(line) } catch {}
106
- try {
107
- const dataDir = resolvePluginData()
108
- mkdirSync(dataDir, { recursive: true })
109
- if (!_hookPipeLogsPruned) {
110
- _hookPipeLogsPruned = true
111
- pruneStalePluginDataLogSiblings(dataDir, DEFAULT_STALE_LOG_SIBLING_MAX)
112
- }
113
- void appendFile(join(dataDir, 'session-start.log'), line).catch(() => {})
114
- } catch {}
115
- }
116
-
117
- function dispatchTimeoutMsForPayload(payload) {
118
- const event = payload?.hook_event_name || payload?.hookEventName || ''
119
- if (event !== 'SessionStart') return DEFAULT_DISPATCH_TIMEOUT_MS
120
- const argsArr = payload?._args || []
121
- const partArg = argsArr.find(a => a.startsWith('--part='))
122
- const part = partArg ? partArg.slice('--part='.length) : ''
123
- return (part === 'core' || part === 'recap')
124
- ? SESSION_START_MEMORY_DISPATCH_TIMEOUT_MS
125
- : DEFAULT_DISPATCH_TIMEOUT_MS
126
- }
127
-
128
- // ── post-tool-use handler ────────────────────────────────────────────────────
129
-
130
- function sweepStaleSignalsThrottled(now = Date.now()) {
131
- try {
132
- let lastSweep = 0
133
- try { lastSweep = statSync(SWEEP_MARKER).mtimeMs } catch {}
134
- if (now - lastSweep < SWEEP_INTERVAL_MS) return
135
- try { writeFileSync(SWEEP_MARKER, String(now)) } catch {}
136
- const entries = readdirSync(RUNTIME_ROOT)
137
- for (const name of entries) {
138
- if (!SIGNAL_RE_GENERIC.test(name)) continue
139
- const p = join(RUNTIME_ROOT, name)
140
- try {
141
- const st = statSync(p)
142
- if (now - st.mtimeMs > SIGNAL_TTL_MS) unlinkSync(p)
143
- } catch {}
144
- }
145
- } catch {}
146
- }
147
-
148
- function handlePostToolUse(payload) {
149
- const toolName = payload?.tool_name || payload?.toolName || ''
150
- if (!toolName) return null
151
- if (_subagentSignalConsumers <= 0 &&
152
- !existsSync(SIGNAL_CONSUMER_MARKER) &&
153
- !existsSync(SUBAGENT_SIGNAL_CONSUMER_MARKER)) {
154
- return null
155
- }
156
- const filePath = payload?.tool_input?.file_path || payload?.toolInput?.file_path || ''
157
- const toolUseId = payload?.tool_use_id || payload?.toolUseId || ''
158
-
159
- try { if (!existsSync(RUNTIME_ROOT)) mkdirSync(RUNTIME_ROOT, { recursive: true }) } catch {}
160
- sweepStaleSignalsThrottled()
161
-
162
- try {
163
- const rand = randomBytes(4).toString('hex')
164
- const signalFile = join(RUNTIME_ROOT, `tool-exec-${Date.now()}-${rand}.signal`)
165
- writeFileSync(signalFile, JSON.stringify({ toolName, filePath, toolUseId, ts: Date.now() }))
166
- } catch (err) {
167
- process.stderr.write(`[hook-pipe] post-tool-use signal write failed: ${err?.message || err}\n`)
168
- }
169
- return null
170
- }
171
-
172
- // ── pre-mcp-sandbox handler ──────────────────────────────────────────────────
173
-
174
- function handlePreMcpSandbox(payload) {
175
- const toolName = payload?.tool_name || payload?.toolName || ''
176
- if (!isMcpToolName(toolName)) return null
177
-
178
- const toolInput = payload?.tool_input ?? payload?.toolInput ?? {}
179
-
180
- let userCwdRaw = payload?.cwd || ''
181
- if (!userCwdRaw) {
182
- try { userCwdRaw = readFileSync(join(resolvePluginData(), 'user-cwd.txt'), 'utf8').trim() } catch {}
183
- }
184
- if (!userCwdRaw) userCwdRaw = process.cwd()
185
-
186
- const userCwd = pathResolve(userCwdRaw)
187
- const projectDir = payload?.projectDir || payload?.project_dir ||
188
- process.env.MIXDOG_PROJECT_DIR || userCwd
189
- const permissionMode = payload?.permissionMode || payload?.permission_mode || undefined
190
-
191
- let settingsPerms, evaluatePermission
192
- try {
193
- const settingsLoader = moduleRequire('../../../hooks/lib/settings-loader.cjs')
194
- settingsPerms = settingsLoader.loadPermissions(projectDir)
195
- } catch (err) {
196
- process.stderr.write(`[hook-pipe] pre-mcp-sandbox settings-loader unavailable: ${err?.message || err}\n`)
197
- return null
198
- }
199
- try {
200
- const ev = moduleRequire('../../../hooks/lib/permission-evaluator.cjs')
201
- evaluatePermission = ev.evaluatePermission
202
- } catch (err) {
203
- process.stderr.write(`[hook-pipe] pre-mcp-sandbox evaluator unavailable: ${err?.message || err}\n`)
204
- return null
205
- }
206
-
207
- const evalResult = evaluatePermission({ toolName, toolInput, permissionMode, projectDir, userCwd, permissions: settingsPerms })
208
- const { decision, reason } = evalResult
209
-
210
- // Pi-like practical: no permission prompts. Only hard-deny and explicit
211
- // user deny rules block; every other evaluator result is allowed.
212
- if (decision === 'deny') return makeDecision('deny', reason)
213
- return null
214
- }
215
-
216
- function handleNativeFileLookup(payload) {
217
- const toolName = payload?.tool_name || payload?.toolName || ''
218
- if (!NATIVE_FILE_LOOKUP_TOOLS.has(toolName)) return null
219
- return makeDecision(
220
- 'deny',
221
- `Native ${toolName} is disabled by Mixdog. Use the Mixdog MCP read/grep/glob/list tools instead.`
222
- )
223
- }
224
-
225
- function makeDecision(decision, reason, updatedInput) {
226
- const out = {
227
- hookSpecificOutput: {
228
- hookEventName: 'PreToolUse',
229
- permissionDecision: decision,
230
- permissionDecisionReason: reason,
231
- },
232
- }
233
- if (updatedInput !== undefined) out.hookSpecificOutput.updatedInput = updatedInput
234
- return out
235
- }
236
-
237
- // ── pre-tool-subagent handler (Discord permission flow, async) ───────────────
238
-
239
- function sanitize(value) {
240
- return String(value).replace(/[^a-zA-Z0-9._-]/g, '_')
241
- }
242
-
243
- function readDiscordConfig() {
244
- try {
245
- const { readSection } = moduleRequire('../../../lib/config-cjs.cjs')
246
- return readSection('channels')
247
- } catch { return {} }
248
- }
249
-
250
- function isProtectedPath(filePath, cwd) {
251
- if (!filePath) return false
252
- const norm = pathResolve(filePath).replace(/\\/g, '/').toLowerCase()
253
- const cwdNorm = (cwd || process.cwd()).replace(/\\/g, '/').toLowerCase()
254
- const insideCwd = cwdNorm && (norm === cwdNorm || norm.startsWith(cwdNorm.endsWith('/') ? cwdNorm : cwdNorm + '/'))
255
- return !insideCwd
256
- }
257
-
258
- function findAndClaimSignal(toolName, filePath, toolUseId, hookStartedAt) {
259
- let entries
260
- try { entries = readdirSync(RUNTIME_ROOT) } catch { return null }
261
- for (const name of entries) {
262
- const m = SIGNAL_RE_CAPTURE.exec(name)
263
- if (!m) continue
264
- const ts = Number(m[1])
265
- if (!Number.isFinite(ts) || ts < hookStartedAt) continue
266
- const p = join(RUNTIME_ROOT, name)
267
- let raw
268
- try { raw = readFileSync(p, 'utf8') } catch { continue }
269
- let parsed
270
- try { parsed = JSON.parse(raw) } catch { continue }
271
- if (parsed?.toolName !== toolName) continue
272
- if (parsed?.filePath !== filePath) continue
273
- if (toolUseId && parsed?.toolUseId !== toolUseId) continue
274
- try { unlinkSync(p) } catch {}
275
- return p
276
- }
277
- return null
278
- }
279
-
280
- function discordApi(method, apiPath, token, body) {
281
- return new Promise((resolve, reject) => {
282
- const data = body ? JSON.stringify(body) : ''
283
- const headers = { 'Authorization': 'Bot ' + token, 'Content-Type': 'application/json' }
284
- if (data) headers['Content-Length'] = Buffer.byteLength(data)
285
- const req = httpsRequest({ hostname: 'discord.com', path: apiPath, method, headers },
286
- res => { let out = ''; res.on('data', d => { out += d }); res.on('end', () => { try { resolve(JSON.parse(out)) } catch { resolve({}) } }) })
287
- req.setTimeout(10_000, () => req.destroy())
288
- req.on('error', reject)
289
- if (data) req.write(data)
290
- req.end()
291
- })
292
- }
293
-
294
- const sleep = (ms) => new Promise(r => setTimeout(r, ms))
295
-
296
- async function handlePreToolSubagent(payload) {
297
- if (process.env.MIXDOG_CHANNELS_NO_CONNECT) return null
298
- // Pi-like practical: Mixdog no longer opens Discord permission popups for
299
- // subagent Edit/Write outside cwd. Native/role/tool guards still apply in
300
- // their own layers; this hook simply stops adding an approval workflow.
301
- return null
302
- }
303
-
304
- // ── statusline handler (via dynamic ESM import) ──────────────────────────────
305
-
306
- let _statusLineMod = null
307
- let _statusLineLoadPromise = null
308
- let _statusLineModMtimeMs = 0
309
-
310
- async function ensureStatusLineMod() {
311
- let mtimeMs = 0
312
- try { mtimeMs = statSync(new URL('../../../bin/statusline-lib.mjs', import.meta.url)).mtimeMs } catch {}
313
- if (_statusLineMod && mtimeMs && mtimeMs === _statusLineModMtimeMs) return _statusLineMod
314
- if (_statusLineLoadPromise) return _statusLineLoadPromise
315
- _statusLineLoadPromise = import(`../../../bin/statusline-lib.mjs?mtime=${encodeURIComponent(String(mtimeMs || Date.now()))}`)
316
- .then(mod => { _statusLineMod = mod; _statusLineModMtimeMs = mtimeMs; _statusLineLoadPromise = null; return mod })
317
- .catch(err => {
318
- process.stderr.write(`[hook-pipe] statusline-lib import failed: ${err?.message || err}\n`)
319
- _statusLineLoadPromise = null
320
- return null
321
- })
322
- return _statusLineLoadPromise
323
- }
324
-
325
- async function handleStatusLine(payload) {
326
- const mod = await ensureStatusLineMod()
327
- if (!mod || typeof mod.renderStatusLine !== 'function') return null
328
- try {
329
- return await mod.renderStatusLine(JSON.stringify(payload || {}))
330
- } catch (err) {
331
- process.stderr.write(`[hook-pipe] statusline render failed: ${err?.message || err}\n`)
332
- return null
333
- }
334
- }
335
-
336
- // ── SessionStart: rules/core/recap handlers (via require'd cjs) ──────────────
337
- //
338
- // session-start.cjs accesses fd 0 at the top level — we gate that behind
339
- // MIXDOG_SKIP_TOP_STDIN so it doesn't consume the daemon's MCP stdio pipe.
340
- // Each SessionStart slot gets a fresh CJS module instance. That keeps the
341
- // module-globals (_event, PART, _emitSink) isolated, so rules/core/recap can
342
- // run concurrently without a daemon-wide lock.
343
- function loadSessionStartMod() {
344
- const prev = process.env.MIXDOG_SKIP_TOP_STDIN
345
- process.env.MIXDOG_SKIP_TOP_STDIN = '1'
346
- const moduleId = moduleRequire.resolve('../../../hooks/session-start.cjs')
347
- delete moduleRequire.cache[moduleId]
348
- traceSessionStart('fresh require start path=../../../hooks/session-start.cjs')
349
- try {
350
- const mod = moduleRequire(moduleId)
351
- delete moduleRequire.cache[moduleId]
352
- traceSessionStart(`fresh require ok exports=${Object.keys(mod || {}).join(',')}`)
353
- return mod
354
- } catch (err) {
355
- process.stderr.write(`[hook-pipe] session-start.cjs require failed: ${err?.message || err}\n`)
356
- traceSessionStart(`require failed err=${formatError(err)}`)
357
- return null
358
- } finally {
359
- if (prev === undefined) delete process.env.MIXDOG_SKIP_TOP_STDIN
360
- else process.env.MIXDOG_SKIP_TOP_STDIN = prev
361
- }
362
- }
363
-
364
- async function handleSessionStartPart(args, payload) {
365
- if (payload?.isSidechain || payload?.is_sidechain) {
366
- traceSessionStart(`skip reason=sidechain source=${payload?.source || ''}`)
367
- return null
368
- }
369
- if (payload?.agentId || payload?.agent_id) {
370
- traceSessionStart(`skip reason=agent source=${payload?.source || ''} agent=${payload?.agentId || payload?.agent_id || ''}`)
371
- return null
372
- }
373
- if (payload?.kind && payload.kind !== 'interactive') {
374
- traceSessionStart(`skip reason=kind source=${payload?.source || ''} kind=${payload.kind}`)
375
- return null
376
- }
377
-
378
- const partArg = (args || []).find(a => a.startsWith('--part='))
379
- const part = partArg ? partArg.slice('--part='.length) : null
380
- if (!part || (part !== 'rules' && part !== 'core' && part !== 'recap')) {
381
- traceSessionStart(`skip reason=invalid-part source=${payload?.source || ''} args=${JSON.stringify(args || [])}`)
382
- return null
383
- }
384
-
385
- const mod = loadSessionStartMod()
386
- if (!mod) {
387
- traceSessionStart(`skip reason=require-null part=${part} source=${payload?.source || ''}`)
388
- return null
389
- }
390
-
391
- let buf = ''
392
- let failed = false
393
- const t0 = Date.now()
394
- try {
395
- traceSessionStart(
396
- `run start part=${part} source=${payload?.source || ''} cwd=${payload?.cwd || ''} ` +
397
- `sessionId=${payload?.session_id || payload?.sessionId || ''}`
398
- )
399
- try { mod.setEvent(payload || {}) } catch (err) {
400
- failed = true
401
- traceSessionStart(`setEvent failed part=${part} err=${formatError(err)}`)
402
- }
403
- try {
404
- if (typeof mod.setPart === 'function') mod.setPart(part)
405
- else traceSessionStart(`setPart unavailable part=${part}`)
406
- } catch (err) {
407
- failed = true
408
- traceSessionStart(`setPart failed part=${part} err=${formatError(err)}`)
409
- }
410
- try { mod.setEmitSink(s => { buf += String(s) }) } catch (err) {
411
- failed = true
412
- traceSessionStart(`setEmitSink failed part=${part} err=${formatError(err)}`)
413
- }
414
- if (part === 'rules') await mod.runRulesPart()
415
- else if (part === 'core') await mod.runCorePart()
416
- else if (part === 'recap') await mod.runRecapPart()
417
- } catch (err) {
418
- failed = true
419
- process.stderr.write(`[hook-pipe] session-start ${part} failed: ${err?.message || err}\n`)
420
- traceSessionStart(`run failed part=${part} err=${formatError(err)}`)
421
- } finally {
422
- try { mod.setEmitSink(null) } catch (err) {
423
- failed = true
424
- traceSessionStart(`clearEmitSink failed part=${part} err=${formatError(err)}`)
425
- }
426
- traceSessionStart(
427
- `run done part=${part} source=${payload?.source || ''} ` +
428
- `bytes=${Buffer.byteLength(buf, 'utf8')} elapsed=${Date.now() - t0}ms failed=${failed}`
429
- )
430
- }
431
- return buf || null
432
- }
433
-
434
- // ── SessionStart: clear-active-session handler ───────────────────────────────
435
-
436
- function handleSessionStartClear() {
437
- // Clear the active orchestrator session pointer so each Mixdog session
438
- // starts fresh. Stored sessions on disk are NOT deleted — only the pointer.
439
- try {
440
- const dataDir = resolvePluginData()
441
- const target = join(dataDir, 'active-session.txt')
442
- try { unlinkSync(target) } catch {}
443
- } catch (err) {
444
- process.stderr.write(`[hook-pipe] session-start clear failed: ${err?.message || err}\n`)
445
- }
446
- return null
447
- }
448
-
449
- // ── dispatch ─────────────────────────────────────────────────────────────────
450
-
451
- async function dispatch(payload) {
452
- const event = payload?.hook_event_name || payload?.hookEventName || ''
453
- const tool = payload?.tool_name || payload?.toolName || ''
454
- const argsArr = payload?._args || []
455
-
456
- // CLI-arg-driven routing (statusline + future entry points without a
457
- // hook_event_name field).
458
- const kindArg = argsArr.find(a => a.startsWith('--kind='))
459
- if (kindArg) {
460
- const kind = kindArg.slice('--kind='.length)
461
- if (kind === 'statusline') return await handleStatusLine(payload)
462
- }
463
-
464
- try {
465
- if (event === 'PreToolUse') {
466
- if (NATIVE_FILE_LOOKUP_TOOLS.has(tool)) {
467
- return handleNativeFileLookup(payload)
468
- }
469
- if (tool === 'Edit' || tool === 'Write' || tool === 'MultiEdit') {
470
- return await handlePreToolSubagent(payload)
471
- }
472
- if (isMcpToolName(tool)) {
473
- return handlePreMcpSandbox(payload)
474
- }
475
- } else if (event === 'PostToolUse') {
476
- return handlePostToolUse(payload)
477
- } else if (event === 'SessionStart') {
478
- const argsArr = payload?._args || []
479
- const hasPart = argsArr.some(a => a.startsWith('--part='))
480
- if (hasPart) {
481
- return await handleSessionStartPart(argsArr, payload)
482
- }
483
- // No --part: clear-active-session entry.
484
- return handleSessionStartClear()
485
- }
486
- } catch (err) {
487
- process.stderr.write(`[hook-pipe] dispatch error: ${err?.message || err}\n`)
488
- }
489
- return null
490
- }
491
-
492
- // ── server ───────────────────────────────────────────────────────────────────
493
-
494
- export function startHookPipeServer() {
495
- if (_server) return _server
496
-
497
- _server = createServer((socket) => {
498
- let buf = ''
499
- let handled = false
500
- // Resource guards: a connection that never sends a newline-terminated
501
- // payload would otherwise grow buf unbounded and hold the socket open
502
- // forever. Cap the buffered bytes and idle-close a stalled connection.
503
- const MAX_BUF_BYTES = 1 << 20 // 1 MiB
504
- const IDLE_TIMEOUT_MS = 30_000
505
- socket.setTimeout(IDLE_TIMEOUT_MS, () => {
506
- if (!handled) { try { socket.destroy() } catch {} }
507
- })
508
- socket.on('data', async (chunk) => {
509
- if (handled) return
510
- buf += chunk.toString('utf8')
511
- if (Buffer.byteLength(buf, 'utf8') > MAX_BUF_BYTES) {
512
- handled = true
513
- process.stderr.write(`[hook-pipe] payload exceeded ${MAX_BUF_BYTES} bytes without newline; dropping connection\n`)
514
- try { socket.destroy() } catch {}
515
- return
516
- }
517
- const firstNl = buf.indexOf('\n')
518
- if (firstNl < 0) return
519
- const firstLine = buf.slice(0, firstNl)
520
-
521
- // Optional `args=` prefix line. When present, the actual payload is the
522
- // second line; otherwise the first line IS the payload.
523
- let args = []
524
- let payloadLine
525
- if (firstLine.startsWith('args=')) {
526
- const secondNl = buf.indexOf('\n', firstNl + 1)
527
- if (secondNl < 0) return // wait for more
528
- args = firstLine.slice(5).split(' ').filter(Boolean)
529
- payloadLine = buf.slice(firstNl + 1, secondNl)
530
- } else {
531
- payloadLine = firstLine
532
- }
533
-
534
- handled = true
535
- let payload = null
536
- try { payload = payloadLine ? JSON.parse(payloadLine) : null } catch {}
537
- if (payload && args.length > 0) payload._args = args
538
-
539
- // Per-request deadline: a hung handler would otherwise hold the hook
540
- // client waiting for EOF forever, stalling the hook step. Race
541
- // dispatch against a real timer; on timeout, write the no-op fallback and
542
- // end the socket so the client unblocks.
543
- const dispatchTimeoutMs = payload ? dispatchTimeoutMsForPayload(payload) : DEFAULT_DISPATCH_TIMEOUT_MS
544
- let timedOut = false
545
- let deadlineTimer = null
546
- let reply = null
547
- try {
548
- if (payload) {
549
- reply = await new Promise((resolve, reject) => {
550
- deadlineTimer = setTimeout(() => {
551
- timedOut = true
552
- reject(new Error(`dispatch exceeded ${dispatchTimeoutMs}ms`))
553
- }, dispatchTimeoutMs)
554
- dispatch(payload).then(resolve, reject)
555
- })
556
- }
557
- } catch (err) {
558
- if (timedOut) {
559
- process.stderr.write(`[hook-pipe] dispatch timed out after ${dispatchTimeoutMs}ms; writing no-op fallback\n`)
560
- try { socket.write('null\n') } catch {}
561
- try { socket.end() } catch {}
562
- return
563
- }
564
- process.stderr.write(`[hook-pipe] handler threw: ${err?.message || err}\n`)
565
- } finally {
566
- if (deadlineTimer) { clearTimeout(deadlineTimer); deadlineTimer = null }
567
- }
568
-
569
- // Response shape:
570
- // • object → JSON-stringified single line (legacy decision protocol)
571
- // • string → raw text (multi-line session-start / statusline output)
572
- // • null/undefined → "null" (no-op marker)
573
- let out
574
- if (reply == null) out = 'null'
575
- else if (typeof reply === 'string') out = reply
576
- else out = JSON.stringify(reply)
577
-
578
- try { socket.write(out) } catch {}
579
- if (!out.endsWith('\n')) { try { socket.write('\n') } catch {} }
580
- try { socket.end() } catch {}
581
- })
582
- socket.on('error', () => {})
583
- })
584
- _server.on('error', (err) => {
585
- const msg = String(err?.message || err || '')
586
- if (err?.code === 'EADDRINUSE' || msg.includes('EADDRINUSE') || msg.includes('Failed to listen')) {
587
- hookPipeDebugStderr(`[hook-pipe] ${PIPE_PATH} already owned by a peer daemon; standby for hook IPC\n`)
588
- _server = null
589
- _started = false
590
- return
591
- }
592
- process.stderr.write(`[hook-pipe] server error: ${err?.message || err}\n`)
593
- })
594
-
595
- const beginListen = () => {
596
- try {
597
- _server.listen(PIPE_PATH, () => {
598
- _started = true
599
- hookPipeDebugStderr(`[hook-pipe] listening on ${PIPE_PATH}\n`)
600
- })
601
- } catch (err) {
602
- process.stderr.write(`[hook-pipe] listen failed: ${err?.message || err}\n`)
603
- _server = null
604
- }
605
- }
606
-
607
- if (process.platform === 'win32') {
608
- // Windows named pipes refuse a second listener with EADDRINUSE on their
609
- // own, so no pre-listen probe is needed.
610
- beginListen()
611
- } else {
612
- // Unix: a leftover socket file from a crashed prior daemon would make
613
- // listen() fail with EADDRINUSE. But blindly unlinking would also steal
614
- // the socket from a live sibling daemon, leaving it orphaned. Probe the
615
- // path first — only unlink when nothing answers.
616
- probeUnixSocketAlive(PIPE_PATH).then((alive) => {
617
- if (alive) {
618
- process.stderr.write(
619
- `[hook-pipe] another mixdog daemon is already listening on ${PIPE_PATH}; refusing to start a second instance\n`
620
- )
621
- _server = null
622
- return
623
- }
624
- try { unlinkSync(PIPE_PATH) } catch {}
625
- beginListen()
626
- })
627
- }
628
- return _server
629
- }
630
-
631
- // Best-effort liveness check for a Unix socket path. Resolves true when
632
- // something is listening (connect succeeds), false when the path is dead
633
- // (ECONNREFUSED) or absent (ENOENT). Other errors / timeout resolve true so
634
- // we err on the side of NOT stealing a possibly-live peer's socket.
635
- function probeUnixSocketAlive(socketPath) {
636
- return new Promise((resolve) => {
637
- let done = false
638
- const finish = (alive) => {
639
- if (done) return
640
- done = true
641
- try { client.destroy() } catch {}
642
- clearTimeout(timer)
643
- resolve(alive)
644
- }
645
- let client
646
- try {
647
- client = createConnection(socketPath)
648
- } catch {
649
- resolve(false)
650
- return
651
- }
652
- const timer = setTimeout(() => finish(true), 300)
653
- client.once('connect', () => finish(true))
654
- client.once('error', (err) => {
655
- const code = err && err.code
656
- finish(!(code === 'ECONNREFUSED' || code === 'ENOENT'))
657
- })
658
- })
659
- }
660
-
661
- export function stopHookPipeServer() {
662
- if (_server) {
663
- try { _server.close() } catch {}
664
- _server = null
665
- _started = false
666
- }
667
- }
668
-
669
- export function isHookPipeServerStarted() {
670
- return _started
671
- }