mixdog 0.9.18 → 0.9.20

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 (214) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -29
  3. package/package.json +5 -3
  4. package/scripts/build-runtime-windows.ps1 +242 -242
  5. package/scripts/build-tui.mjs +6 -0
  6. package/scripts/hook-bus-test.mjs +8 -0
  7. package/scripts/log-writer-guard-smoke.mjs +131 -0
  8. package/scripts/output-style-smoke.mjs +2 -2
  9. package/scripts/reactive-compact-persist-smoke.mjs +22 -6
  10. package/scripts/recall-bench-cases.json +10 -0
  11. package/scripts/recall-bench.mjs +91 -2
  12. package/scripts/recall-quality-cases.json +1 -2
  13. package/scripts/recall-usecase-cases.json +1 -1
  14. package/scripts/session-ingest-compaction-smoke.mjs +241 -0
  15. package/scripts/smoke-runtime-negative.ps1 +106 -106
  16. package/scripts/tool-efficiency-diag.mjs +5 -2
  17. package/scripts/tool-smoke.mjs +251 -72
  18. package/src/agents/debugger/AGENT.md +3 -3
  19. package/src/agents/heavy-worker/AGENT.md +7 -10
  20. package/src/agents/maintainer/AGENT.md +1 -2
  21. package/src/agents/reviewer/AGENT.md +1 -2
  22. package/src/agents/worker/AGENT.md +5 -8
  23. package/src/defaults/agents.json +3 -0
  24. package/src/help.mjs +2 -5
  25. package/src/lib/mixdog-debug.cjs +13 -0
  26. package/src/mixdog-session-runtime.mjs +7 -3311
  27. package/src/rules/agent/00-core.md +4 -3
  28. package/src/rules/agent/30-explorer.md +53 -22
  29. package/src/rules/lead/02-channels.md +3 -3
  30. package/src/rules/lead/lead-tool.md +3 -2
  31. package/src/rules/shared/01-tool.md +24 -29
  32. package/src/runtime/agent/orchestrator/context/collect.mjs +33 -9
  33. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +1 -1
  34. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
  35. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +24 -3
  36. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +3 -3
  37. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +663 -0
  38. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +6 -0
  39. package/src/runtime/agent/orchestrator/session/context-utils.mjs +2 -2
  40. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +153 -0
  41. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +49 -0
  42. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +250 -35
  43. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -1
  44. package/src/runtime/agent/orchestrator/session/loop.mjs +8 -1860
  45. package/src/runtime/agent/orchestrator/session/manager/agent-runtime-singleton.mjs +29 -0
  46. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +686 -0
  47. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +60 -12
  48. package/src/runtime/agent/orchestrator/session/manager/env-utils.mjs +8 -0
  49. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +159 -0
  50. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +143 -0
  51. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +268 -0
  52. package/src/runtime/agent/orchestrator/session/manager/provider-cache-key.mjs +22 -0
  53. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +26 -0
  54. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +124 -0
  55. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +258 -0
  56. package/src/runtime/agent/orchestrator/session/manager/session-errors.mjs +20 -0
  57. package/src/runtime/agent/orchestrator/session/manager/session-id.mjs +9 -0
  58. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +475 -0
  59. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +23 -0
  60. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +6 -4
  61. package/src/runtime/agent/orchestrator/session/manager.mjs +88 -2285
  62. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +440 -0
  63. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +153 -0
  64. package/src/runtime/agent/orchestrator/session/store.mjs +4 -1
  65. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +642 -0
  66. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +23 -3
  67. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +198 -6
  68. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -2
  69. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +13 -15
  70. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +6 -1
  71. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +45 -3
  72. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -2
  73. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +195 -3
  74. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +108 -2
  75. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +15 -3
  76. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -0
  77. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +24 -2
  78. package/src/runtime/agent/orchestrator/tools/builtin.mjs +17 -1
  79. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +38 -0
  80. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -4
  81. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
  82. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +72 -8
  83. package/src/runtime/agent/orchestrator/tools/shell-policy-danger-target.mjs +5 -1
  84. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +1 -1
  85. package/src/runtime/channels/backends/discord-gateway.mjs +14 -1
  86. package/src/runtime/channels/backends/discord.mjs +43 -1
  87. package/src/runtime/channels/index.mjs +6 -2096
  88. package/src/runtime/channels/lib/inbound-handler.mjs +328 -0
  89. package/src/runtime/channels/lib/inbound-routing.mjs +19 -12
  90. package/src/runtime/channels/lib/interaction-handlers.mjs +260 -0
  91. package/src/runtime/channels/lib/memory-client.mjs +32 -14
  92. package/src/runtime/channels/lib/network-retry.mjs +23 -0
  93. package/src/runtime/channels/lib/output-forwarder.mjs +38 -7
  94. package/src/runtime/channels/lib/owned-runtime.mjs +547 -0
  95. package/src/runtime/channels/lib/owner-heartbeat.mjs +13 -4
  96. package/src/runtime/channels/lib/runtime-paths.mjs +35 -1
  97. package/src/runtime/channels/lib/tool-dispatch.mjs +17 -7
  98. package/src/runtime/channels/lib/transcript-binding.mjs +336 -0
  99. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +39 -2
  100. package/src/runtime/channels/lib/worker-bootstrap.mjs +97 -0
  101. package/src/runtime/channels/lib/worker-ipc.mjs +201 -0
  102. package/src/runtime/channels/lib/worker-main.mjs +771 -0
  103. package/src/runtime/memory/index.mjs +73 -1725
  104. package/src/runtime/memory/lib/embedding-provider.mjs +4 -2
  105. package/src/runtime/memory/lib/embedding-worker.mjs +47 -6
  106. package/src/runtime/memory/lib/http-router.mjs +772 -0
  107. package/src/runtime/memory/lib/memory-action-handlers.mjs +901 -0
  108. package/src/runtime/memory/lib/memory-embed.mjs +28 -7
  109. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +8 -2
  110. package/src/runtime/memory/lib/memory-recall-store.mjs +182 -9
  111. package/src/runtime/memory/lib/query-handlers.mjs +38 -6
  112. package/src/runtime/memory/lib/recall-format.mjs +106 -6
  113. package/src/runtime/memory/lib/session-ingest-runtime.mjs +401 -0
  114. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  115. package/src/runtime/shared/atomic-file.mjs +10 -4
  116. package/src/runtime/shared/background-tasks.mjs +4 -2
  117. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  118. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  119. package/src/runtime/shared/tool-surface.mjs +30 -1
  120. package/src/session-runtime/boot-profile.mjs +36 -0
  121. package/src/session-runtime/channel-config-api.mjs +70 -0
  122. package/src/session-runtime/config-lifecycle.mjs +1 -1
  123. package/src/session-runtime/context-status.mjs +181 -0
  124. package/src/session-runtime/cwd-plugins.mjs +46 -3
  125. package/src/session-runtime/env.mjs +17 -0
  126. package/src/session-runtime/lifecycle-api.mjs +242 -0
  127. package/src/session-runtime/mcp-glue.mjs +24 -3
  128. package/src/session-runtime/model-route-api.mjs +198 -0
  129. package/src/session-runtime/output-styles.mjs +44 -10
  130. package/src/session-runtime/provider-auth-api.mjs +135 -0
  131. package/src/session-runtime/resource-api.mjs +282 -0
  132. package/src/session-runtime/runtime-core.mjs +2046 -0
  133. package/src/session-runtime/session-turn-api.mjs +274 -0
  134. package/src/session-runtime/tool-catalog.mjs +18 -264
  135. package/src/session-runtime/tool-defs.mjs +2 -2
  136. package/src/session-runtime/workflow-agents-api.mjs +238 -0
  137. package/src/session-runtime/workflow.mjs +16 -1
  138. package/src/standalone/agent-tool.mjs +2 -2
  139. package/src/standalone/channel-worker.mjs +78 -5
  140. package/src/standalone/explore-tool.mjs +1 -1
  141. package/src/standalone/memory-runtime-proxy.mjs +56 -6
  142. package/src/tui/App.jsx +88 -97
  143. package/src/tui/app/channel-pickers.mjs +45 -0
  144. package/src/tui/app/core-memory-picker.mjs +1 -1
  145. package/src/tui/app/slash-commands.mjs +0 -1
  146. package/src/tui/app/slash-dispatch.mjs +0 -16
  147. package/src/tui/app/transcript-window.mjs +44 -1
  148. package/src/tui/app/use-mouse-input.mjs +15 -2
  149. package/src/tui/app/use-prompt-handlers.mjs +7 -94
  150. package/src/tui/app/use-transcript-scroll.mjs +82 -4
  151. package/src/tui/app/use-transcript-window.mjs +65 -5
  152. package/src/tui/components/PromptInput.jsx +33 -64
  153. package/src/tui/components/ToolExecution.jsx +2 -2
  154. package/src/tui/dist/index.mjs +7908 -7558
  155. package/src/tui/engine/context-state.mjs +145 -0
  156. package/src/tui/engine/prompt-history.mjs +27 -0
  157. package/src/tui/engine/session-api-ext.mjs +478 -0
  158. package/src/tui/engine/session-api.mjs +545 -0
  159. package/src/tui/engine/session-flow.mjs +485 -0
  160. package/src/tui/engine/turn.mjs +1078 -0
  161. package/src/tui/engine.mjs +69 -2582
  162. package/src/tui/index.jsx +7 -0
  163. package/src/tui/lib/voice-setup.mjs +166 -0
  164. package/src/tui/paste-attachments.mjs +12 -5
  165. package/src/tui/prompt-history-store.mjs +125 -12
  166. package/vendor/ink/build/ink.js +16 -1
  167. package/vendor/ink/build/output.js +30 -4
  168. package/vendor/ink/build/render.js +5 -0
  169. package/scripts/bench/cache-probe-tasks.json +0 -8
  170. package/scripts/bench/lead-review-tasks-r3.json +0 -20
  171. package/scripts/bench/lead-review-tasks.json +0 -20
  172. package/scripts/bench/r4-mixed-tasks.json +0 -20
  173. package/scripts/bench/r5-orchestrated-task.json +0 -7
  174. package/scripts/bench/review-tasks.json +0 -20
  175. package/scripts/bench/round-codex.json +0 -114
  176. package/scripts/bench/round-mixdog-lead-r3.json +0 -269
  177. package/scripts/bench/round-mixdog-lead.json +0 -269
  178. package/scripts/bench/round-mixdog.json +0 -126
  179. package/scripts/bench/round-r10-bigsample.json +0 -679
  180. package/scripts/bench/round-r11-codexalign.json +0 -257
  181. package/scripts/bench/round-r13-clientmeta.json +0 -464
  182. package/scripts/bench/round-r14-betafeatures.json +0 -466
  183. package/scripts/bench/round-r15-fulldefault.json +0 -462
  184. package/scripts/bench/round-r16-sessionid.json +0 -466
  185. package/scripts/bench/round-r17-wirebytes.json +0 -456
  186. package/scripts/bench/round-r18-prewarm.json +0 -468
  187. package/scripts/bench/round-r19-clean.json +0 -472
  188. package/scripts/bench/round-r20-prewarm-clean.json +0 -475
  189. package/scripts/bench/round-r21-delta-retry.json +0 -473
  190. package/scripts/bench/round-r22-full-probe.json +0 -693
  191. package/scripts/bench/round-r23-itemprobe.json +0 -701
  192. package/scripts/bench/round-r24-shapefix.json +0 -677
  193. package/scripts/bench/round-r25-serial.json +0 -464
  194. package/scripts/bench/round-r26-parallel3.json +0 -671
  195. package/scripts/bench/round-r27-parallel10.json +0 -894
  196. package/scripts/bench/round-r28-parallel10-stagger.json +0 -882
  197. package/scripts/bench/round-r29-parallel10-stagger166.json +0 -886
  198. package/scripts/bench/round-r30-instid.json +0 -253
  199. package/scripts/bench/round-r31-upgradeprobe.json +0 -256
  200. package/scripts/bench/round-r32-vs-codex-lead.json +0 -254
  201. package/scripts/bench/round-r33-vs-codex-codex.json +0 -115
  202. package/scripts/bench/round-r34-orchestrated.json +0 -120
  203. package/scripts/bench/round-r35-orchestrated-codex.json +0 -61
  204. package/scripts/bench/round-r36-orchestrated-capped.json +0 -128
  205. package/scripts/bench/round-r4-codex.json +0 -114
  206. package/scripts/bench/round-r4-mixed.json +0 -225
  207. package/scripts/bench/round-r5-gpt-lead.json +0 -259
  208. package/scripts/bench/round-r6-codex.json +0 -114
  209. package/scripts/bench/round-r6-solo.json +0 -257
  210. package/scripts/bench/round-r7-full.json +0 -254
  211. package/scripts/bench/round-r8-fulldefault.json +0 -255
  212. package/src/tui/components/prompt-input/voice-indicator.mjs +0 -39
  213. package/src/tui/lib/voice-recorder.mjs +0 -469
  214. package/src/workflows/sequential/WORKFLOW.md +0 -51
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { dirname, join, resolve } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
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';
7
8
  import { buildExplorerPrompt, EXPLORE_TOOL, MAX_FANOUT_QUERIES, normalizeExploreQueries } from '../src/standalone/explore-tool.mjs';
8
9
  import { AGENT_TOOL, createStandaloneAgent } from '../src/standalone/agent-tool.mjs';
9
10
  import { parseHeadlessRoleCommand } from '../src/app.mjs';
@@ -508,8 +509,8 @@ if (!/^read 2\b/m.test(String(readRegionBatchOut))
508
509
  || (String(readRegionBatchOut).match(/scripts\/smoke\.mjs \[full\] \[ok\]/g) || []).length < 2
509
510
  || !/1→import \{ spawnSync \}/.test(String(readRegionBatchOut))
510
511
  || !/3→import \{ fileURLToPath \}/.test(String(readRegionBatchOut))
511
- || !/pass offset:2 to continue/.test(String(readRegionBatchOut))
512
- || !/pass offset:4 to continue/.test(String(readRegionBatchOut))) {
512
+ || !/(pass offset:2 to continue|ONE window: offset:2, limit:\d+)/.test(String(readRegionBatchOut))
513
+ || !/(pass offset:4 to continue|ONE window: offset:4, limit:\d+)/.test(String(readRegionBatchOut))) {
513
514
  throw new Error(`read region batch must preserve both requested spans:\n${readRegionBatchOut}`);
514
515
  }
515
516
 
@@ -534,6 +535,46 @@ if (readStringifiedLineErr || readStringifiedLineArgs.path[0].offset !== 7 || re
534
535
  throw new Error(`read guard must losslessly convert legacy line/context inside stringified arrays to offset/limit: err=${readStringifiedLineErr} args=${JSON.stringify(readStringifiedLineArgs)}`);
535
536
  }
536
537
 
538
+ // Absorb shape 1: region array + top-level offset/limit → top-level becomes
539
+ // the default window for regions that lack their own; no hard error.
540
+ const readRegionPlusTopLevelArgs = {
541
+ path: [{ path: 'scripts/smoke.mjs', offset: 3, limit: 4 }, { path: 'scripts/smoke.mjs' }],
542
+ offset: 0,
543
+ limit: 2,
544
+ };
545
+ const readRegionPlusTopLevelErr = validateBuiltinArgs('read', readRegionPlusTopLevelArgs);
546
+ if (readRegionPlusTopLevelErr
547
+ || 'offset' in readRegionPlusTopLevelArgs || 'limit' in readRegionPlusTopLevelArgs
548
+ || readRegionPlusTopLevelArgs.path[0].offset !== 3 || readRegionPlusTopLevelArgs.path[0].limit !== 4
549
+ || readRegionPlusTopLevelArgs.path[1].offset !== 0 || readRegionPlusTopLevelArgs.path[1].limit !== 2) {
550
+ throw new Error(`read guard must absorb region-array + top-level offset/limit: err=${readRegionPlusTopLevelErr} args=${JSON.stringify(readRegionPlusTopLevelArgs)}`);
551
+ }
552
+
553
+ // Absorb shape 2: parallel offset/limit as JSON-stringified arrays with path[]
554
+ // → zipped into per-file region objects (pairwise recovery), no int error.
555
+ const readZipWindowArgs = {
556
+ path: ['scripts/smoke.mjs', 'scripts/smoke.mjs'],
557
+ offset: '[0, 5]',
558
+ limit: '[2, 3]',
559
+ };
560
+ const readZipWindowErr = validateBuiltinArgs('read', readZipWindowArgs);
561
+ if (readZipWindowErr || !Array.isArray(readZipWindowArgs.path)
562
+ || readZipWindowArgs.path[0].offset !== 0 || readZipWindowArgs.path[0].limit !== 2
563
+ || readZipWindowArgs.path[1].offset !== 5 || readZipWindowArgs.path[1].limit !== 3
564
+ || 'offset' in readZipWindowArgs || 'limit' in readZipWindowArgs) {
565
+ throw new Error(`read guard must zip stringified offset/limit arrays onto path[]: err=${readZipWindowErr} args=${JSON.stringify(readZipWindowArgs)}`);
566
+ }
567
+
568
+ // Absorb shape 3: code_graph file/files as a JSON-stringified array → parsed to
569
+ // a real array before lookup (dispatched into files[]).
570
+ const cgStringifiedFileArgs = { mode: 'symbols', file: JSON.stringify(['a.mjs', 'b.mjs']) };
571
+ const cgStringifiedFileErr = validateBuiltinArgs('code_graph', cgStringifiedFileArgs);
572
+ if (cgStringifiedFileErr || 'file' in cgStringifiedFileArgs
573
+ || !Array.isArray(cgStringifiedFileArgs.files)
574
+ || cgStringifiedFileArgs.files[0] !== 'a.mjs' || cgStringifiedFileArgs.files[1] !== 'b.mjs') {
575
+ throw new Error(`code_graph guard must parse JSON-stringified file array: err=${cgStringifiedFileErr} args=${JSON.stringify(cgStringifiedFileArgs)}`);
576
+ }
577
+
537
578
  const graphOut = await executeCodeGraphTool('code_graph', {
538
579
  mode: 'symbols',
539
580
  file: 'scripts/smoke.mjs',
@@ -549,6 +590,17 @@ if (!/# symbol_search executeBuiltinTool\b/.test(String(graphSymbolBatchOut)) ||
549
590
  throw new Error(`code_graph symbol_search symbols[] batch execution failed:\n${graphSymbolBatchOut}`);
550
591
  }
551
592
 
593
+ // Absorb shape 3 (real dispatch): file as a JSON-stringified array batches per
594
+ // file instead of hitting "file not found: [...]".
595
+ const graphStringifiedFileOut = await executeCodeGraphTool('code_graph', {
596
+ mode: 'symbols',
597
+ file: JSON.stringify(['scripts/smoke.mjs']),
598
+ }, root);
599
+ if (/file not found/.test(String(graphStringifiedFileOut))
600
+ || !/binding|spawnSync|symbol/i.test(String(graphStringifiedFileOut))) {
601
+ throw new Error(`code_graph must parse JSON-stringified file array before lookup:\n${graphStringifiedFileOut}`);
602
+ }
603
+
552
604
  const graphMissingFileOut = await executeCodeGraphTool('code_graph', {
553
605
  mode: 'symbols',
554
606
  file: 'src/runtime/loop.mjs',
@@ -593,6 +645,72 @@ if (!/^Error[\s:[]/.test(String(stalePatchOut)) || !/apply_patch/i.test(String(s
593
645
  throw new Error(`apply_patch stale context must return an Error result, not throw or pass:\n${stalePatchOut}`);
594
646
  }
595
647
 
648
+ // Malformed-but-unambiguous patch openings must be absorbed (dry-run, so no
649
+ // write). Each targets the same known-good smoke.mjs line the cases above use.
650
+ const smokeBody = `@@
651
+ -process.stdout.write('smoke passed ✓\\n');
652
+ +process.stdout.write('smoke passed ok\\n');
653
+ *** End Patch
654
+ `;
655
+ const absorbCases = [
656
+ ['leading blank lines', `\n\n*** Begin Patch\n*** Update File: scripts/smoke.mjs\n${smokeBody}`],
657
+ ['decorated begin header', `*** Begin Patch (V4A) ***\n*** Update File: scripts/smoke.mjs\n${smokeBody}`],
658
+ ['bare file path opening', `*** Begin Patch\nscripts/smoke.mjs\n${smokeBody}`],
659
+ ['File: prefixed opening', `*** Begin Patch\nFile: scripts/smoke.mjs\n${smokeBody}`],
660
+ ['unified body in envelope', `*** Begin Patch\n--- scripts/smoke.mjs\n+++ scripts/smoke.mjs\n${smokeBody}`],
661
+ ];
662
+ for (const [label, patch] of absorbCases) {
663
+ const out = await executePatchTool('apply_patch', { base_path: root, dry_run: true, fuzzy: false, patch }, root);
664
+ assertOk(`apply_patch absorbs ${label}`, out, /checked|validated|dry|OK/i);
665
+ }
666
+
667
+ const ambiguousPatchOut = await executePatchTool('apply_patch', {
668
+ base_path: root,
669
+ dry_run: true,
670
+ fuzzy: false,
671
+ patch: `*** Begin Patch\nthis line is not a valid opening\n${smokeBody}`,
672
+ }, root);
673
+ if (!/^Error[\s:[]/.test(String(ambiguousPatchOut)) || !/before a file header|V4A/i.test(String(ambiguousPatchOut))) {
674
+ throw new Error(`apply_patch must keep erroring on genuinely ambiguous openings:\n${ambiguousPatchOut}`);
675
+ }
676
+
677
+ // Unified-looking first body line but real V4A file sections appear later: the
678
+ // envelope must NOT be stripped to unified — it stays ambiguous and errors.
679
+ const mixedPatchOut = await executePatchTool('apply_patch', {
680
+ base_path: root,
681
+ dry_run: true,
682
+ fuzzy: false,
683
+ patch: `*** Begin Patch\n--- scripts/smoke.mjs\n*** Update File: scripts/smoke.mjs\n${smokeBody}`,
684
+ }, root);
685
+ if (!/^Error[\s:[]/.test(String(mixedPatchOut)) || !/before a file header|V4A/i.test(String(mixedPatchOut))) {
686
+ throw new Error(`apply_patch must keep erroring on mixed unified/V4A openings:\n${mixedPatchOut}`);
687
+ }
688
+
689
+ // Compacted-history placeholder guard: EVERY [mixdog compacted …] variant must
690
+ // be rejected with the corrective message BEFORE format dispatch/salvage, both
691
+ // as the first line and standalone mid-body (after a *** Begin Patch header).
692
+ const compactedGuardCases = [
693
+ ['legacy key: prefix', '[mixdog compacted patch: 4096 chars, sha256:deadbeefdeadbeef]\n*** Begin Patch\n*** Update File: a.txt\n+x\n*** End Patch\n'],
694
+ ['variant key form', '[mixdog compacted patch v4a, sha256:deadbeefdeadbeef]\n*** Begin Patch\n*** Update File: a.txt\n+x\n*** End Patch\n'],
695
+ ['no chars/sha detail', '[mixdog compacted old_string]\n'],
696
+ ['mid-body standalone', '*** Begin Patch\n*** Update File: a.txt\n[mixdog compacted patch v4a, sha256:deadbeefdeadbeef]\n*** End Patch\n'],
697
+ ];
698
+ for (const [label, patch] of compactedGuardCases) {
699
+ const out = await executePatchTool('apply_patch', { base_path: root, dry_run: true, fuzzy: false, patch }, root);
700
+ if (!/^Error[\s:[]/.test(String(out)) || !/compacted-history placeholder/i.test(String(out))) {
701
+ throw new Error(`apply_patch must reject compacted placeholder (${label}):\n${out}`);
702
+ }
703
+ }
704
+ // A legit unified edit whose body content mentions the literal text on a diff
705
+ // line (+/-/space) must still parse — the guard only trips on non-diff lines.
706
+ const compactedFalsePositiveOut = await executePatchTool('apply_patch', {
707
+ base_path: root,
708
+ dry_run: true,
709
+ fuzzy: false,
710
+ patch: `*** Begin Patch\n*** Add File: compacted-note.txt\n+[mixdog compacted patch: 10 chars, sha256:abc]\n*** End Patch\n`,
711
+ }, root);
712
+ assertOk('apply_patch keeps diff-line placeholder text', compactedFalsePositiveOut, /checked|validated|dry|OK/i);
713
+
596
714
  const shellOutPromise = executeBuiltinTool('shell', {
597
715
  command: 'node --version',
598
716
  cwd: root,
@@ -634,6 +752,33 @@ if (!/^Error[\s:[]/.test(String(shellTimeoutOut)) || !/\[timeout: 500ms\b/.test(
634
752
  throw new Error(`bash timeout must be milliseconds and classified as Error:\n${shellTimeoutOut}`);
635
753
  }
636
754
 
755
+ // Auto-promotion: a sync foreground command still running past the (soft)
756
+ // promotion budget is detached into a tracked background task and returns the
757
+ // same task_id envelope as explicit async — the caller never pre-chose async.
758
+ // Shrink the budget via MIXDOG_SHELL_AUTO_BACKGROUND_MS so the smoke stays fast.
759
+ const _priorAutoBgBudget = process.env.MIXDOG_SHELL_AUTO_BACKGROUND_MS;
760
+ process.env.MIXDOG_SHELL_AUTO_BACKGROUND_MS = '800';
761
+ let shellAutoPromoteOut;
762
+ try {
763
+ shellAutoPromoteOut = await executeBuiltinTool('shell', {
764
+ command: 'Start-Sleep -Seconds 6; Write-Output tool-smoke-autopromote-done',
765
+ cwd: root,
766
+ timeout: 30_000,
767
+ shell: 'powershell',
768
+ }, root);
769
+ } finally {
770
+ if (_priorAutoBgBudget === undefined) delete process.env.MIXDOG_SHELL_AUTO_BACKGROUND_MS;
771
+ else process.env.MIXDOG_SHELL_AUTO_BACKGROUND_MS = _priorAutoBgBudget;
772
+ }
773
+ if (!/auto-backgrounded/i.test(String(shellAutoPromoteOut)) || !/task_id:\s*\S+/i.test(String(shellAutoPromoteOut))) {
774
+ throw new Error(`shell auto-promotion must return a background task envelope (task_id + auto-backgrounded):\n${shellAutoPromoteOut}`);
775
+ }
776
+ // Clean up the promoted job so it doesn't outlive the smoke as an orphan.
777
+ const _autoPromoteTaskId = (/task_id:\s*(\S+)/i.exec(String(shellAutoPromoteOut)) || [])[1];
778
+ if (_autoPromoteTaskId) {
779
+ try { await executeBuiltinTool('task', { action: 'cancel', task_id: _autoPromoteTaskId }, root); } catch {}
780
+ }
781
+
637
782
  const legacyEscapedAlternationErr = validateBuiltinArgs('grep', { pattern: 'state\\.items\\.map\\|items\\.map', path: root });
638
783
  if (legacyEscapedAlternationErr) {
639
784
  throw new Error(`grep legacy \\| alternation should be accepted: ${legacyEscapedAlternationErr}`);
@@ -1217,8 +1362,10 @@ setInternalToolsProvider({
1217
1362
  if (workerToolNames.includes('tool_search')) {
1218
1363
  throw new Error(`agent session schema must not expose deferred tool_search: ${workerToolNames.join(', ')}`);
1219
1364
  }
1220
- if (workerToolNames.includes('shell')) {
1221
- throw new Error(`read-write agent session schema must not expose shell: ${workerToolNames.join(', ')}`);
1365
+ for (const name of ['shell', 'task']) {
1366
+ if (!workerToolNames.includes(name)) {
1367
+ throw new Error(`read-write agent session schema must expose ${name} for self-verification: ${workerToolNames.join(', ')}`);
1368
+ }
1222
1369
  }
1223
1370
  for (const name of ['skills_list', 'skill_view', 'skill_execute']) {
1224
1371
  if (workerToolNames.includes(name)) {
@@ -1266,7 +1413,7 @@ setInternalToolsProvider({
1266
1413
  const fullTools = (fullAgentSession.tools || []).map((tool) => tool?.name).filter(Boolean);
1267
1414
  const publicExploreTools = (publicExploreSession.tools || []).map((tool) => tool?.name).filter(Boolean);
1268
1415
  const expectedReadTools = ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'explore', 'search', 'web_fetch', 'Skill'];
1269
- const expectedWriteTools = ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'apply_patch', 'explore', 'search', 'web_fetch', 'Skill'];
1416
+ const expectedWriteTools = ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'apply_patch', 'shell', 'task', 'explore', 'search', 'web_fetch', 'Skill'];
1270
1417
  if (JSON.stringify(readTools) !== JSON.stringify(expectedReadTools)) {
1271
1418
  throw new Error(`read agent schema must be fixed allow-list: expected=${expectedReadTools.join(', ')} actual=${readTools.join(', ')}`);
1272
1419
  }
@@ -1276,20 +1423,17 @@ setInternalToolsProvider({
1276
1423
  if (readTools.includes('tool_search') || writeTools.includes('tool_search')) {
1277
1424
  throw new Error(`agent session fixed schemas must omit tool_search: read=${readTools.join(', ')} write=${writeTools.join(', ')}`);
1278
1425
  }
1279
- if (readTools.includes('shell') || writeTools.includes('shell')) {
1280
- throw new Error(`read/read-write agent schemas must omit shell: read=${readTools.join(', ')} write=${writeTools.join(', ')}`);
1426
+ if (readTools.includes('shell')) {
1427
+ throw new Error(`read agent schema must omit shell: read=${readTools.join(', ')}`);
1281
1428
  }
1282
1429
  for (const name of ['shell', 'apply_patch', 'task']) {
1283
1430
  if (readTools.includes(name)) {
1284
1431
  throw new Error(`read agent schema must omit non-read tool ${name}: read=${readTools.join(', ')}`);
1285
1432
  }
1286
1433
  }
1287
- for (const name of ['apply_patch', 'task']) {
1288
- if (name === 'apply_patch' && !writeTools.includes(name)) {
1289
- throw new Error(`read-write agent schema must preserve apply_patch: write=${writeTools.join(', ')}`);
1290
- }
1291
- if (name !== 'apply_patch' && writeTools.includes(name)) {
1292
- throw new Error(`read-write agent schema must omit non-edit tool ${name}: write=${writeTools.join(', ')}`);
1434
+ for (const name of ['apply_patch', 'shell', 'task']) {
1435
+ if (!writeTools.includes(name)) {
1436
+ throw new Error(`read-write agent schema must preserve ${name}: write=${writeTools.join(', ')}`);
1293
1437
  }
1294
1438
  }
1295
1439
  for (const name of ['memory', 'recall', 'reply']) {
@@ -1334,7 +1478,7 @@ setInternalToolsProvider({
1334
1478
  try {
1335
1479
  const resumed = await resumeSession(resumeAgentSession.id, 'full');
1336
1480
  const resumedTools = (resumed?.tools || []).map((tool) => tool?.name).filter(Boolean);
1337
- const expectedWriteTools = ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'apply_patch', 'explore', 'search', 'web_fetch', 'Skill'];
1481
+ const expectedWriteTools = ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'apply_patch', 'shell', 'task', 'explore', 'search', 'web_fetch', 'Skill'];
1338
1482
  if (JSON.stringify(resumedTools) !== JSON.stringify(expectedWriteTools)) {
1339
1483
  throw new Error(`resumed read-write agent schema must keep fixed allow-list: expected=${expectedWriteTools.join(', ')} actual=${resumedTools.join(', ')}`);
1340
1484
  }
@@ -1408,7 +1552,10 @@ setInternalToolsProvider({
1408
1552
  const resumed = await resumeSession(session.id, 'full');
1409
1553
  const resumedTools = (resumed?.tools || []).map((tool) => tool?.name).filter(Boolean);
1410
1554
  const expected = expectedForHiddenAgent(permission, schemaAllowedTools);
1411
- if (expected && (JSON.stringify(tools) !== JSON.stringify(expected) || JSON.stringify(resumedTools) !== JSON.stringify(expected))) {
1555
+ // Order-insensitive: the session tool surface follows catalog order, while
1556
+ // schemaAllowedTools declares an allow-set; only set equality is contractual.
1557
+ const asSet = (list) => JSON.stringify(list.slice().sort());
1558
+ if (expected && (asSet(tools) !== asSet(expected) || asSet(resumedTools) !== asSet(expected))) {
1412
1559
  throw new Error(`hidden agent ${agent} schema mismatch: expected=${expected.join(', ')} tools=${tools.join(', ')} resumed=${resumedTools.join(', ')}`);
1413
1560
  }
1414
1561
  const leaked = tools.filter((name) => hiddenBadTools.has(name) && !(expected || []).includes(name));
@@ -1522,7 +1669,7 @@ const readPathDescription = readPathSchema.description || '';
1522
1669
  if (!/File path/i.test(readPathDescription) || !/\{path,offset,limit\}\[\]/i.test(readPathDescription) || !/Pass arrays directly/i.test(readPathDescription) || !/legacy recovery only/i.test(readPathDescription)) {
1523
1670
  throw new Error('read schema must keep directory-vs-file guidance');
1524
1671
  }
1525
- if (!/Dirs use list/i.test((BUILTIN_TOOLS.find((tool) => tool.name === 'read')?.description) || '')) {
1672
+ if (!/Not for directory listing/i.test((BUILTIN_TOOLS.find((tool) => tool.name === 'read')?.description) || '')) {
1526
1673
  throw new Error('read description must keep directory-vs-file guidance');
1527
1674
  }
1528
1675
  const readTool = BUILTIN_TOOLS.find((tool) => tool.name === 'read');
@@ -1533,7 +1680,7 @@ const readArrayItemAnyOf = readArraySchema?.items?.anyOf || [];
1533
1680
  if (!readArrayItemAnyOf.some((entry) => entry?.type === 'object' && entry?.properties?.offset && entry?.properties?.limit)) {
1534
1681
  throw new Error('read schema must expose array-of-region objects for batched spans');
1535
1682
  }
1536
- if (/line\+context/i.test(readDescription) || !/numeric offset\+limit/i.test(readDescription) || !/Batch paths\/regions as real arrays/i.test(readDescription) || !/grep content_with_context or code_graph anchors/i.test(readDescription)) {
1683
+ if (/line\+context/i.test(readDescription) || !/numeric offset\+limit/i.test(readDescription) || !/Batch paths\/regions as real arrays/i.test(readDescription)) {
1537
1684
  throw new Error('read description must expose offset/limit as the single window form');
1538
1685
  }
1539
1686
  if (readProps.line || readProps.context) {
@@ -1567,15 +1714,25 @@ if (/line\/context/i.test(JSON.stringify(readTool?.inputSchema || {}))) {
1567
1714
  // session's provider/model in place, or a mid-conversation model/provider
1568
1715
  // switch silently forces a full prompt-cache rewrite (seen as a
1569
1716
  // promptΔ spike + cache_ratio=0% turn in session-bench).
1570
- const runtimeSrc = readFileSync(resolve(root, 'src/mixdog-session-runtime.mjs'), 'utf8');
1717
+ // God-file splits move implementation into module dirs; scan facade + all
1718
+ // split modules so these source-text guards survive refactors.
1719
+ const readMjsSources = (rel) => {
1720
+ const abs = resolve(root, rel);
1721
+ if (rel.endsWith('.mjs')) return readFileSync(abs, 'utf8');
1722
+ return readdirSync(abs, { recursive: true })
1723
+ .filter((f) => String(f).endsWith('.mjs'))
1724
+ .map((f) => readFileSync(resolve(abs, String(f)), 'utf8'))
1725
+ .join('\n');
1726
+ };
1727
+ const runtimeSrc = [readMjsSources('src/mixdog-session-runtime.mjs'), readMjsSources('src/session-runtime')].join('\n');
1571
1728
  const setRouteBlock = runtimeSrc.match(/async setRoute\(next, options = \{\}\) \{[\s\S]*?\n \},\n/)?.[0] || '';
1572
1729
  if (!/applyToCurrentSession = options\?\.applyToCurrentSession === true/.test(setRouteBlock)) {
1573
1730
  throw new Error('setRoute must default applyToCurrentSession to false (model changes apply to the next session only)');
1574
1731
  }
1575
- if (!/if \(!applyToCurrentSession\)/.test(setRouteBlock) || !/return route;/.test(setRouteBlock)) {
1732
+ if (!/if \(!applyToCurrentSession\)/.test(setRouteBlock) || !/return (?:route|getRoute\(\));/.test(setRouteBlock)) {
1576
1733
  throw new Error('setRoute must early-return before touching the live session when applyToCurrentSession is false');
1577
1734
  }
1578
- const engineSrc = readFileSync(resolve(root, 'src/tui/engine.mjs'), 'utf8');
1735
+ const engineSrc = [readMjsSources('src/tui/engine.mjs'), readMjsSources('src/tui/engine')].join('\n');
1579
1736
  if (/setRoute\(\{ model: m \}, \{ applyToCurrentSession: true \}\)/.test(engineSrc)) {
1580
1737
  throw new Error('TUI setModel must not force applyToCurrentSession:true (model changes must apply to the next session only)');
1581
1738
  }
@@ -1596,9 +1753,6 @@ if (codeGraphSymbolSearchErr) {
1596
1753
  if (!/code structure\/flow/i.test(codeGraphDescription) || !/symbols\/references\/calls\/deps/i.test(codeGraphDescription)) {
1597
1754
  throw new Error('code_graph description must stay structure-oriented and name its symbol modes');
1598
1755
  }
1599
- if (!/before text grep/i.test(codeGraphDescription)) {
1600
- throw new Error('code_graph description must steer symbol/caller lookups to structure lookup before grep');
1601
- }
1602
1756
  if (!/repo-local/i.test(codeGraphDescription) || !/NOT web search|not web/i.test(codeGraphDescription)) {
1603
1757
  throw new Error('code_graph description must mark itself repo-local (not web search)');
1604
1758
  }
@@ -1700,16 +1854,23 @@ const toolSearchSession = {
1700
1854
  deferredToolCatalog: smokeCatalog.slice(),
1701
1855
  deferredSelectedTools: [...fullDefaults],
1702
1856
  };
1703
- const searchOnlyResult = JSON.parse(__renderToolSearchForTest({ query: 'shell' }, toolSearchSession, 'full'));
1704
- for (const name of ['shell', 'task']) {
1705
- if (!searchOnlyResult.selected?.tools?.added?.includes(name)) {
1706
- throw new Error(`tool_search high-confidence query should auto-load ${name}: ${JSON.stringify(searchOnlyResult.selected)}`);
1707
- }
1857
+ // A plain query is a case-insensitive substring filter over name+description.
1858
+ // It lists matches only it never auto-loads or ranks.
1859
+ const listQueryResult = JSON.parse(__renderToolSearchForTest({ query: 'shell' }, toolSearchSession, 'full'));
1860
+ if (listQueryResult.selected) {
1861
+ throw new Error(`tool_search plain query must not auto-load: ${JSON.stringify(listQueryResult.selected)}`);
1708
1862
  }
1709
- if (!searchOnlyResult.activeTools.includes('shell') || !searchOnlyResult.activeTools.includes('task')) {
1710
- throw new Error(`tool_search query should activate legacy selected tools: ${searchOnlyResult.activeTools.join(',')}`);
1863
+ if (!listQueryResult.matches.some((row) => row.name === 'shell')) {
1864
+ throw new Error(`tool_search plain query should list substring matches: ${JSON.stringify(listQueryResult.matches)}`);
1711
1865
  }
1866
+ if (listQueryResult.activeTools.includes('shell') || (Array.isArray(listQueryResult.discoveredTools) && listQueryResult.discoveredTools.includes('shell'))) {
1867
+ throw new Error(`tool_search plain query must not activate/discover tools: ${JSON.stringify(listQueryResult)}`);
1868
+ }
1869
+ // query "select:a,b" is the explicit query-side loader (aliases expand).
1712
1870
  const bulkSelectResult = JSON.parse(__renderToolSearchForTest({ query: 'select:shell,recall' }, toolSearchSession, 'full'));
1871
+ if (bulkSelectResult.selected?.mode !== 'select') {
1872
+ throw new Error(`tool_search query-select must report select mode: ${JSON.stringify(bulkSelectResult.selected)}`);
1873
+ }
1713
1874
  for (const name of ['shell', 'task', 'recall']) {
1714
1875
  if (!bulkSelectResult.activeTools.includes(name)) {
1715
1876
  throw new Error(`tool_search bulk select missing ${name}: ${JSON.stringify(bulkSelectResult)}`);
@@ -1784,7 +1945,8 @@ if (nativeGrokPatchTool?.type !== 'function' || nativeGrokPatchTool?.format || n
1784
1945
  if (nativeGrokPatchTool.parameters?.properties?.patch?.type !== 'string') {
1785
1946
  throw new Error(`Grok native tool_search apply_patch must preserve patch JSON schema: ${JSON.stringify(nativeGrokPatchTool)}`);
1786
1947
  }
1787
- const nativeRunQuerySession = {
1948
+ // Native query-select discovers (without mutating active schemas); aliases expand.
1949
+ const nativeSelectQuerySession = {
1788
1950
  tools: smokeCatalog.filter((tool) => fullDefaults.has(tool?.name)),
1789
1951
  deferredToolCatalog: smokeCatalog.slice(),
1790
1952
  deferredSelectedTools: [...fullDefaults],
@@ -1792,16 +1954,20 @@ const nativeRunQuerySession = {
1792
1954
  deferredProviderMode: 'native',
1793
1955
  deferredNativeTools: true,
1794
1956
  };
1795
- const nativeRunQueryResult = JSON.parse(__renderToolSearchForTest({ query: 'run tests' }, nativeRunQuerySession, 'full'));
1796
- for (const name of ['shell', 'task']) {
1797
- if (!nativeRunQueryResult.discoveredTools.includes(name)) {
1798
- throw new Error(`native tool_search run/tests query should discover ${name}: ${JSON.stringify(nativeRunQueryResult)}`);
1957
+ const nativeSelectQueryResult = JSON.parse(__renderToolSearchForTest({ query: 'select:search' }, nativeSelectQuerySession, 'full'));
1958
+ for (const name of ['search', 'web_fetch']) {
1959
+ if (!nativeSelectQueryResult.discoveredTools.includes(name)) {
1960
+ throw new Error(`native tool_search query-select should discover ${name}: ${JSON.stringify(nativeSelectQueryResult)}`);
1799
1961
  }
1800
1962
  }
1801
- if (nativeRunQueryResult.activeTools.includes('shell') || nativeRunQueryResult.activeTools.includes('task')) {
1802
- throw new Error(`native tool_search query must not mutate active schemas: ${JSON.stringify(nativeRunQueryResult)}`);
1963
+ if (nativeSelectQueryResult.activeTools.includes('search') || nativeSelectQueryResult.activeTools.includes('web_fetch')) {
1964
+ throw new Error(`native tool_search must not mutate active schemas: ${JSON.stringify(nativeSelectQueryResult)}`);
1965
+ }
1966
+ if (!nativeSelectQueryResult.nativeToolSearch?.toolReferences?.includes('search')) {
1967
+ throw new Error(`native query-select must return nativeToolSearch payload: ${JSON.stringify(nativeSelectQueryResult.nativeToolSearch)}`);
1803
1968
  }
1804
- const nativeWebQuerySession = {
1969
+ // A plain query never auto-loads/discovers, even on native providers.
1970
+ const nativePlainQuerySession = {
1805
1971
  tools: smokeCatalog.filter((tool) => fullDefaults.has(tool?.name)),
1806
1972
  deferredToolCatalog: smokeCatalog.slice(),
1807
1973
  deferredSelectedTools: [...fullDefaults],
@@ -1809,35 +1975,48 @@ const nativeWebQuerySession = {
1809
1975
  deferredProviderMode: 'native',
1810
1976
  deferredNativeTools: true,
1811
1977
  };
1812
- const nativeWebQueryResult = JSON.parse(__renderToolSearchForTest({ query: 'web docs' }, nativeWebQuerySession, 'full'));
1813
- for (const name of ['search', 'web_fetch']) {
1814
- if (!nativeWebQueryResult.discoveredTools.includes(name)) {
1815
- throw new Error(`native tool_search web/docs query should discover ${name}: ${JSON.stringify(nativeWebQueryResult)}`);
1978
+ for (const q of ['run tests', 'web docs', 'memory previous', 'status']) {
1979
+ const r = JSON.parse(__renderToolSearchForTest({ query: q }, nativePlainQuerySession, 'full'));
1980
+ if (r.selected || r.discoveredTools.length) {
1981
+ throw new Error(`native tool_search plain query "${q}" must not auto-load/discover: ${JSON.stringify(r)}`);
1816
1982
  }
1817
1983
  }
1818
- const nativeRecallQuerySession = {
1819
- tools: smokeCatalog.filter((tool) => fullDefaults.has(tool?.name)),
1820
- deferredToolCatalog: smokeCatalog.slice(),
1821
- deferredSelectedTools: [...fullDefaults],
1822
- deferredDiscoveredTools: [],
1823
- deferredProviderMode: 'native',
1824
- deferredNativeTools: true,
1984
+ // Skill-style deferred manifest: `- name: description` lines, `<`/`>` sanitized,
1985
+ // bare names allowed, header instructs direct calls, empty pool → ''.
1986
+ const manifestText = buildDeferredToolManifest([
1987
+ { name: 'shell', description: 'Run commands.' },
1988
+ { name: 'search', description: 'Web <search> now.' },
1989
+ 'recall',
1990
+ ]);
1991
+ if (!/<available-deferred-tools>/.test(manifestText) || !/- shell: Run commands\./.test(manifestText)) {
1992
+ throw new Error(`deferred manifest must render "- name: description" lines: ${manifestText}`);
1993
+ }
1994
+ if (!/call any tool listed below directly/i.test(manifestText)) {
1995
+ throw new Error(`deferred manifest must tell the model it can call listed tools directly: ${manifestText}`);
1996
+ }
1997
+ if (!/^- recall$/m.test(manifestText)) {
1998
+ throw new Error(`deferred manifest must allow bare names without descriptions: ${manifestText}`);
1999
+ }
2000
+ if (/[<>]/.test(manifestText.replace(/<\/?available-deferred-tools>/g, ''))) {
2001
+ throw new Error(`deferred manifest must sanitize angle brackets in descriptions: ${manifestText}`);
2002
+ }
2003
+ if (buildDeferredToolManifest([]) !== '') {
2004
+ throw new Error('empty deferred pool must yield an empty manifest');
2005
+ }
2006
+ const bp1ManifestSession = {
2007
+ messages: [{ role: 'system', content: 'BASE PROMPT' }],
2008
+ deferredToolCatalog: [
2009
+ { name: 'shell', description: 'Run commands.' },
2010
+ { name: 'recall', description: 'Recall prior work.' },
2011
+ ],
1825
2012
  };
1826
- const nativeRecallQueryResult = JSON.parse(__renderToolSearchForTest({ query: 'memory previous' }, nativeRecallQuerySession, 'full'));
1827
- if (!nativeRecallQueryResult.discoveredTools.includes('recall') || nativeRecallQueryResult.discoveredTools.includes('memory')) {
1828
- throw new Error(`native tool_search memory previous should discover recall only: ${JSON.stringify(nativeRecallQueryResult)}`);
2013
+ applyInitialDeferredToolManifestToBp1(bp1ManifestSession, ['shell', 'recall']);
2014
+ const bp1ManifestText = bp1ManifestSession.messages[0].content;
2015
+ if (!/- shell: Run commands\./.test(bp1ManifestText) || !/- recall: Recall prior work\./.test(bp1ManifestText)) {
2016
+ throw new Error(`BP1 deferred manifest must carry catalog descriptions: ${bp1ManifestText}`);
1829
2017
  }
1830
- const ambiguousStatusSession = {
1831
- tools: smokeCatalog.filter((tool) => fullDefaults.has(tool?.name)),
1832
- deferredToolCatalog: smokeCatalog.slice(),
1833
- deferredSelectedTools: [...fullDefaults],
1834
- deferredDiscoveredTools: [],
1835
- deferredProviderMode: 'native',
1836
- deferredNativeTools: true,
1837
- };
1838
- const ambiguousStatusResult = JSON.parse(__renderToolSearchForTest({ query: 'status' }, ambiguousStatusSession, 'full'));
1839
- if (ambiguousStatusResult.selected || ambiguousStatusResult.discoveredTools.length) {
1840
- throw new Error(`tool_search ambiguous status query must not auto-load: ${JSON.stringify(ambiguousStatusResult)}`);
2018
+ if (bp1ManifestSession.deferredToolBp1Applied !== true) {
2019
+ throw new Error('BP1 deferred manifest injection must mark deferredToolBp1Applied');
1841
2020
  }
1842
2021
  const replyTool = CHANNEL_TOOL_DEFS.find((tool) => tool.name === 'reply');
1843
2022
  if (!/configured channel/i.test(replyTool?.description || '') || !/local .*paths/i.test(replyTool?.inputSchema?.properties?.files?.description || '')) {
@@ -1853,23 +2032,23 @@ const grepPathDescription = grepTool?.inputSchema?.properties?.path?.description
1853
2032
  const grepGlobDescription = grepTool?.inputSchema?.properties?.glob?.description || '';
1854
2033
  const grepOutputModeDescription = grepTool?.inputSchema?.properties?.output_mode?.description || '';
1855
2034
  const grepHeadLimitDescription = grepTool?.inputSchema?.properties?.head_limit?.description || '';
1856
- if (!/synonyms in pattern\[\]/i.test(grepPatternDescription) || !/OR in ONE grep/i.test(grepPatternDescription) || !/serial rewording/i.test(grepPatternDescription) || !/equivalent repeats/i.test(grepPatternDescription) || !/Narrowest file\/dir/i.test(grepPathDescription) || !/broad scopes return paths first/i.test(grepPathDescription) || !/refine from returned paths/i.test(grepPathDescription)) {
2035
+ if (!/Array = variants in one call/i.test(grepPatternDescription) || !/Known file or dir/i.test(grepPathDescription)) {
1857
2036
  throw new Error('grep schema must keep compact pattern/path guidance');
1858
2037
  }
1859
- if (!/same grep/i.test(grepGlobDescription) || !/no follow-up grep/i.test(grepGlobDescription) || !/equivalent scope changes/i.test(grepGlobDescription)) {
1860
- throw new Error('grep glob schema must steer narrowing into the same grep call');
2038
+ if (!/narrow scope/i.test(grepGlobDescription)) {
2039
+ throw new Error('grep glob schema must describe scope narrowing');
1861
2040
  }
1862
- if (!/Broad scope: files_with_matches\/count/i.test(grepOutputModeDescription) || !/Narrow scope: content_with_context/i.test(grepOutputModeDescription) || !/answer from/i.test(grepOutputModeDescription) || !/skip read/i.test(grepOutputModeDescription) || !/span is not shown/i.test(grepOutputModeDescription)) {
1863
- throw new Error('grep output_mode schema must steer content_with_context away from follow-up reads');
2041
+ if (!/files_with_matches\/count/i.test(grepOutputModeDescription) || !/content_with_context/i.test(grepOutputModeDescription)) {
2042
+ throw new Error('grep output_mode schema must name its output shapes');
1864
2043
  }
1865
- if (grepTool?.inputSchema?.properties?.head_limit?.minimum !== 0 || !/keep small/i.test(grepHeadLimitDescription)) {
2044
+ if (grepTool?.inputSchema?.properties?.head_limit?.minimum !== 0 || !/Max results/i.test(grepHeadLimitDescription)) {
1866
2045
  throw new Error('grep head_limit schema must keep locator caps explicit');
1867
2046
  }
1868
2047
  if (grepTool?.inputSchema?.properties?.type) {
1869
2048
  throw new Error('grep type schema must stay hidden; prefer glob for extension narrowing');
1870
2049
  }
1871
- if (!/code flow before text search/i.test(codeGraphProps.mode?.description || '') || !/one anchor is enough/i.test(codeGraphProps.symbols?.description || '') || !/one call/i.test(codeGraphProps.symbols?.description || '')) {
1872
- throw new Error('code_graph schema fields must steer away from repeated lookup loops');
2050
+ if (!/Repo-local/i.test(codeGraphProps.mode?.description || '') || !/one call/i.test(codeGraphProps.symbols?.description || '')) {
2051
+ throw new Error('code_graph schema fields must stay compact and repo-local');
1873
2052
  }
1874
2053
 
1875
2054
  const longToolSearchText = compactToolSearchDescription(`${patchDescription}\n${patchDescription}`);
@@ -8,7 +8,7 @@ Root-cause analysis agent.
8
8
 
9
9
  Smallest confirmed cause chain before fixes. Return likely cause, evidence
10
10
  (`file:line`), smallest next check/fix. Mark confirmed facts vs inferences;
11
- avoid broad speculation. Fragments only; no narration, no report bloat.
11
+ avoid broad speculation.
12
12
 
13
- Converge, don't sweep: when new reads stop adding evidence, report the best
14
- cause chain so far — a partial/blocked report is a valid completion.
13
+ Converge, don't sweep: when new evidence stops accruing, report the best
14
+ cause chain so far.
@@ -5,16 +5,13 @@ permission: read-write
5
5
  # Heavy Worker
6
6
  Broad implementation agent.
7
7
 
8
- Bounded slices; smallest coherent change, not rewrite. Stop: unclear scope,
9
- growing blast radius, or Lead-only verification.
8
+ Bounded slices; smallest coherent change, not rewrite. Stop when scope is
9
+ unclear or the blast radius grows.
10
10
 
11
- EDIT-FIRST DISCIPLINE. Survey the slice with ONE batched read/grep round, then
12
- patch the first bounded piece — edit incrementally, don't read exhaustively.
13
- NEVER "one more confirming read": a plausible anchor means the next call is
14
- `apply_patch`. Repeated read-only turns without an edit = stalling; on a
15
- runtime reminder, patch the piece you understand or report blocked — a valid
16
- completion. Self-check comes AFTER edits; deep verification is Lead's.
11
+ EDIT-FIRST DISCIPLINE. Survey the slice with one batched retrieval round,
12
+ then patch the first bounded piece — edit incrementally, don't read
13
+ exhaustively. Repeated read-only turns without an edit = stalling; on a
14
+ runtime reminder, patch the piece you understand or report blocked.
17
15
 
18
- Minimal checks + how-to-verify. Hand off outcome as fragments anchored to
19
- `file:line`; no narration or bloat.
16
+ Self-verify edits with shell (targeted test/build).
20
17
 
@@ -7,5 +7,4 @@ permission: read-write
7
7
  Maintenance and cleanup agent.
8
8
 
9
9
  Smallest coherent change; no scope growth. Repeated read-only turns without
10
- an edit = stalling; patch or return blocked — a blocked report is a valid
11
- completion. Hand off as fragments (`file:line`); no narration.
10
+ an edit = stalling; patch or return blocked.
@@ -8,5 +8,4 @@ Regression/risk review agent.
8
8
 
9
9
  Find actionable correctness/regression/security/verification risks. Findings
10
10
  first, severity-ordered, one line with `file:line`; skip non-risky nits. If
11
- clean, one line + only material residual risk. Fragments only; no narration,
12
- no report bloat.
11
+ clean, one line + only material residual risk.
@@ -7,13 +7,10 @@ Scoped implementation agent.
7
7
 
8
8
  Smallest scoped change; no drive-by cleanup. Stop when done/blocked.
9
9
 
10
- EDIT-FIRST DISCIPLINE. Brief anchors (`file:line`) are pre-verified — trust and
11
- patch. No anchor: locate with AT MOST 1-2 reads, then edit — NEVER "one more
12
- confirming read"; if you know the file and the change, the next call is
13
- `apply_patch`. Repeated read-only turns without an edit = stalling; on a
14
- runtime reminder, patch now or return blocked with what's missing — a blocked
15
- report is a valid completion. Threshold is "plausible", not "proven";
16
- self-check comes AFTER the edit, and Lead/Reviewer own final verification.
10
+ EDIT-FIRST DISCIPLINE. Brief anchors (`file:line`) are pre-verified — trust
11
+ and patch. No anchor: one locating batch, then `apply_patch`. Repeated
12
+ read-only turns without an edit = stalling; on a runtime reminder, patch now
13
+ or return blocked. Threshold is "plausible", not "proven".
17
14
 
18
- Hand off outcome as fragments anchored to `file:line`; no narration or bloat.
15
+ Self-verify edits with shell (node --check/test).
19
16
 
@@ -7,6 +7,7 @@
7
7
  "description": "Filesystem navigation agent invoked by the `explore` MCP tool",
8
8
  "invokedBy": "explore",
9
9
  "toolSchemaProfile": "read",
10
+ "schemaAllowedTools": ["grep", "find", "glob", "code_graph"],
10
11
  "kind": "retrieval",
11
12
  "permission": "read",
12
13
  "stallCap": { "idleSeconds": 240, "toolRunningSeconds": 180 }
@@ -54,6 +55,7 @@
54
55
  "description": "Scheduled-task executor invoked by scheduler tick",
55
56
  "invokedBy": "scheduler",
56
57
  "toolSchemaProfile": "read-write-search",
58
+ "schemaAllowedTools": ["code_graph", "find", "glob", "list", "grep", "read", "apply_patch", "search", "web_fetch"],
57
59
  "kind": "maintenance",
58
60
  "permission": "read-write",
59
61
  "inboundEvent": true,
@@ -67,6 +69,7 @@
67
69
  "description": "Webhook payload handler invoked by inbound webhook events",
68
70
  "invokedBy": "webhook",
69
71
  "toolSchemaProfile": "read-write-search",
72
+ "schemaAllowedTools": ["code_graph", "find", "glob", "list", "grep", "read", "apply_patch", "search", "web_fetch"],
70
73
  "kind": "maintenance",
71
74
  "permission": "read-write",
72
75
  "inboundEvent": true,
package/src/help.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { stdout } from 'node:process';
2
2
 
3
- /** Help text shared by `--help` and the `/help` slash command. */
3
+ /** Help text printed by `--help`. */
4
4
  export const HELP_LINES = [
5
5
  'mixdog — standalone mixdog CLI/TUI coding agent.',
6
6
  '',
@@ -11,16 +11,13 @@ export const HELP_LINES = [
11
11
  ' mixdog --help',
12
12
  '',
13
13
  'Slash commands (inside mixdog):',
14
- ' /help show this help',
15
14
  ' /clear reset the conversation and clear the screen',
16
15
  ' /compact compact older conversation context',
17
16
  ' /model <name> switch model/preset for subsequent turns',
18
17
  ' /OutputStyle [name] show or switch Lead output style',
19
18
  ' /providers manage provider auth and local endpoints',
20
19
  ' /agents show available workflow agents',
21
- ' /agent manage active agents and async tasks',
22
- ' /mode <name> switch tool surface: full | readonly',
23
- ' /exit quit',
20
+ ' /quit quit (aliases: /exit, /q)',
24
21
  '',
25
22
  'History: use ↑ / ↓ to recall previous inputs.',
26
23
  ];