mixdog 0.9.62 → 0.9.64

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 (403) hide show
  1. package/README.md +3 -0
  2. package/package.json +9 -2
  3. package/scripts/code-graph-description-contract.mjs +3 -3
  4. package/scripts/verify-release-assets.mjs +2 -2
  5. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +11 -14
  6. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +6 -6
  7. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -2
  8. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +1 -0
  9. package/src/runtime/agent/orchestrator/config.mjs +19 -124
  10. package/src/runtime/agent/orchestrator/context/collect.mjs +6 -6
  11. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +2 -2
  12. package/src/runtime/agent/orchestrator/internal-agents.mjs +2 -2
  13. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +1 -1
  14. package/src/runtime/agent/orchestrator/mcp/client.mjs +4 -4
  15. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +2 -2
  16. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +4 -4
  17. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +2 -2
  18. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +4 -4
  19. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +333 -0
  20. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +2 -2
  21. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +4 -4
  22. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +1 -1
  23. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +3 -277
  24. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +2 -2
  25. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +1 -1
  26. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +2 -2
  27. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -6
  28. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +196 -0
  29. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +460 -0
  30. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +14 -593
  31. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +1 -1
  32. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +1 -1
  33. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +1 -1
  34. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +4 -4
  35. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -2
  36. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +5 -280
  37. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +357 -0
  38. package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +216 -0
  39. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +2 -188
  40. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
  41. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -2
  42. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +1 -1
  43. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +1 -1
  44. package/src/runtime/agent/orchestrator/providers/registry.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +2 -2
  46. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +1 -1
  47. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +1 -1
  48. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +3 -3
  49. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +2 -2
  50. package/src/runtime/agent/orchestrator/session/compact/file-reattach.mjs +2 -2
  51. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +1 -1
  52. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +1 -1
  53. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +2 -2
  54. package/src/runtime/agent/orchestrator/session/context-utils.mjs +4 -4
  55. package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +1 -1
  56. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +17 -0
  57. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +1 -1
  58. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +18 -26
  59. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +1 -1
  60. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +3 -3
  61. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +1 -0
  62. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +1 -1
  63. package/src/runtime/agent/orchestrator/session/manager/delivered-completions.mjs +1 -1
  64. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -1
  65. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +49 -0
  66. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +49 -0
  67. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +260 -0
  68. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +72 -0
  69. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +163 -0
  70. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +1 -1
  71. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +1 -1
  72. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +4 -1
  73. package/src/runtime/agent/orchestrator/session/store.mjs +9 -561
  74. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +1 -1
  75. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  76. package/src/runtime/agent/orchestrator/stall-policy.mjs +11 -11
  77. package/src/runtime/agent/orchestrator/tools/bash-policy-scan.mjs +1 -1
  78. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
  79. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
  80. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +2 -2
  81. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +1 -1
  82. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +2 -154
  83. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +1 -1
  84. package/src/runtime/agent/orchestrator/tools/builtin/cwd-utils.mjs +1 -1
  85. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +1 -1
  86. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
  87. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/tools/builtin/path-locks.mjs +1 -1
  89. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -2
  91. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +2 -3
  92. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +4 -4
  93. package/src/runtime/agent/orchestrator/tools/builtin/read-open.mjs +1 -1
  94. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +1 -1
  95. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +2 -2
  96. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -5
  97. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +2 -2
  99. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +2 -2
  100. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +519 -0
  101. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +5 -449
  102. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +212 -0
  104. package/src/runtime/agent/orchestrator/tools/builtin.mjs +1 -1
  105. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +1 -7
  106. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +9 -36
  107. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +15 -3
  108. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +429 -0
  109. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +2 -395
  110. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +2 -2
  111. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +1 -1
  112. package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +1 -1
  113. package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +1 -1
  114. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +3 -3
  115. package/src/runtime/agent/orchestrator/tools/mutation-planner.mjs +5 -5
  116. package/src/runtime/agent/orchestrator/tools/next-call-utils.mjs +1 -1
  117. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +1 -1
  118. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +7 -7
  119. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +72 -9
  120. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +4 -4
  121. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +1 -1
  122. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +1 -1
  123. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -4
  124. package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
  125. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +3 -3
  126. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +2 -444
  127. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +505 -0
  128. package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +2 -2
  129. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +1 -1
  130. package/src/runtime/channels/lib/backend-dispatch.mjs +20 -5
  131. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  132. package/src/runtime/channels/lib/telegram-format.mjs +1 -1
  133. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +4 -4
  134. package/src/runtime/channels/tool-defs.mjs +2 -3
  135. package/src/runtime/memory/index.mjs +1 -5
  136. package/src/runtime/memory/lib/core-memory-candidates.mjs +357 -0
  137. package/src/runtime/memory/lib/core-memory-store.mjs +7 -357
  138. package/src/runtime/memory/lib/cycle-signatures.mjs +1 -1
  139. package/src/runtime/memory/lib/embedding-model-config.mjs +1 -1
  140. package/src/runtime/memory/lib/embedding-provider.mjs +1 -5
  141. package/src/runtime/memory/lib/embedding-worker.mjs +1 -5
  142. package/src/runtime/memory/lib/ko-morph.mjs +2 -2
  143. package/src/runtime/memory/lib/memory-config-flags.mjs +2 -5
  144. package/src/runtime/memory/lib/memory-cycle-requests.mjs +3 -7
  145. package/src/runtime/memory/lib/memory-cycle1.mjs +4 -8
  146. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -1
  147. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -1
  148. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +2 -5
  149. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -5
  150. package/src/runtime/memory/lib/memory-embed.mjs +2 -6
  151. package/src/runtime/memory/lib/memory-log.mjs +8 -0
  152. package/src/runtime/memory/lib/memory-ops-policy.mjs +4 -8
  153. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +1 -1
  154. package/src/runtime/memory/lib/memory-recall-store.mjs +2 -193
  155. package/src/runtime/memory/lib/memory-score.mjs +1 -1
  156. package/src/runtime/memory/lib/memory-text-utils.mjs +2 -2
  157. package/src/runtime/memory/lib/memory.mjs +2 -6
  158. package/src/runtime/memory/lib/pg/adapter.mjs +2 -6
  159. package/src/runtime/memory/lib/pg/process.mjs +1 -5
  160. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -5
  161. package/src/runtime/memory/lib/recall-format.mjs +2 -2
  162. package/src/runtime/memory/lib/recall-scoring.mjs +195 -0
  163. package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -5
  164. package/src/runtime/memory/lib/trace-store.mjs +2 -6
  165. package/src/runtime/search/lib/http-fetch.mjs +6 -6
  166. package/src/runtime/search/lib/web-tools.mjs +2 -2
  167. package/src/runtime/shared/buffered-appender.mjs +1 -1
  168. package/src/runtime/shared/child-guardian.mjs +1 -1
  169. package/src/runtime/shared/child-spawn-gate.mjs +1 -1
  170. package/src/runtime/shared/config.mjs +4 -4
  171. package/src/runtime/shared/launcher-control.mjs +9 -10
  172. package/src/runtime/shared/llm/index.mjs +5 -20
  173. package/src/runtime/shared/markdown-frontmatter.mjs +1 -1
  174. package/src/runtime/shared/memory-snapshot.mjs +4 -4
  175. package/src/runtime/shared/pristine-execution.mjs +1 -1
  176. package/src/runtime/shared/process-lifecycle.mjs +0 -1
  177. package/src/runtime/shared/process-shutdown.mjs +1 -1
  178. package/src/runtime/shared/resource-admission.mjs +2 -2
  179. package/src/runtime/shared/schedule-session-run.mjs +1 -1
  180. package/src/runtime/shared/schedules-db.mjs +1 -1
  181. package/src/runtime/shared/service-discovery.mjs +2 -2
  182. package/src/runtime/shared/staged-update.mjs +6 -6
  183. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  184. package/src/runtime/shared/tool-primitives.mjs +1 -1
  185. package/src/runtime/shared/tool-surface.mjs +2 -2
  186. package/src/runtime/shared/update-checker.mjs +1 -1
  187. package/src/runtime/shared/user-cwd.mjs +3 -3
  188. package/src/runtime/shared/user-data-guard.mjs +4 -4
  189. package/src/runtime/shared/workspace-router.mjs +5 -5
  190. package/src/session-runtime/config-helpers.mjs +2 -3
  191. package/src/session-runtime/config-lifecycle.mjs +1 -1
  192. package/src/session-runtime/hitch-profile.mjs +1 -1
  193. package/src/session-runtime/lifecycle-api.mjs +6 -4
  194. package/src/session-runtime/model-recency.mjs +5 -5
  195. package/src/session-runtime/plugin-mcp.mjs +1 -1
  196. package/src/session-runtime/provider-request-snapshot.mjs +338 -0
  197. package/src/session-runtime/session-text.mjs +4 -4
  198. package/src/session-runtime/tool-catalog-schema.mjs +197 -0
  199. package/src/session-runtime/tool-catalog.mjs +6 -483
  200. package/src/session-runtime/tool-defs.mjs +1 -1
  201. package/src/standalone/agent-task-status.mjs +1 -1
  202. package/src/standalone/agent-tool/helpers.mjs +2 -2
  203. package/src/standalone/channel-admin.mjs +4 -4
  204. package/src/standalone/explore-tool.mjs +3 -3
  205. package/src/standalone/hook-bus/config.mjs +1 -1
  206. package/src/standalone/hook-bus/constants.mjs +1 -1
  207. package/src/standalone/hook-bus/rules.mjs +1 -1
  208. package/src/standalone/projects.mjs +1 -1
  209. package/src/standalone/provider-admin.mjs +2 -2
  210. package/src/tui/app/app-format.mjs +2 -2
  211. package/src/tui/app/clipboard.mjs +3 -3
  212. package/src/tui/app/input-parsers.mjs +1 -1
  213. package/src/tui/app/text-layout.mjs +1 -1
  214. package/src/tui/app/transcript-row-estimate.mjs +360 -0
  215. package/src/tui/app/transcript-window.mjs +5 -347
  216. package/src/tui/app/use-mouse-input.mjs +1 -1
  217. package/src/tui/app/use-prompt-handlers.mjs +1 -1
  218. package/src/tui/components/tool-execution/surface-detail.mjs +9 -9
  219. package/src/tui/components/tool-execution/text-format.mjs +2 -2
  220. package/src/tui/components/tool-output-format.mjs +7 -7
  221. package/src/tui/display-width.mjs +4 -4
  222. package/src/tui/dist/index.mjs +63 -97
  223. package/src/tui/engine/agent-envelope.mjs +5 -5
  224. package/src/tui/engine/notification-plan.mjs +1 -1
  225. package/src/tui/engine/queue-helpers.mjs +3 -3
  226. package/src/tui/engine/render-timing.mjs +4 -5
  227. package/src/tui/engine/session-api-ext.mjs +3 -3
  228. package/src/tui/engine/tool-result-status.mjs +5 -5
  229. package/src/tui/engine/tool-result-text.mjs +1 -1
  230. package/src/tui/engine/transcript-spill.mjs +585 -0
  231. package/src/tui/engine/tui-steering-persist.mjs +1 -1
  232. package/src/tui/engine.mjs +2 -474
  233. package/src/tui/lib/voice-setup.mjs +1 -1
  234. package/src/tui/markdown/format-token.mjs +1 -1
  235. package/src/tui/markdown/render-ansi.mjs +1 -1
  236. package/src/tui/markdown/table-layout.mjs +4 -4
  237. package/src/tui/paste-attachments.mjs +1 -1
  238. package/src/tui/prompt-history-store.mjs +3 -3
  239. package/src/tui/statusline-ansi-bridge.mjs +2 -2
  240. package/src/tui/theme.mjs +1 -1
  241. package/src/tui/transcript-tool-failures.mjs +2 -2
  242. package/src/ui/ansi.mjs +0 -9
  243. package/src/ui/statusline-format.mjs +2 -4
  244. package/scripts/_bench-cwc.json +0 -20
  245. package/scripts/_jitter-fuzz.jsx +0 -42
  246. package/scripts/_jitter-fuzz2.jsx +0 -30
  247. package/scripts/_jitter-probe.jsx +0 -35
  248. package/scripts/_jp2.jsx +0 -16
  249. package/scripts/_smoke_wd.mjs +0 -7
  250. package/scripts/abort-recovery-test.mjs +0 -175
  251. package/scripts/agent-dispatch-abort-compose-test.mjs +0 -31
  252. package/scripts/agent-live-arg-guard-smoke.mjs +0 -20
  253. package/scripts/agent-live-path-suffix-smoke.mjs +0 -34
  254. package/scripts/agent-live-toolcall-args-smoke.mjs +0 -45
  255. package/scripts/agent-loop-policy-test.mjs +0 -37
  256. package/scripts/agent-model-liveness-test.mjs +0 -763
  257. package/scripts/agent-parallel-smoke.mjs +0 -435
  258. package/scripts/agent-route-batch-test.mjs +0 -40
  259. package/scripts/agent-tag-reuse-smoke.mjs +0 -441
  260. package/scripts/agent-terminal-reap-test.mjs +0 -252
  261. package/scripts/agent-trace-io-test.mjs +0 -133
  262. package/scripts/ansi-color-capability-test.mjs +0 -90
  263. package/scripts/anthropic-admission-retry-integration-test.mjs +0 -119
  264. package/scripts/anthropic-maxtokens-test.mjs +0 -119
  265. package/scripts/anthropic-oauth-refresh-race-test.mjs +0 -397
  266. package/scripts/anthropic-transport-policy-test.mjs +0 -726
  267. package/scripts/apply-patch-edit-smoke.mjs +0 -71
  268. package/scripts/arg-guard-test.mjs +0 -93
  269. package/scripts/async-notify-settlement-test.mjs +0 -99
  270. package/scripts/atomic-lock-tryonce-test.mjs +0 -125
  271. package/scripts/background-task-meta-smoke.mjs +0 -38
  272. package/scripts/bench-run.mjs +0 -508
  273. package/scripts/boot-smoke.mjs +0 -137
  274. package/scripts/build-runtime-windows.ps1 +0 -242
  275. package/scripts/channel-daemon-smoke.mjs +0 -1103
  276. package/scripts/code-graph-aggregate-cwd-test.mjs +0 -158
  277. package/scripts/code-graph-disk-hit-test.mjs +0 -290
  278. package/scripts/code-graph-dispatch-test.mjs +0 -96
  279. package/scripts/code-graph-root-federation-test.mjs +0 -273
  280. package/scripts/compact-active-turn-test.mjs +0 -68
  281. package/scripts/compact-file-reattach-test.mjs +0 -88
  282. package/scripts/compact-pressure-test.mjs +0 -788
  283. package/scripts/compact-prior-context-flatten-test.mjs +0 -252
  284. package/scripts/compact-recall-digest-test.mjs +0 -57
  285. package/scripts/compact-smoke.mjs +0 -1017
  286. package/scripts/compact-trigger-migration-smoke.mjs +0 -261
  287. package/scripts/compacted-placeholder-scrub-test.mjs +0 -63
  288. package/scripts/context-mcp-metering-test.mjs +0 -1406
  289. package/scripts/debounced-skills-async-save-test.mjs +0 -57
  290. package/scripts/deferred-tool-loading-test.mjs +0 -250
  291. package/scripts/desktop-session-bridge-test.mjs +0 -1013
  292. package/scripts/dispatch-persist-recovery-test.mjs +0 -141
  293. package/scripts/embedding-worker-exit-test.mjs +0 -76
  294. package/scripts/execution-completion-dedup-test.mjs +0 -205
  295. package/scripts/execution-pending-resume-kick-test.mjs +0 -151
  296. package/scripts/execution-resume-esc-integration-test.mjs +0 -176
  297. package/scripts/explore-bench-tmp.mjs +0 -36
  298. package/scripts/explore-bench.mjs +0 -248
  299. package/scripts/explore-prompt-policy-test.mjs +0 -256
  300. package/scripts/explore-timeout-cancel-test.mjs +0 -345
  301. package/scripts/find-fuzzy-hidden-test.mjs +0 -267
  302. package/scripts/forwarder-rebind-tail-test.mjs +0 -67
  303. package/scripts/freevar-smoke.mjs +0 -98
  304. package/scripts/gemini-provider-test.mjs +0 -1448
  305. package/scripts/generate-oc-icons.mjs +0 -11
  306. package/scripts/grok-oauth-refresh-race-test.mjs +0 -273
  307. package/scripts/headless-pristine-execution-test.mjs +0 -614
  308. package/scripts/hook-bus-test.mjs +0 -552
  309. package/scripts/ingest-pure-conversation-smoke.mjs +0 -175
  310. package/scripts/internal-comms-bench-test.mjs +0 -226
  311. package/scripts/internal-comms-bench.mjs +0 -853
  312. package/scripts/internal-comms-smoke.mjs +0 -277
  313. package/scripts/internal-tools-normalization-test.mjs +0 -62
  314. package/scripts/interrupted-turn-history-test.mjs +0 -399
  315. package/scripts/lead-workflow-smoke.mjs +0 -592
  316. package/scripts/lifecycle-api-test.mjs +0 -137
  317. package/scripts/live-share-test.mjs +0 -148
  318. package/scripts/live-worker-smoke.mjs +0 -333
  319. package/scripts/log-writer-guard-smoke.mjs +0 -131
  320. package/scripts/maintenance-default-routes-test.mjs +0 -164
  321. package/scripts/max-output-recovery-persist-test.mjs +0 -81
  322. package/scripts/max-output-recovery-test.mjs +0 -368
  323. package/scripts/mcp-client-normalization-test.mjs +0 -45
  324. package/scripts/mcp-grace-deferred-test.mjs +0 -225
  325. package/scripts/memory-core-input-test.mjs +0 -167
  326. package/scripts/memory-cycle-routing-test.mjs +0 -111
  327. package/scripts/memory-meta-concurrency-test.mjs +0 -20
  328. package/scripts/memory-pg-recovery-test.mjs +0 -59
  329. package/scripts/memory-rule-contract-test.mjs +0 -93
  330. package/scripts/model-list-sanitize-test.mjs +0 -37
  331. package/scripts/mouse-tracking-restore-smoke.mjs +0 -107
  332. package/scripts/native-edit-wire-test.mjs +0 -152
  333. package/scripts/notify-completion-mirror-test.mjs +0 -73
  334. package/scripts/openai-oauth-refresh-race-test.mjs +0 -120
  335. package/scripts/openai-oauth-ws-1006-retry-test.mjs +0 -954
  336. package/scripts/openai-ws-early-settle-test.mjs +0 -216
  337. package/scripts/output-style-bench.mjs +0 -292
  338. package/scripts/output-style-smoke.mjs +0 -145
  339. package/scripts/parent-abort-link-test.mjs +0 -90
  340. package/scripts/patch-binary-cache-test.mjs +0 -181
  341. package/scripts/path-suffix-test.mjs +0 -57
  342. package/scripts/pending-completion-drop-test.mjs +0 -239
  343. package/scripts/pending-messages-lock-nonblocking-test.mjs +0 -62
  344. package/scripts/pretool-ask-runtime-test.mjs +0 -54
  345. package/scripts/process-lifecycle-test.mjs +0 -461
  346. package/scripts/prompt-immediate-render-test.mjs +0 -89
  347. package/scripts/prompt-input-parity-test.mjs +0 -145
  348. package/scripts/provider-admission-scheduler-test.mjs +0 -681
  349. package/scripts/provider-contract-test.mjs +0 -632
  350. package/scripts/provider-stream-stall-test.mjs +0 -276
  351. package/scripts/provider-toolcall-test.mjs +0 -3063
  352. package/scripts/reactive-compact-persist-smoke.mjs +0 -203
  353. package/scripts/recall-bench.mjs +0 -359
  354. package/scripts/resource-admission-test.mjs +0 -792
  355. package/scripts/result-classification-test.mjs +0 -75
  356. package/scripts/rg-runner-test.mjs +0 -240
  357. package/scripts/routing-corpus-test.mjs +0 -349
  358. package/scripts/sanitize-tool-pairs-test.mjs +0 -260
  359. package/scripts/session-bench-cache-break-test.mjs +0 -102
  360. package/scripts/session-bench.mjs +0 -1708
  361. package/scripts/session-context-bench.mjs +0 -344
  362. package/scripts/session-heartbeat-lifecycle-test.mjs +0 -68
  363. package/scripts/session-ingest-compaction-smoke.mjs +0 -279
  364. package/scripts/session-ingest-smoke.mjs +0 -177
  365. package/scripts/session-orphan-sweep-test.mjs +0 -109
  366. package/scripts/set-effort-config-test.mjs +0 -41
  367. package/scripts/shell-failure-diagnostics-test.mjs +0 -280
  368. package/scripts/shell-hardening-test.mjs +0 -209
  369. package/scripts/shell-jobs-windows-hide-test.mjs +0 -164
  370. package/scripts/ship-mode-test.mjs +0 -98
  371. package/scripts/smoke-loop-failure-summary-test.mjs +0 -38
  372. package/scripts/smoke-loop-failure-summary.mjs +0 -16
  373. package/scripts/smoke-loop-report.mjs +0 -221
  374. package/scripts/smoke-loop.mjs +0 -202
  375. package/scripts/smoke-runtime-negative.ps1 +0 -106
  376. package/scripts/statusline-quota-hysteresis-test.mjs +0 -62
  377. package/scripts/steering-drain-buckets-test.mjs +0 -467
  378. package/scripts/steering-fold-provenance-test.mjs +0 -71
  379. package/scripts/streaming-tail-window-test.mjs +0 -175
  380. package/scripts/submit-commandbusy-race-test.mjs +0 -257
  381. package/scripts/task-bench.mjs +0 -205
  382. package/scripts/terminal-bench-isolation-guards-test.mjs +0 -102
  383. package/scripts/termio-input-smoke.mjs +0 -199
  384. package/scripts/tool-result-hook-test.mjs +0 -48
  385. package/scripts/tool-smoke.mjs +0 -2453
  386. package/scripts/tool-tui-presentation-test.mjs +0 -286
  387. package/scripts/toolcall-args-test.mjs +0 -239
  388. package/scripts/tui-ambiguous-width-test.mjs +0 -113
  389. package/scripts/tui-background-failure-smoke.mjs +0 -73
  390. package/scripts/tui-perf-run.ps1 +0 -26
  391. package/scripts/tui-render-smoke.mjs +0 -90
  392. package/scripts/tui-runtime-load-bench-entry.jsx +0 -608
  393. package/scripts/tui-runtime-load-bench.mjs +0 -50
  394. package/scripts/tui-store-frame-batch-test.mjs +0 -99
  395. package/scripts/tui-transcript-jitter-harness-entry.jsx +0 -383
  396. package/scripts/tui-transcript-perf-test.mjs +0 -934
  397. package/scripts/usage-metrics-epoch-smoke.mjs +0 -114
  398. package/scripts/verify-release-assets-test.mjs +0 -281
  399. package/scripts/web-fetch-routing-test.mjs +0 -158
  400. package/scripts/webhook-smoke.mjs +0 -201
  401. package/scripts/windows-hide-spawn-options-test.mjs +0 -19
  402. package/scripts/worker-notify-rejection-test.mjs +0 -51
  403. package/scripts/write-backpressure-test.mjs +0 -147
@@ -1,2453 +0,0 @@
1
- #!/usr/bin/env node
2
- import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { dirname, join, resolve } from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
- import { __applyStandaloneToolDefaultsForTest, __renderToolSearchForTest, compactToolSearchDescription, defaultDeferredToolNames, SKILL_TOOL, TOOL_SEARCH_TOOL } from '../src/mixdog-session-runtime.mjs';
7
- import { applyInitialDeferredToolManifestToBp1, buildDeferredToolManifest } from '../src/runtime/agent/orchestrator/context/collect.mjs';
8
- import { buildExplorerPrompt, EXPLORE_TOOL, MAX_FANOUT_QUERIES, normalizeExploreQueries } from '../src/standalone/explore-tool.mjs';
9
- import { AGENT_TOOL, createStandaloneAgent } from '../src/standalone/agent-tool.mjs';
10
- import { parseHeadlessRoleCommand } from '../src/app.mjs';
11
- import { buildHeadlessSpawnArgs } from '../src/headless-role.mjs';
12
- import { createStandaloneChannelWorker } from '../src/standalone/channel-worker.mjs';
13
- import { OpenAIOAuthProvider, buildRequestBody, sendViaHttpSse } from '../src/runtime/agent/orchestrator/providers/openai-oauth.mjs';
14
- import { _test as _anthropicOAuthTest } from '../src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs';
15
- import { _logicalResponseItemMatch } from '../src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs';
16
- import { _mergePendingMessageEntries, applyAskTerminalUsageTotals, closeSession, createSession, drainPendingMessages, enqueuePendingMessage, resumeSession } from '../src/runtime/agent/orchestrator/session/manager.mjs';
17
- import {
18
- contentHasImage,
19
- normalizeContentForAnthropic,
20
- normalizeContentForGeminiParts,
21
- normalizeContentForOpenAIChat,
22
- normalizeContentForOpenAIResponses,
23
- sanitizeContentForStoredHistory,
24
- } from '../src/runtime/agent/orchestrator/providers/media-normalization.mjs';
25
- import { initProviders } from '../src/runtime/agent/orchestrator/providers/registry.mjs';
26
- import {
27
- cacheCapabilityForProvider,
28
- resolveCacheStrategy,
29
- shouldMarkWarmForProvider,
30
- shouldRecordObservedForProvider,
31
- } from '../src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs';
32
- import { executeBuiltinTool } from '../src/runtime/agent/orchestrator/tools/builtin.mjs';
33
- import { executeFuzzyFindTool } from '../src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs';
34
- import { applyGrepContextLeadPolicy, GREP_CONTEXT_MAX, validateBuiltinArgs } from '../src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs';
35
- import { normaliseReadLineWindowArgs } from '../src/runtime/agent/orchestrator/tools/builtin/read-args.mjs';
36
- import { BUILTIN_TOOLS } from '../src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs';
37
- import { runResultCacheInFlight } from '../src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs';
38
- import { executeCodeGraphTool } from '../src/runtime/agent/orchestrator/tools/code-graph.mjs';
39
- import { CODE_GRAPH_TOOL_DEFS } from '../src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs';
40
- import { executePatchTool } from '../src/runtime/agent/orchestrator/tools/patch.mjs';
41
- import { PATCH_TOOL_DEFS } from '../src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs';
42
- import { TOOL_DEFS as MEMORY_TOOL_DEFS } from '../src/runtime/memory/tool-defs.mjs';
43
- import { mergeSessionRowsIntoGlobal } from '../src/runtime/memory/lib/memory-session-merge.mjs';
44
- import { TOOL_DEFS as SEARCH_TOOL_DEFS } from '../src/runtime/search/tool-defs.mjs';
45
- import { TOOL_DEFS as CHANNEL_TOOL_DEFS } from '../src/runtime/channels/tool-defs.mjs';
46
- import { AGENT_OWNER } from '../src/runtime/agent/orchestrator/agent-owner.mjs';
47
- import { recursiveWrapperToolNameForPublicAgent } from '../src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs';
48
- import {
49
- applyDeferredToolSurface,
50
- reconcileDeferredMcpToolCatalog,
51
- } from '../src/session-runtime/tool-catalog.mjs';
52
- import { prepareDeferredToolCallThrough } from '../src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs';
53
- import { composeSystemPrompt } from '../src/runtime/agent/orchestrator/context/collect.mjs';
54
- import { setInternalToolsProvider } from '../src/runtime/agent/orchestrator/internal-tools.mjs';
55
- import { prepareAgentSession } from '../src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs';
56
- import { resolveHiddenRoleSchemaAllowedTools } from '../src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs';
57
- import { assertCodeGraphDescriptionContract } from './code-graph-description-contract.mjs';
58
- import { getHiddenAgent, resolveAgentSessionPermission } from '../src/runtime/agent/orchestrator/internal-agents.mjs';
59
-
60
- const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
61
-
62
- function assert(condition, message) {
63
- if (!condition) throw new Error(message);
64
- }
65
-
66
- function assertOk(name, result, pattern = null) {
67
- const text = String(result || '');
68
- if (!text || /^Error[\s:[]/.test(text)) {
69
- throw new Error(`${name} failed:\n${text}`);
70
- }
71
- if (pattern && !pattern.test(text)) {
72
- throw new Error(`${name} returned unexpected output:\n${text.slice(0, 1000)}`);
73
- }
74
- return text;
75
- }
76
-
77
- {
78
- const session = { provider: 'openai-oauth' };
79
- applyAskTerminalUsageTotals(session, {
80
- usage: { inputTokens: 100_000, outputTokens: 10, cachedTokens: 98_000, cacheWriteTokens: 0 },
81
- });
82
- assert(session.lastInputTokens === 100_000, `inclusive last input should retain provider total: ${JSON.stringify(session)}`);
83
- assert(session.lastUncachedInputTokens === 2_000, `inclusive last uncached input should subtract cache reads: ${JSON.stringify(session)}`);
84
- assert(session.totalUncachedInputTokens === 2_000, `inclusive total uncached input should be tracked: ${JSON.stringify(session)}`);
85
- }
86
-
87
- {
88
- const session = { provider: 'anthropic-oauth' };
89
- applyAskTerminalUsageTotals(session, {
90
- usage: { inputTokens: 2_000, outputTokens: 10, cachedTokens: 90_000, cacheWriteTokens: 8_000 },
91
- });
92
- assert(session.lastInputTokens === 2_000, `additive last input should retain provider input field: ${JSON.stringify(session)}`);
93
- assert(session.lastUncachedInputTokens === 10_000, `additive uncached input should include cache writes: ${JSON.stringify(session)}`);
94
- assert(session.lastContextTokens === 100_000, `additive context should include input+cache read+cache write: ${JSON.stringify(session)}`);
95
- assert(session.totalUncachedInputTokens === 10_000, `additive total uncached input should include cache writes: ${JSON.stringify(session)}`);
96
- }
97
-
98
- {
99
- const sid = `tool-smoke-rich-pending-${process.pid}-${Date.now()}`.replace(/[^A-Za-z0-9_-]/g, '_');
100
- const richContent = [
101
- { type: 'text', text: 'look at this' },
102
- { type: 'image', data: 'abc', mimeType: 'image/png' },
103
- ];
104
- const depth = enqueuePendingMessage(sid, { text: 'look at this\n[Image]', content: richContent });
105
- assert(depth >= 1, `rich pending enqueue should return queue depth, got ${depth}`);
106
- const drained = drainPendingMessages(sid);
107
- assert(drained.length === 1, `rich pending drain should dedupe memory+persisted entries, got ${drained.length}`);
108
- assert(Array.isArray(drained[0]?.content), `rich pending drain should preserve content array: ${JSON.stringify(drained)}`);
109
- assert(drained[0].content.some((part) => part?.type === 'image' && part?.data === 'abc'), `rich pending drain lost image part: ${JSON.stringify(drained)}`);
110
- const merged = _mergePendingMessageEntries([...drained, 'plain follow-up']);
111
- assert(Array.isArray(merged?.content), `rich pending merge should preserve structured content: ${JSON.stringify(merged)}`);
112
- assert(merged.content.some((part) => part?.type === 'image' && part?.data === 'abc'), `rich pending merge lost image part: ${JSON.stringify(merged)}`);
113
- assert(
114
- merged.content.some((part) => part?.type === 'text' && /plain follow-up/.test(part.text || '')),
115
- `rich pending merge should keep later text follow-up: ${JSON.stringify(merged)}`,
116
- );
117
- assert(drainPendingMessages(sid).length === 0, 'rich pending drain should remove persisted fallback after first drain');
118
- await new Promise((resolve) => setImmediate(resolve));
119
- assert(drainPendingMessages(sid).length === 0, 'rich pending async mirror must not resurrect an already-drained message');
120
- }
121
-
122
- {
123
- const sid = `tool-smoke-async-pending-${process.pid}-${Date.now()}`.replace(/[^A-Za-z0-9_-]/g, '_');
124
- enqueuePendingMessage(sid, 'persisted pending text');
125
- await new Promise((resolve) => setImmediate(resolve));
126
- const drained = drainPendingMessages(sid);
127
- assert(drained.length === 1 && drained[0] === 'persisted pending text', `async pending mirror should persist fallback text: ${JSON.stringify(drained)}`);
128
- }
129
-
130
- {
131
- let computes = 0;
132
- const key = `tool-smoke-inflight-${Date.now()}-${Math.random()}`;
133
- const [a, b] = await Promise.all([
134
- runResultCacheInFlight(key, async () => {
135
- computes += 1;
136
- await new Promise((resolve) => setTimeout(resolve, 15));
137
- return 'shared-result';
138
- }),
139
- runResultCacheInFlight(key, async () => {
140
- computes += 1;
141
- return 'duplicate-result';
142
- }),
143
- ]);
144
- assert(computes === 1, `in-flight result cache should compute once, computed ${computes}`);
145
- assert(a === 'shared-result' && b === 'shared-result', 'in-flight result cache should share the first result');
146
- }
147
-
148
- {
149
- const originalFunctionCall = {
150
- type: 'function_call',
151
- call_id: 'call_tool_1',
152
- name: 'shell',
153
- arguments: JSON.stringify({ command: 'Get-Content -Path src/runtime/agent/orchestrator/session/loop.mjs' }),
154
- };
155
- const compactedReplayFunctionCall = {
156
- type: 'function_call',
157
- call_id: 'call_tool_1',
158
- name: 'shell',
159
- arguments: JSON.stringify({ command: '[mixdog compacted 74 bytes]' }),
160
- };
161
- assert(
162
- _logicalResponseItemMatch(compactedReplayFunctionCall, originalFunctionCall),
163
- 'function_call replay should match by call_id/name even when history compacts arguments',
164
- );
165
- assert(
166
- !_logicalResponseItemMatch({ ...compactedReplayFunctionCall, call_id: 'call_tool_2' }, originalFunctionCall),
167
- 'function_call replay must not match a different call_id',
168
- );
169
- const originalCustomCall = {
170
- type: 'custom_tool_call',
171
- call_id: 'call_patch_1',
172
- name: 'apply_patch',
173
- input: '*** Begin Patch\n*** Add File: a.txt\n+ok\n*** End Patch\n',
174
- };
175
- assert(
176
- _logicalResponseItemMatch({ ...originalCustomCall, input: '[mixdog compacted patch]' }, originalCustomCall),
177
- 'custom_tool_call replay should match by call_id/name even when history compacts patch input',
178
- );
179
- assert(
180
- !_logicalResponseItemMatch({ ...originalCustomCall, call_id: 'call_patch_2' }, originalCustomCall),
181
- 'custom_tool_call replay must not match a different call_id',
182
- );
183
- }
184
-
185
- {
186
- const publicStrategy = resolveCacheStrategy('worker');
187
- assert(publicStrategy.tools === 'none', `Anthropic tools must not spend a cache_control BP: ${JSON.stringify(publicStrategy)}`);
188
- // BP1~3 and the volatile message tail stay 1h; see resolveCacheStrategy.
189
- assert(publicStrategy.system === '1h' && publicStrategy.tier3 === '1h' && publicStrategy.messages === '1h', `public cache tiers changed unexpectedly: ${JSON.stringify(publicStrategy)}`);
190
- assert(cacheCapabilityForProvider('anthropic-oauth') === 'explicit-breakpoint', 'Anthropic OAuth should remain explicit-breakpoint');
191
- assert(cacheCapabilityForProvider('openai-oauth') === 'key-prefix', 'OpenAI OAuth should remain key-prefix');
192
- assert(cacheCapabilityForProvider('xai') === 'key-prefix', 'xAI should remain key-prefix');
193
- assert(cacheCapabilityForProvider('grok-oauth') === 'key-prefix', 'Grok OAuth should remain key-prefix');
194
- assert(cacheCapabilityForProvider('gemini') === 'managed-explicit', 'Gemini should be provider-managed explicit cachedContents');
195
- assert(shouldMarkWarmForProvider('gemini') === true, 'Gemini provider-managed cache should count as warmable');
196
- assert(shouldRecordObservedForProvider('gemini') === false, 'Gemini is no longer implicit-observed only');
197
- assert(shouldRecordObservedForProvider('deepseek') === true, 'DeepSeek should remain observed-only');
198
- }
199
-
200
- {
201
- const prevTraceDisable = process.env.MIXDOG_AGENT_TRACE_DISABLE;
202
- const prevOaiTransport = process.env.MIXDOG_OAI_TRANSPORT;
203
- process.env.MIXDOG_AGENT_TRACE_DISABLE = '1';
204
- // This smoke intentionally verifies the pinned WS-only escape hatch. Default
205
- // transport is now refs-style auto (WS-first with HTTP fallback), so
206
- // forceHttpFallback would legitimately call fakeHttp unless we pin ws-delta.
207
- process.env.MIXDOG_OAI_TRANSPORT = 'ws-delta';
208
- try {
209
- const provider = new OpenAIOAuthProvider({});
210
- provider.ensureAuth = async () => ({ access_token: 'fake-token' });
211
- const calls = [];
212
- const fakeWs = async () => {
213
- calls.push('ws');
214
- return { content: 'ws-ok' };
215
- };
216
- const fakeHttp = async () => {
217
- calls.push('http');
218
- return { content: 'http-ok' };
219
- };
220
- const imageTurnContent = [
221
- { type: 'text', text: 'look' },
222
- { type: 'image', data: 'abc', mimeType: 'image/png' },
223
- ];
224
- await provider.send(
225
- [
226
- { role: 'system', content: 'sys' },
227
- { role: 'user', content: imageTurnContent },
228
- ],
229
- 'gpt-5.5',
230
- [],
231
- { _sendViaWebSocketFn: fakeWs, _sendViaHttpSseFn: fakeHttp, sessionId: 'tool-smoke-image-ws' },
232
- );
233
- if (provider._forceHttpFallback) {
234
- throw new Error('image WS send must not poison future OpenAI OAuth sends');
235
- }
236
- const storedImageTurnContent = sanitizeContentForStoredHistory(imageTurnContent);
237
- if (contentHasImage(storedImageTurnContent)) {
238
- throw new Error(`stored image history must not retain provider-visible image parts: ${JSON.stringify(storedImageTurnContent)}`);
239
- }
240
- await provider.send(
241
- [
242
- { role: 'system', content: 'sys' },
243
- { role: 'user', content: storedImageTurnContent },
244
- { role: 'assistant', content: 'image received' },
245
- { role: 'user', content: 'plain ping text, no image' },
246
- ],
247
- 'gpt-5.5',
248
- [],
249
- { _sendViaWebSocketFn: fakeWs, _sendViaHttpSseFn: fakeHttp, sessionId: 'tool-smoke-plain-after-image' },
250
- );
251
- await provider.send(
252
- [
253
- { role: 'system', content: 'sys' },
254
- { role: 'user', content: 'forced HTTP fallback probe' },
255
- ],
256
- 'gpt-5.5',
257
- [],
258
- { _sendViaWebSocketFn: fakeWs, _sendViaHttpSseFn: fakeHttp, forceHttpFallback: true, sessionId: 'tool-smoke-forced-http-fallback' },
259
- );
260
- if (calls.join(',') !== 'ws,ws,ws') {
261
- throw new Error(`image and forced-fallback probes should keep WS under the pinned transport policy: ${calls.join(',')}`);
262
- }
263
- } finally {
264
- if (prevTraceDisable == null) delete process.env.MIXDOG_AGENT_TRACE_DISABLE;
265
- else process.env.MIXDOG_AGENT_TRACE_DISABLE = prevTraceDisable;
266
- if (prevOaiTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
267
- else process.env.MIXDOG_OAI_TRANSPORT = prevOaiTransport;
268
- }
269
- }
270
-
271
- {
272
- const anthropicImages = normalizeContentForAnthropic([
273
- { type: 'input_image', image_url: 'data:image/png;base64,abc' },
274
- { type: 'image_url', image_url: { url: 'https://example.com/a.png' } },
275
- { type: 'input_image', file_id: 'file_123' },
276
- { type: 'input_text', text: 'look' },
277
- ]);
278
- assert(
279
- anthropicImages[0]?.type === 'image'
280
- && anthropicImages[0]?.source?.type === 'base64'
281
- && anthropicImages[0]?.source?.media_type === 'image/png'
282
- && anthropicImages[0]?.source?.data === 'abc',
283
- `Anthropic data-url image normalization failed: ${JSON.stringify(anthropicImages[0])}`,
284
- );
285
- assert(
286
- anthropicImages[1]?.type === 'image'
287
- && anthropicImages[1]?.source?.type === 'url'
288
- && anthropicImages[1]?.source?.url === 'https://example.com/a.png',
289
- `Anthropic URL image normalization failed: ${JSON.stringify(anthropicImages[1])}`,
290
- );
291
- assert(
292
- anthropicImages[2]?.type === 'image'
293
- && anthropicImages[2]?.source?.type === 'file'
294
- && anthropicImages[2]?.source?.file_id === 'file_123',
295
- `Anthropic file image normalization failed: ${JSON.stringify(anthropicImages[2])}`,
296
- );
297
- const storedFileImage = sanitizeContentForStoredHistory([{ type: 'input_image', file_id: 'file_123' }]);
298
- assert(!contentHasImage(storedFileImage), `stored file image history must be sanitized: ${JSON.stringify(storedFileImage)}`);
299
- }
300
-
301
- {
302
- const geminiImages = normalizeContentForGeminiParts([
303
- { type: 'input_image', image_url: 'data:image/png;base64,abc' },
304
- { type: 'image_url', image_url: { url: 'https://example.com/a.png' } },
305
- { fileData: { mimeType: 'image/jpeg', fileUri: 'https://generativelanguage.googleapis.com/v1beta/files/abc' } },
306
- { type: 'input_image', file_id: 'file_123' },
307
- ]);
308
- assert(
309
- geminiImages[0]?.inlineData?.mimeType === 'image/png'
310
- && geminiImages[0]?.inlineData?.data === 'abc',
311
- `Gemini data-url image normalization failed: ${JSON.stringify(geminiImages[0])}`,
312
- );
313
- assert(
314
- geminiImages[1]?.fileData?.fileUri === 'https://example.com/a.png',
315
- `Gemini URL image normalization failed: ${JSON.stringify(geminiImages[1])}`,
316
- );
317
- assert(
318
- geminiImages[2]?.fileData?.mimeType === 'image/jpeg'
319
- && geminiImages[2]?.fileData?.fileUri === 'https://generativelanguage.googleapis.com/v1beta/files/abc',
320
- `Gemini fileData image normalization failed: ${JSON.stringify(geminiImages[2])}`,
321
- );
322
- assert(
323
- /unsupported image file_id for Gemini/.test(geminiImages[3]?.text || ''),
324
- `Gemini incompatible file_id must be explicit text, got: ${JSON.stringify(geminiImages[3])}`,
325
- );
326
- }
327
-
328
- {
329
- const grokChatImages = normalizeContentForOpenAIChat([
330
- { type: 'image_url', image_url: { url: 'https://example.com/a.png' } },
331
- { type: 'input_image', file_id: 'file_123' },
332
- ]);
333
- assert(
334
- grokChatImages[0]?.type === 'image_url'
335
- && grokChatImages[0]?.image_url?.url === 'https://example.com/a.png',
336
- `OpenAI-compatible URL image normalization failed: ${JSON.stringify(grokChatImages[0])}`,
337
- );
338
- assert(
339
- /unsupported image file_id for OpenAI Chat-compatible/.test(grokChatImages[1]?.text || ''),
340
- `OpenAI-compatible chat file_id must be explicit text, got: ${JSON.stringify(grokChatImages[1])}`,
341
- );
342
- const grokResponsesImages = normalizeContentForOpenAIResponses([
343
- { type: 'input_image', file_id: 'file_123' },
344
- ]);
345
- assert(
346
- grokResponsesImages[0]?.type === 'input_image' && grokResponsesImages[0]?.file_id === 'file_123',
347
- `OpenAI-compatible Responses file_id normalization failed: ${JSON.stringify(grokResponsesImages[0])}`,
348
- );
349
- }
350
-
351
- const listOut = await executeBuiltinTool('list', { path: 'scripts', head_limit: 20 }, root);
352
- assertOk('list', listOut, /smoke\.mjs/);
353
-
354
- const grepOut = await executeBuiltinTool('grep', {
355
- pattern: ['standalone mixdog CLI/TUI coding agent', 'smoke passed'],
356
- path: 'scripts',
357
- glob: '*.mjs',
358
- output_mode: 'content_with_context',
359
- head_limit: 10,
360
- }, root);
361
- assertOk('grep', grepOut, /smoke\.mjs/);
362
-
363
- const grepBracketPathOut = await executeBuiltinTool('grep', {
364
- pattern: 'tool-smoke',
365
- path: '[]',
366
- glob: '*.mjs',
367
- head_limit: 5,
368
- }, root);
369
- assertOk('grep path [] coerces to cwd', grepBracketPathOut, /tool-smoke\.mjs/);
370
-
371
- const grepRedirectOut = await executeBuiltinTool('grep', {
372
- pattern: 'assertOk',
373
- path: 'bogus/wrong/prefix/scripts/tool-smoke.mjs',
374
- head_limit: 3,
375
- }, root);
376
- if (!/^\[redirected from/.test(grepRedirectOut) || !/assertOk/.test(grepRedirectOut)) {
377
- throw new Error(`grep ENOENT should auto-redirect on unique suffix hit:\n${grepRedirectOut.slice(0, 800)}`);
378
- }
379
-
380
- const redundantAllFilesGlobGrepOut = await executeBuiltinTool('grep', {
381
- pattern: 'standalone mixdog CLI/TUI coding agent',
382
- glob: '**/*',
383
- head_limit: 10,
384
- }, root);
385
- assertOk('grep redundant all-files glob', redundantAllFilesGlobGrepOut, /scripts[\\/](?:boot-smoke|tool-smoke|smoke)\.mjs|src[\\/]help\.mjs/);
386
-
387
- const implicitRefsGlobOut = await executeBuiltinTool('glob', {
388
- pattern: '**/agent-session.ts',
389
- head_limit: 20,
390
- }, root);
391
- if (/refs[\\/]/i.test(String(implicitRefsGlobOut))) {
392
- throw new Error(`glob default search must exclude refs unless explicitly targeted:\n${implicitRefsGlobOut}`);
393
- }
394
-
395
- const explicitSrcGlobOut = await executeBuiltinTool('glob', {
396
- pattern: '**/engine.mjs',
397
- path: 'src',
398
- head_limit: 20,
399
- }, root);
400
- assertOk('glob explicit src', explicitSrcGlobOut, /src[\\/].*engine\.mjs/i);
401
-
402
- const globPathOnlyOut = await executeBuiltinTool('glob', {
403
- path: 'scripts',
404
- head_limit: 200,
405
- }, root);
406
- assertOk('glob path-only default *', globPathOnlyOut, /tool-smoke\.mjs/i);
407
-
408
- const grepNoPatternGlobOut = await executeBuiltinTool('grep', {
409
- path: 'scripts',
410
- glob: 'tool-smoke.mjs',
411
- head_limit: 5,
412
- }, root);
413
- assertOk('grep without pattern routes to glob', grepNoPatternGlobOut, /tool-smoke\.mjs/i);
414
-
415
- const grepManyPatterns = [
416
- 'tool-smoke',
417
- ...Array.from({ length: 20 }, (_, i) => `__tool_smoke_miss_${i}__`),
418
- ];
419
- const grepManyPatternsOut = await executeBuiltinTool('grep', {
420
- pattern: grepManyPatterns,
421
- path: 'scripts',
422
- glob: '*.mjs',
423
- head_limit: 5,
424
- }, root);
425
- if (/exceeds the \d+-pattern cap/i.test(String(grepManyPatternsOut))) {
426
- throw new Error(`grep should truncate oversized pattern[] instead of error:\n${grepManyPatternsOut.slice(0, 400)}`);
427
- }
428
- assertOk('grep >10 pattern cap keeps first patterns', grepManyPatternsOut, /tool-smoke\.mjs/i);
429
- if (!/\[capped at 10 of 21 patterns\]/.test(String(grepManyPatternsOut))) {
430
- throw new Error(`grep >10 patterns should emit cap note:\n${grepManyPatternsOut.slice(0, 400)}`);
431
- }
432
-
433
- function grepCountTotalMatches(body) {
434
- const m = String(body).match(/\[total (\d+) match/i);
435
- return m ? Number(m[1]) : null;
436
- }
437
-
438
- const grepCountSingleOut = await executeBuiltinTool('grep', {
439
- pattern: 'assertOk',
440
- path: 'scripts/tool-smoke.mjs',
441
- output_mode: 'count',
442
- }, root);
443
- const singleCountTotal = grepCountTotalMatches(grepCountSingleOut);
444
- if (singleCountTotal == null || singleCountTotal < 1) {
445
- throw new Error(`grep count baseline failed:\n${grepCountSingleOut.slice(0, 400)}`);
446
- }
447
-
448
- const grepCountOverlapPatterns = [
449
- 'assertOk',
450
- ...Array.from({ length: 8 }, (_, i) => `__count_overlap_a_${i}__`),
451
- 'assertOk',
452
- ];
453
- const grepCountOverlapOut = await executeBuiltinTool('grep', {
454
- pattern: grepCountOverlapPatterns,
455
- path: 'scripts/tool-smoke.mjs',
456
- output_mode: 'count',
457
- }, root);
458
- const overlapCountTotal = grepCountTotalMatches(grepCountOverlapOut);
459
- if (overlapCountTotal !== singleCountTotal) {
460
- throw new Error(
461
- `multi-pattern count must not double-count overlapping lines (single=${singleCountTotal} overlap=${overlapCountTotal}):\n${grepCountOverlapOut.slice(0, 600)}`,
462
- );
463
- }
464
-
465
- const grepChunkContextPatterns = [
466
- 'grepCountTotalMatches',
467
- ...Array.from({ length: 20 }, (_, i) => `__ctx_chunk_miss_${i}__`),
468
- ];
469
- const grepChunkContextOut = await executeBuiltinTool('grep', {
470
- pattern: grepChunkContextPatterns,
471
- path: 'scripts',
472
- glob: 'tool-smoke.mjs',
473
- '-C': 1,
474
- head_limit: 30,
475
- }, root);
476
- if (!/\[capped at 10 of 21 patterns\]/.test(String(grepChunkContextOut))) {
477
- throw new Error(`oversized -C pattern[] should emit cap note:\n${grepChunkContextOut.slice(0, 500)}`);
478
- }
479
- if (!/tool-smoke\.mjs:\d+:/.test(String(grepChunkContextOut))) {
480
- throw new Error(`capped -C must emit path-prefixed match lines:\n${grepChunkContextOut.slice(0, 800)}`);
481
- }
482
- if (!/tool-smoke\.mjs-\d+-/.test(String(grepChunkContextOut))) {
483
- throw new Error(`capped -C must keep path-prefixed context lines:\n${grepChunkContextOut.slice(0, 800)}`);
484
- }
485
- const ctxBodyLines = String(grepChunkContextOut).split('\n').filter((l) => l && !/^\[/.test(l) && !/^\(no matches\)/.test(l));
486
- const orphanLineOnlyContext = ctxBodyLines.some((l) => /^\d+-/.test(l));
487
- if (orphanLineOnlyContext) {
488
- throw new Error(`capped -C must not leave line-only context orphans:\n${grepChunkContextOut.slice(0, 800)}`);
489
- }
490
- if (!/function grepCountTotalMatches/.test(String(grepChunkContextOut))) {
491
- throw new Error(`capped -C should include match span:\n${grepChunkContextOut.slice(0, 800)}`);
492
- }
493
-
494
- const findOut = await executeBuiltinTool('find', {
495
- query: 'tool smoke',
496
- path: '.',
497
- head_limit: 10,
498
- }, root);
499
- assertOk('find', findOut, /scripts[\\/]tool-smoke\.mjs/i);
500
-
501
- // End-to-end find query[] batch: fan-out must emit one section per query in
502
- // caller order.
503
- const findBatchOut = await executeBuiltinTool('find', {
504
- query: ['tool smoke', 'smoke'],
505
- path: '.',
506
- head_limit: 5,
507
- }, root);
508
- {
509
- const s = String(findBatchOut);
510
- const iA = s.indexOf('# find tool smoke');
511
- const iB = s.indexOf('# find smoke');
512
- if (iA < 0 || iB < 0) {
513
- throw new Error(`find query[] must emit a section per query:\n${s.slice(0, 600)}`);
514
- }
515
- if (!(iA < iB)) {
516
- throw new Error(`find query[] must preserve caller order:\n${s.slice(0, 600)}`);
517
- }
518
- if (!/scripts[\\/]tool-smoke\.mjs/i.test(s)) {
519
- throw new Error(`find query[] sections must carry match bodies:\n${s.slice(0, 600)}`);
520
- }
521
- }
522
-
523
- // Exercise the test-only rg seam directly so this verifies the in-flight
524
- // single-flight rather than merely observing cached output from the real tree.
525
- // TTL=0 rules out persistent broad-enumeration cache reuse; the delayed listing
526
- // keeps both exact-name workers concurrent while the first sweep is in flight.
527
- {
528
- const previousFindEnumCacheTtl = process.env.MIXDOG_FIND_ENUM_CACHE_TTL_MS;
529
- const firstQuery = 'tool-smoke-single-flight-first.mjs';
530
- const secondQuery = 'tool-smoke-single-flight-second.mjs';
531
- const firstPath = `fixtures/${firstQuery}`;
532
- const secondPath = `fixtures/${secondQuery}`;
533
- let broadEnumerationCalls = 0;
534
- process.env.MIXDOG_FIND_ENUM_CACHE_TTL_MS = '0';
535
- try {
536
- const singleFlightOut = await executeFuzzyFindTool({
537
- query: [firstQuery, secondQuery],
538
- path: '.',
539
- head_limit: 5,
540
- }, root, {
541
- __runRg: async () => {
542
- broadEnumerationCalls += 1;
543
- await new Promise((resolve) => setTimeout(resolve, 20));
544
- return `${firstPath}\n${secondPath}\n`;
545
- },
546
- });
547
- const s = String(singleFlightOut);
548
- const iA = s.indexOf(`# find ${firstQuery}`);
549
- const iB = s.indexOf(`# find ${secondQuery}`);
550
- if (broadEnumerationCalls !== 1) {
551
- throw new Error(`find query[] must share exactly one broad enumeration, got ${broadEnumerationCalls}`);
552
- }
553
- if (iA < 0 || iB < 0 || !(iA < iB)) {
554
- throw new Error(`find query[] seam must preserve section order:\n${s}`);
555
- }
556
- if (!s.slice(iA, iB).includes(firstPath) || !s.slice(iB).includes(secondPath)) {
557
- throw new Error(`find query[] seam must retain both exact-name bodies:\n${s}`);
558
- }
559
- } finally {
560
- if (previousFindEnumCacheTtl === undefined) delete process.env.MIXDOG_FIND_ENUM_CACHE_TTL_MS;
561
- else process.env.MIXDOG_FIND_ENUM_CACHE_TTL_MS = previousFindEnumCacheTtl;
562
- }
563
- }
564
-
565
- const readOut = await executeBuiltinTool('read', {
566
- path: 'scripts/smoke.mjs',
567
- offset: 0,
568
- limit: 4,
569
- }, root);
570
- assertOk('read', readOut, /spawnSync/);
571
-
572
- const readDirOut = await executeBuiltinTool('read', {
573
- path: 'scripts',
574
- }, root);
575
- if (!/^Error[\s:[]/.test(String(readDirOut)) || !/read expects a file/i.test(String(readDirOut))) {
576
- throw new Error(`read directory must be classified as Error:\n${readDirOut}`);
577
- }
578
-
579
- const readRegionBatchOut = await executeBuiltinTool('read', {
580
- path: [
581
- { path: 'scripts/smoke.mjs', offset: 0, limit: 2 },
582
- { path: 'scripts/smoke.mjs', offset: 2, limit: 2 },
583
- ],
584
- }, root);
585
- if (!/^read 2\b/m.test(String(readRegionBatchOut))
586
- || (String(readRegionBatchOut).match(/scripts\/smoke\.mjs \[full\] \[ok\]/g) || []).length < 2
587
- || !/1→import \{ spawnSync \}/.test(String(readRegionBatchOut))
588
- || !/3→import \{ fileURLToPath \}/.test(String(readRegionBatchOut))
589
- || !/(pass offset:2 to continue|ONE window: offset:2, limit:\d+)/.test(String(readRegionBatchOut))
590
- || !/(pass offset:4 to continue|ONE window: offset:4, limit:\d+)/.test(String(readRegionBatchOut))) {
591
- throw new Error(`read region batch must preserve both requested spans:\n${readRegionBatchOut}`);
592
- }
593
-
594
- const readStringifiedRegionArgs = {
595
- path: JSON.stringify([{ path: 'scripts/smoke.mjs', offset: 0, limit: 2 }]),
596
- };
597
- const readStringifiedRegionErr = validateBuiltinArgs('read', readStringifiedRegionArgs);
598
- if (readStringifiedRegionErr || !Array.isArray(readStringifiedRegionArgs.path)) {
599
- throw new Error(`read guard must losslessly coerce stringified path arrays: err=${readStringifiedRegionErr} args=${JSON.stringify(readStringifiedRegionArgs)}`);
600
- }
601
- const readStringifiedRegionOut = await executeBuiltinTool('read', {
602
- path: JSON.stringify([{ path: 'scripts/smoke.mjs', offset: 0, limit: 2 }]),
603
- }, root);
604
- if (!/^read 1\b/m.test(String(readStringifiedRegionOut)) || !/scripts\/smoke\.mjs \[full\] \[ok\]/.test(String(readStringifiedRegionOut)) || !/1→import \{ spawnSync \}/.test(String(readStringifiedRegionOut))) {
605
- throw new Error(`read stringified region batch must execute after guard coercion:\n${readStringifiedRegionOut}`);
606
- }
607
- const readStringifiedLineArgs = {
608
- path: JSON.stringify([{ path: 'scripts/smoke.mjs', line: 10, context: 2 }]),
609
- };
610
- const readStringifiedLineErr = validateBuiltinArgs('read', readStringifiedLineArgs);
611
- if (readStringifiedLineErr || readStringifiedLineArgs.path[0].offset !== 7 || readStringifiedLineArgs.path[0].limit !== 5) {
612
- throw new Error(`read guard must losslessly convert legacy line/context inside stringified arrays to offset/limit: err=${readStringifiedLineErr} args=${JSON.stringify(readStringifiedLineArgs)}`);
613
- }
614
-
615
- // Absorb shape 1: region array + top-level offset/limit → top-level becomes
616
- // the default window for regions that lack their own; no hard error.
617
- const readRegionPlusTopLevelArgs = {
618
- path: [{ path: 'scripts/smoke.mjs', offset: 3, limit: 4 }, { path: 'scripts/smoke.mjs' }],
619
- offset: 0,
620
- limit: 2,
621
- };
622
- const readRegionPlusTopLevelErr = validateBuiltinArgs('read', readRegionPlusTopLevelArgs);
623
- if (readRegionPlusTopLevelErr
624
- || 'offset' in readRegionPlusTopLevelArgs || 'limit' in readRegionPlusTopLevelArgs
625
- || readRegionPlusTopLevelArgs.path[0].offset !== 3 || readRegionPlusTopLevelArgs.path[0].limit !== 4
626
- || readRegionPlusTopLevelArgs.path[1].offset !== 0 || readRegionPlusTopLevelArgs.path[1].limit !== 2) {
627
- throw new Error(`read guard must absorb region-array + top-level offset/limit: err=${readRegionPlusTopLevelErr} args=${JSON.stringify(readRegionPlusTopLevelArgs)}`);
628
- }
629
-
630
- // Absorb shape 2: parallel offset/limit as JSON-stringified arrays with path[]
631
- // → zipped into per-file region objects (pairwise recovery), no int error.
632
- const readZipWindowArgs = {
633
- path: ['scripts/smoke.mjs', 'scripts/smoke.mjs'],
634
- offset: '[0, 5]',
635
- limit: '[2, 3]',
636
- };
637
- const readZipWindowErr = validateBuiltinArgs('read', readZipWindowArgs);
638
- if (readZipWindowErr || !Array.isArray(readZipWindowArgs.path)
639
- || readZipWindowArgs.path[0].offset !== 0 || readZipWindowArgs.path[0].limit !== 2
640
- || readZipWindowArgs.path[1].offset !== 5 || readZipWindowArgs.path[1].limit !== 3
641
- || 'offset' in readZipWindowArgs || 'limit' in readZipWindowArgs) {
642
- throw new Error(`read guard must zip stringified offset/limit arrays onto path[]: err=${readZipWindowErr} args=${JSON.stringify(readZipWindowArgs)}`);
643
- }
644
-
645
- // Absorb shape 3: code_graph file/files as a JSON-stringified array → parsed to
646
- // a real array before lookup (dispatched into files[]).
647
- const cgStringifiedFileArgs = { mode: 'symbols', file: JSON.stringify(['a.mjs', 'b.mjs']) };
648
- const cgStringifiedFileErr = validateBuiltinArgs('code_graph', cgStringifiedFileArgs);
649
- if (cgStringifiedFileErr || 'file' in cgStringifiedFileArgs
650
- || !Array.isArray(cgStringifiedFileArgs.files)
651
- || cgStringifiedFileArgs.files[0] !== 'a.mjs' || cgStringifiedFileArgs.files[1] !== 'b.mjs') {
652
- throw new Error(`code_graph guard must parse JSON-stringified file array: err=${cgStringifiedFileErr} args=${JSON.stringify(cgStringifiedFileArgs)}`);
653
- }
654
-
655
- const graphOut = await executeCodeGraphTool('code_graph', {
656
- mode: 'symbols',
657
- file: 'scripts/smoke.mjs',
658
- }, root);
659
- assertOk('code_graph', graphOut, /binding|spawnSync|symbol/i);
660
- const graphStringSymbolOut = await executeCodeGraphTool('code_graph', {
661
- mode: 'symbols',
662
- symbols: 'executeBuiltinTool',
663
- }, root);
664
- assertOk('code_graph string symbols', graphStringSymbolOut, /executeBuiltinTool|symbol_search/i);
665
- const graphRootAnchorOut = await executeCodeGraphTool('code_graph', {
666
- mode: 'symbol_search',
667
- symbol: 'executeBuiltinTool',
668
- file: root,
669
- }, root);
670
- if (/file not found|outside cwd|arbitrary tree/i.test(String(graphRootAnchorOut))) {
671
- throw new Error(`code_graph redundant root anchor was not normalized:\n${graphRootAnchorOut}`);
672
- }
673
-
674
- const graphSymbolBatchOut = await executeCodeGraphTool('code_graph', {
675
- mode: 'symbol_search',
676
- symbols: ['executeBuiltinTool', 'validateBuiltinArgs'],
677
- limit: 2,
678
- }, root);
679
- if (!/# symbol_search executeBuiltinTool\b/.test(String(graphSymbolBatchOut)) || !/# symbol_search validateBuiltinArgs\b/.test(String(graphSymbolBatchOut))) {
680
- throw new Error(`code_graph symbol_search symbols[] batch execution failed:\n${graphSymbolBatchOut}`);
681
- }
682
-
683
- // Absorb shape 3 (real dispatch): file as a JSON-stringified array batches per
684
- // file instead of hitting "file not found: [...]".
685
- const graphStringifiedFileOut = await executeCodeGraphTool('code_graph', {
686
- mode: 'symbols',
687
- file: JSON.stringify(['scripts/smoke.mjs']),
688
- }, root);
689
- if (/file not found/.test(String(graphStringifiedFileOut))
690
- || !/binding|spawnSync|symbol/i.test(String(graphStringifiedFileOut))) {
691
- throw new Error(`code_graph must parse JSON-stringified file array before lookup:\n${graphStringifiedFileOut}`);
692
- }
693
-
694
- const graphMissingFileOut = await executeCodeGraphTool('code_graph', {
695
- mode: 'symbols',
696
- file: 'src/runtime/loop.mjs',
697
- }, root);
698
- if (!/^Error: code_graph: file not found: src\/runtime\/loop\.mjs/.test(String(graphMissingFileOut))) {
699
- throw new Error(`code_graph missing-file fast path failed:\n${graphMissingFileOut}`);
700
- }
701
-
702
- const graphDotDirOut = await executeCodeGraphTool('code_graph', {
703
- mode: 'overview',
704
- file: '.',
705
- }, root);
706
- assertOk('code_graph dot directory anchor', graphDotDirOut, /files\s+\d+|edges\s+\d+/i);
707
-
708
- const patchOut = await executePatchTool('apply_patch', {
709
- base_path: root,
710
- dry_run: true,
711
- fuzzy: false,
712
- patch: `*** Begin Patch
713
- *** Update File: scripts/smoke.mjs
714
- @@
715
- -process.stdout.write('smoke passed ✓\\n');
716
- +process.stdout.write('smoke passed ok\\n');
717
- *** End Patch
718
- `,
719
- }, root);
720
- assertOk('apply_patch dry_run', patchOut, /checked|validated|dry|OK/i);
721
-
722
- const stalePatchOut = await executePatchTool('apply_patch', {
723
- base_path: root,
724
- dry_run: true,
725
- fuzzy: false,
726
- patch: `*** Begin Patch
727
- *** Update File: scripts/smoke.mjs
728
- @@
729
- -definitely-not-current-smoke-line
730
- +definitely-not-current-smoke-line-2
731
- *** End Patch
732
- `,
733
- }, root);
734
- if (!/^Error[\s:[]/.test(String(stalePatchOut)) || !/apply_patch/i.test(String(stalePatchOut))) {
735
- throw new Error(`apply_patch stale context must return an Error result, not throw or pass:\n${stalePatchOut}`);
736
- }
737
-
738
- // Malformed-but-unambiguous patch openings must be absorbed (dry-run, so no
739
- // write). Each targets the same known-good smoke.mjs line the cases above use.
740
- const smokeBody = `@@
741
- -process.stdout.write('smoke passed ✓\\n');
742
- +process.stdout.write('smoke passed ok\\n');
743
- *** End Patch
744
- `;
745
- const absorbCases = [
746
- ['leading blank lines', `\n\n*** Begin Patch\n*** Update File: scripts/smoke.mjs\n${smokeBody}`],
747
- ['decorated begin header', `*** Begin Patch (V4A) ***\n*** Update File: scripts/smoke.mjs\n${smokeBody}`],
748
- ['bare file path opening', `*** Begin Patch\nscripts/smoke.mjs\n${smokeBody}`],
749
- ['File: prefixed opening', `*** Begin Patch\nFile: scripts/smoke.mjs\n${smokeBody}`],
750
- ['unified body in envelope', `*** Begin Patch\n--- scripts/smoke.mjs\n+++ scripts/smoke.mjs\n${smokeBody}`],
751
- ];
752
- for (const [label, patch] of absorbCases) {
753
- const out = await executePatchTool('apply_patch', { base_path: root, dry_run: true, fuzzy: false, patch }, root);
754
- assertOk(`apply_patch absorbs ${label}`, out, /checked|validated|dry|OK/i);
755
- }
756
-
757
- const ambiguousPatchOut = await executePatchTool('apply_patch', {
758
- base_path: root,
759
- dry_run: true,
760
- fuzzy: false,
761
- patch: `*** Begin Patch\nthis line is not a valid opening\n${smokeBody}`,
762
- }, root);
763
- if (!/^Error[\s:[]/.test(String(ambiguousPatchOut)) || !/before a file header|V4A/i.test(String(ambiguousPatchOut))) {
764
- throw new Error(`apply_patch must keep erroring on genuinely ambiguous openings:\n${ambiguousPatchOut}`);
765
- }
766
-
767
- // Unified-looking first body line but real V4A file sections appear later: the
768
- // envelope must NOT be stripped to unified — it stays ambiguous and errors.
769
- const mixedPatchOut = await executePatchTool('apply_patch', {
770
- base_path: root,
771
- dry_run: true,
772
- fuzzy: false,
773
- patch: `*** Begin Patch\n--- scripts/smoke.mjs\n*** Update File: scripts/smoke.mjs\n${smokeBody}`,
774
- }, root);
775
- if (!/^Error[\s:[]/.test(String(mixedPatchOut)) || !/before a file header|V4A/i.test(String(mixedPatchOut))) {
776
- throw new Error(`apply_patch must keep erroring on mixed unified/V4A openings:\n${mixedPatchOut}`);
777
- }
778
-
779
- // Compacted-history placeholder guard: EVERY [mixdog compacted …] variant must
780
- // be rejected with the corrective message BEFORE format dispatch/salvage, both
781
- // as the first line and standalone mid-body (after a *** Begin Patch header).
782
- const compactedGuardCases = [
783
- ['legacy key: prefix', '[mixdog compacted patch: 4096 chars, sha256:deadbeefdeadbeef]\n*** Begin Patch\n*** Update File: a.txt\n+x\n*** End Patch\n'],
784
- ['variant key form', '[mixdog compacted patch v4a, sha256:deadbeefdeadbeef]\n*** Begin Patch\n*** Update File: a.txt\n+x\n*** End Patch\n'],
785
- ['no chars/sha detail', '[mixdog compacted old_string]\n'],
786
- ['mid-body standalone', '*** Begin Patch\n*** Update File: a.txt\n[mixdog compacted patch v4a, sha256:deadbeefdeadbeef]\n*** End Patch\n'],
787
- ];
788
- for (const [label, patch] of compactedGuardCases) {
789
- const out = await executePatchTool('apply_patch', { base_path: root, dry_run: true, fuzzy: false, patch }, root);
790
- if (!/^Error[\s:[]/.test(String(out))
791
- || !/compacted-history placeholder/i.test(String(out))
792
- || !/re-read the current target file contents now/i.test(String(out))
793
- || !/fresh full patch/i.test(String(out))) {
794
- throw new Error(`apply_patch must reject compacted placeholder (${label}):\n${out}`);
795
- }
796
- }
797
- // A legit unified edit whose body content mentions the literal text on a diff
798
- // line (+/-/space) must still parse — the guard only trips on non-diff lines.
799
- const compactedFalsePositiveOut = await executePatchTool('apply_patch', {
800
- base_path: root,
801
- dry_run: true,
802
- fuzzy: false,
803
- patch: `*** Begin Patch\n*** Add File: compacted-note.txt\n+[mixdog compacted patch: 10 chars, sha256:abc]\n*** End Patch\n`,
804
- }, root);
805
- assertOk('apply_patch keeps diff-line placeholder text', compactedFalsePositiveOut, /checked|validated|dry|OK/i);
806
-
807
- const shellOutPromise = executeBuiltinTool('shell', {
808
- command: 'node --version',
809
- cwd: root,
810
- timeout: 30_000,
811
- shell: 'powershell',
812
- }, root);
813
-
814
- const shellFailOutPromise = executeBuiltinTool('shell', {
815
- command: 'Write-Error "tool-smoke-bash-fail"; exit 7',
816
- cwd: root,
817
- timeout: 30_000,
818
- shell: 'powershell',
819
- }, root);
820
-
821
- const shellTimeoutOutPromise = executeBuiltinTool('shell', {
822
- command: 'Start-Sleep -Seconds 2; Write-Output tool-smoke-timeout-missed',
823
- cwd: root,
824
- timeout: 500,
825
- shell: 'powershell',
826
- }, root);
827
-
828
- const shellWorkdirOutPromise = executeBuiltinTool('shell', {
829
- command: 'Get-Location | Select-Object -ExpandProperty Path',
830
- workdir: resolve(root, 'scripts'),
831
- timeout: 30_000,
832
- shell: 'powershell',
833
- }, root);
834
-
835
- const shellOut = await shellOutPromise;
836
- assertOk('bash explicit shell/cwd', shellOut, /v\d+\.\d+\.\d+/);
837
-
838
- const shellFailOut = await shellFailOutPromise;
839
- if (!/^Error[\s:[]/.test(String(shellFailOut)) || !/\[shell-run-failed\]/.test(String(shellFailOut)) || !/\[exit code: 7\]/.test(String(shellFailOut))) {
840
- throw new Error(`bash non-zero exit must be classified as shell-run-failed Error:\n${shellFailOut}`);
841
- }
842
-
843
- const shellTimeoutOut = await shellTimeoutOutPromise;
844
- if (!/^Error[\s:[]/.test(String(shellTimeoutOut)) || !/\[shell-run-failed\]/.test(String(shellTimeoutOut)) || !/\[timeout: 500ms\b/.test(String(shellTimeoutOut))) {
845
- throw new Error(`bash timeout must be milliseconds and classified as shell-run-failed Error:\n${shellTimeoutOut}`);
846
- }
847
-
848
- const shellArgFailOut = await executeBuiltinTool('shell', {
849
- command: '',
850
- cwd: root,
851
- shell: 'powershell',
852
- }, root);
853
- if (!/^Error[\s:[]/.test(String(shellArgFailOut)) || !/\[shell-tool-failed\]/.test(String(shellArgFailOut))) {
854
- throw new Error(`shell tool/preflight failures must be classified as shell-tool-failed Error:\n${shellArgFailOut}`);
855
- }
856
-
857
- // Auto-promotion: a sync foreground command still running past the (soft)
858
- // promotion budget is detached into a tracked background task and returns the
859
- // same task_id envelope as explicit async — the caller never pre-chose async.
860
- // Shrink the budget via MIXDOG_SHELL_AUTO_BACKGROUND_MS so the smoke stays fast.
861
- const _priorAutoBgBudget = process.env.MIXDOG_SHELL_AUTO_BACKGROUND_MS;
862
- process.env.MIXDOG_SHELL_AUTO_BACKGROUND_MS = '800';
863
- let shellAutoPromoteOut;
864
- try {
865
- shellAutoPromoteOut = await executeBuiltinTool('shell', {
866
- command: 'Start-Sleep -Seconds 6; Write-Output tool-smoke-autopromote-done',
867
- cwd: root,
868
- timeout: 30_000,
869
- shell: 'powershell',
870
- }, root);
871
- } finally {
872
- if (_priorAutoBgBudget === undefined) delete process.env.MIXDOG_SHELL_AUTO_BACKGROUND_MS;
873
- else process.env.MIXDOG_SHELL_AUTO_BACKGROUND_MS = _priorAutoBgBudget;
874
- }
875
- if (!/auto-backgrounded/i.test(String(shellAutoPromoteOut)) || !/task_id:\s*\S+/i.test(String(shellAutoPromoteOut))) {
876
- throw new Error(`shell auto-promotion must return a background task envelope (task_id + auto-backgrounded):\n${shellAutoPromoteOut}`);
877
- }
878
- // Clean up the promoted job so it doesn't outlive the smoke as an orphan.
879
- const _autoPromoteTaskId = (/task_id:\s*(\S+)/i.exec(String(shellAutoPromoteOut)) || [])[1];
880
- if (_autoPromoteTaskId) {
881
- try { await executeBuiltinTool('task', { action: 'cancel', task_id: _autoPromoteTaskId }, root); } catch {}
882
- }
883
-
884
- const legacyEscapedAlternationErr = validateBuiltinArgs('grep', { pattern: 'state\\.items\\.map\\|items\\.map', path: root });
885
- if (legacyEscapedAlternationErr) {
886
- throw new Error(`grep legacy \\| alternation should be accepted: ${legacyEscapedAlternationErr}`);
887
- }
888
- const legacyEscapedAlternationOut = await executeBuiltinTool('grep', {
889
- pattern: 'standalone mixdog CLI/TUI coding agent\\|smoke passed',
890
- path: 'scripts',
891
- glob: '*.mjs',
892
- head_limit: 10,
893
- }, root);
894
- assertOk('grep legacy \\| alternation', legacyEscapedAlternationOut, /smoke\.mjs/);
895
- const literalBackslashPipeArray = validateBuiltinArgs('grep', {
896
- pattern: ['contains \\\\|', 'conflicting window args'],
897
- path: root,
898
- });
899
- if (literalBackslashPipeArray) {
900
- throw new Error(`grep array literal \\| should be allowed: ${literalBackslashPipeArray}`);
901
- }
902
-
903
- const grepContextPolicyArgs = { pattern: 'smoke', path: root, context: GREP_CONTEXT_MAX + 999 };
904
- applyGrepContextLeadPolicy(grepContextPolicyArgs);
905
- if (grepContextPolicyArgs['-C'] !== GREP_CONTEXT_MAX || Object.prototype.hasOwnProperty.call(grepContextPolicyArgs, 'context')) {
906
- throw new Error(`grep context policy must canonicalize and clamp explicit context: ${JSON.stringify(grepContextPolicyArgs)}`);
907
- }
908
-
909
- // Multiple absolute paths in one string are now auto-split into a path array
910
- // (arg-guard splitMultipleAbsoluteWindowsPaths) instead of rejected.
911
- const multiGrepPathArgs = {
912
- pattern: 'providerStatus',
913
- path: 'C:\\Project\\mixdog\\src\\tui C:\\Project\\mixdog\\src\\mixdog-session-runtime.mjs',
914
- };
915
- const multiGrepPathErr = validateBuiltinArgs('grep', multiGrepPathArgs);
916
- if (multiGrepPathErr) {
917
- throw new Error(`grep multi-path auto-split should pass validation: ${multiGrepPathErr}`);
918
- }
919
- if (!Array.isArray(multiGrepPathArgs.path) || multiGrepPathArgs.path.length !== 2) {
920
- throw new Error(`grep multi-path auto-split should coerce to 2-element array: ${JSON.stringify(multiGrepPathArgs.path)}`);
921
- }
922
-
923
- // Lookaround/backrefs are no longer rejected at validation time: search-tool
924
- // routes them to rg --pcre2 at runtime (arg-guard.mjs comment near globKeys).
925
- const lookaroundGrepErr = validateBuiltinArgs('grep', {
926
- pattern: 'C:\\\\Project(?!\\\\mixdog)',
927
- path: root,
928
- });
929
- if (lookaroundGrepErr) {
930
- throw new Error(`grep lookaround pattern should pass validation (PCRE2 runtime routing): ${lookaroundGrepErr}`);
931
- }
932
-
933
- // Windows drive path + no explicit shell used to be rejected with a retry hint;
934
- // the guard now auto-coerces to shell:'powershell' (drive paths are a definitive
935
- // powershell signal — they can never work under Git Bash unconverted).
936
- const shellDrivePathArgs = {
937
- command: 'cd C:\\Project\\mixdog && node scripts/build-tui.mjs',
938
- };
939
- const shellDrivePathErr = validateBuiltinArgs('shell', shellDrivePathArgs);
940
- if (process.platform === 'win32') {
941
- if (shellDrivePathErr !== null) {
942
- throw new Error(`shell Windows-path auto-coercion failed: ${shellDrivePathErr}`);
943
- }
944
- if (shellDrivePathArgs.shell !== 'powershell') {
945
- throw new Error(`shell Windows-path auto-coercion did not set shell:'powershell' (got ${JSON.stringify(shellDrivePathArgs.shell)})`);
946
- }
947
- }
948
-
949
- const invalidShellCwdAliasConflict = validateBuiltinArgs('shell', {
950
- command: 'pwd',
951
- cwd: root,
952
- workdir: resolve(root, 'scripts'),
953
- shell: 'powershell',
954
- });
955
- if (!/cwd.*workdir.*conflict/i.test(invalidShellCwdAliasConflict || '')) {
956
- throw new Error(`shell cwd/workdir conflict guard failed: ${invalidShellCwdAliasConflict}`);
957
- }
958
-
959
- const shellWorkdirOut = await shellWorkdirOutPromise;
960
- assertOk('shell workdir alias', shellWorkdirOut, /scripts\s*$/i);
961
-
962
- const offsetReadWindow = {
963
- path: 'scripts/smoke.mjs',
964
- offset: 0,
965
- limit: 20,
966
- };
967
- const readWindowErr = validateBuiltinArgs('read', offsetReadWindow);
968
- if (readWindowErr) {
969
- throw new Error(`read offset/limit window guard failed: err=${readWindowErr} args=${JSON.stringify(offsetReadWindow)}`);
970
- }
971
- const readLineArgs = { path: 'scripts/smoke.mjs', line: 10, context: 2 };
972
- const readLineErr = validateBuiltinArgs('read', readLineArgs);
973
- if (readLineErr || readLineArgs.offset !== 7 || readLineArgs.limit !== 5 || 'line' in readLineArgs || 'context' in readLineArgs) {
974
- throw new Error(`read guard must losslessly convert top-level legacy line/context args to offset/limit: err=${readLineErr} args=${JSON.stringify(readLineArgs)}`);
975
- }
976
- const batchedReadLineArgs = { path: [{ path: 'scripts/smoke.mjs', line: 10, context: 2 }] };
977
- const batchedReadLineErr = validateBuiltinArgs('read', batchedReadLineArgs);
978
- if (batchedReadLineErr || batchedReadLineArgs.path[0].offset !== 7 || batchedReadLineArgs.path[0].limit !== 5) {
979
- throw new Error(`read guard must losslessly convert batched legacy line/context args to offset/limit: err=${batchedReadLineErr} args=${JSON.stringify(batchedReadLineArgs)}`);
980
- }
981
- const pathLineWithLimit = normaliseReadLineWindowArgs({ path: 'scripts/smoke.mjs#L10', limit: 5 }, root);
982
- if (pathLineWithLimit.offset !== 9 || pathLineWithLimit.limit !== 5) {
983
- throw new Error(`read path#line compatibility must anchor offset when limit is explicit: ${JSON.stringify(pathLineWithLimit)}`);
984
- }
985
-
986
- function assertHas(set, name) {
987
- if (!set.has(name)) throw new Error(`default tool surface missing ${name}: ${[...set].join(', ')}`);
988
- }
989
-
990
- function assertLacks(set, name) {
991
- if (set.has(name)) throw new Error(`default tool surface should not include ${name}: ${[...set].join(', ')}`);
992
- }
993
-
994
- const smokeCatalog = [
995
- ...BUILTIN_TOOLS,
996
- ...CODE_GRAPH_TOOL_DEFS,
997
- ...PATCH_TOOL_DEFS,
998
- ...MEMORY_TOOL_DEFS,
999
- ...SEARCH_TOOL_DEFS,
1000
- ...CHANNEL_TOOL_DEFS,
1001
- EXPLORE_TOOL,
1002
- AGENT_TOOL,
1003
- SKILL_TOOL,
1004
- TOOL_SEARCH_TOOL,
1005
- ].filter(Boolean);
1006
-
1007
- const fullDefaults = defaultDeferredToolNames(smokeCatalog, 'full');
1008
- if (fullDefaults.size !== 10) {
1009
- throw new Error(`full default surface should stay 10 tools, got ${fullDefaults.size}: ${[...fullDefaults].join(', ')}`);
1010
- }
1011
- for (const name of ['read', 'code_graph', 'grep', 'find', 'glob', 'list', 'apply_patch', 'explore', 'Skill', 'load_tool']) {
1012
- assertHas(fullDefaults, name);
1013
- }
1014
- for (const name of ['shell', 'task', 'agent', 'recall', 'search', 'web_fetch', 'cwd']) {
1015
- assertLacks(fullDefaults, name);
1016
- }
1017
-
1018
- const leadDefaults = defaultDeferredToolNames(smokeCatalog, 'lead');
1019
- if (leadDefaults.size !== 16) {
1020
- throw new Error(`lead default surface should stay 16 tools for this static catalog, got ${leadDefaults.size}: ${[...leadDefaults].join(', ')}`);
1021
- }
1022
- for (const name of ['read', 'code_graph', 'grep', 'find', 'glob', 'list', 'shell', 'task', 'apply_patch', 'explore', 'agent', 'recall', 'search', 'web_fetch', 'Skill', 'load_tool']) {
1023
- assertHas(leadDefaults, name);
1024
- }
1025
- if (TOOL_SEARCH_TOOL.annotations?.agentHidden !== true) {
1026
- throw new Error('tool_search must stay Lead-only / standalone-only; agent sessions keep fixed schemas without deferred loading');
1027
- }
1028
- function toolSchemaSize(tool) {
1029
- const desc = String(tool?.description || '');
1030
- const schema = JSON.stringify(tool?.input_schema || tool?.inputSchema || {});
1031
- return desc.length + schema.length;
1032
- }
1033
-
1034
- const surfaceSize = [...fullDefaults].reduce((sum, name) => {
1035
- const tool = smokeCatalog.find((item) => item?.name === name);
1036
- return sum + toolSchemaSize(tool);
1037
- }, 0);
1038
- if (surfaceSize > 17000) {
1039
- throw new Error(`full default tool surface too large: ${surfaceSize} chars (cap 17000)`);
1040
- }
1041
- for (const [name, cap] of [
1042
- ['apply_patch', 1300],
1043
- ['code_graph', 1550],
1044
- ['agent', 2500],
1045
- ['recall', 2400],
1046
- ['search', 3200],
1047
- ['web_fetch', 900],
1048
- ['load_tool', 900],
1049
- ]) {
1050
- const tool = smokeCatalog.find((item) => item?.name === name);
1051
- const size = toolSchemaSize(tool);
1052
- if (size > cap) throw new Error(`${name} schema/description too large: ${size} chars (cap ${cap})`);
1053
- }
1054
-
1055
- const readonlyDefaults = defaultDeferredToolNames(smokeCatalog, 'readonly');
1056
- if (readonlyDefaults.size !== 9) {
1057
- throw new Error(`readonly default surface should stay 9 tools, got ${readonlyDefaults.size}: ${[...readonlyDefaults].join(', ')}`);
1058
- }
1059
- for (const name of ['read', 'code_graph', 'grep', 'find', 'glob', 'list', 'explore', 'Skill', 'load_tool']) {
1060
- assertHas(readonlyDefaults, name);
1061
- }
1062
- for (const name of ['apply_patch', 'agent', 'shell']) {
1063
- assertLacks(readonlyDefaults, name);
1064
- }
1065
-
1066
- const agentProps = AGENT_TOOL.inputSchema?.properties || {};
1067
- if (agentProps.mode || agentProps.wait) throw new Error('agent schema should not expose execution mode controls');
1068
- {
1069
- const heavyPrompt = composeSystemPrompt({
1070
- agent: 'heavy-worker',
1071
- provider: 'anthropic-oauth',
1072
- agentRules: '# Tool Use',
1073
- skillManifest: '',
1074
- });
1075
- if (!heavyPrompt.stableSystemContext.includes('## heavy-worker')) {
1076
- throw new Error(`heavy-worker AGENT.md must be included in scoped role instructions: ${heavyPrompt.stableSystemContext}`);
1077
- }
1078
- const workerPrompt = composeSystemPrompt({
1079
- agent: 'worker',
1080
- provider: 'anthropic-oauth',
1081
- agentRules: '# Tool Use',
1082
- skillManifest: '',
1083
- });
1084
- if (!workerPrompt.stableSystemContext.includes('## worker')) {
1085
- throw new Error(`worker AGENT.md must be included in scoped role instructions: ${workerPrompt.stableSystemContext}`);
1086
- }
1087
- }
1088
- {
1089
- const shorthand = parseHeadlessRoleCommand(['reviewer', 'check', 'this']);
1090
- if (shorthand?.agent !== 'reviewer' || shorthand?.message !== 'check this') {
1091
- throw new Error(`headless shorthand command parse failed: ${JSON.stringify(shorthand)}`);
1092
- }
1093
- const explicit = parseHeadlessRoleCommand(['role', 'debug', 'trace', 'failure']);
1094
- if (!explicit?.error || !/mixdog <role> <message/.test(explicit.error)) {
1095
- throw new Error(`headless role subcommand must be rejected: ${JSON.stringify(explicit)}`);
1096
- }
1097
- const tuiDefault = parseHeadlessRoleCommand([]);
1098
- if (tuiDefault !== null) {
1099
- throw new Error(`empty argv must keep TUI default: ${JSON.stringify(tuiDefault)}`);
1100
- }
1101
- const modelOnlySpawn = buildHeadlessSpawnArgs({
1102
- agent: 'reviewer',
1103
- tag: 'headless-smoke',
1104
- cwd: root,
1105
- message: 'check this',
1106
- model: 'haiku',
1107
- });
1108
- if (modelOnlySpawn.model !== 'haiku' || modelOnlySpawn.provider) {
1109
- throw new Error(`headless model-only route must preserve --model without forcing provider: ${JSON.stringify(modelOnlySpawn)}`);
1110
- }
1111
- }
1112
- if (!/always start background tasks/i.test(AGENT_TOOL.description || '') || !/distinct tags?/i.test(AGENT_TOOL.description || '') || !/same scope/i.test(AGENT_TOOL.description || '') || !/send/i.test(AGENT_TOOL.description || '') || !/completion notification/i.test(AGENT_TOOL.description || '') || !/do not (?:call|poll) status\/read/i.test(AGENT_TOOL.description || '')) {
1113
- throw new Error('agent description must preserve async tagged delegation contract');
1114
- }
1115
- const agentSmoke = createStandaloneAgent({
1116
- cfgMod: {
1117
- loadConfig: () => ({ providers: {}, presets: [] }),
1118
- resolveRuntimeSpec: () => { throw new Error('agent smoke should not resolve runtime for read/list errors'); },
1119
- },
1120
- reg: { initProviders: async () => {} },
1121
- mgr: {
1122
- getSession: () => null,
1123
- listSessions: () => [],
1124
- closeSession: () => false,
1125
- },
1126
- dataDir: root,
1127
- cwd: root,
1128
- defaultMode: 'async',
1129
- });
1130
- const agentMissingJob = await agentSmoke.execute({ type: 'read', task_id: 'task_missing_smoke' }, { invocationSource: 'model-tool', cwd: root });
1131
- if (!/^Error[\s:[]/.test(String(agentMissingJob)) || !/task_missing_smoke/.test(String(agentMissingJob))) {
1132
- throw new Error(`agent missing task must return Error result:\n${agentMissingJob}`);
1133
- }
1134
- const agentBadType = await agentSmoke.execute({ type: 'definitely_bad_type' }, { invocationSource: 'model-tool', cwd: root });
1135
- if (!/^Error[\s:[]/.test(String(agentBadType)) || !/unknown type/i.test(String(agentBadType))) {
1136
- throw new Error(`agent unknown type must return Error result:\n${agentBadType}`);
1137
- }
1138
-
1139
- async function waitForSmoke(predicate, label, timeoutMs = 5000) {
1140
- const deadline = Date.now() + timeoutMs;
1141
- while (Date.now() < deadline) {
1142
- if (predicate()) return;
1143
- await new Promise((resolveWait) => setTimeout(resolveWait, 20));
1144
- }
1145
- throw new Error(`timed out waiting for ${label}`);
1146
- }
1147
-
1148
- const channelWorkerTmp = mkdtempSync(join(tmpdir(), 'mixdog-channel-worker-env-'));
1149
- let channelEnvWorker = null;
1150
- const prevChannelDaemon = process.env.MIXDOG_CHANNEL_DAEMON;
1151
- const prevChannelSingleton = process.env.MIXDOG_CHANNEL_SINGLETON;
1152
- const prevChannelWorkerProcess = process.env.MIXDOG_CHANNEL_WORKER_PROCESS;
1153
- const prevRuntimeRoot = process.env.MIXDOG_RUNTIME_ROOT;
1154
- const prevEnvOut = process.env.SMOKE_CHANNEL_ENV_OUT;
1155
- const prevDaemonEntry = process.env.MIXDOG_CHANNEL_DAEMON_ENTRY;
1156
- try {
1157
- // Daemon-mode worker env coverage: start() spawn-or-attaches the machine
1158
- // -global daemon (the stub daemon entry — no Discord token) instead of
1159
- // forking `entry`, so assert the flags on the SPAWNED DAEMON's env (the stub
1160
- // dumps them to SMOKE_CHANNEL_ENV_OUT). The old fork-path env assertion died
1161
- // with the fork path itself; full flip/attach coverage lives in
1162
- // scripts/channel-daemon-smoke.mjs.
1163
- const stubEntry = join(root, 'scripts', 'channel-daemon-stub.mjs');
1164
- const dataDir = join(channelWorkerTmp, 'data');
1165
- const runtimeDir = join(channelWorkerTmp, 'runtime');
1166
- const envOut = join(channelWorkerTmp, 'env.json');
1167
- mkdirSync(dataDir, { recursive: true });
1168
- mkdirSync(runtimeDir, { recursive: true });
1169
- process.env.MIXDOG_CHANNEL_DAEMON = '1';
1170
- process.env.MIXDOG_CHANNEL_SINGLETON = '1';
1171
- process.env.MIXDOG_CHANNEL_WORKER_PROCESS = '1';
1172
- process.env.MIXDOG_RUNTIME_ROOT = runtimeDir;
1173
- process.env.SMOKE_CHANNEL_ENV_OUT = envOut;
1174
- process.env.MIXDOG_CHANNEL_DAEMON_ENTRY = stubEntry;
1175
- channelEnvWorker = createStandaloneChannelWorker({
1176
- entry: stubEntry,
1177
- rootDir: root,
1178
- dataDir,
1179
- cwd: root,
1180
- });
1181
- await channelEnvWorker.start();
1182
- const childEnv = JSON.parse(readFileSync(envOut, 'utf8'));
1183
- if (childEnv.daemon !== '1') {
1184
- throw new Error(`channel daemon smoke expected daemon=1, got ${childEnv.daemon}`);
1185
- }
1186
- if (childEnv.cliOwned !== '0') {
1187
- throw new Error(`channel daemon must advertise owner HTTP (MIXDOG_CLI_OWNED=0), got ${childEnv.cliOwned}`);
1188
- }
1189
- } finally {
1190
- try { await channelEnvWorker?.stop?.('channel-worker-env-smoke', { force: true }); } catch {}
1191
- if (prevChannelDaemon == null) delete process.env.MIXDOG_CHANNEL_DAEMON;
1192
- else process.env.MIXDOG_CHANNEL_DAEMON = prevChannelDaemon;
1193
- if (prevChannelSingleton == null) delete process.env.MIXDOG_CHANNEL_SINGLETON;
1194
- else process.env.MIXDOG_CHANNEL_SINGLETON = prevChannelSingleton;
1195
- if (prevChannelWorkerProcess == null) delete process.env.MIXDOG_CHANNEL_WORKER_PROCESS;
1196
- else process.env.MIXDOG_CHANNEL_WORKER_PROCESS = prevChannelWorkerProcess;
1197
- if (prevRuntimeRoot == null) delete process.env.MIXDOG_RUNTIME_ROOT;
1198
- else process.env.MIXDOG_RUNTIME_ROOT = prevRuntimeRoot;
1199
- if (prevEnvOut == null) delete process.env.SMOKE_CHANNEL_ENV_OUT;
1200
- else process.env.SMOKE_CHANNEL_ENV_OUT = prevEnvOut;
1201
- if (prevDaemonEntry == null) delete process.env.MIXDOG_CHANNEL_DAEMON_ENTRY;
1202
- else process.env.MIXDOG_CHANNEL_DAEMON_ENTRY = prevDaemonEntry;
1203
- // Detach only ends OUR attachment; the stub daemon self-shuts after its
1204
- // client-grace window. Give it that window before deleting its tmp root.
1205
- await new Promise((resolveWait) => setTimeout(resolveWait, 700));
1206
- rmSync(channelWorkerTmp, { recursive: true, force: true });
1207
- }
1208
-
1209
- const agentNotifyTmp = mkdtempSync(join(tmpdir(), 'mixdog-agent-notify-'));
1210
- try {
1211
- const ownerNotifications = [];
1212
- const workerQueued = [];
1213
- const agentNotifySmoke = createStandaloneAgent({
1214
- cfgMod: {
1215
- loadConfig: () => ({
1216
- providers: { 'openai-oauth': { enabled: true } },
1217
- presets: [
1218
- { id: 'sonnet-high', name: 'sonnet-high', provider: 'openai-oauth', model: 'smoke-model', type: 'agent', tools: 'full' },
1219
- { id: 'haiku', name: 'HAIKU', provider: 'openai-oauth', model: 'smoke-haiku', type: 'agent', tools: 'full' },
1220
- ],
1221
- }),
1222
- resolveRuntimeSpec: () => ({ scopeKey: 'smoke-notify', lane: 'agent' }),
1223
- },
1224
- reg: { initProviders },
1225
- mgr: {
1226
- askSession: async (sessionId, _prompt, _context, _onToolCall, _cwdOverride, _prefetch, askOpts = {}) => {
1227
- const nestedText = `background task\ntask_id: task_shell_notify_smoke\nsurface: shell\noperation: shell\nstatus: completed\nstarted: 2026-01-01T00:00:00.000Z\nfinished: 2026-01-01T00:00:01.000Z\n\nnested background done for ${sessionId}`;
1228
- askOpts.notifyFn?.(nestedText, {
1229
- type: 'shell_task_result',
1230
- execution_surface: 'shell',
1231
- execution_id: 'task_shell_notify_smoke',
1232
- status: 'completed',
1233
- });
1234
- askOpts.onTerminalResult?.({ content: 'worker completed' }, { sessionId, beforeSave: true });
1235
- return { content: 'worker completed' };
1236
- },
1237
- enqueuePendingMessage: (sessionId, message) => {
1238
- workerQueued.push({ sessionId, message });
1239
- return 1;
1240
- },
1241
- getSession: () => null,
1242
- listSessions: () => [],
1243
- closeSession: () => false,
1244
- hideSessionFromList: () => false,
1245
- },
1246
- dataDir: agentNotifyTmp,
1247
- cwd: root,
1248
- defaultMode: 'async',
1249
- });
1250
- const notifyContext = {
1251
- invocationSource: 'model-tool',
1252
- callerCwd: root,
1253
- callerSessionId: 'sess_owner_notify_smoke',
1254
- clientHostPid: 424242,
1255
- notifyFn: (text, meta) => {
1256
- ownerNotifications.push({ text, meta });
1257
- return true;
1258
- },
1259
- };
1260
- const notifyStart = await agentNotifySmoke.execute({ type: 'spawn', agent: 'worker', tag: 'notify-smoke', prompt: 'notify smoke' }, notifyContext);
1261
- if (!/agent task:/i.test(String(notifyStart)) || !/status: running/i.test(String(notifyStart))) {
1262
- throw new Error(`agent async notify smoke did not start task:\n${notifyStart}`);
1263
- }
1264
- await waitForSmoke(
1265
- () => ownerNotifications.some((event) => /task_shell_notify_smoke/.test(event.text))
1266
- && workerQueued.some((event) => /task_shell_notify_smoke/.test(String(event.message?.text || event.message?.content || event.message))),
1267
- 'agent child background completion routing',
1268
- );
1269
- await waitForSmoke(
1270
- () => ownerNotifications.some((event) => /worker completed/.test(event.text)),
1271
- 'agent early completion routing',
1272
- );
1273
- const agentCompletionCount = ownerNotifications.filter((event) => /worker completed/.test(event.text)).length;
1274
- if (agentCompletionCount !== 1) {
1275
- throw new Error(`agent early completion should suppress duplicate final notify, got ${agentCompletionCount}: ${JSON.stringify(ownerNotifications)}`);
1276
- }
1277
- await agentNotifySmoke.execute({ type: 'cleanup', force: true }, notifyContext);
1278
- } finally {
1279
- rmSync(agentNotifyTmp, { recursive: true, force: true });
1280
- }
1281
- if (EXPLORE_TOOL.annotations?.readOnlyHint !== true || EXPLORE_TOOL.annotations?.destructiveHint === true) {
1282
- throw new Error('explore must stay read-only so readonly surfaces can use it');
1283
- }
1284
- if (EXPLORE_TOOL.annotations?.agentHidden === true) {
1285
- throw new Error('explore must stay visible to agent sessions');
1286
- }
1287
- {
1288
- const runtimeSearchTool = __applyStandaloneToolDefaultsForTest(SEARCH_TOOL_DEFS.find((tool) => tool?.name === 'search'));
1289
- if (runtimeSearchTool?.annotations?.agentHidden === true) {
1290
- throw new Error('production search tool must stay visible to agent sessions');
1291
- }
1292
- if (TOOL_SEARCH_TOOL.annotations?.agentHidden !== true) {
1293
- throw new Error('deferred tool_search wrapper must stay hidden from agent sessions');
1294
- }
1295
- }
1296
- const exploreProps = EXPLORE_TOOL.inputSchema?.properties || {};
1297
- if (!/broad\/uncertain/i.test(EXPLORE_TOOL.description || '') || !/machine-wide/i.test(EXPLORE_TOOL.description || '') || !/independent targets/i.test(EXPLORE_TOOL.description || '') || (EXPLORE_TOOL.description || '').length > 600) {
1298
- throw new Error('explore description must keep the locator + facet fan-out contract');
1299
- }
1300
- if (!/Narrow locator query/i.test(exploreProps.query?.description || '') || !/independent facets/i.test(exploreProps.query?.description || '') || !/Project\/root/i.test(exploreProps.cwd?.description || '')) {
1301
- throw new Error('explore schema must stay compact and preserve query/cwd shape');
1302
- }
1303
- const normalizedExplore = normalizeExploreQueries('["where is model selection?"," ","which file owns agent async?"]');
1304
- if (normalizedExplore.length !== 2 || normalizedExplore[0] !== 'where is model selection?') {
1305
- throw new Error(`explore query normalization failed: ${JSON.stringify(normalizedExplore)}`);
1306
- }
1307
- if (MAX_FANOUT_QUERIES !== 8) throw new Error(`explore fanout cap changed: ${MAX_FANOUT_QUERIES}`);
1308
- const explorerPrompt = buildExplorerPrompt('where is <agent> & status?');
1309
- if (explorerPrompt !== '<query>where is &lt;agent&gt; &amp; status?</query>') {
1310
- throw new Error(`explorer prompt contract failed: ${explorerPrompt}`);
1311
- }
1312
- if (/Reminder:|BUDGET|STOP and answer|verdicts|ratings|recommendations|grep|code_graph|find|glob/i.test(explorerPrompt)) {
1313
- throw new Error(`explorer prompt must not duplicate the system routing/fan-out contract: ${explorerPrompt}`);
1314
- }
1315
- setInternalToolsProvider({
1316
- executor: async () => 'tool-smoke internal tool',
1317
- tools: [
1318
- EXPLORE_TOOL,
1319
- { name: 'memory', description: 'Destructive memory surface.', inputSchema: { type: 'object', properties: {} }, annotations: { destructiveHint: true } },
1320
- { name: 'recall', description: 'Memory recall surface.', inputSchema: { type: 'object', properties: {} }, annotations: { readOnlyHint: true } },
1321
- { name: 'search', description: 'Web search surface.', inputSchema: { type: 'object', properties: {} }, annotations: { readOnlyHint: true, openWorldHint: true } },
1322
- { name: 'reply', description: 'Channel reply surface.', inputSchema: { type: 'object', properties: {} }, annotations: { destructiveHint: true } },
1323
- { name: 'web_fetch', description: 'Web fetch surface.', inputSchema: { type: 'object', properties: {} }, annotations: { readOnlyHint: true, openWorldHint: true } },
1324
- ],
1325
- });
1326
- {
1327
- await initProviders({ 'openai-oauth': { enabled: true } });
1328
- const skillManifestTmp = mkdtempSync(join(tmpdir(), 'mixdog-skill-manifest-'));
1329
- try {
1330
- const skillDir = join(skillManifestTmp, '.mixdog', 'skills', 'demo-skill');
1331
- mkdirSync(skillDir, { recursive: true });
1332
- writeFileSync(join(skillDir, 'SKILL.md'), [
1333
- '---',
1334
- 'name: demo-skill',
1335
- 'description: Use when validating compact skill manifest matching.',
1336
- '---',
1337
- '',
1338
- '# Demo Skill',
1339
- '',
1340
- 'Use this skill for manifest smoke tests.',
1341
- '',
1342
- ].join('\n'));
1343
- const skillSession = createSession({
1344
- provider: 'openai-oauth',
1345
- model: 'tool-smoke-model',
1346
- owner: 'cli',
1347
- agent: 'lead',
1348
- cwd: skillManifestTmp,
1349
- permission: 'read-write',
1350
- });
1351
- try {
1352
- const visible = (skillSession.messages || []).map((m) => String(m.content || '')).join('\n');
1353
- if (!/available-skills/i.test(visible) || !/demo-skill/i.test(visible) || !/Skill\(\{"name":"<skill-name>"\}\)/.test(visible)) {
1354
- throw new Error(`lead skill manifest missing compact skill listing: ${visible.slice(0, 1200)}`);
1355
- }
1356
- const skillToolNames = (skillSession.tools || []).map((tool) => tool?.name).filter(Boolean);
1357
- if (!skillToolNames.includes('Skill')) {
1358
- throw new Error(`lead skill manifest session must expose Skill loader: ${skillToolNames.join(', ')}`);
1359
- }
1360
- } finally {
1361
- closeSession(skillSession.id, 'tool-smoke');
1362
- }
1363
- const agentSkillSession = createSession({
1364
- provider: 'openai-oauth',
1365
- model: 'tool-smoke-model',
1366
- owner: AGENT_OWNER,
1367
- agent: 'worker',
1368
- cwd: skillManifestTmp,
1369
- permission: 'read-write',
1370
- });
1371
- try {
1372
- const systemVisible = (agentSkillSession.messages || [])
1373
- .filter((m) => m?.role === 'system')
1374
- .map((m) => String(m.content || ''))
1375
- .join('\n');
1376
- // Agent (Pool B/C) sessions FREEZE the Skill meta-tool into the schema
1377
- // unconditionally so the tool bytes stay bit-identical across roles/cwds
1378
- // (provider cache shard stability). The BP1 manifest rides alongside it
1379
- // so the model knows which Skill names exist — a loader without the
1380
- // manifest cannot be targeted. Both must be present together.
1381
- if (!/available-skills/i.test(systemVisible) || !/demo-skill/i.test(systemVisible) || !/Skill\(\{"name":"<skill-name>"\}\)/.test(systemVisible)) {
1382
- throw new Error(`agent BP1 must carry the compact skill manifest alongside the frozen Skill tool: ${systemVisible.slice(0, 1200)}`);
1383
- }
1384
- if (!/# Tool Use/i.test(systemVisible) || !/# Agent Constraints/i.test(systemVisible)) {
1385
- throw new Error(`agent system layers must carry BP1 tool policy and BP2 role rules: ${systemVisible.slice(0, 1200)}`);
1386
- }
1387
- const agentSkillToolNames = (agentSkillSession.tools || []).map((tool) => tool?.name).filter(Boolean);
1388
- if (!agentSkillToolNames.includes('Skill')) {
1389
- throw new Error(`read-write agent schema must expose Skill loader with the manifest: ${agentSkillToolNames.join(', ')}`);
1390
- }
1391
- } finally {
1392
- closeSession(agentSkillSession.id, 'tool-smoke');
1393
- }
1394
- } finally {
1395
- rmSync(skillManifestTmp, { recursive: true, force: true });
1396
- }
1397
- const explorerSession = createSession({
1398
- provider: 'openai-oauth',
1399
- model: 'tool-smoke-model',
1400
- owner: AGENT_OWNER,
1401
- agent: 'explorer',
1402
- cwd: root,
1403
- permission: 'read',
1404
- skipSkills: true,
1405
- schemaAllowedTools: ['code_graph', 'find', 'glob', 'list', 'grep', 'read'],
1406
- });
1407
- try {
1408
- const visible = (explorerSession.messages || []).map((m) => String(m.content || '')).join('\n');
1409
- const systemVisible = (explorerSession.messages || [])
1410
- .filter((m) => m?.role === 'system')
1411
- .map((m) => String(m.content || ''))
1412
- .join('\n');
1413
- const userReminderVisible = (explorerSession.messages || [])
1414
- .filter((m) => m?.role === 'user')
1415
- .map((m) => String(m.content || ''))
1416
- .join('\n');
1417
- if (!/Read-only retrieval role/i.test(visible) || /# environment/i.test(visible) || /git operations deferred to Lead/i.test(visible)) {
1418
- throw new Error(`explorer hidden retrieval context should stay slim: ${visible.slice(0, 1200)}`);
1419
- }
1420
- if (!/# Role: explorer/i.test(systemVisible) || /# Role: explorer/i.test(userReminderVisible) || !/only WHERE/i.test(systemVisible)) {
1421
- throw new Error(`explorer role md must ride BP2 system, not BP3 user reminder: system=${systemVisible.slice(0, 600)} user=${userReminderVisible.slice(0, 600)}`);
1422
- }
1423
- // System layers (BP1 tool policy + BP2 role md) are shared/frozen and sized
1424
- // elsewhere; the slimness cap guards only the per-session injected layers
1425
- // (BP3 user reminder etc.), so measure non-system messages only.
1426
- const injectedVisible = (explorerSession.messages || [])
1427
- .filter((m) => m?.role !== 'system')
1428
- .map((m) => String(m.content || ''))
1429
- .join('\n');
1430
- const injectedBytes = Buffer.byteLength(injectedVisible, 'utf8');
1431
- if (injectedBytes > 1800) {
1432
- throw new Error(`explorer hidden retrieval context too large: ${injectedBytes} bytes (injected layers only)`);
1433
- }
1434
- } finally {
1435
- closeSession(explorerSession.id, 'tool-smoke');
1436
- }
1437
- const workerSession = createSession({
1438
- provider: 'openai-oauth',
1439
- model: 'tool-smoke-model',
1440
- owner: AGENT_OWNER,
1441
- agent: 'worker',
1442
- cwd: root,
1443
- permission: 'read-write',
1444
- taskBrief: 'Implement a scoped smoke check.',
1445
- });
1446
- try {
1447
- const visible = (workerSession.messages || []).map((m) => String(m.content || '')).join('\n');
1448
- const userReminderVisible = (workerSession.messages || [])
1449
- .filter((m) => m?.role === 'user')
1450
- .map((m) => String(m.content || ''))
1451
- .join('\n');
1452
- if (/(^|\n)# role\n/i.test(visible) || /(^|\n)permission:/i.test(visible)) {
1453
- throw new Error(`agent context must not repeat raw role/permission labels: ${visible.slice(0, 1200)}`);
1454
- }
1455
- if (/# role-identity/i.test(visible)) {
1456
- throw new Error(`agent context must not repeat role identity: ${visible.slice(0, 1200)}`);
1457
- }
1458
- if (/# task-brief/i.test(visible)) {
1459
- throw new Error(`agent context must not repeat task brief: ${visible.slice(0, 1200)}`);
1460
- }
1461
- if (/available-skills/i.test(userReminderVisible)) {
1462
- throw new Error(`agent skill manifest must stay in system BP1, not user reminders: ${userReminderVisible.slice(0, 1200)}`);
1463
- }
1464
- if (/(^|\n)# environment/i.test(visible)) {
1465
- throw new Error(`agent context must not inject environment reminder: ${visible.slice(0, 1200)}`);
1466
- }
1467
- const workerToolNames = (workerSession.tools || []).map((tool) => tool?.name).filter(Boolean);
1468
- if (workerToolNames.includes('load_tool')) {
1469
- throw new Error(`agent session schema must not expose deferred load_tool: ${workerToolNames.join(', ')}`);
1470
- }
1471
- for (const name of ['shell', 'task']) {
1472
- if (!workerToolNames.includes(name)) {
1473
- throw new Error(`read-write agent session schema must expose ${name} for self-verification: ${workerToolNames.join(', ')}`);
1474
- }
1475
- }
1476
- for (const name of ['skills_list', 'skill_view', 'skill_execute']) {
1477
- if (workerToolNames.includes(name)) {
1478
- throw new Error(`agent session schema must not expose legacy skill tool ${name}: ${workerToolNames.join(', ')}`);
1479
- }
1480
- }
1481
- } finally {
1482
- closeSession(workerSession.id, 'tool-smoke');
1483
- }
1484
- const readAgentSession = createSession({
1485
- provider: 'openai-oauth',
1486
- model: 'tool-smoke-model',
1487
- owner: AGENT_OWNER,
1488
- agent: 'worker',
1489
- cwd: root,
1490
- permission: 'read',
1491
- });
1492
- const writeAgentSession = createSession({
1493
- provider: 'openai-oauth',
1494
- model: 'tool-smoke-model',
1495
- owner: AGENT_OWNER,
1496
- agent: 'worker',
1497
- cwd: root,
1498
- permission: 'read-write',
1499
- });
1500
- const fullAgentSession = createSession({
1501
- provider: 'openai-oauth',
1502
- model: 'tool-smoke-model',
1503
- owner: AGENT_OWNER,
1504
- agent: 'worker',
1505
- cwd: root,
1506
- permission: 'full',
1507
- });
1508
- const publicExploreSession = createSession({
1509
- provider: 'openai-oauth',
1510
- model: 'tool-smoke-model',
1511
- owner: AGENT_OWNER,
1512
- role: 'explore',
1513
- cwd: root,
1514
- permission: 'read',
1515
- });
1516
- try {
1517
- const readTools = (readAgentSession.tools || []).map((tool) => tool?.name).filter(Boolean);
1518
- const writeTools = (writeAgentSession.tools || []).map((tool) => tool?.name).filter(Boolean);
1519
- const fullTools = (fullAgentSession.tools || []).map((tool) => tool?.name).filter(Boolean);
1520
- const publicExploreTools = (publicExploreSession.tools || []).map((tool) => tool?.name).filter(Boolean);
1521
- // Read-role AGENT sessions carry shell/task so review/debug agents can run
1522
- // their own verification (build/test); the plain readonly preset (public
1523
- // explore role) still omits them.
1524
- const expectedReadTools = ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'shell', 'task', 'explore', 'search', 'web_fetch', 'Skill'];
1525
- const expectedPublicReadTools = ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'explore', 'search', 'web_fetch', 'Skill'];
1526
- const expectedWriteTools = ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'apply_patch', 'shell', 'task', 'explore', 'search', 'web_fetch', 'Skill'];
1527
- if (JSON.stringify(readTools) !== JSON.stringify(expectedReadTools)) {
1528
- throw new Error(`read agent schema must be fixed allow-list: expected=${expectedReadTools.join(', ')} actual=${readTools.join(', ')}`);
1529
- }
1530
- if (JSON.stringify(writeTools) !== JSON.stringify(expectedWriteTools)) {
1531
- throw new Error(`read-write agent schema must be fixed allow-list: expected=${expectedWriteTools.join(', ')} actual=${writeTools.join(', ')}`);
1532
- }
1533
- if (readTools.includes('load_tool') || writeTools.includes('load_tool')) {
1534
- throw new Error(`agent session fixed schemas must omit load_tool: read=${readTools.join(', ')} write=${writeTools.join(', ')}`);
1535
- }
1536
- if (readTools.includes('apply_patch')) {
1537
- throw new Error(`read agent schema must omit apply_patch: read=${readTools.join(', ')}`);
1538
- }
1539
- for (const name of ['shell', 'task']) {
1540
- if (!readTools.includes(name)) {
1541
- throw new Error(`read agent schema must carry verification tool ${name}: read=${readTools.join(', ')}`);
1542
- }
1543
- if (publicExploreTools.includes(name)) {
1544
- throw new Error(`public explore role must omit ${name}: explore=${publicExploreTools.join(', ')}`);
1545
- }
1546
- }
1547
- for (const name of ['apply_patch', 'shell', 'task']) {
1548
- if (!writeTools.includes(name)) {
1549
- throw new Error(`read-write agent schema must preserve ${name}: write=${writeTools.join(', ')}`);
1550
- }
1551
- }
1552
- for (const name of ['memory', 'recall', 'reply']) {
1553
- if (readTools.includes(name) || writeTools.includes(name)) {
1554
- throw new Error(`read/read-write agent schema must not expose full-runtime internal tool ${name}: read=${readTools.join(', ')} write=${writeTools.join(', ')}`);
1555
- }
1556
- }
1557
- if (!readTools.includes('explore') || !writeTools.includes('explore')) {
1558
- throw new Error(`read/read-write agent schemas must expose explore: read=${readTools.join(', ')} write=${writeTools.join(', ')}`);
1559
- }
1560
- if (!fullTools.includes('shell')) {
1561
- throw new Error(`full agent schema must retain shell: full=${fullTools.join(', ')}`);
1562
- }
1563
- if (!fullTools.includes('explore')) {
1564
- throw new Error(`full agent schema must expose explore: full=${fullTools.join(', ')}`);
1565
- }
1566
- // The explore wrapper stays IN the schema for every read role — including
1567
- // the explore agent itself. Recursion is broken at call time in
1568
- // pre-dispatch-deny.mjs via recursiveWrapperToolNameForPublicAgent, not by
1569
- // schema stripping. (Read AGENT sessions add shell/task on top, so the
1570
- // public explore bundle is its own cache group now.)
1571
- if (JSON.stringify(publicExploreTools) !== JSON.stringify(expectedPublicReadTools)) {
1572
- throw new Error(`public explore role must ship the readonly bundle (incl. explore): expected=${expectedPublicReadTools.join(', ')} actual=${publicExploreTools.join(', ')}`);
1573
- }
1574
- if (recursiveWrapperToolNameForPublicAgent('explore') !== 'explore') {
1575
- throw new Error('call-time anti-recursion must map public explore agent to its own wrapper tool');
1576
- }
1577
- } finally {
1578
- closeSession(readAgentSession.id, 'tool-smoke');
1579
- closeSession(writeAgentSession.id, 'tool-smoke');
1580
- closeSession(fullAgentSession.id, 'tool-smoke');
1581
- closeSession(publicExploreSession.id, 'tool-smoke');
1582
- }
1583
- const resumeAgentSession = createSession({
1584
- provider: 'openai-oauth',
1585
- model: 'tool-smoke-model',
1586
- owner: AGENT_OWNER,
1587
- agent: 'worker',
1588
- cwd: root,
1589
- permission: 'read-write',
1590
- });
1591
- try {
1592
- const resumed = await resumeSession(resumeAgentSession.id, 'full');
1593
- const resumedTools = (resumed?.tools || []).map((tool) => tool?.name).filter(Boolean);
1594
- const expectedWriteTools = ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'apply_patch', 'shell', 'task', 'explore', 'search', 'web_fetch', 'Skill'];
1595
- if (JSON.stringify(resumedTools) !== JSON.stringify(expectedWriteTools)) {
1596
- throw new Error(`resumed read-write agent schema must keep fixed allow-list: expected=${expectedWriteTools.join(', ')} actual=${resumedTools.join(', ')}`);
1597
- }
1598
- } finally {
1599
- closeSession(resumeAgentSession.id, 'tool-smoke');
1600
- }
1601
- const noneAgentSession = createSession({
1602
- provider: 'openai-oauth',
1603
- model: 'tool-smoke-model',
1604
- owner: AGENT_OWNER,
1605
- agent: 'worker',
1606
- cwd: root,
1607
- permission: 'none',
1608
- });
1609
- try {
1610
- const resumedNone = await resumeSession(noneAgentSession.id, 'full');
1611
- const noneTools = (resumedNone?.tools || []).map((tool) => tool?.name).filter(Boolean);
1612
- if (noneTools.length !== 0) {
1613
- throw new Error(`resumed permission=none agent schema must stay empty: actual=${noneTools.join(', ')}`);
1614
- }
1615
- } finally {
1616
- closeSession(noneAgentSession.id, 'tool-smoke');
1617
- }
1618
- const objectPermissionSession = createSession({
1619
- provider: 'openai-oauth',
1620
- model: 'tool-smoke-model',
1621
- owner: AGENT_OWNER,
1622
- agent: 'worker',
1623
- cwd: root,
1624
- permission: { allow: ['read', 'grep'], deny: ['grep'] },
1625
- });
1626
- try {
1627
- const resumedObject = await resumeSession(objectPermissionSession.id, 'full');
1628
- const objectTools = (resumedObject?.tools || []).map((tool) => tool?.name).filter(Boolean);
1629
- if (JSON.stringify(objectTools) !== JSON.stringify(['read'])) {
1630
- throw new Error(`resumed object-permission agent schema must reapply allow/deny and agent filters: actual=${objectTools.join(', ')}`);
1631
- }
1632
- } finally {
1633
- closeSession(objectPermissionSession.id, 'tool-smoke');
1634
- }
1635
- const hiddenAgents = JSON.parse(readFileSync(join(root, 'src', 'defaults', 'agents.json'), 'utf8')).agents || [];
1636
- const hiddenPreset = { id: 'hidden-smoke', name: 'hidden-smoke', type: 'agent', provider: 'openai-oauth', model: 'tool-smoke-model', tools: 'full' };
1637
- const hiddenRuntimeSpec = { scopeKey: 'hidden-role-smoke', lane: 'agent' };
1638
- const hiddenBadTools = new Set(['shell', 'task', 'Skill', 'memory', 'reply', 'recall']);
1639
- const expectedForHiddenAgent = (permission, schemaAllowedTools) => {
1640
- if (Array.isArray(schemaAllowedTools)) return schemaAllowedTools.slice();
1641
- if (permission === 'none') return [];
1642
- if (permission === 'read') return ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'explore', 'search', 'web_fetch'];
1643
- if (permission === 'read-write') return ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'apply_patch', 'explore', 'search', 'web_fetch'];
1644
- return null;
1645
- };
1646
- for (const entry of hiddenAgents) {
1647
- const agent = String(entry?.agent || '').trim();
1648
- if (!agent) continue;
1649
- const hidden = getHiddenAgent(agent);
1650
- const permission = resolveAgentSessionPermission(agent, hidden?.permission || null);
1651
- const schemaAllowedTools = resolveHiddenRoleSchemaAllowedTools(hidden);
1652
- const { session } = prepareAgentSession({
1653
- agent,
1654
- presetName: 'hidden-smoke',
1655
- preset: hiddenPreset,
1656
- runtimeSpec: hiddenRuntimeSpec,
1657
- permission,
1658
- cwd: root,
1659
- sourceType: 'hidden-role-smoke',
1660
- sourceName: agent,
1661
- schemaAllowedTools,
1662
- });
1663
- try {
1664
- const tools = (session.tools || []).map((tool) => tool?.name).filter(Boolean);
1665
- const resumed = await resumeSession(session.id, 'full');
1666
- const resumedTools = (resumed?.tools || []).map((tool) => tool?.name).filter(Boolean);
1667
- const expected = expectedForHiddenAgent(permission, schemaAllowedTools);
1668
- // Order-insensitive: the session tool surface follows catalog order, while
1669
- // schemaAllowedTools declares an allow-set; only set equality is contractual.
1670
- const asSet = (list) => JSON.stringify(list.slice().sort());
1671
- if (expected && (asSet(tools) !== asSet(expected) || asSet(resumedTools) !== asSet(expected))) {
1672
- throw new Error(`hidden agent ${agent} schema mismatch: expected=${expected.join(', ')} tools=${tools.join(', ')} resumed=${resumedTools.join(', ')}`);
1673
- }
1674
- const leaked = tools.filter((name) => hiddenBadTools.has(name) && !(expected || []).includes(name));
1675
- if (leaked.length) {
1676
- throw new Error(`hidden agent ${agent} leaked forbidden full-runtime tools: ${leaked.join(', ')} from ${tools.join(', ')}`);
1677
- }
1678
- const systemVisible = (session.messages || [])
1679
- .filter((m) => m?.role === 'system')
1680
- .map((m) => String(m.content || ''))
1681
- .join('\n');
1682
- if (/available-skills|Skill\(/i.test(systemVisible)) {
1683
- throw new Error(`hidden agent ${agent} must not carry Skill manifest without Skill tool`);
1684
- }
1685
- if (/effective-cwd|Override cwd|# environment|# task-brief/i.test(systemVisible)) {
1686
- throw new Error(`hidden agent ${agent} must not carry cwd/environment/task-brief injection`);
1687
- }
1688
- } finally {
1689
- closeSession(session.id, 'tool-smoke');
1690
- }
1691
- }
1692
- }
1693
- const patchTool = PATCH_TOOL_DEFS[0];
1694
- const patchDescription = patchTool?.inputSchema?.properties?.patch?.description || '';
1695
- if (!/V4A/i.test(patchDescription) || !/one (?:file )?block per target file/i.test(patchDescription) || !/exact current context/i.test(patchDescription)) {
1696
- throw new Error('apply_patch JSON fallback schema must keep V4A, per-target block, and exact-context guidance');
1697
- }
1698
- if (!/FREEFORM tool/i.test(patchTool?.freeformDescription || '') || patchTool?.freeform?.type !== 'grammar' || patchTool?.freeform?.syntax !== 'lark') {
1699
- throw new Error(`apply_patch must expose freeform grammar metadata: ${JSON.stringify(patchTool)}`);
1700
- }
1701
- for (const requiredGrammarLine of [
1702
- 'start: begin_patch hunk+ end_patch',
1703
- 'add_hunk: "*** Add File: " filename LF add_line+',
1704
- 'change_move: "*** Move to: " filename LF',
1705
- '%import common.LF',
1706
- ]) {
1707
- if (!patchTool.freeform.definition.includes(requiredGrammarLine)) {
1708
- throw new Error(`apply_patch freeform grammar missing required line: ${requiredGrammarLine}`);
1709
- }
1710
- }
1711
- {
1712
- const rawPatch = '*** Begin Patch\n*** Add File: custom-wire.txt\n+ok\n*** End Patch\n';
1713
- const body = buildRequestBody(
1714
- [
1715
- { role: 'system', content: 'sys' },
1716
- { role: 'user', content: 'patch please' },
1717
- {
1718
- role: 'assistant',
1719
- content: '',
1720
- toolCalls: [{ id: 'call_patch_1', name: 'apply_patch', arguments: { patch: rawPatch }, nativeType: 'custom_tool_call' }],
1721
- },
1722
- { role: 'tool', toolCallId: 'call_patch_1', content: 'OK' },
1723
- ],
1724
- 'gpt-5.5',
1725
- PATCH_TOOL_DEFS,
1726
- {},
1727
- );
1728
- const wirePatchTool = body.tools?.find((tool) => tool.name === 'apply_patch');
1729
- if (wirePatchTool?.type !== 'custom' || wirePatchTool?.format?.syntax !== 'lark') {
1730
- throw new Error(`OpenAI Responses apply_patch must serialize as a custom grammar tool: ${JSON.stringify(wirePatchTool)}`);
1731
- }
1732
- if (!/FREEFORM tool/i.test(wirePatchTool.description || '')) {
1733
- throw new Error(`OpenAI Responses apply_patch must use freeform description: ${JSON.stringify(wirePatchTool)}`);
1734
- }
1735
- const customCall = body.input?.find((item) => item.type === 'custom_tool_call');
1736
- const customOutput = body.input?.find((item) => item.type === 'custom_tool_call_output');
1737
- if (customCall?.input !== rawPatch || customCall?.call_id !== 'call_patch_1') {
1738
- throw new Error(`custom apply_patch replay must keep raw patch input: ${JSON.stringify(body.input)}`);
1739
- }
1740
- if (customOutput?.call_id !== 'call_patch_1' || customOutput?.output !== 'OK') {
1741
- throw new Error(`custom apply_patch output must replay as custom_tool_call_output: ${JSON.stringify(body.input)}`);
1742
- }
1743
- }
1744
- {
1745
- const rawPatch = '*** Begin Patch\n*** Add File: custom-parser.txt\n+ok\n*** End Patch\n';
1746
- const encoder = new TextEncoder();
1747
- const frames = [
1748
- { type: 'response.created', response: { id: 'resp_custom_patch', model: 'gpt-5.5' } },
1749
- { type: 'response.custom_tool_call_input.delta', delta: rawPatch.slice(0, 16) },
1750
- { type: 'response.output_item.done', item: { type: 'custom_tool_call', call_id: 'call_patch_sse', name: 'apply_patch', input: rawPatch } },
1751
- { type: 'response.completed', response: { id: 'resp_custom_patch', model: 'gpt-5.5', usage: { input_tokens: 1, output_tokens: 1 }, output: [] } },
1752
- ];
1753
- const bodyText = frames.map((frame) => `data: ${JSON.stringify(frame)}\n\n`).join('');
1754
- let emitted = null;
1755
- const response = await sendViaHttpSse({
1756
- auth: { access_token: 'fake-token', account_id: '' },
1757
- body: { model: 'gpt-5.5', input: [], stream: true },
1758
- opts: {},
1759
- onToolCall: (call) => { emitted = call; },
1760
- externalSignal: null,
1761
- poolKey: 'tool-smoke-custom-patch',
1762
- cacheKey: 'tool-smoke-custom-patch',
1763
- iteration: 1,
1764
- useModel: 'gpt-5.5',
1765
- fetchFn: async () => new Response(new ReadableStream({
1766
- start(controller) {
1767
- controller.enqueue(encoder.encode(bodyText));
1768
- controller.close();
1769
- },
1770
- }), { status: 200, headers: { 'content-type': 'text/event-stream' } }),
1771
- });
1772
- const call = response.toolCalls?.[0];
1773
- if (call?.nativeType !== 'custom_tool_call' || call?.name !== 'apply_patch' || call?.arguments?.patch !== rawPatch) {
1774
- throw new Error(`custom apply_patch SSE parser must produce internal patch args: ${JSON.stringify(response.toolCalls)}`);
1775
- }
1776
- if (emitted?.arguments?.patch !== rawPatch) {
1777
- throw new Error(`custom apply_patch SSE parser must eager-emit patch args: ${JSON.stringify(emitted)}`);
1778
- }
1779
- }
1780
- const readPathSchema = BUILTIN_TOOLS.find((tool) => tool.name === 'read')?.inputSchema?.properties?.path || {};
1781
- const readPathDescription = readPathSchema.description || '';
1782
- if (!/\{path,offset,limit\}\[\]/i.test(readPathDescription) || !/real arrays/i.test(readPathDescription)) {
1783
- throw new Error('read schema must keep directory-vs-file guidance');
1784
- }
1785
- if (!/Not for director/i.test((BUILTIN_TOOLS.find((tool) => tool.name === 'read')?.description) || '')) {
1786
- throw new Error('read description must keep directory-vs-file guidance');
1787
- }
1788
- const readTool = BUILTIN_TOOLS.find((tool) => tool.name === 'read');
1789
- const readDescription = readTool?.description || '';
1790
- const readProps = readTool?.inputSchema?.properties || {};
1791
- const readArraySchema = readPathSchema.anyOf?.find((entry) => entry?.type === 'array');
1792
- const readArrayItemAnyOf = readArraySchema?.items?.anyOf || [];
1793
- if (!readArrayItemAnyOf.some((entry) => entry?.type === 'object' && entry?.properties?.offset && entry?.properties?.limit)) {
1794
- throw new Error('read schema must expose array-of-region objects for batched spans');
1795
- }
1796
- if (/line\+context/i.test(readDescription) || !/Read file contents/i.test(readDescription) || !/\{path,offset,limit\}\[\]/i.test(readDescription)) {
1797
- throw new Error('read description must expose offset/limit as the single window form');
1798
- }
1799
- if (readProps.line || readProps.context) {
1800
- throw new Error('read schema must not expose legacy line/context window fields');
1801
- }
1802
- if (readProps.offset?.minimum !== 0 || !/Lines to skip/i.test(readProps.offset?.description || '') || !/Max lines/i.test(readProps.limit?.description || '')) {
1803
- throw new Error('read offset schema must describe Mixdog paging cursor semantics');
1804
- }
1805
- if (/line\/context/i.test(JSON.stringify(readTool?.inputSchema || {}))) {
1806
- throw new Error('read schema surface must not mention legacy line/context');
1807
- }
1808
- {
1809
- const benchRunSrc = readFileSync(resolve(root, 'scripts/bench-run.mjs'), 'utf8');
1810
- if (!/task_complete:\s*results\.length > 0 && completed === results\.length/.test(benchRunSrc)) {
1811
- throw new Error('bench-run must require every task to complete before saving a round');
1812
- }
1813
- if (!/score_complete:\s*results\.length > 0 && taskErrors\.length === 0 && scoreErrors\.length === 0 && \(score\?\.cards\?\.length \|\| 0\) === results\.length/.test(benchRunSrc)) {
1814
- throw new Error('bench-run must require a scorecard for every task before saving a round');
1815
- }
1816
- if (!/not saving incomplete round/.test(benchRunSrc) || !/process\.exit\(1\)/.test(benchRunSrc)) {
1817
- throw new Error('bench-run must not save incomplete rounds and must exit non-zero');
1818
- }
1819
- const taskBenchSrc = readFileSync(resolve(root, 'scripts/task-bench.mjs'), 'utf8');
1820
- if (!/const allowPartial = hasFlag\('--allow-partial'\)/.test(taskBenchSrc) || !/skipped\.length && !allowPartial/.test(taskBenchSrc) || !/process\.exit\(1\)/.test(taskBenchSrc)) {
1821
- throw new Error('task-bench must fail partial scoring unless --allow-partial is explicit');
1822
- }
1823
- }
1824
- {
1825
- // setRoute must default to "next session only": a bare
1826
- // runtime.setRoute({model}) call (no options) must NOT rewrite a live
1827
- // session's provider/model in place, or a mid-conversation model/provider
1828
- // switch silently forces a full prompt-cache rewrite (seen as a
1829
- // promptΔ spike + cache_ratio=0% turn in session-bench).
1830
- // God-file splits move implementation into module dirs; scan facade + all
1831
- // split modules so these source-text guards survive refactors.
1832
- const readMjsSources = (rel) => {
1833
- const abs = resolve(root, rel);
1834
- if (rel.endsWith('.mjs')) return readFileSync(abs, 'utf8');
1835
- return readdirSync(abs, { recursive: true })
1836
- .filter((f) => String(f).endsWith('.mjs'))
1837
- .map((f) => readFileSync(resolve(abs, String(f)), 'utf8'))
1838
- .join('\n');
1839
- };
1840
- const runtimeSrc = [readMjsSources('src/mixdog-session-runtime.mjs'), readMjsSources('src/session-runtime')].join('\n');
1841
- const setRouteBlock = runtimeSrc.match(/async setRoute\(next, options = \{\}\) \{[\s\S]*?\n \},\n/)?.[0] || '';
1842
- if (!/applyToCurrentSession = options\?\.applyToCurrentSession === true/.test(setRouteBlock)) {
1843
- throw new Error('setRoute must default applyToCurrentSession to false (model changes apply to the next session only)');
1844
- }
1845
- if (!/const applyLive = applyToCurrentSession \|\| currentSessionEmpty/.test(setRouteBlock)
1846
- || !/if \(!applyLive\)/.test(setRouteBlock)
1847
- || !/return getRoute\(\);/.test(setRouteBlock)) {
1848
- throw new Error('setRoute must early-return before touching a non-empty live session when applyToCurrentSession is false');
1849
- }
1850
- // Empty current session must apply live so /model before the first chat
1851
- // updates route + statusline at once, but compact summary anchors are route
1852
- // history and must keep a compacted session next-session-only. Seeded system
1853
- // or synthetic assistant/tool rows alone must NOT make the session non-empty.
1854
- if (!/!hasRouteHistoryMessage\(session\.messages\)/.test(setRouteBlock)
1855
- || !/!hasRouteHistoryMessage\(session\.liveTurnMessages\)/.test(setRouteBlock)
1856
- || !/SUMMARY_PREFIX/.test(runtimeSrc)
1857
- || !/hasUserConversationMessage\(list\) \|\| list\.some\(isSummaryAnchorMessage\)/.test(runtimeSrc)
1858
- || !/function hasRouteHistoryMessage/.test(runtimeSrc)) {
1859
- throw new Error('setRoute must apply live only to route-empty sessions and must treat compact summary anchors as non-empty route history');
1860
- }
1861
- if (!/createCurrentSession\('model-switch-empty'\)/.test(setRouteBlock)
1862
- || !/createCurrentSession\('model-switch-empty-drain'\)/.test(setRouteBlock)
1863
- || !/const emptySession = getSession\(\)/.test(setRouteBlock)
1864
- || !/cli-model-switch-empty/.test(setRouteBlock)
1865
- || !/pushTranscriptRebind\?\.\(\)/.test(setRouteBlock)
1866
- || !/invalidatePreSessionToolSurface\?\.\(\)/.test(setRouteBlock)) {
1867
- throw new Error('setRoute must drain in-flight create then recreate/rebind empty live sessions so provider-specific BP1/tool surface is rebuilt for /model before first chat');
1868
- }
1869
- const sessionLifecycleSrc = readMjsSources('src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs');
1870
- const updateSessionRouteBlock = sessionLifecycleSrc.match(/export function updateSessionRoute\(id, route = \{\}\) \{[\s\S]*?\n\}/)?.[0] || '';
1871
- if (!/session\.promptCacheKey = providerCacheKey\(session\.provider\)/.test(updateSessionRouteBlock)
1872
- || !/session\.providerCacheOpts = buildSessionProviderCacheOpts\(session\.provider, session\.id, session\.agent\) \|\| null/.test(updateSessionRouteBlock)) {
1873
- throw new Error('updateSessionRoute must refresh provider-scoped prompt cache fields when an empty live session changes provider/model');
1874
- }
1875
- const engineSrc = [readMjsSources('src/tui/engine.mjs'), readMjsSources('src/tui/engine')].join('\n');
1876
- if (/setRoute\(\{ model: m \}, \{ applyToCurrentSession: true \}\)/.test(engineSrc)) {
1877
- throw new Error('TUI setModel must not force applyToCurrentSession:true (model changes must apply to the next session only)');
1878
- }
1879
- if (!/routeOpts\.applyToCurrentSession === true/.test(engineSrc)) {
1880
- throw new Error('TUI setRoute wrapper must default applyToCurrentSession to false');
1881
- }
1882
- }
1883
- const codeGraphDescription = CODE_GRAPH_TOOL_DEFS[0]?.description || '';
1884
- const codeGraphProps = CODE_GRAPH_TOOL_DEFS[0]?.inputSchema?.properties || {};
1885
- const codeGraphSymbolSearchErr = validateBuiltinArgs('code_graph', { mode: 'symbol_search', symbols: ['hook', 'deny'], limit: 5 });
1886
- if (codeGraphSymbolSearchErr) {
1887
- throw new Error(`code_graph guard must accept symbol_search with symbols[] batching: ${codeGraphSymbolSearchErr}`);
1888
- }
1889
- // code_graph description stays structure-oriented and must actively route
1890
- // symbol/definition/caller lookups AWAY from repeated grep (the grep_retry +
1891
- // find_symbol_noscope anti-patterns). It is allowed to be verbose enough to
1892
- // enumerate modes, but must not drift into web-search territory.
1893
- if (!/Repo code structure/i.test(codeGraphDescription) || !/find_symbol\/symbol_search\/search\/references\/callers\/callees/i.test(codeGraphDescription)) {
1894
- throw new Error('code_graph description must stay structure-oriented and name its symbol modes');
1895
- }
1896
- if (!/take files\[\]/i.test(codeGraphDescription) || !/Batch targets per mode/i.test(codeGraphDescription)) {
1897
- throw new Error('code_graph description must route unknown file paths through locators first');
1898
- }
1899
- if (!/files\[\]/i.test(codeGraphProps.mode?.description || '') || !/Source file path/i.test(codeGraphProps.files?.description || '')) {
1900
- throw new Error('code_graph schema must keep compact, repo-local field descriptions');
1901
- }
1902
- const recallTool = MEMORY_TOOL_DEFS.find((tool) => tool.name === 'recall');
1903
- const recallProps = recallTool?.inputSchema?.properties || {};
1904
- if (!/prior-work context/i.test(recallTool?.description || '') || !recallProps.id?.anyOf || !/Do not invent ids/i.test(recallProps.id?.description || '')) {
1905
- throw new Error('recall schema must preserve scoped prior-context guidance and id lookup shape');
1906
- }
1907
- if (!/array for independent fan-out/i.test(recallProps.query?.description || '') || !/Project pool selector/i.test(recallProps.projectScope?.description || '')) {
1908
- throw new Error('recall schema must explain fan-out query and project scope filters');
1909
- }
1910
- // Cross-session / raw recall surface: includeMembers stays a chunk-member
1911
- // output knob, includeRaw exposes unchunked raw/episode turns, and sessionOnly
1912
- // is the explicit opt-in that restores the old single-session hard scope.
1913
- if (!/chunk members/i.test(recallProps.includeMembers?.description || '') || !/does not widen the search pool/i.test(recallProps.includeMembers?.description || '')) {
1914
- throw new Error('recall includeMembers must stay scoped to chunk-member output only');
1915
- }
1916
- if (!recallProps.includeRaw || !/raw\/episode/i.test(recallProps.includeRaw?.description || '')) {
1917
- throw new Error('recall schema must expose includeRaw for unchunked raw/episode turns');
1918
- }
1919
- if (!recallProps.sessionOnly || !/session only/i.test(recallProps.sessionOnly?.description || '')) {
1920
- throw new Error('recall schema must expose sessionOnly as the explicit single-session opt-in');
1921
- }
1922
- // Behaviour-level checks for the cross-session merge contract. These exercise
1923
- // the pure mergeSessionRowsIntoGlobal() helper (no DB) so the starve-prevention
1924
- // + dedupe + includeRaw-parity invariants are guarded, not just the schema.
1925
- {
1926
- // 1) Starve prevention: a flood of session rows must NOT push global hybrid
1927
- // hits off the first page. Global rows carry a real retrievalScore; the
1928
- // session rows (score 0) must sort AFTER them under importance.
1929
- const globalHits = [
1930
- { id: 1, retrievalScore: 0.9, ts: 100 },
1931
- { id: 2, retrievalScore: 0.8, ts: 110 },
1932
- ];
1933
- const sessionFlood = Array.from({ length: 20 }, (_, i) => ({ id: 1000 + i, retrievalScore: 0, ts: 200 + i }));
1934
- const mergedImportance = mergeSessionRowsIntoGlobal(globalHits, sessionFlood, { sort: 'importance' });
1935
- if (mergedImportance.slice(0, 2).map((r) => r.id).join(',') !== '1,2') {
1936
- throw new Error(`session merge must not starve global first page under importance: ${JSON.stringify(mergedImportance.slice(0, 3))}`);
1937
- }
1938
- if (mergedImportance.length !== globalHits.length + sessionFlood.length) {
1939
- throw new Error('session merge must append all non-duplicate session rows');
1940
- }
1941
- // 2) Dedupe by id AND by global root member id (member/leaf double-output).
1942
- const globalWithMembers = [{ id: 5, retrievalScore: 0.7, ts: 100, members: [{ id: 51 }, { id: 52 }] }];
1943
- const sessionDupes = [
1944
- { id: 5, retrievalScore: 0, ts: 300 }, // dup root id
1945
- { id: 51, retrievalScore: 0, ts: 301 }, // dup member id
1946
- { id: 99, retrievalScore: 0, ts: 302 }, // genuinely new
1947
- ];
1948
- const mergedDedupe = mergeSessionRowsIntoGlobal(globalWithMembers, sessionDupes, { sort: 'importance' });
1949
- const dedupeIds = mergedDedupe.map((r) => Number(r.id)).sort((a, b) => a - b);
1950
- if (dedupeIds.join(',') !== '5,99') {
1951
- throw new Error(`session merge must dedupe root+member ids, leaving only new rows: ${JSON.stringify(dedupeIds)}`);
1952
- }
1953
- // 3) date sort keeps newest-first across the merged set.
1954
- const mergedDate = mergeSessionRowsIntoGlobal(
1955
- [{ id: 1, retrievalScore: 0.9, ts: 100 }],
1956
- [{ id: 2, retrievalScore: 0, ts: 999 }],
1957
- { sort: 'date' },
1958
- );
1959
- if (Number(mergedDate[0].id) !== 2) {
1960
- throw new Error(`session merge under date sort must order by ts desc: ${JSON.stringify(mergedDate)}`);
1961
- }
1962
- // 4) Empty session rows is a no-op passthrough (no crash, same array).
1963
- const passthrough = mergeSessionRowsIntoGlobal(globalHits, [], { sort: 'importance' });
1964
- if (passthrough.length !== globalHits.length) {
1965
- throw new Error('session merge with no session rows must be a passthrough');
1966
- }
1967
- }
1968
- const memoryTool = MEMORY_TOOL_DEFS.find((tool) => tool.name === 'memory');
1969
- const memoryProps = memoryTool?.inputSchema?.properties || {};
1970
- if (!/mutation/i.test(memoryTool?.description || '') || !/Exact confirmation phrase/i.test(memoryProps.confirm?.description || '')) {
1971
- throw new Error('memory schema must preserve mutation/destructive confirmation guidance');
1972
- }
1973
- if (memoryProps.category || /category/i.test(memoryTool?.description || '')) {
1974
- throw new Error('memory mutation schema must not expose category');
1975
- }
1976
- const searchTool = SEARCH_TOOL_DEFS.find((tool) => tool.name === 'search');
1977
- const searchProps = searchTool?.inputSchema?.properties || {};
1978
- if (!/Runs synchronously/i.test(searchTool?.description || '') || searchProps.mode || searchProps.action || searchProps.task_id || !searchProps.query?.anyOf || !/array for fan-out/i.test(searchProps.query?.description || '')) {
1979
- throw new Error('search schema must preserve sync execution guidance and string/array query shape');
1980
- }
1981
- if (!/Default web/i.test(searchProps.type?.description || '') || !/locale hint/i.test(searchProps.locale?.description || '') || !/Default low/i.test(searchProps.contextSize?.description || '')) {
1982
- throw new Error('search schema must describe type, locale, and contextSize defaults');
1983
- }
1984
- const webFetchTool = SEARCH_TOOL_DEFS.find((tool) => tool.name === 'web_fetch');
1985
- const webFetchProps = webFetchTool?.inputSchema?.properties || {};
1986
- if (!/Use after search/i.test(webFetchTool?.description || '') || !webFetchProps.url?.anyOf || !/array of URLs/i.test(webFetchProps.url?.description || '')) {
1987
- throw new Error('web_fetch schema must preserve after-search guidance and string/array url shape');
1988
- }
1989
- if (!/offset/i.test(webFetchProps.startIndex?.description || '') || !/Maximum characters/i.test(webFetchProps.maxLength?.description || '')) {
1990
- throw new Error('web_fetch schema must describe paging window fields');
1991
- }
1992
- if (!/deferred tools/i.test(TOOL_SEARCH_TOOL.description || '')
1993
- || !TOOL_SEARCH_TOOL.inputSchema?.properties?.names
1994
- || !TOOL_SEARCH_TOOL.inputSchema?.properties?.select
1995
- || TOOL_SEARCH_TOOL.inputSchema?.additionalProperties !== false) {
1996
- throw new Error('load_tool schema must preserve loader guidance plus names + legacy select fields');
1997
- }
1998
- const toolSearchSession = {
1999
- tools: smokeCatalog.filter((tool) => fullDefaults.has(tool?.name)),
2000
- deferredToolCatalog: smokeCatalog.slice(),
2001
- deferredSelectedTools: [...fullDefaults],
2002
- };
2003
- // load_tool is a pure loader: a free-text query is NOT a search. It loads
2004
- // nothing, returns an error steering to names[], and never activates tools.
2005
- const listQueryResult = JSON.parse(__renderToolSearchForTest({ query: 'shell' }, toolSearchSession, 'full'));
2006
- if (listQueryResult.selected || (Array.isArray(listQueryResult.loaded) && listQueryResult.loaded.length)) {
2007
- throw new Error(`load_tool free-text query must not load: ${JSON.stringify(listQueryResult)}`);
2008
- }
2009
- if (!listQueryResult.error || !/names/i.test(listQueryResult.error)) {
2010
- throw new Error(`load_tool free-text query must steer to names[]: ${JSON.stringify(listQueryResult)}`);
2011
- }
2012
- if (listQueryResult.activeTools.includes('shell') || (Array.isArray(listQueryResult.discoveredTools) && listQueryResult.discoveredTools.includes('shell'))) {
2013
- throw new Error(`load_tool free-text query must not activate/discover tools: ${JSON.stringify(listQueryResult)}`);
2014
- }
2015
- // names[] is the primary loader input (aliases expand, tools activate).
2016
- const namesLoadResult = JSON.parse(__renderToolSearchForTest({ names: ['shell', 'recall'] }, {
2017
- tools: smokeCatalog.filter((tool) => fullDefaults.has(tool?.name)),
2018
- deferredToolCatalog: smokeCatalog.slice(),
2019
- deferredSelectedTools: [...fullDefaults],
2020
- }, 'full'));
2021
- for (const name of ['shell', 'recall']) {
2022
- if (!namesLoadResult.activeTools.includes(name) || !namesLoadResult.loaded.includes(name)) {
2023
- throw new Error(`load_tool names[] must load ${name}: ${JSON.stringify(namesLoadResult)}`);
2024
- }
2025
- }
2026
- // query "select:a,b" is the explicit query-side loader (aliases expand).
2027
- const bulkSelectResult = JSON.parse(__renderToolSearchForTest({ query: 'select:shell,recall' }, toolSearchSession, 'full'));
2028
- if (bulkSelectResult.selected?.mode !== 'select') {
2029
- throw new Error(`tool_search query-select must report select mode: ${JSON.stringify(bulkSelectResult.selected)}`);
2030
- }
2031
- for (const name of ['shell', 'task', 'recall']) {
2032
- if (!bulkSelectResult.activeTools.includes(name)) {
2033
- throw new Error(`tool_search bulk select missing ${name}: ${JSON.stringify(bulkSelectResult)}`);
2034
- }
2035
- }
2036
- const prefixedSelectSession = {
2037
- tools: smokeCatalog.filter((tool) => fullDefaults.has(tool?.name)),
2038
- deferredToolCatalog: smokeCatalog.slice(),
2039
- deferredSelectedTools: [...fullDefaults],
2040
- };
2041
- const prefixedSelectResult = JSON.parse(__renderToolSearchForTest({ select: 'select:shell,recall' }, prefixedSelectSession, 'full'));
2042
- if (!prefixedSelectResult.activeTools.includes('shell') || !prefixedSelectResult.activeTools.includes('recall')) {
2043
- throw new Error(`tool_search select field should accept select: prefix: ${JSON.stringify(prefixedSelectResult)}`);
2044
- }
2045
- if (!Array.isArray(toolSearchSession.deferredDiscoveredTools) || !toolSearchSession.deferredDiscoveredTools.includes('shell')) {
2046
- throw new Error('tool_search must persist discovered tool state on the session');
2047
- }
2048
- const nativeToolSearchSession = {
2049
- provider: 'openai-oauth',
2050
- tools: smokeCatalog.filter((tool) => fullDefaults.has(tool?.name)),
2051
- deferredToolCatalog: smokeCatalog.slice(),
2052
- deferredSelectedTools: [...fullDefaults],
2053
- deferredDiscoveredTools: [],
2054
- deferredProviderMode: 'native',
2055
- deferredNativeTools: true,
2056
- };
2057
- nativeToolSearchSession.deferredCallableTools = nativeToolSearchSession.tools.map((tool) => tool.name);
2058
- const nativeBaseToolsJson = JSON.stringify(nativeToolSearchSession.tools);
2059
- const nativeBaseRequest = buildRequestBody(
2060
- [{ role: 'user', content: 'load shell' }],
2061
- 'gpt-5.4',
2062
- nativeToolSearchSession.tools,
2063
- { sessionId: 'deferred-stability', session: nativeToolSearchSession },
2064
- );
2065
- const nativeSelectResult = JSON.parse(__renderToolSearchForTest({ select: 'shell,recall' }, nativeToolSearchSession, 'full'));
2066
- for (const name of ['shell', 'task', 'recall']) {
2067
- if (!nativeSelectResult.activeTools.includes(name)) {
2068
- throw new Error(`native load_tool must register ${name} as callable: ${JSON.stringify(nativeSelectResult)}`);
2069
- }
2070
- }
2071
- if (JSON.stringify(nativeToolSearchSession.tools) !== nativeBaseToolsJson
2072
- || nativeToolSearchSession.tools.some((tool) => tool?.name === 'shell')) {
2073
- throw new Error(`native load_tool must keep the base tools array byte-stable: ${JSON.stringify(nativeToolSearchSession.tools)}`);
2074
- }
2075
- if (!nativeSelectResult.nativeToolSearch?.openaiTools?.some((tool) => tool?.name === 'shell' && tool?.defer_loading === true)) {
2076
- throw new Error(`native tool_search must return OpenAI loadable deferred tools: ${JSON.stringify(nativeSelectResult.nativeToolSearch)}`);
2077
- }
2078
- if (!nativeSelectResult.nativeToolSearch?.toolReferences?.includes('shell')) {
2079
- throw new Error(`native tool_search must return Anthropic tool references: ${JSON.stringify(nativeSelectResult.nativeToolSearch)}`);
2080
- }
2081
- const nativeToolCountAfterFirstLoad = nativeToolSearchSession.tools.length;
2082
- const nativeRepeatResult = JSON.parse(__renderToolSearchForTest({ select: 'shell,recall' }, nativeToolSearchSession, 'full'));
2083
- if (nativeRepeatResult.loaded.length
2084
- || !['shell', 'task', 'recall'].every((name) => nativeRepeatResult.alreadyActive.includes(name))
2085
- || nativeRepeatResult.nativeToolSearch?.toolReferences?.length
2086
- || nativeRepeatResult.nativeToolSearch?.openaiTools?.length
2087
- || nativeToolSearchSession.tools.length !== nativeToolCountAfterFirstLoad) {
2088
- throw new Error(`repeated native load_tool must report already-active without reinjection: ${JSON.stringify(nativeRepeatResult)}`);
2089
- }
2090
- const nativeHistory = [
2091
- { role: 'user', content: 'load shell' },
2092
- {
2093
- role: 'assistant',
2094
- content: '',
2095
- toolCalls: [{ id: 'search-1', name: 'load_tool', arguments: { names: ['shell'] }, nativeType: 'tool_search_call' }],
2096
- },
2097
- {
2098
- role: 'tool',
2099
- toolCallId: 'search-1',
2100
- content: nativeSelectResult.nativeToolSearch.summary,
2101
- nativeToolSearch: nativeSelectResult.nativeToolSearch,
2102
- },
2103
- ];
2104
- const nativeFollowupRequest = buildRequestBody(
2105
- nativeHistory,
2106
- 'gpt-5.4',
2107
- nativeToolSearchSession.tools,
2108
- { sessionId: 'deferred-stability', session: nativeToolSearchSession },
2109
- );
2110
- if (JSON.stringify(nativeFollowupRequest.tools) !== JSON.stringify(nativeBaseRequest.tools)
2111
- || nativeFollowupRequest.prompt_cache_key !== nativeBaseRequest.prompt_cache_key) {
2112
- throw new Error('OpenAI native loading must not change tools or prompt_cache_key');
2113
- }
2114
- const nativeOutput = nativeFollowupRequest.input.find((item) => item?.type === 'tool_search_output');
2115
- if (!nativeOutput?.tools?.some((tool) => tool?.name === 'shell')
2116
- || nativeFollowupRequest.tools.some((tool) => tool?.name === 'shell')) {
2117
- throw new Error(`OpenAI loaded schemas must exist only in tool_search_output history: ${JSON.stringify(nativeFollowupRequest)}`);
2118
- }
2119
- const directMcpSession = {
2120
- provider: 'openai-oauth',
2121
- toolSpec: 'full',
2122
- tools: [{ name: 'load_tool', inputSchema: { type: 'object', properties: {} } }],
2123
- deferredToolCatalog: [
2124
- { name: 'load_tool', inputSchema: { type: 'object', properties: {} } },
2125
- { name: 'mcp__demo__ping', annotations: { readOnlyHint: true }, inputSchema: { type: 'object', properties: {} } },
2126
- ],
2127
- deferredCallableTools: ['load_tool', 'mcp__demo__ping'],
2128
- deferredProviderMode: 'native',
2129
- deferredNativeTools: true,
2130
- };
2131
- prepareDeferredToolCallThrough(directMcpSession, 'mcp__demo__ping', {});
2132
- if (directMcpSession.tools.some((tool) => tool?.name === 'mcp__demo__ping')
2133
- || !directMcpSession.deferredCallableTools.includes('mcp__demo__ping')) {
2134
- throw new Error('subsequent native MCP calls must use the callable registry without session.tools promotion');
2135
- }
2136
- const readonlyReportingSession = {
2137
- tools: [TOOL_SEARCH_TOOL],
2138
- deferredToolCatalog: smokeCatalog.slice(),
2139
- deferredSelectedTools: ['load_tool'],
2140
- };
2141
- const readonlyReportingResult = JSON.parse(__renderToolSearchForTest(
2142
- { names: ['shell', 'definitely_missing_tool'] },
2143
- readonlyReportingSession,
2144
- 'readonly',
2145
- {
2146
- mcpStatus: () => ({
2147
- servers: [
2148
- { name: 'connecting-mcp', status: 'disconnected' },
2149
- { name: 'failed-mcp', status: 'failed' },
2150
- ],
2151
- }),
2152
- },
2153
- ));
2154
- if (!readonlyReportingResult.blocked?.some((entry) => entry?.name === 'shell' && entry?.reason === 'readonly mode')
2155
- || !readonlyReportingResult.missing.includes('definitely_missing_tool')
2156
- || !readonlyReportingResult.pendingMcpServers?.includes('connecting-mcp')
2157
- || !readonlyReportingResult.failedMcpServers?.includes('failed-mcp')
2158
- || !/retry next turn/i.test(readonlyReportingResult.note || '')
2159
- || !/unavailable/i.test(readonlyReportingResult.note || '')) {
2160
- throw new Error(`load_tool must preserve readonly and MCP status reporting: ${JSON.stringify(readonlyReportingResult)}`);
2161
- }
2162
- const nativePatchSearchSession = {
2163
- provider: 'openai-oauth',
2164
- tools: smokeCatalog.filter((tool) => fullDefaults.has(tool?.name) && tool?.name !== 'apply_patch'),
2165
- deferredToolCatalog: smokeCatalog.slice(),
2166
- deferredSelectedTools: [...fullDefaults].filter((name) => name !== 'apply_patch'),
2167
- deferredDiscoveredTools: [],
2168
- deferredProviderMode: 'native',
2169
- deferredNativeTools: true,
2170
- };
2171
- const nativePatchSelectResult = JSON.parse(__renderToolSearchForTest({ select: 'apply_patch' }, nativePatchSearchSession, 'full'));
2172
- const nativePatchTool = nativePatchSelectResult.nativeToolSearch?.openaiTools?.find((tool) => tool?.name === 'apply_patch');
2173
- if (nativePatchTool?.type !== 'custom' || nativePatchTool?.format?.syntax !== 'lark') {
2174
- throw new Error(`native tool_search must preserve apply_patch as OpenAI custom freeform: ${JSON.stringify(nativePatchSelectResult.nativeToolSearch)}`);
2175
- }
2176
- if (nativePatchTool.defer_loading === true || nativePatchTool.parameters) {
2177
- throw new Error(`native tool_search custom apply_patch must not be downgraded to deferred function schema: ${JSON.stringify(nativePatchTool)}`);
2178
- }
2179
- const grokCanonicalSession = { provider: 'grok-oauth', tools: [], messages: [] };
2180
- applyDeferredToolSurface(grokCanonicalSession, 'full', smokeCatalog, { provider: 'grok-oauth' });
2181
- const grokCanonicalJson = JSON.stringify(grokCanonicalSession.tools);
2182
- const grokLoadResult = JSON.parse(__renderToolSearchForTest({ names: ['apply_patch'] }, grokCanonicalSession, 'full'));
2183
- if (grokCanonicalSession.deferredNativeTools
2184
- || grokLoadResult.nativeToolSearch
2185
- || JSON.stringify(grokCanonicalSession.tools) !== grokCanonicalJson
2186
- || !grokLoadResult.alreadyActive.includes('apply_patch')) {
2187
- throw new Error(`Grok must use a fixed canonical ordinary-function surface: ${JSON.stringify(grokLoadResult)}`);
2188
- }
2189
- // Native query-select explicitly loads onto the active surface; aliases expand.
2190
- const nativeSelectQuerySession = {
2191
- tools: smokeCatalog.filter((tool) => fullDefaults.has(tool?.name)),
2192
- deferredToolCatalog: smokeCatalog.slice(),
2193
- deferredSelectedTools: [...fullDefaults],
2194
- deferredDiscoveredTools: [],
2195
- deferredProviderMode: 'native',
2196
- deferredNativeTools: true,
2197
- };
2198
- const nativeSelectQueryResult = JSON.parse(__renderToolSearchForTest({ query: 'select:search' }, nativeSelectQuerySession, 'full'));
2199
- for (const name of ['search', 'web_fetch']) {
2200
- if (!nativeSelectQueryResult.activeTools.includes(name)) {
2201
- throw new Error(`native tool_search query-select should load ${name}: ${JSON.stringify(nativeSelectQueryResult)}`);
2202
- }
2203
- }
2204
- if (!nativeSelectQueryResult.nativeToolSearch?.toolReferences?.includes('search')) {
2205
- throw new Error(`native query-select must return nativeToolSearch payload: ${JSON.stringify(nativeSelectQueryResult.nativeToolSearch)}`);
2206
- }
2207
- // Native late-MCP selections must resolve against the boot+late catalog union,
2208
- // otherwise the load result says "loaded" but omits the provider payload.
2209
- const nativeLateMcpSearchSession = {
2210
- provider: 'openai-oauth',
2211
- tools: [],
2212
- deferredToolCatalog: [{ name: 'load_tool', description: 'Loader.', inputSchema: { type: 'object', properties: {} } }],
2213
- deferredLateToolCatalog: [{ name: 'mcp__late__ping', description: 'Late MCP tool.', inputSchema: { type: 'object', properties: {} } }],
2214
- deferredDiscoveredTools: [],
2215
- deferredProviderMode: 'native',
2216
- deferredNativeTools: true,
2217
- };
2218
- const nativeLateMcpSelectResult = JSON.parse(__renderToolSearchForTest({ names: ['mcp__late__ping'] }, nativeLateMcpSearchSession, 'full'));
2219
- if (nativeLateMcpSearchSession.tools.some((tool) => tool?.name === 'mcp__late__ping')) {
2220
- throw new Error(`native late MCP load must not promote its schema onto session.tools: ${JSON.stringify(nativeLateMcpSearchSession.tools)}`);
2221
- }
2222
- if (!nativeLateMcpSelectResult.nativeToolSearch?.toolReferences?.includes('mcp__late__ping')) {
2223
- throw new Error(`native late MCP load must include nativeToolSearch payload: ${JSON.stringify(nativeLateMcpSelectResult)}`);
2224
- }
2225
- if (!nativeLateMcpSelectResult.nativeToolSearch?.openaiTools?.some((tool) => tool?.name === 'mcp__late__ping' && tool?.defer_loading === true)) {
2226
- throw new Error(`native late MCP load must include OpenAI loadable tool spec: ${JSON.stringify(nativeLateMcpSelectResult.nativeToolSearch)}`);
2227
- }
2228
- // A plain query never auto-loads/discovers, even on native providers.
2229
- const nativePlainQuerySession = {
2230
- tools: smokeCatalog.filter((tool) => fullDefaults.has(tool?.name)),
2231
- deferredToolCatalog: smokeCatalog.slice(),
2232
- deferredSelectedTools: [...fullDefaults],
2233
- deferredDiscoveredTools: [],
2234
- deferredProviderMode: 'native',
2235
- deferredNativeTools: true,
2236
- };
2237
- for (const q of ['run tests', 'web docs', 'memory previous', 'status']) {
2238
- const r = JSON.parse(__renderToolSearchForTest({ query: q }, nativePlainQuerySession, 'full'));
2239
- if (r.selected || r.discoveredTools.length) {
2240
- throw new Error(`native tool_search plain query "${q}" must not auto-load/discover: ${JSON.stringify(r)}`);
2241
- }
2242
- }
2243
- const geminiManifestSession = { provider: 'gemini', tools: [], messages: [] };
2244
- const manifestBase = [
2245
- { name: 'load_tool', inputSchema: { type: 'object', properties: {} } },
2246
- { name: 'read', annotations: { readOnlyHint: true }, inputSchema: { type: 'object', properties: {} } },
2247
- ];
2248
- applyDeferredToolSurface(geminiManifestSession, 'full', manifestBase, { provider: 'gemini' });
2249
- const geminiTurnManifest = JSON.stringify(geminiManifestSession.tools);
2250
- const geminiLate = { name: 'mcp__gemini__late', inputSchema: { type: 'object', properties: {} } };
2251
- // Continuations use the same array; only the next user-turn reconciliation may replace it.
2252
- if (JSON.stringify(geminiManifestSession.tools) !== geminiTurnManifest) {
2253
- throw new Error('Gemini manifest changed within a user turn');
2254
- }
2255
- reconcileDeferredMcpToolCatalog(geminiManifestSession, [geminiLate]);
2256
- if (!geminiManifestSession.tools.some((tool) => tool.name === 'mcp__gemini__late')) {
2257
- throw new Error('Gemini must adopt the complete ordered live manifest at the next user turn');
2258
- }
2259
- // Skill-style deferred manifest: `- name: description` lines, `<`/`>` sanitized,
2260
- // bare names allowed, header instructs direct calls, empty pool → ''.
2261
- const manifestText = buildDeferredToolManifest([
2262
- { name: 'shell', description: 'Run commands.' },
2263
- { name: 'search', description: 'Web <search> now.' },
2264
- 'recall',
2265
- ]);
2266
- if (!/<available-deferred-tools>/.test(manifestText) || !/- shell: Run commands\./.test(manifestText)) {
2267
- throw new Error(`deferred manifest must render "- name: description" lines: ${manifestText}`);
2268
- }
2269
- if (!/call any tool listed below directly/i.test(manifestText)) {
2270
- throw new Error(`deferred manifest must tell the model it can call listed tools directly: ${manifestText}`);
2271
- }
2272
- if (!/^- recall$/m.test(manifestText)) {
2273
- throw new Error(`deferred manifest must allow bare names without descriptions: ${manifestText}`);
2274
- }
2275
- if (/[<>]/.test(manifestText.replace(/<\/?available-deferred-tools>/g, ''))) {
2276
- throw new Error(`deferred manifest must sanitize angle brackets in descriptions: ${manifestText}`);
2277
- }
2278
- if (buildDeferredToolManifest([]) !== '') {
2279
- throw new Error('empty deferred pool must yield an empty manifest');
2280
- }
2281
- const bp1ManifestSession = {
2282
- messages: [{ role: 'system', content: 'BASE PROMPT' }],
2283
- deferredToolCatalog: [
2284
- { name: 'shell', description: 'Run commands.' },
2285
- { name: 'recall', description: 'Recall prior work.' },
2286
- ],
2287
- };
2288
- applyInitialDeferredToolManifestToBp1(bp1ManifestSession, ['shell', 'recall']);
2289
- const bp1ManifestText = bp1ManifestSession.messages[0].content;
2290
- if (!/- shell: Run commands\./.test(bp1ManifestText) || !/- recall: Recall prior work\./.test(bp1ManifestText)) {
2291
- throw new Error(`BP1 deferred manifest must carry catalog descriptions: ${bp1ManifestText}`);
2292
- }
2293
- if (bp1ManifestSession.deferredToolBp1Applied !== true) {
2294
- throw new Error('BP1 deferred manifest injection must mark deferredToolBp1Applied');
2295
- }
2296
- const replyTool = CHANNEL_TOOL_DEFS.find((tool) => tool.name === 'reply');
2297
- if (!/configured channel/i.test(replyTool?.description || '') || !/local .*paths/i.test(replyTool?.inputSchema?.properties?.files?.description || '')) {
2298
- throw new Error('channel reply schema must describe target channel and attachment paths');
2299
- }
2300
- const fetchTool = CHANNEL_TOOL_DEFS.find((tool) => tool.name === 'fetch');
2301
- if (!/NOT for URLs/i.test(fetchTool?.description || '') || !/web_fetch/i.test(fetchTool?.description || '')) {
2302
- throw new Error('channel fetch schema must distinguish Discord fetch from web_fetch');
2303
- }
2304
- const grepTool = BUILTIN_TOOLS.find((tool) => tool.name === 'grep');
2305
- const grepPatternDescription = grepTool?.inputSchema?.properties?.pattern?.description || '';
2306
- const grepPathDescription = grepTool?.inputSchema?.properties?.path?.description || '';
2307
- const grepGlobDescription = grepTool?.inputSchema?.properties?.glob?.description || '';
2308
- const grepOutputModeDescription = grepTool?.inputSchema?.properties?.output_mode?.description || '';
2309
- const grepHeadLimitDescription = grepTool?.inputSchema?.properties?.head_limit?.description || '';
2310
- if (!/pattern\[\] batches variants/i.test(grepPatternDescription) || !/File\/dir scope/i.test(grepPathDescription)) {
2311
- throw new Error('grep schema must keep compact pattern/path guidance');
2312
- }
2313
- const grepPredicateNegator = /\b(?:no|not|never|cannot|can(?:not|'t)|does(?:\s+not|n't)|do(?:\s+not|n't)|did(?:\s+not|n't)|unable(?:\s+|-)to|fail(?:s|ed|ing)?(?:\s*[-–]\s*|\s+)to|without|forbid(?:s|den)?|prohibit(?:s|ed|ing)?)(?:\s+\w+){0,2}\s+/i;
2314
- const hasPositiveGrepSequence = (description, pattern, negatedPredicate) => {
2315
- const match = description.match(pattern);
2316
- return Boolean(match) && !(negatedPredicate?.test(match[0]));
2317
- };
2318
- const grepRoutingVerb = '(?:us(?:e|es|ed|ing)|rout(?:e|es|ed|ing)|target(?:s|ed|ing)?)';
2319
- const grepRouteRelation = `(?:\\s*→\\s*|\\s+\\b${grepRoutingVerb}\\b(?:\\s+(?:to|via|with))?\\s+)\\bgrep\\b`;
2320
- const grepGroupedRoutePattern = new RegExp(`\\bliteral\\b\\s*(?:or|and|/)\\s*\\bregex\\b(?:(?![.;]|→|\\b${grepRoutingVerb}\\b)[\\s\\S]){0,80}?${grepRouteRelation}`, 'i');
2321
- const grepRoutePattern = (subject, alternateSubject) => new RegExp(`\\b${subject}\\b(?:(?![.;]|→|\\b(?:${alternateSubject}|${grepRoutingVerb})\\b)[\\s\\S]){0,80}?${grepRouteRelation}`, 'i');
2322
- const grepNegatedRoute = new RegExp(`${grepPredicateNegator.source}\\bgrep\\b`, 'i');
2323
- const hasGrepDescriptionContract = (description) => (
2324
- (hasPositiveGrepSequence(description, grepGroupedRoutePattern, grepNegatedRoute)
2325
- || (hasPositiveGrepSequence(description, grepRoutePattern('literal', 'regex'), grepNegatedRoute)
2326
- && hasPositiveGrepSequence(description, grepRoutePattern('regex', 'literal'), grepNegatedRoute)))
2327
- && hasPositiveGrepSequence(description, /\bnonzero\b[\s\S]{0,80}?\bcontent_with_context\b[\s\S]{0,80}?\bresolve\w*\b/i, new RegExp(`${grepPredicateNegator.source}\\bresolve\\w*\\b`, 'i'))
2328
- && hasPositiveGrepSequence(description, /\bonly\b[\s\S]{0,80}?\bzero(?:\s*\/\s*|\s+or\s+)error\b[\s\S]{0,80}?\bchange\w*\b[\s\S]{0,80}?\btoken(?:s)?\b[\s\S]{0,80}?\bscope\b/i, new RegExp(`${grepPredicateNegator.source}\\bchange\\w*\\b`, 'i'))
2329
- && hasPositiveGrepSequence(description, /\bfiles_with_matches(?:\s*\/\s*|\s+and\s+)count\b[\s\S]{0,80}?\bexistence\b[\s\S]{0,80}?\bmodes?\b/i, new RegExp(`${grepPredicateNegator.source}\\bexistence\\s+modes?\\b`, 'i'))
2330
- );
2331
- const grepDescriptionProbe = 'literal and regex → grep; nonzero content_with_context resolves; only zero/error results may change tokens and scope; files_with_matches/count are existence modes.';
2332
- const grepEquivalentPositiveProbe = 'literal and regex use grep; nonzero content_with_context returns not an empty result and resolves; only zero/error results may change tokens and scope; files_with_matches/count are existence modes.';
2333
- const grepSeparateClausePositiveProbe = 'literal uses grep; regex routes to grep; nonzero content_with_context resolves; only zero/error results may change tokens and scope; files_with_matches/count are existence modes.';
2334
- const grepPolarityMutations = [
2335
- grepDescriptionProbe.replace('literal and regex → grep', 'literal → read; regex → grep'),
2336
- grepDescriptionProbe.replace('literal and regex → grep', 'literal targets read, while regex uses grep'),
2337
- grepDescriptionProbe.replace('literal and regex → grep', 'literal and regex do not use grep'),
2338
- grepDescriptionProbe.replace('resolves', 'does not resolve'),
2339
- grepDescriptionProbe.replace('resolves', 'fails to resolve'),
2340
- grepDescriptionProbe.replace('may change', 'cannot change'),
2341
- grepDescriptionProbe.replace('are existence modes', 'are never existence modes'),
2342
- ];
2343
- if (!hasGrepDescriptionContract(grepTool?.description || '') || !hasGrepDescriptionContract(grepEquivalentPositiveProbe) || !hasGrepDescriptionContract(grepSeparateClausePositiveProbe) || grepPolarityMutations.some(hasGrepDescriptionContract)) {
2344
- throw new Error('grep description must state its literal/regex locator, resolution qualifiers, and result-mode contract');
2345
- }
2346
- if (!/Glob filter/i.test(grepGlobDescription)) {
2347
- throw new Error('grep glob schema must describe scope narrowing');
2348
- }
2349
- if (!/files_with_matches\/count/i.test(grepOutputModeDescription) || !/content_with_context/i.test(grepOutputModeDescription)) {
2350
- throw new Error('grep output_mode schema must name its output shapes');
2351
- }
2352
- if (grepTool?.inputSchema?.properties?.head_limit?.minimum !== 0 || !/Max results/i.test(grepHeadLimitDescription)) {
2353
- throw new Error('grep head_limit schema must keep locator caps explicit');
2354
- }
2355
- if (grepTool?.inputSchema?.properties?.type) {
2356
- throw new Error('grep type schema must stay hidden; prefer glob for extension narrowing');
2357
- }
2358
- const globTool = BUILTIN_TOOLS.find((tool) => tool.name === 'glob');
2359
- const findTool = BUILTIN_TOOLS.find((tool) => tool.name === 'find');
2360
- const listTool = BUILTIN_TOOLS.find((tool) => tool.name === 'list');
2361
- if (!/exact glob patterns/i.test(globTool?.description || '')) {
2362
- throw new Error('glob description must route exact-pattern unknown paths before read/grep/list');
2363
- }
2364
- const findClauseNegator = '(?:not|never|(?:do|does|did)\\s+not|(?:do|does|did)n\'t|(?:is|are|was|were)\\s+not|(?:is|are|was|were)n\'t|should\\s+not|shouldn\'t|must\\s+not|mustn\'t|can(?:not|\\s+not)|can\'t|could\\s+not|couldn\'t|will\\s+not|won\'t|fail(?:s|ed|ing)?\\s+to)';
2365
- const findPredicateModifiers = '(?:\\s+(?:necessarily|generally|strictly|really|simply|always|still|currently|typically|ordinarily|be|been|being|considered|deemed|regarded)){0,3}';
2366
- const hasPositiveFindClause = (description, positive, negated) => positive.test(description) && !negated.test(description);
2367
- const findUnknownPathsPattern = /\b(?:fuzzy\s+)?(?:lookup|find)\b(?:\s+[\w']+){0,3}\s+only\s+for\s+unknown\s+partial\s+paths\/names\b/i;
2368
- const findUnknownPathsNegation = new RegExp(`\\b${findClauseNegator}${findPredicateModifiers}\\s+(?:use\\s+)?(?:fuzzy\\s+)?(?:lookup|find)\\b(?:\\s+[\\w']+){0,3}\\s+only\\s+for\\s+unknown\\s+partial\\s+paths\\/names|\\b(?:fuzzy\\s+)?(?:lookup|find)\\b(?:\\s+[\\w']+){0,3}\\s+${findClauseNegator}${findPredicateModifiers}\\s+only\\s+for\\s+unknown\\s+partial\\s+paths\\/names`, 'i');
2369
- const findRootExclusionPattern = /\b(?:not\s+for|excludes?)\s+(?:the\s+)?project\s+root\s+(?:or|and)\s+already-verified\s+roots\b|\b(?:the\s+)?project\s+root\s+(?:or|and)\s+already-verified\s+roots\s+(?:are\s+)?excluded\b|\balready-verified\s+roots\s+(?:or|and)\s+(?:the\s+)?project\s+root\s+(?:are\s+)?excluded\b/i;
2370
- const findRootExclusionNegation = new RegExp(`\\b${findClauseNegator}${findPredicateModifiers}\\s+exclude\\s+(?:the\\s+)?(?:project\\s+root|already-verified\\s+roots)\\b|\\b(?:project\\s+root\\s+(?:or|and)\\s+already-verified\\s+roots|already-verified\\s+roots\\s+(?:or|and)\\s+project\\s+root|project\\s+root|already-verified\\s+roots)\\s+(?:(?:is|are|was|were)\\s+)?${findClauseNegator}${findPredicateModifiers}\\s+excluded\\b`, 'i');
2371
- const findVerifiedPathsPattern = /\b(?:output|returned)\s+paths\b(?:\s+[\w']+){0,4}\s+verified\s+(?:downstream|for\s+downstream\s+use)\b|\bdownstream\s+paths\b(?:\s+[\w']+){0,4}\s+verified\b|\bpaths\b(?:\s+[\w']+){0,4}\s+verified\s+(?:downstream|for\s+downstream\s+use)\b/i;
2372
- const findVerifiedPathsNegation = new RegExp(`\\b(?:output|returned|downstream)\\s+paths\\b(?:\\s+[\\w']+){0,4}\\s+${findClauseNegator}${findPredicateModifiers}\\s+verified\\b|\\bpaths\\b(?:\\s+[\\w']+){0,4}\\s+${findClauseNegator}${findPredicateModifiers}\\s+verified\\b|\\b(?:output|returned|downstream)\\s+paths\\b(?:\\s+[\\w']+){0,4}\\s+unverified\\b`, 'i');
2373
- const findFileContentsPattern = /\b(?:not\s+for|excludes?)\s+file\s+contents\b|\bfile\s+contents\b(?:\s+[\w']+){0,3}\s+excluded\b/i;
2374
- const findFileContentsNegation = new RegExp(`\\b${findClauseNegator}${findPredicateModifiers}\\s+exclude\\s+file\\s+contents\\b|\\bfile\\s+contents\\s+(?:(?:is|are|was|were)\\s+)?${findClauseNegator}${findPredicateModifiers}\\s+excluded\\b`, 'i');
2375
- const hasFindDescriptionContract = (description) => (
2376
- hasPositiveFindClause(description, findUnknownPathsPattern, findUnknownPathsNegation)
2377
- && hasPositiveFindClause(description, findRootExclusionPattern, findRootExclusionNegation)
2378
- && hasPositiveFindClause(description, findVerifiedPathsPattern, findVerifiedPathsNegation)
2379
- && hasPositiveFindClause(description, findFileContentsPattern, findFileContentsNegation)
2380
- );
2381
- const findDescriptionProbe = 'Fuzzy lookup only for unknown partial paths/names; excludes project root and already-verified roots. Output paths are verified downstream. Not for file contents.';
2382
- const findEquivalentPositiveProbe = 'File contents are excluded. Paths verified downstream are returned. Already-verified roots and project root are excluded. Lookup is only for unknown partial paths/names.';
2383
- const findPolarityMutations = [
2384
- findDescriptionProbe.replace('Fuzzy lookup only', 'Do not use fuzzy lookup only'),
2385
- findDescriptionProbe.replace('excludes project root', 'does not exclude project root'),
2386
- findDescriptionProbe.replace('Output paths are verified downstream', 'Downstream paths not verified'),
2387
- findDescriptionProbe.replace('Not for file contents', 'Does not exclude file contents'),
2388
- findDescriptionProbe.replace('Fuzzy lookup only', 'Fuzzy lookup is not only'),
2389
- findDescriptionProbe.replace('excludes project root', 'should not exclude project root'),
2390
- findDescriptionProbe.replace('excludes project root', 'must not exclude project root'),
2391
- findDescriptionProbe.replace('excludes project root', 'can not exclude project root'),
2392
- findDescriptionProbe.replace('Output paths are verified downstream', 'Output paths cannot be verified downstream'),
2393
- findDescriptionProbe.replace('Not for file contents', 'File contents must not be excluded'),
2394
- findDescriptionProbe.replace('Output paths are verified downstream', 'Output paths are verified'),
2395
- findDescriptionProbe.replace('Fuzzy lookup only', 'Fuzzy lookup isn\'t only'),
2396
- findDescriptionProbe.replace('excludes project root and already-verified roots', 'project root and already-verified roots shouldn\'t be excluded'),
2397
- findDescriptionProbe.replace('Output paths are verified downstream', 'Output paths can\'t be verified downstream'),
2398
- findDescriptionProbe.replace('Not for file contents', 'File contents aren\'t excluded'),
2399
- findDescriptionProbe.replace('Fuzzy lookup only', 'Lookup isn\'t necessarily only'),
2400
- findDescriptionProbe.replace('Output paths are verified downstream', 'Output paths shouldn\'t be considered verified downstream'),
2401
- ];
2402
- if (!hasFindDescriptionContract(findTool?.description || '') || !hasFindDescriptionContract(findEquivalentPositiveProbe) || findPolarityMutations.some(hasFindDescriptionContract)) {
2403
- throw new Error('find description must limit lookup to unknown partial paths/names, exclude roots and file contents, and return verified paths');
2404
- }
2405
- if (!/List directory entries/i.test(listTool?.description || '') || !/path\[\]/i.test(listTool?.inputSchema?.properties?.path?.description || '')) {
2406
- throw new Error('list description must require verified directories and locator-first unknown dirs');
2407
- }
2408
- const codeGraphModeDescription = codeGraphProps.mode?.description || '';
2409
- const codeGraphSymbolsDescription = codeGraphProps.symbols?.description || '';
2410
- assertCodeGraphDescriptionContract({
2411
- description: codeGraphDescription,
2412
- modeDescription: codeGraphModeDescription,
2413
- symbolsDescription: codeGraphSymbolsDescription,
2414
- });
2415
-
2416
- const longToolSearchText = compactToolSearchDescription(`${patchDescription}\n${patchDescription}`);
2417
- if (longToolSearchText.length > 220 || /\n/.test(longToolSearchText)) {
2418
- throw new Error(`tool_search descriptions must be compact single-line snippets, got ${longToolSearchText.length} chars`);
2419
- }
2420
-
2421
- {
2422
- // Regression guard for the sonnet-5 16384 cap bug (thinking exhausted the
2423
- // whole output budget). Both the catalog path (outputTokens=128000 → capped
2424
- // 65536) and the catalog-miss heuristic (sonnet 5+ → 65536) must yield
2425
- // 65536, so assert the exact value with the env override cleared.
2426
- const _prevMaxOut = process.env.MIXDOG_ANTHROPIC_MAX_OUTPUT_TOKENS;
2427
- delete process.env.MIXDOG_ANTHROPIC_MAX_OUTPUT_TOKENS;
2428
- try {
2429
- const sonnet5MaxTokens = _anthropicOAuthTest.resolveMaxTokens('claude-sonnet-5');
2430
- if (sonnet5MaxTokens !== 65536) {
2431
- throw new Error(`resolveMaxTokens('claude-sonnet-5') must be 65536 (catalog-capped or sonnet-5+ fallback), got ${sonnet5MaxTokens}`);
2432
- }
2433
- const sonnet46MaxTokens = _anthropicOAuthTest.resolveMaxTokens('claude-sonnet-4-6');
2434
- if (!(sonnet46MaxTokens >= 16384)) {
2435
- throw new Error(`resolveMaxTokens('claude-sonnet-4-6') must be >= 16384, got ${sonnet46MaxTokens}`);
2436
- }
2437
- process.env.MIXDOG_ANTHROPIC_MAX_OUTPUT_TOKENS = 'garbage';
2438
- const garbageOverride = _anthropicOAuthTest.resolveMaxTokens('claude-sonnet-5');
2439
- if (garbageOverride !== 65536) {
2440
- throw new Error(`invalid MIXDOG_ANTHROPIC_MAX_OUTPUT_TOKENS must be ignored (catalog/fallback path), got ${garbageOverride}`);
2441
- }
2442
- process.env.MIXDOG_ANTHROPIC_MAX_OUTPUT_TOKENS = '32768';
2443
- const validOverride = _anthropicOAuthTest.resolveMaxTokens('claude-sonnet-5');
2444
- if (validOverride !== 32768) {
2445
- throw new Error(`valid MIXDOG_ANTHROPIC_MAX_OUTPUT_TOKENS=32768 must win, got ${validOverride}`);
2446
- }
2447
- } finally {
2448
- if (_prevMaxOut === undefined) delete process.env.MIXDOG_ANTHROPIC_MAX_OUTPUT_TOKENS;
2449
- else process.env.MIXDOG_ANTHROPIC_MAX_OUTPUT_TOKENS = _prevMaxOut;
2450
- }
2451
- }
2452
-
2453
- process.stdout.write(`tool smoke passed surface_chars=${surfaceSize}\n`);