mixdog 0.9.124 → 0.9.126

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 (247) hide show
  1. package/LICENSES/codex-NOTICE.txt +3 -0
  2. package/LICENSES/ripgrep-NOTICE.txt +6 -0
  3. package/README.md +14 -8
  4. package/package.json +8 -3
  5. package/scripts/build-token-addon.mjs +32 -0
  6. package/scripts/daemon-stub.mjs +6 -9
  7. package/scripts/fixtures/patch-replay-corpus.json +128 -0
  8. package/scripts/lib/recall-bench-eval.mjs +241 -0
  9. package/scripts/native-spawn-test-runtime.mjs +16 -0
  10. package/scripts/recall-bench-cases.json +7 -7
  11. package/scripts/recall-event-cases.json +43 -0
  12. package/scripts/recall-history-cases.json +38 -0
  13. package/scripts/recall-quality-cases.json +17 -9
  14. package/scripts/recall-repo-cases.json +26 -0
  15. package/scripts/recall-usecase-cases.json +16 -16
  16. package/scripts/tool-stress.mjs +3 -0
  17. package/scripts/verify-release-assets.mjs +46 -1
  18. package/src/app.mjs +9 -11
  19. package/src/cli.mjs +2 -2
  20. package/src/defaults/memory-promote-prompt.md +20 -0
  21. package/src/defaults/skills/setup/SKILL.md +462 -119
  22. package/src/headless-command.mjs +43 -35
  23. package/src/headless-exec.mjs +686 -0
  24. package/src/headless-exec.test.mjs +277 -0
  25. package/src/help.mjs +5 -4
  26. package/src/lib/keychain-cjs.cjs +32 -2
  27. package/src/lib/rules-builder.cjs +32 -15
  28. package/src/rules/agent/00-common.md +3 -2
  29. package/src/rules/agent/00-core.md +3 -4
  30. package/src/rules/lead/01-general.md +11 -1
  31. package/src/rules/shared/01-tool.md +57 -55
  32. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +5 -15
  33. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +5 -5
  34. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -7
  35. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +69 -2
  36. package/src/runtime/agent/orchestrator/agent-trace.mjs +2 -0
  37. package/src/runtime/agent/orchestrator/context/collect.mjs +106 -62
  38. package/src/runtime/agent/orchestrator/mcp/client.mjs +97 -12
  39. package/src/runtime/agent/orchestrator/mcp/security.test.mjs +64 -0
  40. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +48 -2
  41. package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +23 -1
  42. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +3 -4
  43. package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +21 -4
  44. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +5 -4
  45. package/src/runtime/agent/orchestrator/providers/registry.mjs +29 -14
  46. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +3 -6
  47. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -3
  48. package/src/runtime/agent/orchestrator/session/compact/handoff.mjs +247 -0
  49. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +85 -19
  50. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +17 -2
  51. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +1 -1
  52. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +3 -2
  53. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +4 -33
  54. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +1 -32
  55. package/src/runtime/agent/orchestrator/session/loop.mjs +0 -1
  56. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +9 -23
  57. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +4 -3
  58. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +0 -4
  59. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +41 -23
  60. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +21 -6
  61. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +1 -9
  62. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +3 -30
  63. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +2 -7
  64. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +41 -58
  65. package/src/runtime/agent/orchestrator/session/read-dedup.mjs +0 -1
  66. package/src/runtime/agent/orchestrator/session/store/listing.mjs +0 -2
  67. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +0 -2
  68. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +12 -7
  69. package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +50 -0
  70. package/src/runtime/agent/orchestrator/session/token-native-worker.mjs +35 -0
  71. package/src/runtime/agent/orchestrator/session/token-native.mjs +210 -108
  72. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +65 -35
  73. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +43 -3
  74. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +94 -119
  75. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +12 -15
  76. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +38 -1
  77. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-insights.mjs +21 -147
  78. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +137 -176
  79. package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +57 -0
  80. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +209 -64
  81. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +37 -0
  82. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +118 -0
  83. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +13 -12
  84. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +5 -34
  85. package/src/runtime/agent/orchestrator/tools/builtin/shell-direct-exe.mjs +183 -0
  86. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +230 -756
  87. package/src/runtime/agent/orchestrator/tools/builtin/shell-lossless-compact.mjs +396 -0
  88. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +21 -0
  89. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +50 -121
  90. package/src/runtime/agent/orchestrator/tools/builtin.mjs +13 -1
  91. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -4
  92. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +21 -3
  93. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +32 -2
  94. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +30 -5
  95. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +34 -0
  96. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -4
  97. package/src/runtime/agent/orchestrator/tools/destructive-warning.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +47 -21
  99. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.test.mjs +19 -0
  100. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  101. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +475 -0
  102. package/src/runtime/agent/orchestrator/tools/lib/shell-spawn-retry.mjs +18 -21
  103. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +4 -0
  104. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +29 -8
  105. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +37 -9
  106. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +5 -1
  107. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +243 -72
  108. package/src/runtime/agent/orchestrator/tools/patch-binary-fetcher.mjs +9 -3
  109. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  110. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +6 -6
  111. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +2 -2
  112. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +42 -306
  113. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +4 -87
  114. package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +3 -6
  115. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +35 -24
  116. package/src/runtime/agent/orchestrator/tools/spawn-binary-fetcher.mjs +148 -0
  117. package/src/runtime/agent/orchestrator/tools/spawn-binary-fetcher.test.mjs +52 -0
  118. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +26 -0
  119. package/src/runtime/agent/orchestrator/tools/{token-binary-fetcher.mjs → token-addon-fetcher.mjs} +58 -64
  120. package/src/runtime/agent/orchestrator/tools/token-manifest.json +11 -11
  121. package/src/runtime/attachments/store.mjs +31 -2
  122. package/src/runtime/attachments/store.test.mjs +21 -0
  123. package/src/runtime/channels/lib/boot-profile.mjs +5 -4
  124. package/src/runtime/channels/lib/crash-log.mjs +3 -3
  125. package/src/runtime/channels/lib/executor.mjs +3 -2
  126. package/src/runtime/channels/lib/inbound-routing.mjs +2 -2
  127. package/src/runtime/channels/lib/memory-client.mjs +3 -4
  128. package/src/runtime/channels/lib/runtime-paths.mjs +4 -6
  129. package/src/runtime/channels/lib/scheduler.mjs +2 -2
  130. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +16 -29
  131. package/src/runtime/channels/lib/webhook/deliveries.mjs +11 -4
  132. package/src/runtime/channels/lib/webhook/deliveries.test.mjs +18 -0
  133. package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +122 -22
  134. package/src/runtime/channels/lib/webhook/relay-tunnel.test.mjs +68 -0
  135. package/src/runtime/channels/lib/webhook.mjs +10 -5
  136. package/src/runtime/channels/lib/worker-main.mjs +5 -4
  137. package/src/runtime/media/adapters/codex-image.mjs +6 -1
  138. package/src/runtime/media/adapters/gemini-image.mjs +2 -1
  139. package/src/runtime/media/adapters/gemini-video.mjs +6 -4
  140. package/src/runtime/media/adapters/xai-media.mjs +3 -4
  141. package/src/runtime/media/download.mjs +102 -0
  142. package/src/runtime/media/download.test.mjs +52 -0
  143. package/src/runtime/media/jobs.mjs +10 -0
  144. package/src/runtime/memory/index.mjs +2 -3
  145. package/src/runtime/memory/lib/agent-ipc.mjs +3 -5
  146. package/src/runtime/memory/lib/archive-security.test.mjs +108 -0
  147. package/src/runtime/memory/lib/http-router.mjs +6 -0
  148. package/src/runtime/memory/lib/http-wire.mjs +9 -5
  149. package/src/runtime/memory/lib/http-wire.test.mjs +15 -0
  150. package/src/runtime/memory/lib/ko-morph.mjs +72 -19
  151. package/src/runtime/memory/lib/memory-action-handlers.mjs +8 -1
  152. package/src/runtime/memory/lib/memory-config-flags.mjs +2 -4
  153. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +140 -11
  154. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +43 -0
  155. package/src/runtime/memory/lib/memory-cycle2.mjs +58 -5
  156. package/src/runtime/memory/lib/memory-recall-id-patch.mjs +1 -1
  157. package/src/runtime/memory/lib/memory-recall-store.mjs +263 -23
  158. package/src/runtime/memory/lib/memory-retrievers.mjs +6 -4
  159. package/src/runtime/memory/lib/memory-session-merge.mjs +3 -1
  160. package/src/runtime/memory/lib/memory-text-utils.mjs +112 -38
  161. package/src/runtime/memory/lib/memory.mjs +40 -3
  162. package/src/runtime/memory/lib/pg/supervisor.mjs +5 -5
  163. package/src/runtime/memory/lib/query-handlers.mjs +581 -86
  164. package/src/runtime/memory/lib/query-maintenance-handlers.mjs +3 -3
  165. package/src/runtime/memory/lib/recall-event-context.mjs +177 -0
  166. package/src/runtime/memory/lib/recall-format.mjs +132 -16
  167. package/src/runtime/memory/lib/recall-order.mjs +20 -0
  168. package/src/runtime/memory/lib/recall-page-cursor.mjs +35 -0
  169. package/src/runtime/memory/lib/recall-scoring.mjs +18 -17
  170. package/src/runtime/memory/lib/runtime-fetcher.mjs +68 -17
  171. package/src/runtime/memory/lib/session-ingest-runtime.mjs +7 -4
  172. package/src/runtime/memory/lib/tool-call-handler.mjs +2 -1
  173. package/src/runtime/memory/lib/transcript-ingest.mjs +17 -8
  174. package/src/runtime/memory/tool-defs.mjs +4 -2
  175. package/src/runtime/shared/background-tasks.mjs +5 -4
  176. package/src/runtime/shared/bounded-download.mjs +100 -0
  177. package/src/runtime/shared/bounded-download.test.mjs +41 -0
  178. package/src/runtime/shared/pristine-execution-contract.json +7 -12
  179. package/src/runtime/shared/pristine-execution.mjs +8 -8
  180. package/src/runtime/shared/runtime-root.mjs +83 -0
  181. package/src/runtime/shared/runtime-root.test.mjs +34 -0
  182. package/src/runtime/shared/service-discovery.mjs +4 -6
  183. package/src/runtime/shared/time-format.mjs +71 -0
  184. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  185. package/src/runtime/shared/transcript-writer.mjs +5 -1
  186. package/src/session-runtime/context-status.mjs +1 -1
  187. package/src/session-runtime/cwd-plugins.mjs +4 -6
  188. package/src/session-runtime/effort.mjs +17 -10
  189. package/src/session-runtime/lifecycle-api.mjs +4 -13
  190. package/src/session-runtime/mcp-glue.mjs +8 -11
  191. package/src/session-runtime/model-capabilities.mjs +3 -2
  192. package/src/session-runtime/model-route-api.mjs +28 -4
  193. package/src/session-runtime/model-settings-persist.test.mjs +99 -0
  194. package/src/session-runtime/prewarm.mjs +53 -17
  195. package/src/session-runtime/prewarm.test.mjs +53 -0
  196. package/src/session-runtime/provider-request-snapshot.mjs +1 -1
  197. package/src/session-runtime/runtime-core.mjs +30 -1
  198. package/src/session-runtime/session-lifecycle.mjs +9 -3
  199. package/src/session-runtime/session-turn-api.mjs +5 -3
  200. package/src/session-runtime/settings-api.mjs +7 -6
  201. package/src/session-runtime/tool-catalog-schema.mjs +1 -1
  202. package/src/session-runtime/tool-catalog.mjs +27 -16
  203. package/src/session-runtime/tool-defs.mjs +1 -1
  204. package/src/session-runtime/tool-policy-refresh.mjs +107 -0
  205. package/src/session-runtime/tool-policy-surface.test.mjs +163 -0
  206. package/src/session-runtime/tool-surface.mjs +1 -0
  207. package/src/session-runtime/warmup-schedulers.mjs +0 -8
  208. package/src/session-runtime/workflow-agents-api.mjs +4 -1
  209. package/src/session-runtime/workflow.mjs +28 -0
  210. package/src/standalone/agent-tool/tool-def.mjs +1 -1
  211. package/src/standalone/agent-tool/worker-index.mjs +2 -2
  212. package/src/standalone/agent-tool/worker-rows.mjs +7 -0
  213. package/src/standalone/channel-client.mjs +1 -1
  214. package/src/standalone/channel-transport.mjs +1 -1
  215. package/src/standalone/channel-worker.mjs +13 -11
  216. package/src/standalone/daemon.mjs +6 -12
  217. package/src/standalone/hook-bus/handlers.mjs +27 -9
  218. package/src/standalone/hook-bus/handlers.test.mjs +88 -0
  219. package/src/standalone/memory-runtime-proxy.mjs +3 -2
  220. package/src/standalone/plugin-admin.mjs +193 -70
  221. package/src/standalone/plugin-admin.test.mjs +166 -0
  222. package/src/standalone/session-client.mjs +15 -8
  223. package/src/standalone/session-runtime-pool.mjs +33 -2
  224. package/src/standalone/session-runtime-worker.mjs +2 -0
  225. package/src/standalone/session-service.mjs +15 -2
  226. package/src/standalone/session-transport.mjs +1 -1
  227. package/src/tui/app/app-view.jsx +1 -0
  228. package/src/tui/app/model-options.mjs +4 -1
  229. package/src/tui/app/use-prompt-queue-history.mjs +63 -16
  230. package/src/tui/components/PromptInput.jsx +4 -2
  231. package/src/tui/components/prompt-input/escape-policy.d.mts +25 -0
  232. package/src/tui/components/prompt-input/restore-policy.d.mts +32 -0
  233. package/src/tui/components/prompt-input/restore-policy.mjs +69 -0
  234. package/src/tui/dist/index.mjs +125 -36
  235. package/src/tui/session/queue-helpers.mjs +1 -1
  236. package/src/tui/session/session-api-ext.mjs +2 -2
  237. package/src/vendor/statusline/bin/statusline-route.mjs +2 -3
  238. package/src/workflows/default/WORKFLOW.md +1 -0
  239. package/src/workflows/solo/WORKFLOW.md +1 -0
  240. package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +0 -42
  241. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +0 -816
  242. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-spawn-helpers.mjs +0 -107
  243. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +0 -845
  244. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +0 -288
  245. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -553
  246. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +0 -551
  247. package/src/workflows/solo-bench/WORKFLOW.md +0 -17
@@ -4,3 +4,6 @@ Copyright 2025 OpenAI
4
4
  This project includes code derived from [Ratatui](https://github.com/ratatui/ratatui), licensed under the MIT license.
5
5
  Copyright (c) 2016-2022 Florian Dehau
6
6
  Copyright (c) 2023-2025 The Ratatui Developers
7
+
8
+ The native fuzzy-search architecture is derived from OpenAI Codex file search,
9
+ licensed under the Apache License 2.0.
@@ -0,0 +1,6 @@
1
+ This project includes code from ripgrep's grep library crates,
2
+ licensed under the MIT License or the Unlicense.
3
+
4
+ Copyright (c) 2015 Andrew Gallant
5
+
6
+ See LICENSES/MIT.txt for the MIT license text.
package/README.md CHANGED
@@ -97,8 +97,8 @@ every number above live under `benchmarks/terminal-bench-2.1/`.
97
97
  **Any environment**
98
98
 
99
99
  - Full-screen TUI with slash commands, provider setup, model/workflow
100
- pickers, statusline integration, and detailed tool cards — plus headless
101
- role mode for scripting.
100
+ pickers, statusline integration, and detailed tool cards — plus `exec`
101
+ mode for headless scripting.
102
102
  - Mixdog Desktop: a full agent workbench for Windows/macOS/Linux (see
103
103
  below).
104
104
  - Installable web app over relay pairing — scan a QR code to open your
@@ -156,16 +156,22 @@ mixdog --remote
156
156
  mixdog --onboarding
157
157
  ```
158
158
 
159
- Headless role mode is also supported. It requires an explicit
160
- provider/model pair and runs with ephemeral config host behavioral config
161
- and personal state are not loaded:
159
+ Headless `exec` is also supported. It runs one non-interactive primary-model
160
+ session and requires an explicit provider/model pair. Ephemeral config keeps
161
+ host behavioral config and personal state out of the run:
162
162
 
163
163
  ```bash
164
- mixdog --provider anthropic-oauth --model claude-opus-5 worker "fix the failing test"
165
- mixdog --provider openai-oauth --model gpt-5.6-sol reviewer "review the current diff"
164
+ mixdog exec --provider anthropic-oauth --model claude-opus-5 "fix the failing test"
165
+ mixdog exec --provider openai-oauth --model gpt-5.6-sol --effort xhigh --fast "review the current diff"
166
+ mixdog exec --provider openai-oauth --model gpt-5.6-sol --json "fix the failing test"
166
167
  ```
167
168
 
168
- Roles: `worker`, `heavy-worker`, `reviewer`, `maintainer`, `web-researcher`.
169
+ `--json` emits timestamped JSONL. The stream contains thread/turn lifecycle,
170
+ provider request timing, reasoning and assistant messages, tool start/completion
171
+ with arguments/output and queue/dispatch/execution/batch-wait/postprocess timing,
172
+ background notifications, final usage, and a
173
+ terminal success/error result. Without `--json`, stdout remains final text only.
174
+ In JSON mode stdout is JSONL-only; runtime diagnostics remain on stderr.
169
175
 
170
176
  ## TUI basics
171
177
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.124",
3
+ "version": "0.9.126",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -55,14 +55,17 @@
55
55
  "smoke:loop": "node scripts/smoke-loop.mjs",
56
56
  "smoke:loop:final": "node scripts/smoke-loop-report.mjs --require-complete --min-elapsed 5h --min-iterations 400 --max-gap 60s --max-smoke-ms 10000 --max-avg-smoke-ms 8500 --max-step-ms smoke.mjs=4000 --max-step-ms boot-smoke.mjs=8000 --max-rss-mb 140 --max-rss-growth-mb 50",
57
57
  "smoke:loop:report": "node scripts/smoke-loop-report.mjs",
58
- "test:tool-contracts": "node scripts/tool-smoke.mjs",
58
+ "build:spawn:test": "cargo build --locked --manifest-path native/mixdog-spawn/Cargo.toml",
59
+ "test:tool-contracts": "npm run build:spawn:test && node scripts/tool-smoke.mjs",
59
60
  "smoke:patch": "node scripts/apply-patch-edit-smoke.mjs",
60
- "test:shellhardening": "node --test scripts/suite-shellhardening-test.mjs",
61
+ "test:shellhardening": "npm run build:spawn:test && node --test scripts/suite-shellhardening-test.mjs scripts/suite-shell-direct-exe-test.mjs",
61
62
  "test:providers": "node --test scripts/provider-toolcall-test.mjs scripts/provider-contract-test.mjs scripts/provider-stream-outcome-test.mjs",
62
63
  "test:embedding-runtime:warmup": "node scripts/verify-embedding-runtime.mjs --warmup",
63
64
  "test:release-assets": "node --check scripts/verify-release-assets.mjs && node --check scripts/release-gate-test.mjs && node --test scripts/release-gate-test.mjs",
65
+ "test:native-edit-wire": "node scripts/native-edit-wire-test.mjs",
64
66
  "test:release-focused": "npm run test:release-critical && npm run test:compact && npm run test:shellhardening && npm run test:tool-contracts && npm run test:session && npm run test:session-transport",
65
67
  "test:release-critical": "npm run test:release-assets && npm run smoke:patch && npm run test:providers",
68
+ "test:spec-advisory": "node scripts/advisory-spec-test.mjs && npm run test:spec-advisory --prefix apps/desktop",
66
69
  "test:session-transport": "node --test scripts/session-transport-test.mjs scripts/daemon-bootstrap-test.mjs",
67
70
  "test:session": "node --test scripts/runtime-turn-contract-test.mjs scripts/session-save-fault-store-test.mjs",
68
71
  "test:media": "node --test src/runtime/media/store.test.mjs src/runtime/media/renditions.test.mjs src/runtime/media/adapters/codex-image.test.mjs",
@@ -76,6 +79,8 @@
76
79
  "bench:corpus": "node scripts/routing-corpus.mjs",
77
80
  "bench:run": "node scripts/bench-run.mjs",
78
81
  "bench:recall": "node scripts/recall-bench.mjs",
82
+ "bench:recall:strict": "node scripts/recall-bench.mjs --strict",
83
+ "test:recall-validator": "node --test scripts/recall-bench-test.mjs",
79
84
  "bench:tui-load": "node scripts/tui-runtime-load-bench.mjs",
80
85
  "bench:output-style": "node scripts/output-style-bench.mjs",
81
86
  "bench:session-context": "node scripts/session-context-bench.mjs",
@@ -0,0 +1,32 @@
1
+ import { copyFile, mkdir } from 'node:fs/promises';
2
+ import { spawn } from 'node:child_process';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
7
+ const manifest = join(root, 'native', 'mixdog-token', 'Cargo.toml');
8
+ const release = process.argv.includes('--release');
9
+ const profile = release ? 'release' : 'debug';
10
+ const sourceName = process.platform === 'win32'
11
+ ? 'mixdog_token.dll'
12
+ : process.platform === 'darwin' ? 'libmixdog_token.dylib' : 'libmixdog_token.so';
13
+ const source = join(root, 'native', 'mixdog-token', 'target', profile, sourceName);
14
+ const destination = join(root, 'native', 'mixdog-token', 'target', profile, 'mixdog-token.node');
15
+
16
+ if (process.argv.includes('--build')) {
17
+ const args = ['build', '--locked', '--manifest-path', manifest];
18
+ if (release) args.splice(1, 0, '--release');
19
+ const child = spawn('cargo', args, { cwd: root, stdio: 'inherit', shell: process.platform === 'win32' });
20
+ const code = await new Promise((resolveExit, reject) => {
21
+ child.once('error', reject);
22
+ child.once('exit', (value, signal) => {
23
+ if (signal) reject(new Error(`cargo build terminated by ${signal}`));
24
+ else resolveExit(value ?? 1);
25
+ });
26
+ });
27
+ if (code !== 0) process.exit(code);
28
+ }
29
+
30
+ await mkdir(dirname(destination), { recursive: true });
31
+ await copyFile(source, destination);
32
+ process.stdout.write(`${destination}\n`);
@@ -8,18 +8,14 @@ process.env.MIXDOG_WORKER_MODE = process.env.MIXDOG_WORKER_MODE || '1';
8
8
 
9
9
  import os from 'node:os';
10
10
  import path from 'node:path';
11
- import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
11
+ import { rmSync, writeFileSync } from 'node:fs';
12
12
  import { writeJsonAtomicSync } from '../src/runtime/shared/atomic-file.mjs';
13
+ import { ensurePrivateRuntimeRoot, resolveRuntimeRoot } from '../src/runtime/shared/runtime-root.mjs';
13
14
  import { claimSingletonOwner, releaseSingletonOwner } from '../src/runtime/shared/singleton-owner.mjs';
14
15
  import { safeIpcSend } from '../src/runtime/shared/safe-ipc-send.mjs';
15
16
  import { createChannelTransport } from '../src/standalone/channel-transport.mjs';
16
17
 
17
- function runtimeRoot() {
18
- return process.env.MIXDOG_RUNTIME_ROOT
19
- ? path.resolve(process.env.MIXDOG_RUNTIME_ROOT)
20
- : path.join(os.tmpdir(), 'mixdog');
21
- }
22
- const RUNTIME_ROOT = runtimeRoot();
18
+ const RUNTIME_ROOT = resolveRuntimeRoot();
23
19
  const DATA_DIR = process.env.MIXDOG_DATA_DIR ? path.resolve(process.env.MIXDOG_DATA_DIR) : RUNTIME_ROOT;
24
20
  const DISCOVERY_PATH = path.join(RUNTIME_ROOT, 'daemon.json');
25
21
  const OWNER_PATH = path.join(DATA_DIR, 'daemon-owner.json');
@@ -38,7 +34,7 @@ async function shutdown(reason, code = 0) {
38
34
  }
39
35
 
40
36
  async function main() {
41
- try { mkdirSync(RUNTIME_ROOT, { recursive: true }); } catch {}
37
+ ensurePrivateRuntimeRoot(RUNTIME_ROOT);
42
38
  const claim = claimSingletonOwner(OWNER_PATH, { kind: 'mixdog-daemon', pid: process.pid, meta: { cwd: process.cwd() } });
43
39
  if (!claim.owned) { process.exit(0); } // race loser → spawner attaches to winner
44
40
  process.on('exit', () => { try { releaseSingletonOwner(OWNER_PATH, process.pid); } catch {} });
@@ -50,6 +46,7 @@ async function main() {
50
46
  writeFileSync(process.env.SMOKE_CHANNEL_ENV_OUT, JSON.stringify({
51
47
  cliOwned: process.env.MIXDOG_CLI_OWNED,
52
48
  host: process.env.MIXDOG_DAEMON_HOST,
49
+ supervisorPid: process.env.MIXDOG_SUPERVISOR_PID,
53
50
  }));
54
51
  } catch { /* smoke-only, best-effort */ }
55
52
  }
@@ -75,7 +72,7 @@ async function main() {
75
72
  pid: process.pid,
76
73
  startedAt: Date.now(),
77
74
  endpoints: { channel: { port, token } },
78
- }, { compact: true });
75
+ }, { compact: true, secret: true });
79
76
  safeIpcSend(process, { type: 'ready', port, token });
80
77
  log(`ready port=${port} pid=${process.pid}`);
81
78
  }
@@ -94,5 +94,133 @@
94
94
  "expect_error": "\"patch\" is required",
95
95
  "file_snapshots": {},
96
96
  "args": { "base_path": null }
97
+ },
98
+ {
99
+ "id": "eof-star-as-context",
100
+ "note": "observed: models close the EOF marker as *** End of File ***; that used to become a fake old line",
101
+ "expect": "applied",
102
+ "expect_content": { "a.txt": "one\nTWO\nthree\n" },
103
+ "file_snapshots": { "a.txt": "one\ntwo\nthree\n" },
104
+ "args": { "patch": "*** Begin Patch\n*** Update File: a.txt\n@@\n one\n-two\n+TWO\n three\n*** End of File ***\n*** End Patch\n" }
105
+ },
106
+ {
107
+ "id": "opcode-dash-list-item",
108
+ "note": "observed: markdown/list line `- Plan` sent as a delete of ` Plan`",
109
+ "expect": "applied",
110
+ "expect_content": { "a.md": "- Plan keep\n new body\ntail\n" },
111
+ "file_snapshots": { "a.md": "- Plan keep\n old body\ntail\n" },
112
+ "args": { "patch": "*** Begin Patch\n*** Update File: a.md\n@@\n- Plan keep\n- old body\n+ new body\n*** End Patch\n" }
113
+ },
114
+ {
115
+ "id": "indent-only-unique-window",
116
+ "note": "observed (tool-failures indent-only): same text, 2-space vs 4-space; unique window must apply",
117
+ "expect": "applied",
118
+ "expect_content": { "a.js": " // landed thumbnail (each restart re-rendered the whole grid).\n const generating = true;\n" },
119
+ "file_snapshots": { "a.js": " // landed thumbnail (each restart re-rendered the whole grid).\n const generating = false;\n" },
120
+ "args": { "patch": "*** Begin Patch\n*** Update File: a.js\n@@\n // landed thumbnail (each restart re-rendered the whole grid).\n- const generating = false;\n+ const generating = true;\n*** End Patch\n" }
121
+ },
122
+ {
123
+ "id": "indent-only-ambiguous-brace",
124
+ "note": "guard: indent-only `});` must not pick a window when it occurs twice",
125
+ "expect": "rejected",
126
+ "expect_error": "context not found",
127
+ "file_snapshots": { "a.js": "function a() {\n });\n}\nfunction b() {\n });\n}\n" },
128
+ "args": { "patch": "*** Begin Patch\n*** Update File: a.js\n@@\n});\n*** End Patch\n" }
129
+ },
130
+ {
131
+ "id": "opcode-plus-list-item",
132
+ "note": "observed: markdown/diff line `+ TODO` sent as an add of ` TODO`",
133
+ "expect": "applied",
134
+ "expect_content": { "a.md": "+ TODO keep\n new body\ntail\n" },
135
+ "file_snapshots": { "a.md": "+ TODO keep\n old body\ntail\n" },
136
+ "args": { "patch": "*** Begin Patch\n*** Update File: a.md\n@@\n+ TODO keep\n- old body\n+ new body\n*** End Patch\n" }
137
+ },
138
+ {
139
+ "id": "opcode-plus-after-context",
140
+ "note": "observed: `+ TODO` after a real context line must stay context, not insert",
141
+ "expect": "applied",
142
+ "expect_content": { "a.md": "keep\n+ TODO keep\n new body\ntail\n" },
143
+ "file_snapshots": { "a.md": "keep\n+ TODO keep\n old body\ntail\n" },
144
+ "args": { "patch": "*** Begin Patch\n*** Update File: a.md\n@@\n keep\n+ TODO keep\n- old body\n+ new body\n*** End Patch\n" }
145
+ },
146
+ {
147
+ "id": "escape-equiv-unique",
148
+ "note": "file stores literal \\uXXXX; unique window must apply the real-character patch",
149
+ "expect": "applied",
150
+ "expect_content": { "a.js": "const a = \"x\";\nmid\n" },
151
+ "file_snapshots": { "a.js": "const a = \"\\u00e9\";\nmid\n" },
152
+ "args": { "patch": "*** Begin Patch\n*** Update File: a.js\n@@\n-const a = \"é\";\n+const a = \"x\";\n*** End Patch\n" }
153
+ },
154
+ {
155
+ "id": "escape-equiv-two-windows",
156
+ "note": "guard: escape-equivalence must not pick a window when it occurs twice",
157
+ "expect": "rejected",
158
+ "expect_error": "context not found",
159
+ "file_snapshots": { "a.js": "const label = \"\\u00e9\";\nmid\nconst label = \"\\u00e9\";\n" },
160
+ "args": { "patch": "*** Begin Patch\n*** Update File: a.js\n@@\n-const label = \"é\";\n+const label = \"x\";\n*** End Patch\n" }
161
+ },
162
+ {
163
+ "id": "crlf-rename",
164
+ "note": "V4A rename must keep the source file's CRLF endings",
165
+ "expect": "applied",
166
+ "expect_content": { "new.js": "line1\r\nLINE2\r\n" },
167
+ "file_snapshots": { "old.js": "line1\r\nline2\r\n" },
168
+ "args": { "mode": "atomic", "patch": "*** Begin Patch\n*** Update File: old.js\n*** Move to: new.js\n@@\n line1\n-line2\n+LINE2\n*** End Patch\n" }
169
+ },
170
+ {
171
+ "id": "add-overwrite-crlf",
172
+ "note": "Add File overwrite of an existing CRLF file must keep CRLF",
173
+ "expect": "applied",
174
+ "expect_content": { "a.txt": "NEW\r\n" },
175
+ "file_snapshots": { "a.txt": "old\r\n" },
176
+ "args": { "patch": "*** Begin Patch\n*** Add File: a.txt\n+NEW\n*** End Patch\n" }
177
+ },
178
+ {
179
+ "id": "indent-new-follows-file",
180
+ "note": "indent recovery must rewrite added lines with the file's leading whitespace",
181
+ "expect": "applied",
182
+ "expect_content": { "a.js": "\tconst x = 1;\n\tconst y = 3;\n" },
183
+ "file_snapshots": { "a.js": "\tconst x = 1;\n\tconst y = 2;\n" },
184
+ "args": { "patch": "*** Begin Patch\n*** Update File: a.js\n@@\n const x = 1;\n- const y = 2;\n+ const y = 3;\n*** End Patch\n" }
185
+ },
186
+ {
187
+ "id": "cr-only-update",
188
+ "note": "classic Mac CR-only files must match and keep CR",
189
+ "expect": "applied",
190
+ "expect_content": { "a.txt": "hello\rWORLD\r" },
191
+ "file_snapshots": { "a.txt": "hello\rworld\r" },
192
+ "args": { "patch": "*** Begin Patch\n*** Update File: a.txt\n@@\n hello\n-world\n+WORLD\n*** End Patch\n" }
193
+ },
194
+ {
195
+ "id": "already-applied-insert",
196
+ "note": "observed: re-sent insertion whose unique new-side is already on disk",
197
+ "expect": "applied",
198
+ "expect_content": { "a.js": "const STATUS = {\n pending: 1,\n archived: 2,\n}\n" },
199
+ "file_snapshots": { "a.js": "const STATUS = {\n pending: 1,\n archived: 2,\n}\n" },
200
+ "args": { "patch": "*** Begin Patch\n*** Update File: a.js\n@@\n const STATUS = {\n pending: 1,\n+ archived: 2,\n }\n*** End Patch\n" }
201
+ },
202
+ {
203
+ "id": "already-applied-replace",
204
+ "note": "observed: re-sent replacement whose unique new-side is already on disk",
205
+ "expect": "applied",
206
+ "expect_content": { "a.js": "head\nreturn ALL.has(v) || v === 'core' || v === 'lineage'\ntail\n" },
207
+ "file_snapshots": { "a.js": "head\nreturn ALL.has(v) || v === 'core' || v === 'lineage'\ntail\n" },
208
+ "args": { "patch": "*** Begin Patch\n*** Update File: a.js\n@@\n head\n-return ALL.has(v) || v === 'core'\n+return ALL.has(v) || v === 'core' || v === 'lineage'\n tail\n*** End Patch\n" }
209
+ },
210
+ {
211
+ "id": "already-applied-mixed-remaining",
212
+ "note": "already-applied hunks skip; a later unique old-side hunk still applies",
213
+ "expect": "applied",
214
+ "expect_content": { "a.js": "const STATUS = {\n pending: 1,\n archived: 2,\n}\n// lineage backfill\n" },
215
+ "file_snapshots": { "a.js": "const STATUS = {\n pending: 1,\n archived: 2,\n}\n// pending/active only\n" },
216
+ "args": { "patch": "*** Begin Patch\n*** Update File: a.js\n@@\n const STATUS = {\n pending: 1,\n+ archived: 2,\n }\n@@\n-// pending/active only\n+// lineage backfill\n*** End Patch\n" }
217
+ },
218
+ {
219
+ "id": "already-applied-duplicate-new",
220
+ "note": "guard: unique new-side required; two copies stay a hard miss",
221
+ "expect": "rejected",
222
+ "expect_error": "context not found",
223
+ "file_snapshots": { "a.js": "flag = false;\nmid\nflag = false;\n" },
224
+ "args": { "patch": "*** Begin Patch\n*** Update File: a.js\n@@\n-flag = true;\n+flag = false;\n*** End Patch\n" }
97
225
  }
98
226
  ]
@@ -0,0 +1,241 @@
1
+ const TIMESTAMP_RE = /^\[(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(?::\d{2}(?:\.\d{3})?)?)\b/;
2
+ const SESSION_HEADER_RE = /^##\s+session\b/i;
3
+
4
+ export function textOfResult(result) {
5
+ if (result && typeof result === 'object' && Array.isArray(result.content)) {
6
+ return result.content.map((part) => (part?.type === 'text' ? part.text || '' : JSON.stringify(part))).join('\n');
7
+ }
8
+ if (result && typeof result === 'object' && typeof result.text === 'string') return result.text;
9
+ if (typeof result === 'string') return result;
10
+ return JSON.stringify(result ?? '');
11
+ }
12
+
13
+ function timestampMs(raw) {
14
+ const value = Date.parse(String(raw || '').replace(' ', 'T'));
15
+ return Number.isFinite(value) ? value : null;
16
+ }
17
+
18
+ export function parseRecallOutput(text) {
19
+ const items = [];
20
+ const headers = [];
21
+ let current = null;
22
+ let group = null;
23
+ let groupLabel = null;
24
+ let groupIndex = -1;
25
+ for (const raw of String(text || '').split('\n')) {
26
+ const line = raw.trim();
27
+ if (!line || line === '(no results)' || line === '(no valid ids)') continue;
28
+ if (SESSION_HEADER_RE.test(line)) {
29
+ groupIndex += 1;
30
+ group = groupIndex;
31
+ groupLabel = line.replace(/^##\s+session\s+/i, '').split(/\s+\(/, 1)[0].trim();
32
+ current = null;
33
+ headers.push({ text: line, group, label: groupLabel });
34
+ continue;
35
+ }
36
+ const stamp = TIMESTAMP_RE.exec(line);
37
+ if (stamp) {
38
+ current = {
39
+ text: line,
40
+ firstLine: line,
41
+ timestampText: stamp[1],
42
+ timestampMs: timestampMs(stamp[1]),
43
+ group,
44
+ groupLabel,
45
+ };
46
+ items.push(current);
47
+ continue;
48
+ }
49
+ if (line.startsWith('[recall truncated') || line.startsWith('note:')) {
50
+ current = null;
51
+ continue;
52
+ }
53
+ if (/^\[[^\]]+\]$/.test(line)) {
54
+ current = null;
55
+ continue;
56
+ }
57
+ if (current) current.text += `\n${line}`;
58
+ }
59
+ return { items, headers };
60
+ }
61
+
62
+ export function topItems(items, n = 3, maxLen = 140) {
63
+ return items.slice(0, n).map((item) => {
64
+ const line = item.text.replace(/\s+/g, ' ');
65
+ return line.length > maxLen ? `${line.slice(0, maxLen - 1)}…` : line;
66
+ });
67
+ }
68
+
69
+ export function scoreTopNContains(items, substrings, n) {
70
+ const lower = items.map((item) => item.text.toLowerCase());
71
+ const perSubstring = substrings.map((needle) => {
72
+ const target = String(needle || '').toLowerCase();
73
+ let rank = null;
74
+ for (let i = 0; i < lower.length; i += 1) {
75
+ if (target && lower[i].includes(target)) {
76
+ rank = i + 1;
77
+ break;
78
+ }
79
+ }
80
+ const hit = rank !== null && rank <= n;
81
+ return { needle, rank, hit, rr: hit ? 1 / rank : 0 };
82
+ });
83
+ const total = perSubstring.length || 1;
84
+ return {
85
+ perSubstring,
86
+ hitAtN: perSubstring.reduce((sum, row) => sum + (row.hit ? 1 : 0), 0) / total,
87
+ mrr: perSubstring.reduce((sum, row) => sum + row.rr, 0) / total,
88
+ n,
89
+ };
90
+ }
91
+
92
+ function firstRecencyBreak(items) {
93
+ for (let i = 1; i < items.length; i += 1) {
94
+ const previous = items[i - 1];
95
+ const current = items[i];
96
+ if (current.timestampMs !== null && previous.timestampMs !== null && current.timestampMs > previous.timestampMs) {
97
+ return { prev: previous.timestampText, cur: current.timestampText };
98
+ }
99
+ }
100
+ return null;
101
+ }
102
+
103
+ export function scoreRecencyOrdered(items) {
104
+ const timestamped = items.filter((item) => item.timestampMs !== null);
105
+ const grouped = timestamped.some((item) => item.group !== null);
106
+ if (!grouped) {
107
+ const firstViolation = firstRecencyBreak(timestamped);
108
+ return {
109
+ parsed: timestamped.length,
110
+ groups: 0,
111
+ ordered: firstViolation === null,
112
+ firstViolation,
113
+ };
114
+ }
115
+ const groups = new Map();
116
+ for (const item of timestamped) {
117
+ const key = item.group ?? -1;
118
+ if (!groups.has(key)) groups.set(key, []);
119
+ groups.get(key).push(item);
120
+ }
121
+ let firstViolation = null;
122
+ for (const rows of groups.values()) {
123
+ const hit = firstRecencyBreak(rows);
124
+ if (hit) {
125
+ firstViolation = { ...hit, scope: 'within-session' };
126
+ break;
127
+ }
128
+ }
129
+ if (!firstViolation) {
130
+ const hit = firstRecencyBreak([...groups.values()].map((rows) => rows[0]));
131
+ if (hit) firstViolation = { ...hit, scope: 'across-sessions' };
132
+ }
133
+ return {
134
+ parsed: timestamped.length,
135
+ groups: groups.size,
136
+ ordered: firstViolation === null,
137
+ firstViolation,
138
+ };
139
+ }
140
+
141
+ function groupHeads(parsed) {
142
+ const heads = [];
143
+ const seen = new Set();
144
+ for (const item of parsed?.items || []) {
145
+ const key = item.groupLabel ?? `group:${item.group ?? -1}`;
146
+ if (seen.has(key) || item.timestampMs === null) continue;
147
+ seen.add(key);
148
+ heads.push(item);
149
+ }
150
+ return heads;
151
+ }
152
+
153
+ export function scorePageAfter(previousParsed, currentParsed) {
154
+ const previousHeads = groupHeads(previousParsed);
155
+ const currentHeads = groupHeads(currentParsed);
156
+ const previousLabels = new Set((previousParsed?.headers || []).map((header) => header.label).filter(Boolean));
157
+ const duplicateGroups = (currentParsed?.headers || [])
158
+ .filter((header) => header.label && previousLabels.has(header.label));
159
+ const previousLast = previousHeads.at(-1) || null;
160
+ const currentFirst = currentHeads[0] || null;
161
+ const inverted = previousLast?.timestampMs != null
162
+ && currentFirst?.timestampMs != null
163
+ && currentFirst.timestampMs > previousLast.timestampMs;
164
+ return {
165
+ ok: duplicateGroups.length === 0 && !inverted,
166
+ duplicateGroups,
167
+ inverted,
168
+ previousLast,
169
+ currentFirst,
170
+ };
171
+ }
172
+
173
+ export function scoreAllContain(items, substrings) {
174
+ const needles = substrings.map((value) => String(value || '').toLowerCase()).filter(Boolean);
175
+ const offenders = items.filter((item) => {
176
+ const text = item.text.toLowerCase();
177
+ return !needles.some((needle) => text.includes(needle));
178
+ });
179
+ return { needles: substrings, offenders, ok: offenders.length === 0 };
180
+ }
181
+
182
+ export function scoreWithinPeriod(items, temporal, toleranceMs = 60_000) {
183
+ const startMs = Number(temporal?.startMs);
184
+ const endMs = Number(temporal?.endMs);
185
+ const hasStart = Number.isFinite(startMs);
186
+ const hasEnd = Number.isFinite(endMs);
187
+ const offenders = items.filter((item) => (
188
+ item.timestampMs !== null
189
+ && ((hasStart && item.timestampMs < startMs - toleranceMs)
190
+ || (hasEnd && item.timestampMs > endMs + toleranceMs))
191
+ ));
192
+ return { checked: items.filter((item) => item.timestampMs !== null).length, offenders, ok: offenders.length === 0 };
193
+ }
194
+
195
+ export function evaluateCase(kase, outcome, quality, recency, allContain, withinPeriod, pageOrder) {
196
+ const warnings = [];
197
+ const expectObj = kase.expect && typeof kase.expect === 'object' ? kase.expect : {};
198
+ const expectKind = expectObj.kind || kase.expect;
199
+ const allowEmpty = expectObj.allowEmpty === true;
200
+ const minResults = Number.isInteger(expectObj.minResults)
201
+ ? Math.max(0, expectObj.minResults)
202
+ : (expectKind === 'empty' || allowEmpty ? 0 : 1);
203
+ const maxResults = Number.isInteger(expectObj.maxResults) ? Math.max(0, expectObj.maxResults) : null;
204
+ if (outcome.isError) warnings.push('error result');
205
+ if (outcome.ms > 3000) warnings.push(`latency ${outcome.ms}ms > 3000ms`);
206
+ if (outcome.count < minResults) warnings.push(`expected at least ${minResults} result(s), got ${outcome.count}`);
207
+ if (maxResults !== null && outcome.count > maxResults) warnings.push(`expected at most ${maxResults} result(s), got ${outcome.count}`);
208
+ if (expectKind === 'empty' && outcome.count > 0) warnings.push(`expected empty but got ${outcome.count} result(s)`);
209
+ if (allContain) {
210
+ for (const item of allContain.offenders) {
211
+ warnings.push(`allContain miss: "${item.firstLine}"`);
212
+ }
213
+ }
214
+ if (quality) {
215
+ for (const row of quality.perSubstring) {
216
+ if (!row.hit) {
217
+ warnings.push(row.rank === null
218
+ ? `topNContains miss: "${row.needle}" not found in results`
219
+ : `topNContains miss: "${row.needle}" found at rank ${row.rank} > topN ${quality.n}`);
220
+ }
221
+ }
222
+ }
223
+ if (recency && !recency.ordered && recency.firstViolation) {
224
+ const violation = recency.firstViolation;
225
+ warnings.push(`recencyOrdered violation (${violation.scope || 'global'}): ${violation.cur} newer than prior ${violation.prev}`);
226
+ }
227
+ if (withinPeriod) {
228
+ for (const item of withinPeriod.offenders) {
229
+ warnings.push(`withinPeriod miss: "${item.firstLine}"`);
230
+ }
231
+ }
232
+ if (pageOrder) {
233
+ for (const header of pageOrder.duplicateGroups) {
234
+ warnings.push(`pageOrder duplicate session: "${header.label}"`);
235
+ }
236
+ if (pageOrder.inverted) {
237
+ warnings.push(`pageOrder inversion: page starts at ${pageOrder.currentFirst?.timestampText} after prior page ended at ${pageOrder.previousLast?.timestampText}`);
238
+ }
239
+ }
240
+ return { status: warnings.length ? 'WARN' : 'PASS', warnings };
241
+ }
@@ -0,0 +1,16 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { _setNativeSpawnBinaryForTest } from '../src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs';
5
+
6
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
7
+ const binary = resolve(
8
+ root,
9
+ 'native/mixdog-spawn/target/debug',
10
+ process.platform === 'win32' ? 'mixdog-spawn.exe' : 'mixdog-spawn',
11
+ );
12
+
13
+ if (!existsSync(binary)) {
14
+ throw new Error(`native spawn test binary missing: run npm run build:spawn:test (${binary})`);
15
+ }
16
+ _setNativeSpawnBinaryForTest(binary);
@@ -1,11 +1,11 @@
1
1
  [
2
- { "id": "browse-last", "label": "query-less recent browse (period=last)", "args": { "period": "last", "limit": 10 }, "expect": "browse" },
3
- { "id": "last-query-topic", "label": "topic inside recent sessions (period=last + query)", "args": { "query": "last", "period": "last", "limit": 5 }, "expect": { "kind": "results", "topNContains": ["last"], "topN": 10, "recencyOrdered": true } },
4
- { "id": "period-24h", "label": "period window 24h", "args": { "period": "24h", "limit": 10 }, "expect": "browse" },
5
- { "id": "period-7d", "label": "period window 7d", "args": { "period": "7d", "limit": 10 }, "expect": "browse" },
2
+ { "id": "browse-last", "label": "query-less recent browse (period=last)", "args": { "period": "last", "projectScope": "mixdog", "limit": 10 }, "expect": { "kind": "browse", "recencyOrdered": true } },
3
+ { "id": "scope-common-exact", "label": "common-scope exact historical query", "args": { "query": "buildRecallFastTrackQuery", "period": "all", "projectScope": "common", "limit": 5 }, "expect": { "kind": "results", "topNContains": ["buildRecallFastTrackQuery"], "topN": 3 } },
4
+ { "id": "period-24h", "label": "period window 24h", "args": { "period": "24h", "projectScope": "mixdog", "limit": 10 }, "expect": { "kind": "browse", "recencyOrdered": true, "withinPeriod": true } },
5
+ { "id": "period-7d", "label": "period window 7d", "args": { "period": "7d", "projectScope": "mixdog", "limit": 10 }, "expect": { "kind": "browse", "recencyOrdered": true, "withinPeriod": true } },
6
6
  { "id": "scope-all", "label": "all-scope query", "args": { "query": "recall", "projectScope": "all", "limit": 10 }, "expect": "results" },
7
- { "id": "recent-dryrun-delta", "label": "recent-work topical: dryrun delta sum", "args": { "query": "\uB4DC\uB77C\uC774\uB7F0 \uB378\uD0C0 \uD569\uC0B0", "limit": 10 }, "expect": { "kind": "results", "topNContains": ["\uB4DC\uB77C\uC774\uB7F0"], "topN": 5 } },
8
- { "id": "recent-remote-singleton", "label": "recent-work topical: remote singleton seat", "args": { "query": "remote \uC2F1\uAE00\uD1A4 \uC88C\uC11D", "limit": 10 }, "expect": { "kind": "results", "topNContains": ["\uC2F1\uAE00\uD1A4"], "topN": 5 } },
9
- { "id": "recency-today", "label": "query+period recency: today's work (24h)", "args": { "query": "\uC624\uB298 \uC9C4\uD589\uD55C \uC791\uC5C5", "period": "24h", "limit": 10 }, "expect": { "kind": "results", "recencyOrdered": true } },
7
+ { "id": "historical-dryrun-delta", "label": "common historical topic: dryrun delta sum", "args": { "query": "\uB4DC\uB77C\uC774\uB7F0 \uB378\uD0C0 \uD569\uC0B0", "period": "all", "projectScope": "common", "limit": 10 }, "expect": { "kind": "results", "topNContains": ["\uB4DC\uB77C\uC774\uB7F0"], "topN": 5 } },
8
+ { "id": "historical-remote-singleton", "label": "common historical topic: remote singleton seat", "args": { "query": "remote \uC2F1\uAE00\uD1A4 \uC88C\uC11D", "period": "all", "projectScope": "common", "limit": 10 }, "expect": { "kind": "results", "topNContains": ["\uC2F1\uAE00\uD1A4"], "topN": 5 } },
9
+ { "id": "recency-today", "label": "query+period recency: current RAG work", "args": { "query": "RAG recall", "period": "24h", "projectScope": "mixdog", "sort": "date", "limit": 10 }, "expect": { "kind": "results", "topNContains": ["RAG"], "topN": 10, "recencyOrdered": true, "withinPeriod": true } },
10
10
  { "id": "xlang-embedding-crash", "label": "cross-language: embedding worker crash (strict topN)", "args": { "query": "embedding worker crash", "limit": 10 }, "expect": { "kind": "results", "topNContains": ["embedding"], "topN": 3 } }
11
11
  ]