mixdog 0.9.3 → 0.9.4

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 (329) hide show
  1. package/package.json +7 -3
  2. package/scripts/bench/lead-review-tasks-r3.json +20 -0
  3. package/scripts/bench/lead-review-tasks.json +20 -0
  4. package/scripts/bench/r4-mixed-tasks.json +20 -0
  5. package/scripts/bench/review-tasks.json +20 -0
  6. package/scripts/bench/round-codex.json +114 -0
  7. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  8. package/scripts/bench/round-mixdog-lead.json +269 -0
  9. package/scripts/bench/round-mixdog.json +126 -0
  10. package/scripts/bench/round-r10-bigsample.json +679 -0
  11. package/scripts/bench/round-r11-codexalign.json +257 -0
  12. package/scripts/bench/round-r4-codex.json +114 -0
  13. package/scripts/bench/round-r4-mixed.json +225 -0
  14. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  15. package/scripts/bench/round-r6-codex.json +114 -0
  16. package/scripts/bench/round-r6-solo.json +257 -0
  17. package/scripts/bench/round-r7-full.json +254 -0
  18. package/scripts/bench/round-r8-fulldefault.json +255 -0
  19. package/scripts/bench-run.mjs +215 -29
  20. package/scripts/freevar-smoke.mjs +95 -0
  21. package/scripts/internal-comms-bench.mjs +1 -0
  22. package/scripts/internal-comms-smoke.mjs +10 -9
  23. package/scripts/mouse-probe.mjs +45 -0
  24. package/scripts/output-style-bench.mjs +13 -6
  25. package/scripts/output-style-smoke.mjs +4 -4
  26. package/scripts/provider-toolcall-test.mjs +7 -3
  27. package/scripts/recall-usecase-cases.json +18 -0
  28. package/scripts/recall-usecase-probe.json +6 -0
  29. package/scripts/session-bench.mjs +152 -6
  30. package/scripts/tool-smoke.mjs +23 -63
  31. package/scripts/tui-render-smoke.mjs +90 -0
  32. package/scripts/webhook-smoke.mjs +208 -0
  33. package/src/agents/debugger/AGENT.md +4 -1
  34. package/src/agents/heavy-worker/AGENT.md +6 -5
  35. package/src/agents/maintainer/AGENT.md +4 -0
  36. package/src/agents/reviewer/AGENT.md +2 -1
  37. package/src/agents/worker/AGENT.md +8 -4
  38. package/src/lib/rules-builder.cjs +4 -0
  39. package/src/mixdog-session-runtime.mjs +632 -2042
  40. package/src/output-styles/default.md +34 -9
  41. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  42. package/src/output-styles/minimal.md +4 -1
  43. package/src/output-styles/simple.md +22 -7
  44. package/src/rules/agent/00-common.md +2 -0
  45. package/src/rules/lead/lead-brief.md +12 -0
  46. package/src/rules/lead/lead-tool.md +0 -11
  47. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  48. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  49. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  50. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  51. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  52. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  53. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  54. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
  55. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  56. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  57. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
  58. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  59. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
  60. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  61. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  62. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  63. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  64. package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
  65. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
  66. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +86 -11
  67. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -0
  68. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  69. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  70. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  71. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  72. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
  73. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +732 -0
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  75. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +297 -2123
  76. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +130 -1002
  77. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
  78. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  79. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -0
  80. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  81. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  82. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  83. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  84. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  85. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  86. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  87. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  88. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  89. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  90. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
  91. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  92. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
  93. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +182 -0
  94. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  95. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  96. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  97. package/src/runtime/agent/orchestrator/session/loop.mjs +251 -397
  98. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  99. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
  100. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
  101. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  102. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  103. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  104. package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
  105. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  106. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  107. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  108. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  109. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +32 -41
  110. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  111. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  113. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
  115. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  116. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
  118. package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
  119. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
  120. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  121. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
  122. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  123. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  124. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  125. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  126. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  127. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  128. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  129. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  130. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  131. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  132. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  133. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  134. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
  135. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
  136. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  137. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  138. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  139. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  140. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  141. package/src/runtime/channels/backends/discord.mjs +12 -292
  142. package/src/runtime/channels/index.mjs +229 -663
  143. package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
  144. package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
  145. package/src/runtime/channels/lib/event-queue.mjs +63 -4
  146. package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
  147. package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
  148. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  149. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  150. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  151. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  152. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  153. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  154. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  155. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  156. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  157. package/src/runtime/channels/lib/webhook/deliveries.mjs +312 -0
  158. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  159. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  160. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  161. package/src/runtime/channels/lib/webhook.mjs +36 -570
  162. package/src/runtime/channels/tool-defs.mjs +11 -130
  163. package/src/runtime/memory/index.mjs +201 -1948
  164. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  165. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  166. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  167. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  168. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  169. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  170. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  171. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  172. package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
  173. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  174. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  175. package/src/runtime/memory/lib/memory-recall-store.mjs +22 -2
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  177. package/src/runtime/memory/lib/query-handlers.mjs +780 -0
  178. package/src/runtime/memory/lib/recall-format.mjs +55 -0
  179. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  180. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  181. package/src/runtime/memory/tool-defs.mjs +5 -13
  182. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  183. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  184. package/src/runtime/search/lib/web-tools.mjs +24 -602
  185. package/src/runtime/shared/atomic-file.mjs +26 -1
  186. package/src/runtime/shared/launcher-control.mjs +2 -2
  187. package/src/runtime/shared/tool-primitives.mjs +308 -0
  188. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  189. package/src/runtime/shared/tool-surface.mjs +80 -898
  190. package/src/runtime/shared/transcript-writer.mjs +23 -0
  191. package/src/runtime/shared/update-checker.mjs +7 -4
  192. package/src/session-runtime/config-helpers.mjs +84 -2
  193. package/src/session-runtime/config-lifecycle.mjs +232 -0
  194. package/src/session-runtime/cwd-plugins.mjs +226 -0
  195. package/src/session-runtime/mcp-glue.mjs +177 -0
  196. package/src/session-runtime/model-recency.mjs +111 -0
  197. package/src/session-runtime/native-search.mjs +247 -0
  198. package/src/session-runtime/output-styles.mjs +11 -9
  199. package/src/session-runtime/prewarm.mjs +142 -0
  200. package/src/session-runtime/provider-models.mjs +278 -0
  201. package/src/session-runtime/provider-usage.mjs +120 -0
  202. package/src/session-runtime/quick-model-rows.mjs +170 -0
  203. package/src/session-runtime/quick-search-models.mjs +46 -0
  204. package/src/session-runtime/session-hooks.mjs +93 -0
  205. package/src/session-runtime/settings-api.mjs +319 -0
  206. package/src/session-runtime/tool-catalog.mjs +29 -29
  207. package/src/session-runtime/tool-defs.mjs +84 -0
  208. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  209. package/src/standalone/agent-tool/helpers.mjs +237 -0
  210. package/src/standalone/agent-tool/notify.mjs +107 -0
  211. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  212. package/src/standalone/agent-tool/render.mjs +152 -0
  213. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  214. package/src/standalone/agent-tool.mjs +110 -671
  215. package/src/standalone/channel-worker.mjs +4 -7
  216. package/src/standalone/explore-tool.mjs +30 -9
  217. package/src/standalone/hook-bus/config.mjs +207 -0
  218. package/src/standalone/hook-bus/constants.mjs +90 -0
  219. package/src/standalone/hook-bus/handlers.mjs +481 -0
  220. package/src/standalone/hook-bus/payload.mjs +31 -0
  221. package/src/standalone/hook-bus/rules.mjs +77 -0
  222. package/src/standalone/hook-bus.mjs +77 -870
  223. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  224. package/src/standalone/opencode-go-login.mjs +5 -1
  225. package/src/standalone/provider-admin.mjs +1 -16
  226. package/src/standalone/usage-dashboard.mjs +3 -1
  227. package/src/tui/App.jsx +945 -8094
  228. package/src/tui/app/app-format.mjs +206 -0
  229. package/src/tui/app/channel-pickers.mjs +510 -0
  230. package/src/tui/app/clipboard.mjs +67 -0
  231. package/src/tui/app/core-memory-picker.mjs +210 -0
  232. package/src/tui/app/extension-pickers.mjs +506 -0
  233. package/src/tui/app/input-parsers.mjs +193 -0
  234. package/src/tui/app/maintenance-pickers.mjs +324 -0
  235. package/src/tui/app/model-options.mjs +330 -0
  236. package/src/tui/app/model-picker.mjs +365 -0
  237. package/src/tui/app/onboarding-steps.mjs +400 -0
  238. package/src/tui/app/project-picker.mjs +247 -0
  239. package/src/tui/app/provider-setup-picker.mjs +580 -0
  240. package/src/tui/app/resume-picker.mjs +55 -0
  241. package/src/tui/app/route-pickers.mjs +419 -0
  242. package/src/tui/app/settings-picker.mjs +490 -0
  243. package/src/tui/app/slash-commands.mjs +101 -0
  244. package/src/tui/app/slash-dispatch.mjs +427 -0
  245. package/src/tui/app/text-layout.mjs +46 -0
  246. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  247. package/src/tui/app/transcript-window.mjs +671 -0
  248. package/src/tui/app/use-mouse-input.mjs +460 -0
  249. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  250. package/src/tui/app/use-transcript-scroll.mjs +510 -0
  251. package/src/tui/app/use-transcript-window.mjs +589 -0
  252. package/src/tui/components/ConfirmBar.jsx +1 -1
  253. package/src/tui/components/Picker.jsx +32 -4
  254. package/src/tui/components/PromptInput.jsx +23 -101
  255. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  256. package/src/tui/components/StatusLine.jsx +63 -12
  257. package/src/tui/components/TextEntryPanel.jsx +11 -0
  258. package/src/tui/components/ToolExecution.jsx +52 -594
  259. package/src/tui/components/TranscriptItem.jsx +105 -0
  260. package/src/tui/components/UsagePanel.jsx +18 -4
  261. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  262. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  263. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  264. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  265. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  266. package/src/tui/display-width.mjs +20 -3
  267. package/src/tui/dist/index.mjs +19652 -18630
  268. package/src/tui/engine/agent-job-feed.mjs +133 -0
  269. package/src/tui/engine/notification-plan.mjs +76 -0
  270. package/src/tui/engine/render-timing.mjs +17 -0
  271. package/src/tui/engine/tool-approval.mjs +94 -0
  272. package/src/tui/engine/tool-card-results.mjs +234 -0
  273. package/src/tui/engine/tool-result-status.mjs +135 -0
  274. package/src/tui/engine.mjs +122 -562
  275. package/src/tui/figures.mjs +5 -0
  276. package/src/tui/index.jsx +105 -0
  277. package/src/tui/input-editing.mjs +2 -2
  278. package/src/tui/markdown/format-token.mjs +4 -1
  279. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  280. package/src/tui/theme.mjs +6 -0
  281. package/src/ui/statusline-agents.mjs +213 -0
  282. package/src/ui/statusline-format.mjs +146 -0
  283. package/src/ui/statusline-segments.mjs +148 -0
  284. package/src/ui/statusline.mjs +67 -501
  285. package/src/ui/tool-card.mjs +0 -1
  286. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  287. package/src/workflows/default/WORKFLOW.md +1 -1
  288. package/src/workflows/sequential/WORKFLOW.md +1 -1
  289. package/vendor/ink/build/display-width.js +19 -3
  290. package/vendor/ink/build/ink.js +103 -6
  291. package/vendor/ink/build/log-update.js +17 -3
  292. package/vendor/ink/build/wrap-text.js +125 -0
  293. package/scripts/_test-folder-dialog.mjs +0 -30
  294. package/scripts/fix-brief-fn.mjs +0 -35
  295. package/scripts/fix-format-tool-surface.mjs +0 -24
  296. package/scripts/fix-tool-exec-visible.mjs +0 -42
  297. package/scripts/patch-agent-brief.mjs +0 -48
  298. package/scripts/patch-app.mjs +0 -21
  299. package/scripts/patch-app2.mjs +0 -18
  300. package/scripts/patch-dist-brief.mjs +0 -96
  301. package/scripts/patch-tool-exec.mjs +0 -70
  302. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  303. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  304. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  305. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  306. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  307. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
  308. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  309. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  310. package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
  311. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  312. package/src/standalone/agent-task-status.test.mjs +0 -76
  313. package/src/tui/components/tool-output-format.test.mjs +0 -399
  314. package/src/tui/display-width.test.mjs +0 -35
  315. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  316. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  317. package/src/tui/input-editing.selection.test.mjs +0 -75
  318. package/src/tui/markdown/format-token.test.mjs +0 -354
  319. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  320. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  321. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  322. package/src/tui/paste-fix.test.mjs +0 -119
  323. package/src/tui/prompt-history-store.test.mjs +0 -52
  324. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  325. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  326. package/src/ui/markdown.test.mjs +0 -70
  327. package/src/ui/statusline-context-label.test.mjs +0 -15
  328. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  329. package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
@@ -1,7 +1,6 @@
1
1
  import { statSync } from 'fs';
2
2
  import { isAbsolute, resolve } from 'path';
3
3
  import { trueCasePath } from './path-utils.mjs';
4
- import { findBySuffixStrip } from './path-diagnostics.mjs';
5
4
  import {
6
5
  canonicalizeGlobSlashes,
7
6
  coerceShapeFlex,
@@ -14,13 +13,37 @@ import {
14
13
  normalizeSearchPattern,
15
14
  resolveAgainstCwd,
16
15
  } from './path-utils.mjs';
16
+ import {
17
+ _suggestIndexedPaths,
18
+ basePathDiagnostic,
19
+ buildNotFoundHint,
20
+ isUncOrSmbPath,
21
+ relativePathPrefix,
22
+ relativeSearchResultPath,
23
+ resolveSearchScope,
24
+ stripEmbeddedPathQuotes,
25
+ uncRefusalMessage,
26
+ } from './search-path-diagnostics.mjs';
27
+ // Facade re-export: path-diagnostic helpers moved to search-path-diagnostics.mjs;
28
+ // keep prior importers of search-tool.mjs unchanged.
29
+ export {
30
+ _suggestIndexedPaths,
31
+ basePathDiagnostic,
32
+ buildNotFoundHint,
33
+ isUncOrSmbPath,
34
+ relativePathPrefix,
35
+ relativeSearchResultPath,
36
+ resolveSearchScope,
37
+ stripEmbeddedPathQuotes,
38
+ uncRefusalMessage,
39
+ } from './search-path-diagnostics.mjs';
17
40
  import {
18
41
  buildGlobCacheKey,
19
42
  buildGrepCacheKey,
20
43
  buildGrepRgArgs,
21
44
  DEFAULT_IGNORE_GLOBS,
22
45
  } from './search-builders.mjs';
23
- import { runRg, runRgWindowedLines } from './rg-runner.mjs';
46
+ import { runRg, runRgWindowedLines, rgSupportsPcre2 } from './rg-runner.mjs';
24
47
  import { markScopedCacheIncomplete } from '../../session/cache/scoped-cache-outcome.mjs';
25
48
  import {
26
49
  groupGrepContentByFile,
@@ -35,7 +58,7 @@ import {
35
58
  statPathsForMtime,
36
59
  } from './cache-layers.mjs';
37
60
  import { recordReadSnapshot } from './read-snapshot-runtime.mjs';
38
- import { applyGrepContextLeadPolicy, GREP_CONTEXT_MAX } from './arg-guard.mjs';
61
+ import { applyGrepContextLeadPolicy, GREP_CONTEXT_MAX, hasUnsupportedRipgrepRegex } from './arg-guard.mjs';
39
62
 
40
63
  // Default surrounding-lines window applied by output_mode:'content_with_context'
41
64
  // when the caller does not pass an explicit -A/-B/-C/context. Sized to cover a
@@ -50,51 +73,6 @@ function expandLegacyEscapedAlternationPattern(rawPattern) {
50
73
  return parts.length > 1 ? parts : null;
51
74
  }
52
75
 
53
- // Deterministic ENOENT recovery: when a grep path does not exist, surface
54
- // indexed files that share the missing path's basename, turning a guessed or
55
- // misplaced path (e.g. session/result-compression.mjs vs the real
56
- // tools/result-compression.mjs) into the actual file in one step. Exact
57
- // basename only — no stem/token fuzzing — so the hint is high-signal and
58
- // noise-free. Invariant: every ENOENT runs the same basename lookup; there is
59
- // no "guessed a lot" branch. Returns '' (appends nothing) when no same-named
60
- // indexed file exists or the glob child is unavailable.
61
- async function _suggestIndexedPaths(missingPath, executeChildBuiltinTool, workDir) {
62
- if (typeof executeChildBuiltinTool !== 'function') return '';
63
- const base = String(missingPath).replace(/\\/g, '/').split('/').pop();
64
- // Skip when there is no usable basename or it carries glob magic (the
65
- // pattern, not a literal filename, would not map to a real file).
66
- if (!base || /[*?[\]{}]/.test(base)) return '';
67
- try {
68
- const out = await executeChildBuiltinTool('glob', { pattern: `**/${base}`, head_limit: 6 }, workDir);
69
- if (typeof out !== 'string') return '';
70
- // Drop ONLY the exact diagnostic forms glob emits: the empty-result line
71
- // ("(no files found ...", "(no entries after offset ...") and the
72
- // suffix/warning lines ("... [N more entries]", "... [warning] ...").
73
- // A broad startsWith('(' / '...' / 'Error') would wrongly drop real
74
- // paths like ErrorBoundary.mjs or a "(draft)/x.mjs" leading segment.
75
- const hits = out.split('\n')
76
- .filter((s) => s && !/^\(no (?:files found|entries\b)/.test(s) && !/^\.\.\. \[/.test(s))
77
- .map((s) => s.trim())
78
- .filter(Boolean)
79
- .slice(0, 5);
80
- return hits.length ? `\n[path not found here; same-named indexed file(s): ${hits.join(', ')}]` : '';
81
- } catch {
82
- return '';
83
- }
84
- }
85
-
86
- function relativePathPrefix(pathPrefix, workDir) {
87
- if (!workDir) return pathPrefix;
88
- const cwdFwd = workDir.replace(/\\/g, '/').replace(/\/+$/, '');
89
- const absFwd = String(pathPrefix || '').replace(/\\/g, '/');
90
- const haystack = process.platform === 'win32' ? absFwd.toLocaleLowerCase() : absFwd;
91
- const needle = process.platform === 'win32' ? cwdFwd.toLocaleLowerCase() : cwdFwd;
92
- if (haystack.startsWith(needle + '/') || haystack === needle) {
93
- return absFwd.slice(cwdFwd.length + 1) || '.';
94
- }
95
- return pathPrefix;
96
- }
97
-
98
76
  function relativeGrepLine(line, workDir, pathOnly = false, outputMode = 'content', filenameOmitted = false) {
99
77
  const normalized = normalizeGrepLine(line, pathOnly, outputMode, filenameOmitted);
100
78
  if (!workDir) return normalized;
@@ -113,15 +91,6 @@ function relativeGrepLine(line, workDir, pathOnly = false, outputMode = 'content
113
91
  return normalized;
114
92
  }
115
93
 
116
- function relativeSearchResultPath(path, workDir) {
117
- const normalizedWorkDir = normalizeOutputPath(workDir);
118
- const normalizedAbs = normalizeOutputPath(path);
119
- if (normalizedAbs.startsWith(normalizedWorkDir + '/') || normalizedAbs.startsWith(normalizedWorkDir + '\\')) {
120
- return normalizedAbs.slice(normalizedWorkDir.length + 1);
121
- }
122
- return normalizedAbs;
123
- }
124
-
125
94
  function uniqueStrings(values) {
126
95
  return Array.from(new Set(values.filter((value) => typeof value === 'string' && value)));
127
96
  }
@@ -197,32 +166,6 @@ function isRedundantAllFilesGlob(value) {
197
166
  return g === '**/*' || g === '**';
198
167
  }
199
168
 
200
- function resolveSearchScope(root, workDir) {
201
- return isAbsolute(root) ? resolve(root) : resolveAgainstCwd(root, workDir);
202
- }
203
-
204
- function isUncOrSmbPath(path) {
205
- if (typeof path !== 'string' || !path) return false;
206
- return path.startsWith('\\\\') || path.startsWith('//');
207
- }
208
-
209
- function uncRefusalMessage(toolName, original, resolved) {
210
- const shown = normalizeOutputPath(resolved || original || '');
211
- return `Error: ${toolName} refuses UNC/SMB path ${JSON.stringify(shown)}; remote share access is blocked to prevent NTLM credential leaks`;
212
- }
213
-
214
- function basePathDiagnostic(basePaths, workDir) {
215
- return basePaths.map((basePath) => {
216
- const resolved = resolveSearchScope(basePath, workDir);
217
- try {
218
- const st = statSync(resolved);
219
- return `${normalizeOutputPath(basePath)}: ${st.isDirectory() ? 'path exists (dir)' : 'path exists (file)'}`;
220
- } catch (err) {
221
- return `${normalizeOutputPath(basePath)}: path does not exist (${err?.code || 'ENOENT'})`;
222
- }
223
- }).join('; ');
224
- }
225
-
226
169
  function grepMissingPatternMessage() {
227
170
  return 'Error: grep requires pattern.';
228
171
  }
@@ -276,11 +219,37 @@ function formatGrepOutput({ windowed, totalWindowed, totalKnown, headLimit, offs
276
219
 
277
220
  export async function executeGrepTool(args, workDir, executeChildBuiltinTool, readStateScope = null, options = {}) {
278
221
  args = normalizeGrepArgs(args);
222
+ // Fan-out guard: batch multiple string paths sequentially, mirroring
223
+ // code_graph files[] batching. Recursive calls pass a single string
224
+ // path, so recursion bottoms out after one level.
225
+ if (Array.isArray(args.path)) {
226
+ const GREP_PATH_CAP = 10;
227
+ const seen = new Set();
228
+ const list = args.path
229
+ .map(p => typeof p === 'string' ? p.trim() : '')
230
+ .filter(p => p && !seen.has(p) && seen.add(p));
231
+ if (list.length > 1) {
232
+ const capped = list.slice(0, GREP_PATH_CAP);
233
+ const parts = [];
234
+ for (const p of capped) {
235
+ let body;
236
+ try {
237
+ body = await executeGrepTool({ ...args, path: p }, workDir, executeChildBuiltinTool, readStateScope, options);
238
+ } catch (err) {
239
+ body = `Error: ${err && err.message ? err.message : err}`;
240
+ }
241
+ parts.push(`# grep ${p}\n${body}`);
242
+ }
243
+ if (list.length > GREP_PATH_CAP) parts.push(`[capped at ${GREP_PATH_CAP} of ${list.length} paths]`);
244
+ return parts.join('\n\n');
245
+ }
246
+ args.path = list[0];
247
+ }
279
248
  // Shape context immediately before deriving rg flags. This keeps the
280
249
  // Lead-direct MCP path and direct executeGrepTool callers on the same
281
250
  // policy even if they bypass or race the outer builtin arg guard.
282
251
  applyGrepContextLeadPolicy(args);
283
- args.path = normalizeInputPath(args.path);
252
+ args.path = stripEmbeddedPathQuotes(normalizeInputPath(args.path));
284
253
  args.pattern = coerceShapeFlex(args.pattern);
285
254
  args.glob = coerceShapeFlex(args.glob);
286
255
  const rawPattern = args.pattern;
@@ -303,13 +272,32 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
303
272
 
304
273
  const GREP_MULTILINE_PATTERN_CAP = 5;
305
274
  const GREP_ARRAY_PATTERN_CAP = 20;
306
- const multilineMode = args.multiline === true;
275
+ // Rescue: a pattern containing a literal two-char "\n" (backslash + n)
276
+ // outside multiline mode makes rg hard-error with "the literal '\"\\n\"'
277
+ // is not allowed in a regex" — rg requires -U/--multiline before a
278
+ // pattern is allowed to match across a newline. Rather than reject-and-
279
+ // retry, auto-enable multiline whenever any pattern carries a literal
280
+ // \n; an explicit multiline:true from the caller still wins outright.
281
+ const patternsWantMultiline = patterns.some((p) => /\\n/.test(p));
282
+ const multilineMode = args.multiline === true || patternsWantMultiline;
307
283
  if (multilineMode && patterns.length > GREP_MULTILINE_PATTERN_CAP) {
308
284
  return `Error: multiline:true with more than ${GREP_MULTILINE_PATTERN_CAP} patterns is not allowed (got ${patterns.length}); split into separate grep calls`;
309
285
  }
310
286
  if (patterns.length > GREP_ARRAY_PATTERN_CAP) {
311
287
  return `Error: pattern array exceeds the ${GREP_ARRAY_PATTERN_CAP}-pattern cap (got ${patterns.length}); split into separate grep calls`;
312
288
  }
289
+ // Rescue: lookaround/backreference patterns are rejected by rg's default
290
+ // Rust regex engine ("look-around ... is not supported" / "backreferences
291
+ // ... not supported"). rg builds compiled with the optional `pcre2`
292
+ // feature accept the same syntax via -P/--pcre2. Probe capability once
293
+ // (cached) and route there instead of hard-erroring; when the installed
294
+ // rg has no PCRE2 support, fall through unchanged and let the existing
295
+ // arg-guard rejection (or rg's own runtime error) stand.
296
+ const patternsWantPcre2 = hasUnsupportedRipgrepRegex(patterns);
297
+ const pcre2Mode = patternsWantPcre2 && await rgSupportsPcre2();
298
+ if (patternsWantPcre2 && !pcre2Mode) {
299
+ return `Error: grep pattern uses regex syntax ripgrep does not support here (lookaround/backrefs), and the installed rg build has no PCRE2 support (-P unavailable). Use plain pattern arrays or simpler regex.`;
300
+ }
313
301
 
314
302
  let searchPath = args.path || '.';
315
303
  const rawGlob = args.glob;
@@ -454,6 +442,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
454
442
  multilineMode,
455
443
  onlyMatching: args['-o'] === true,
456
444
  fileType,
445
+ pcre2: pcre2Mode,
457
446
  });
458
447
  // Single-file grep registers a whole-file read snapshot (parity with
459
448
  // apply_patch), satisfying the read-before-edit guard while keeping drift
@@ -479,10 +468,8 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
479
468
  try { grepStat = statSync(grepResolvedPath); }
480
469
  catch (err) {
481
470
  const msg = `Error: path does not exist: ${normalizeOutputPath(grepResolvedPath)} (${err?.code || 'ENOENT'})`;
482
- const suffixHit = findBySuffixStrip(workDir, grepResolvedPath);
483
- if (suffixHit) {
484
- return msg + ` Not found at this path; the same filename exists at: "${normalizeOutputPath(suffixHit)}". Search that path directly.`;
485
- }
471
+ const hint = buildNotFoundHint(workDir, grepResolvedPath, 'Search', err?.code);
472
+ if (hint) return msg + hint;
486
473
  return msg + await _suggestIndexedPaths(grepResolvedPath, executeChildBuiltinTool, workDir);
487
474
  }
488
475
  const filenameOmitted = grepStat.isFile();
@@ -519,6 +506,7 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
519
506
  multilineMode,
520
507
  fileType,
521
508
  onlyMatching: args['-o'] === true,
509
+ pcre2: pcre2Mode,
522
510
  });
523
511
  let windowed;
524
512
  let totalWindowed = 0;
@@ -736,8 +724,8 @@ export async function executeGrepTool(args, workDir, executeChildBuiltinTool, re
736
724
  export async function executeGlobTool(args, workDir, options = {}) {
737
725
  args = normalizeGlobArgs(args);
738
726
  args.path = Array.isArray(args.path)
739
- ? args.path.map((p) => normalizeInputPath(p)).filter((p) => typeof p === 'string' && p)
740
- : normalizeInputPath(args.path);
727
+ ? args.path.map((p) => stripEmbeddedPathQuotes(normalizeInputPath(p))).filter((p) => typeof p === 'string' && p)
728
+ : stripEmbeddedPathQuotes(normalizeInputPath(args.path));
741
729
  if (Array.isArray(args.path) && args.path.length === 0) {
742
730
  return 'Error: path array must contain at least one base directory';
743
731
  }
@@ -866,8 +854,9 @@ export async function executeGlobTool(args, workDir, options = {}) {
866
854
  rgArgs.push('.');
867
855
  try { statSync(rgCwd); }
868
856
  catch (err) {
857
+ const hint = buildNotFoundHint(workDir, rgCwd, 'Search', err?.code);
869
858
  return {
870
- error: `path does not exist: ${normalizeOutputPath(rgCwd)} (${err?.code || 'ENOENT'})`,
859
+ error: `path does not exist: ${normalizeOutputPath(rgCwd)} (${err?.code || 'ENOENT'})${hint}`,
871
860
  paths: [],
872
861
  stdoutTruncated: false,
873
862
  };
@@ -0,0 +1,161 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
+ import * as fsPromises from 'fs/promises';
3
+ import { join } from 'path';
4
+ import { getPluginData } from '../../config.mjs';
5
+
6
+ // One-shot sweep of stale shell-job artefacts. Each backgrounded `bash`
7
+ // emits five files (.json/.done/.exit/.stdout.log/.stderr.log); the .done
8
+ // flag is written when the job exits, so a .done file older than
9
+ // SHELL_JOB_STALE_MS is the invariant proof its sibling files are also
10
+ // safe to remove. Active and recently-completed jobs are kept so
11
+ // `task` status/read/wait readers still find them. Runs once per mcp child
12
+ // lifetime on first getShellJobsDir() call. Async dirent walk + parallel
13
+ // stat/unlink keeps the main event loop free; fire-and-forget so the
14
+ // synchronous caller receives `dir` immediately.
15
+ const SHELL_JOB_STALE_MS = 24 * 60 * 60 * 1000;
16
+ let shellJobsSwept = false;
17
+ async function sweepStaleShellJobs(dir) {
18
+ if (shellJobsSwept) return;
19
+ shellJobsSwept = true;
20
+ const cutoff = Date.now() - SHELL_JOB_STALE_MS;
21
+ let names;
22
+ try { names = await fsPromises.readdir(dir); } catch { return; }
23
+ const expired = [];
24
+ await Promise.all(names.map(async (name) => {
25
+ if (!name.endsWith('.done')) return;
26
+ const p = join(dir, name);
27
+ try {
28
+ const st = await fsPromises.stat(p);
29
+ if (st.mtimeMs < cutoff) expired.push(name.slice(0, -5));
30
+ } catch {}
31
+ }));
32
+ // Orphan reclaim: a crashed wrapper leaves <id>.json with no .done —
33
+ // forever. Invariant proofs of death (either suffices, both gated on the
34
+ // stale cutoff so a young orphan can't race its own spawn):
35
+ // a) deadline: the wrapper enforces timeoutMs, so an entry older than
36
+ // timeoutMs + grace cannot still be running — pid-reuse-proof. Only
37
+ // trusted on runtime proof: detail.timeoutEnforced:true (PS wrapper,
38
+ // unconditional) or the <id>.enforced marker the posix wrapper
39
+ // touches when its `timeout` branch actually runs.
40
+ // b) ESRCH: the recorded pid no longer exists. Alone this misses pids
41
+ // recycled by unrelated live processes, hence (a).
42
+ const ORPHAN_DEADLINE_GRACE_MS = 30 * 60_000;
43
+ const doneSet = new Set(names.filter(n => n.endsWith('.done')).map(n => n.slice(0, -5)));
44
+ await Promise.all(names.map(async (name) => {
45
+ if (!name.endsWith('.json')) return;
46
+ const jobId = name.slice(0, -5);
47
+ if (doneSet.has(jobId)) return;
48
+ const p = join(dir, name);
49
+ try {
50
+ const st = await fsPromises.stat(p);
51
+ if (st.mtimeMs >= cutoff) return;
52
+ const detail = JSON.parse(await fsPromises.readFile(p, 'utf-8'));
53
+ const tmo = Number(detail?.timeoutMs);
54
+ const enforced = detail?.timeoutEnforced === true
55
+ || existsSync(join(dir, `${jobId}.enforced`));
56
+ const deadlinePassed = enforced
57
+ && Number.isFinite(tmo) && tmo > 0
58
+ && (Date.now() - st.mtimeMs) > tmo + ORPHAN_DEADLINE_GRACE_MS;
59
+ if (!deadlinePassed) {
60
+ const pid = Number(detail?.pid);
61
+ if (Number.isFinite(pid) && pid > 0) {
62
+ try { process.kill(pid, 0); return; } // alive (or EPERM → treated dead only via ESRCH below)
63
+ catch (e) { if (e?.code !== 'ESRCH') return; }
64
+ }
65
+ }
66
+ expired.push(jobId);
67
+ } catch {}
68
+ }));
69
+ // Owner sidecar markers carry a dynamic `.owner-<pid>` suffix, so they
70
+ // can't sit in the fixed-extension list — map each expired jobId to the
71
+ // marker name(s) actually present in this listing and unlink those too.
72
+ const expiredSet = new Set(expired);
73
+ const ownerMarkers = names.filter((n) => {
74
+ const i = n.lastIndexOf('.owner-');
75
+ return i > 0 && expiredSet.has(n.slice(0, i));
76
+ });
77
+ await Promise.all([
78
+ ...expired.flatMap((jobId) =>
79
+ ['.json', '.done', '.exit', '.enforced', '.exit.cmd.sh', '.exit.cmd.ps1', '.stdout.log', '.stderr.log'].map((ext) =>
80
+ fsPromises.unlink(join(dir, jobId + ext)).catch(() => {}),
81
+ ),
82
+ ),
83
+ ...ownerMarkers.map((n) => fsPromises.unlink(join(dir, n)).catch(() => {})),
84
+ ]);
85
+ }
86
+
87
+ export function getShellJobsDir() {
88
+ const dir = join(getPluginData(), 'shell-jobs');
89
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
90
+ sweepStaleShellJobs(dir);
91
+ return dir;
92
+ }
93
+
94
+ export function shellJobDetailPath(jobId) { return join(getShellJobsDir(), `${jobId}.json`); }
95
+ export function shellJobStdoutPath(jobId) { return join(getShellJobsDir(), `${jobId}.stdout.log`); }
96
+ export function shellJobStderrPath(jobId) { return join(getShellJobsDir(), `${jobId}.stderr.log`); }
97
+ export function shellJobExitPath(jobId) { return join(getShellJobsDir(), `${jobId}.exit`); }
98
+ export function shellJobDonePath(jobId) { return join(getShellJobsDir(), `${jobId}.done`); }
99
+ // Runtime proof that the posix wrapper's `timeout` branch actually ran: the
100
+ // wrapper touches this marker immediately before exec'ing `timeout`, so its
101
+ // existence is never optimistic (no spawn-time probe can guarantee the
102
+ // wrapper's own env/cwd resolution). PS jobs don't need it — their wrapper
103
+ // enforces unconditionally and records detail.timeoutEnforced:true.
104
+ export function shellJobEnforcedPath(jobId) { return join(getShellJobsDir(), `${jobId}.enforced`); }
105
+ // Owner sidecar marker: a zero-byte file whose NAME encodes the owning CC host
106
+ // (claude.exe) pid — `<jobId>.owner-<pid>`. It lets the statusline owner-filter
107
+ // jobs from a SINGLE directory listing (no per-job JSON read) so the filter can
108
+ // precede its per-tick scan cap — otherwise other sessions' newer jobs evict
109
+ // this session's live jobs before filtering. Swept alongside the other
110
+ // artefacts.
111
+ export function shellJobOwnerPath(jobId, pid) { return join(getShellJobsDir(), `${jobId}.owner-${pid}`); }
112
+
113
+ // Resolve the CLI host pid that owns a freshly-spawned job. Standalone CLI
114
+ // execution is process-owned: background tasks are scoped to this CLI lifetime,
115
+ // and the pid stamp is only for statusline filtering / legacy sidecars.
116
+ export function resolveJobOwnerHostPid(clientHostPid) {
117
+ const explicit = Number(clientHostPid);
118
+ if (Number.isInteger(explicit) && explicit > 0) return explicit;
119
+ const envPid = Number(process.env.MIXDOG_OWNER_HOST_PID);
120
+ if (Number.isInteger(envPid) && envPid > 0) return envPid;
121
+ return null;
122
+ }
123
+
124
+ export function writeShellJobDetail(detail) {
125
+ // Session scope stamp: every job record is tagged with the CC host pid
126
+ // that owns it (the claude.exe pid). This is the SAME physical pid the
127
+ // statusline shim passes as --client-host-pid, so the statusline can count
128
+ // only its own session's jobs by exact pid equality (no heuristic). The
129
+ // spawn sites set detail.ownerHostPid from the per-request threaded
130
+ // clientHostPid (resolveJobOwnerHostPid); this fallback only stamps the
131
+ // single-client env value when no spawn-site pid was set, and NEVER
132
+ // overwrites an existing stamp — so the field round-trips through disk on
133
+ // refresh/kill and a correct per-terminal stamp is preserved across rewrites.
134
+ if (detail && detail.ownerHostPid == null) {
135
+ const hostPid = Number(process.env.MIXDOG_OWNER_HOST_PID);
136
+ if (Number.isInteger(hostPid) && hostPid > 0) detail.ownerHostPid = hostPid;
137
+ }
138
+ writeFileSync(shellJobDetailPath(detail.jobId), JSON.stringify(detail, null, 2), 'utf-8');
139
+ // Owner sidecar: encode the resolved owner pid in the marker filename so the
140
+ // statusline can owner-filter from the directory listing alone (before its
141
+ // scan cap). Idempotent zero-byte write — safe to repeat on refresh/kill
142
+ // rewrites. Skipped when no owner is known (legacy/unattributed jobs).
143
+ if (detail && Number.isInteger(detail.ownerHostPid) && detail.ownerHostPid > 0) {
144
+ try { writeFileSync(shellJobOwnerPath(detail.jobId, detail.ownerHostPid), '', 'utf-8'); }
145
+ catch { /* best-effort marker */ }
146
+ }
147
+ }
148
+
149
+ export function readShellJobDetail(jobId) {
150
+ try {
151
+ const p = shellJobDetailPath(jobId);
152
+ if (!existsSync(p)) return null;
153
+ return JSON.parse(readFileSync(p, 'utf-8'));
154
+ } catch {
155
+ return null;
156
+ }
157
+ }
158
+
159
+ export function buildJobNotFoundMessage(jobId) {
160
+ return `Error: task not found: ${jobId}`;
161
+ }
@@ -0,0 +1,108 @@
1
+ import { spawn, spawnSync } from 'child_process';
2
+
3
+ // Process/pid lifecycle helpers for background shell jobs: liveness probing,
4
+ // tree-kill, the module-level live-job pid registry, and the CLI-shutdown exit
5
+ // hook that reaps owned children. Async jobs are intentionally CLI-owned; no
6
+ // restart replay or daemon handoff is attempted.
7
+
8
+ export function isPidAlive(pid) {
9
+ if (!Number.isFinite(pid) || pid <= 0) return false;
10
+ try {
11
+ process.kill(pid, 0);
12
+ return true;
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+
18
+ export function killProcessTree(pid, signal = 'SIGTERM') {
19
+ if (!Number.isFinite(pid) || pid <= 0) return false;
20
+ try {
21
+ if (process.platform === 'win32') {
22
+ spawn('taskkill', ['/pid', String(pid), '/t', '/f'], { windowsHide: true, stdio: 'ignore' });
23
+ } else {
24
+ try { process.kill(-pid, signal); }
25
+ catch { process.kill(pid, signal); }
26
+ // SIGKILL escalation: a background child that ignores SIGTERM must
27
+ // not survive (foreground treeKill / persistent _killProcessTree
28
+ // already do this). After a 3s grace, force-kill the group/pid.
29
+ // unref so this backstop never holds the host process open.
30
+ if (signal === 'SIGTERM') {
31
+ const t = setTimeout(() => {
32
+ try { process.kill(-pid, 'SIGKILL'); }
33
+ catch { try { process.kill(pid, 'SIGKILL'); } catch { /* already gone */ } }
34
+ }, 3000);
35
+ if (t.unref) t.unref();
36
+ }
37
+ }
38
+ return true;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ // Module-level tracking of live background-job pids so CLI shutdown can reap
45
+ // owned children. Async jobs are intentionally CLI-owned; no restart replay or
46
+ // daemon handoff is attempted.
47
+ export const _liveJobPids = new Set();
48
+ export const _liveJobIdsByPid = new Map();
49
+ let _shellJobsExitHookInstalled = false;
50
+ export function _registerLiveJobPid(pid, jobId = null) {
51
+ if (Number.isFinite(pid) && pid > 0) {
52
+ _liveJobPids.add(pid);
53
+ if (jobId) _liveJobIdsByPid.set(pid, jobId);
54
+ }
55
+ }
56
+ export function _unregisterLiveJobPid(pid) {
57
+ if (Number.isFinite(pid) && pid > 0) {
58
+ _liveJobPids.delete(pid);
59
+ _liveJobIdsByPid.delete(pid);
60
+ }
61
+ }
62
+ export function _killLiveJobPid(pid, { sync = false } = {}) {
63
+ try {
64
+ if (process.platform === 'win32') {
65
+ if (sync) {
66
+ spawnSync('taskkill.exe', ['/pid', String(pid), '/t', '/f'], {
67
+ windowsHide: true,
68
+ stdio: 'ignore',
69
+ timeout: 1500,
70
+ });
71
+ } else {
72
+ spawn('taskkill', ['/pid', String(pid), '/t', '/f'], { windowsHide: true, stdio: 'ignore' });
73
+ }
74
+ } else {
75
+ try { process.kill(-pid, 'SIGKILL'); }
76
+ catch { try { process.kill(pid, 'SIGKILL'); } catch { /* ignore */ } }
77
+ }
78
+ return true;
79
+ } catch {
80
+ return false;
81
+ }
82
+ }
83
+ export function _sweepLiveJobsSync() {
84
+ for (const pid of _liveJobPids) {
85
+ _killLiveJobPid(pid, { sync: true });
86
+ }
87
+ _liveJobPids.clear();
88
+ _liveJobIdsByPid.clear();
89
+ }
90
+ function _ensureProcessListenerHeadroom(events, extra = 1) {
91
+ try {
92
+ if (typeof process.getMaxListeners !== 'function' || typeof process.setMaxListeners !== 'function') return;
93
+ const current = process.getMaxListeners();
94
+ if (current === 0) return;
95
+ let needed = current;
96
+ for (const event of events) needed = Math.max(needed, process.listenerCount(event) + extra);
97
+ if (needed > current) process.setMaxListeners(needed);
98
+ } catch { /* ignore */ }
99
+ }
100
+ export function _installShellJobsExitHook() {
101
+ if (_shellJobsExitHookInstalled) return;
102
+ _shellJobsExitHookInstalled = true;
103
+ _ensureProcessListenerHeadroom(['exit', 'SIGTERM', 'SIGINT', 'SIGHUP'], 1);
104
+ try { process.on('exit', _sweepLiveJobsSync); } catch { /* ignore */ }
105
+ try { process.on('SIGTERM', _sweepLiveJobsSync); } catch { /* ignore */ }
106
+ try { process.on('SIGINT', _sweepLiveJobsSync); } catch { /* ignore */ }
107
+ try { process.on('SIGHUP', _sweepLiveJobsSync); } catch { /* ignore */ }
108
+ }