mixdog 0.9.0 → 0.9.2

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 (240) hide show
  1. package/package.json +10 -3
  2. package/scripts/_bench-cwc.json +20 -0
  3. package/scripts/agent-loop-policy-test.mjs +37 -0
  4. package/scripts/agent-parallel-smoke.mjs +54 -10
  5. package/scripts/background-task-meta-smoke.mjs +1 -1
  6. package/scripts/bench-run.mjs +262 -0
  7. package/scripts/compact-smoke.mjs +12 -0
  8. package/scripts/compact-trigger-migration-smoke.mjs +67 -1
  9. package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
  10. package/scripts/internal-comms-bench.mjs +727 -0
  11. package/scripts/internal-comms-smoke.mjs +75 -0
  12. package/scripts/lead-workflow-smoke.mjs +4 -4
  13. package/scripts/live-worker-smoke.mjs +9 -9
  14. package/scripts/output-style-bench.mjs +285 -0
  15. package/scripts/output-style-smoke.mjs +13 -10
  16. package/scripts/patch-replay.mjs +90 -0
  17. package/scripts/provider-stream-stall-test.mjs +276 -0
  18. package/scripts/provider-toolcall-test.mjs +599 -1
  19. package/scripts/routing-corpus.mjs +281 -0
  20. package/scripts/session-bench.mjs +1526 -0
  21. package/scripts/session-diag.mjs +595 -0
  22. package/scripts/session-ingest-smoke.mjs +2 -2
  23. package/scripts/task-bench.mjs +207 -0
  24. package/scripts/tool-failures.mjs +6 -6
  25. package/scripts/tool-smoke.mjs +306 -66
  26. package/scripts/toolcall-args-test.mjs +81 -0
  27. package/src/agents/debugger/AGENT.md +4 -4
  28. package/src/agents/heavy-worker/AGENT.md +4 -2
  29. package/src/agents/reviewer/AGENT.md +4 -4
  30. package/src/agents/worker/AGENT.md +4 -2
  31. package/src/app.mjs +10 -6
  32. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  33. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  34. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  35. package/src/headless-role.mjs +14 -14
  36. package/src/help.mjs +1 -0
  37. package/src/lib/mixdog-debug.cjs +0 -22
  38. package/src/lib/plugin-paths.cjs +1 -7
  39. package/src/lib/rules-builder.cjs +34 -56
  40. package/src/mixdog-session-runtime.mjs +710 -319
  41. package/src/output-styles/default.md +12 -7
  42. package/src/output-styles/minimal.md +25 -0
  43. package/src/output-styles/oneline.md +21 -0
  44. package/src/output-styles/simple.md +10 -9
  45. package/src/repl.mjs +12 -4
  46. package/src/rules/agent/00-common.md +7 -5
  47. package/src/rules/agent/30-explorer.md +7 -8
  48. package/src/rules/lead/01-general.md +3 -1
  49. package/src/rules/lead/lead-tool.md +7 -0
  50. package/src/rules/shared/01-tool.md +17 -12
  51. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  52. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  53. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  54. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  55. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  56. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  57. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  58. package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
  59. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  60. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  61. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
  62. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  63. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  64. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  65. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +359 -106
  66. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +63 -51
  67. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
  68. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  69. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  70. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  71. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
  72. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +86 -30
  73. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +254 -280
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +191 -50
  75. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  76. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
  77. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  78. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +265 -1
  79. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  80. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  81. package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
  82. package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
  83. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  84. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  85. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  86. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  87. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -32
  89. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  90. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
  91. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  92. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
  93. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  94. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  95. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  96. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  97. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  98. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
  99. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  100. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
  101. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  102. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  103. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
  104. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  105. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
  106. package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
  107. package/src/runtime/channels/backends/discord.mjs +99 -9
  108. package/src/runtime/channels/backends/telegram.mjs +501 -0
  109. package/src/runtime/channels/index.mjs +441 -1254
  110. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  111. package/src/runtime/channels/lib/config.mjs +54 -3
  112. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  113. package/src/runtime/channels/lib/executor.mjs +0 -3
  114. package/src/runtime/channels/lib/format.mjs +4 -2
  115. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  116. package/src/runtime/channels/lib/output-forwarder.mjs +77 -71
  117. package/src/runtime/channels/lib/runtime-paths.mjs +29 -6
  118. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  119. package/src/runtime/channels/lib/session-discovery.mjs +0 -4
  120. package/src/runtime/channels/lib/telegram-format.mjs +283 -0
  121. package/src/runtime/channels/lib/tool-format.mjs +1 -2
  122. package/src/runtime/channels/lib/transcript-discovery.mjs +20 -11
  123. package/src/runtime/channels/lib/webhook.mjs +59 -31
  124. package/src/runtime/channels/tool-defs.mjs +1 -1
  125. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  126. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  127. package/src/runtime/memory/index.mjs +187 -43
  128. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  129. package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
  130. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  131. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  132. package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
  133. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  134. package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
  135. package/src/runtime/memory/lib/memory.mjs +101 -4
  136. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  137. package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
  138. package/src/runtime/memory/lib/session-ingest.mjs +116 -7
  139. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  140. package/src/runtime/memory/tool-defs.mjs +6 -3
  141. package/src/runtime/search/index.mjs +2 -7
  142. package/src/runtime/search/lib/config.mjs +0 -4
  143. package/src/runtime/search/lib/state.mjs +1 -15
  144. package/src/runtime/search/lib/web-tools.mjs +0 -1
  145. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  146. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  147. package/src/runtime/shared/child-spawn-gate.mjs +0 -6
  148. package/src/runtime/shared/config.mjs +9 -0
  149. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  150. package/src/runtime/shared/schedules-store.mjs +21 -19
  151. package/src/runtime/shared/tool-surface.mjs +98 -13
  152. package/src/runtime/shared/transcript-writer.mjs +129 -0
  153. package/src/runtime/shared/update-checker.mjs +214 -0
  154. package/src/standalone/agent-tool.mjs +255 -109
  155. package/src/standalone/channel-admin.mjs +133 -40
  156. package/src/standalone/channel-worker.mjs +8 -291
  157. package/src/standalone/explore-tool.mjs +2 -2
  158. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  159. package/src/standalone/provider-admin.mjs +11 -0
  160. package/src/standalone/seeds.mjs +1 -11
  161. package/src/standalone/usage-dashboard.mjs +1 -1
  162. package/src/tui/App.jsx +2137 -750
  163. package/src/tui/components/ConfirmBar.jsx +47 -0
  164. package/src/tui/components/ContextPanel.jsx +5 -3
  165. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  166. package/src/tui/components/Markdown.jsx +22 -98
  167. package/src/tui/components/Message.jsx +14 -35
  168. package/src/tui/components/Picker.jsx +87 -12
  169. package/src/tui/components/PromptInput.jsx +146 -9
  170. package/src/tui/components/QueuedCommands.jsx +1 -1
  171. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  172. package/src/tui/components/Spinner.jsx +7 -7
  173. package/src/tui/components/StatusLine.jsx +40 -21
  174. package/src/tui/components/TextEntryPanel.jsx +51 -7
  175. package/src/tui/components/ToolExecution.jsx +177 -100
  176. package/src/tui/components/TurnDone.jsx +4 -4
  177. package/src/tui/components/UsagePanel.jsx +1 -1
  178. package/src/tui/components/tool-output-format.mjs +312 -40
  179. package/src/tui/components/tool-output-format.test.mjs +180 -1
  180. package/src/tui/display-width.mjs +69 -0
  181. package/src/tui/display-width.test.mjs +35 -0
  182. package/src/tui/dist/index.mjs +7324 -2393
  183. package/src/tui/engine.mjs +287 -126
  184. package/src/tui/index.jsx +117 -7
  185. package/src/tui/keyboard-protocol.mjs +42 -0
  186. package/src/tui/lib/voice-recorder.mjs +453 -0
  187. package/src/tui/markdown/format-token.mjs +354 -142
  188. package/src/tui/markdown/format-token.test.mjs +155 -17
  189. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  190. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  191. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  192. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  193. package/src/tui/markdown/table-layout.mjs +9 -9
  194. package/src/tui/paste-attachments.mjs +0 -11
  195. package/src/tui/prompt-history-store.mjs +129 -0
  196. package/src/tui/prompt-history-store.test.mjs +52 -0
  197. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  198. package/src/tui/theme.mjs +41 -647
  199. package/src/tui/themes/base.mjs +86 -0
  200. package/src/tui/themes/basic.mjs +85 -0
  201. package/src/tui/themes/catppuccin.mjs +72 -0
  202. package/src/tui/themes/dracula.mjs +70 -0
  203. package/src/tui/themes/everforest.mjs +71 -0
  204. package/src/tui/themes/gruvbox.mjs +71 -0
  205. package/src/tui/themes/index.mjs +71 -0
  206. package/src/tui/themes/indigo.mjs +78 -0
  207. package/src/tui/themes/kanagawa.mjs +80 -0
  208. package/src/tui/themes/light.mjs +81 -0
  209. package/src/tui/themes/nord.mjs +72 -0
  210. package/src/tui/themes/onedark.mjs +16 -0
  211. package/src/tui/themes/rosepine.mjs +70 -0
  212. package/src/tui/themes/teal.mjs +81 -0
  213. package/src/tui/themes/tokyonight.mjs +79 -0
  214. package/src/tui/themes/utils.mjs +106 -0
  215. package/src/tui/themes/warm.mjs +79 -0
  216. package/src/tui/transcript-tool-failures.mjs +13 -2
  217. package/src/ui/markdown.mjs +1 -1
  218. package/src/ui/model-display.mjs +2 -2
  219. package/src/ui/statusline.mjs +26 -27
  220. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
  221. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  222. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  223. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  224. package/src/workflows/default/WORKFLOW.md +39 -12
  225. package/src/workflows/sequential/WORKFLOW.md +46 -0
  226. package/src/workflows/solo/WORKFLOW.md +7 -0
  227. package/vendor/ink/build/display-width.js +62 -0
  228. package/vendor/ink/build/ink.js +154 -20
  229. package/vendor/ink/build/measure-text.js +4 -1
  230. package/vendor/ink/build/output.js +115 -9
  231. package/vendor/ink/build/render-node-to-output.js +4 -1
  232. package/vendor/ink/build/render.js +4 -0
  233. package/src/hooks/lib/permission-rules.cjs +0 -170
  234. package/src/hooks/lib/settings-loader.cjs +0 -112
  235. package/src/lib/hook-pipe-path.cjs +0 -10
  236. package/src/output-styles/extreme-simple.md +0 -20
  237. package/src/rules/lead/04-workflow.md +0 -51
  238. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  239. package/src/workflows/default/workflow.json +0 -13
  240. package/src/workflows/solo/workflow.json +0 -7
@@ -0,0 +1,143 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { validateBuiltinArgs } from './arg-guard.mjs';
4
+
5
+ test('read: numeric-string offset/limit coerce losslessly', () => {
6
+ const a = { path: 'x.js', offset: '850', limit: '850.0' };
7
+ assert.equal(validateBuiltinArgs('read', a), null);
8
+ assert.equal(a.offset, 850);
9
+ assert.equal(a.limit, 850);
10
+ });
11
+
12
+ test('read: non-integer numeric string still rejected', () => {
13
+ const a = { path: 'x.js', offset: '3.5' };
14
+ assert.match(validateBuiltinArgs('read', a), /must be a finite integer/);
15
+ });
16
+
17
+ test('read: non-numeric string still rejected', () => {
18
+ const a = { path: 'x.js', offset: 'soon' };
19
+ assert.match(validateBuiltinArgs('read', a), /must be a finite integer/);
20
+ });
21
+
22
+ test('read: top-level line/context convert to offset/limit', () => {
23
+ const a = { path: 'x.js', line: 100, context: 10 };
24
+ assert.equal(validateBuiltinArgs('read', a), null);
25
+ assert.equal(a.offset, 89);
26
+ assert.equal(a.limit, 21);
27
+ assert.equal('line' in a, false);
28
+ assert.equal('context' in a, false);
29
+ });
30
+
31
+ test('read: line without context defaults to a single-line window', () => {
32
+ const a = { path: 'x.js', line: 1 };
33
+ assert.equal(validateBuiltinArgs('read', a), null);
34
+ assert.equal(a.offset, 0);
35
+ assert.equal(a.limit, 1);
36
+ });
37
+
38
+ test('read: explicit offset/limit wins over line/context (dropped)', () => {
39
+ const a = { path: 'x.js', line: 100, context: 10, offset: 5, limit: 20 };
40
+ assert.equal(validateBuiltinArgs('read', a), null);
41
+ assert.equal(a.offset, 5);
42
+ assert.equal(a.limit, 20);
43
+ assert.equal('line' in a, false);
44
+ assert.equal('context' in a, false);
45
+ });
46
+
47
+ test('read: path[] entry object line/context converts the same way', () => {
48
+ const a = { path: [{ path: 'x.js', line: 50, context: 5 }] };
49
+ assert.equal(validateBuiltinArgs('read', a), null);
50
+ assert.equal(a.path[0].offset, 44);
51
+ assert.equal(a.path[0].limit, 11);
52
+ });
53
+
54
+ test('read: path[] entry negative numeric-string offset is rejected after coercion', () => {
55
+ const a = { path: [{ path: 'x.js', offset: '-5' }] };
56
+ const err = validateBuiltinArgs('read', a);
57
+ assert.match(err, /path\[0\]\.offset/);
58
+ assert.match(err, />= 0/);
59
+ });
60
+
61
+ test('read: path[] entry negative numeric-string limit is rejected after coercion', () => {
62
+ const a = { path: [{ path: 'x.js', limit: '-1' }] };
63
+ const err = validateBuiltinArgs('read', a);
64
+ assert.match(err, /path\[0\]\.limit/);
65
+ assert.match(err, />= 0/);
66
+ });
67
+
68
+ test('read: path[] entry valid numeric-string offset/limit still coerces and passes', () => {
69
+ const a = { path: [{ path: 'x.js', offset: '10', limit: '5' }] };
70
+ assert.equal(validateBuiltinArgs('read', a), null);
71
+ assert.equal(a.path[0].offset, 10);
72
+ assert.equal(a.path[0].limit, 5);
73
+ });
74
+
75
+ test('read: JSON object string path is accepted as a single-entry batch', () => {
76
+ const a = { path: '{"path":"x.js","offset":10,"limit":5}' };
77
+ assert.equal(validateBuiltinArgs('read', a), null);
78
+ assert.equal(Array.isArray(a.path), true);
79
+ assert.equal(a.path[0].path, 'x.js');
80
+ assert.equal(a.path[0].offset, 10);
81
+ });
82
+
83
+ test('shell: win32 drive path with no shell set defaults to powershell', (t) => {
84
+ if (process.platform !== 'win32') { t.skip('win32 only'); return; }
85
+ const a = { command: 'dir C:\\Project\\mixdog' };
86
+ assert.equal(validateBuiltinArgs('shell', a), null);
87
+ assert.equal(a.shell, 'powershell');
88
+ });
89
+
90
+ test('glob: missing pattern with a path defaults pattern to "*"', () => {
91
+ const a = { path: 'src' };
92
+ assert.equal(validateBuiltinArgs('glob', a), null);
93
+ assert.equal(a.pattern, '*');
94
+ });
95
+
96
+ test('glob: path carrying glob magic is left for the path-magic fallback, not overridden', () => {
97
+ const a = { path: 'src/**/*.mjs' };
98
+ assert.equal(validateBuiltinArgs('glob', a), null);
99
+ assert.equal('pattern' in a, false);
100
+ assert.equal(a.path, 'src/**/*.mjs');
101
+ });
102
+
103
+ test('glob: missing pattern and path is still an error surface (guard passes through)', () => {
104
+ const a = {};
105
+ assert.equal(validateBuiltinArgs('glob', a), null);
106
+ assert.equal('pattern' in a, false);
107
+ });
108
+
109
+ test('grep: output_mode with newline-concatenated debris truncates to valid enum', () => {
110
+ const a = { pattern: 'foo', output_mode: 'content_with_context\ntrue' };
111
+ assert.equal(validateBuiltinArgs('grep', a), null);
112
+ assert.equal(a.output_mode, 'content_with_context');
113
+ });
114
+
115
+ test('grep: trailing literal \\n artifact stripped from pattern', () => {
116
+ const a = { pattern: 'foo bar\\n' };
117
+ assert.equal(validateBuiltinArgs('grep', a), null);
118
+ assert.equal(a.pattern, 'foo bar');
119
+ });
120
+
121
+ test('grep: trailing concatenation artifact ">\\n" stripped from pattern', () => {
122
+ const a = { pattern: 'foo bar">\n' };
123
+ assert.equal(validateBuiltinArgs('grep', a), null);
124
+ assert.equal(a.pattern, 'foo bar');
125
+ });
126
+
127
+ test('grep: mid-pattern \\n is left untouched', () => {
128
+ const a = { pattern: 'foo\\nbar' };
129
+ assert.equal(validateBuiltinArgs('grep', a), null);
130
+ assert.equal(a.pattern, 'foo\\nbar');
131
+ });
132
+
133
+ test('grep: bare trailing ">" with no newline artifact survives untouched', () => {
134
+ const a = { pattern: 'class="active">' };
135
+ assert.equal(validateBuiltinArgs('grep', a), null);
136
+ assert.equal(a.pattern, 'class="active">');
137
+ });
138
+
139
+ test('grep: numeric-string -C context coerces losslessly', () => {
140
+ const a = { pattern: 'foo', '-C': '5' };
141
+ assert.equal(validateBuiltinArgs('grep', a), null);
142
+ assert.equal(a['-C'], 5);
143
+ });
@@ -3,7 +3,7 @@ import { execShellCommand, stripAnsi } from '../shell-command.mjs';
3
3
  import { wrapCommandWithSnapshot } from '../shell-snapshot.mjs';
4
4
  import { getDestructiveCommandWarning } from '../destructive-warning.mjs';
5
5
  import { maybeRewriteWmicProcessCommand } from '../shell-policy.mjs';
6
- import { buildBashPolicyScanTargets, checkExecPolicyMessage, injectionBlockTargets } from '../bash-policy-scan.mjs';
6
+ import { buildBashPolicyScanTargets, checkExecPolicyMessage } from '../bash-policy-scan.mjs';
7
7
  import { markCodeGraphDirtyPaths, drainCodeGraphCache } from '../code-graph-state.mjs';
8
8
  import {
9
9
  buildJobNotFoundMessage,
@@ -27,7 +27,6 @@ import {
27
27
  cancelBackgroundTask,
28
28
  completeBackgroundTask,
29
29
  getBackgroundTask,
30
- listBackgroundTasks,
31
30
  registerBackgroundTask,
32
31
  renderBackgroundTask,
33
32
  renderBackgroundTaskList,
@@ -48,7 +47,6 @@ import { scrubLoaderVars, scrubProviderSecrets } from '../env-scrub.mjs';
48
47
  // closing the "external write -> stale old_string -> code 8" gap when shell is
49
48
  // routed through this tool. Bounded to the tracked-read set (capped) so cost
50
49
  // stays off the whole-cwd path; emits nothing when no read file changed.
51
- const _DRIFT_SCAN_CAP = 256;
52
50
  export function _captureTrackedMtimes(_scope) {
53
51
  return new Map();
54
52
  }
@@ -86,53 +84,12 @@ function _combineAbortSignals(sessionSignal, externalSignal) {
86
84
  return ctl.signal;
87
85
  }
88
86
 
89
- // Decode ANSI-C $'…' and locale $"…" escapes so the blocklist scan sees the
90
- // literal command (e.g. $'\x72m' → "rm"). Defensive against quoting bypass.
91
- function _decodeAnsiCQuotes(s) {
92
- if (typeof s !== 'string') return '';
93
- if (s.indexOf('$') === -1) return s;
94
- return s.replace(/\$(['"])((?:\\.|[^\\])*?)\1/g, (_full, _q, body) =>
95
- body
96
- .replace(/\\x([0-9a-fA-F]{1,2})/g, (_m, h) => String.fromCharCode(parseInt(h, 16)))
97
- .replace(/\\u([0-9a-fA-F]{1,4})/g, (_m, h) => String.fromCharCode(parseInt(h, 16)))
98
- .replace(/\\0([0-7]{1,3})/g, (_m, o) => String.fromCharCode(parseInt(o, 8)))
99
- .replace(/\\([0-7]{1,3})/g, (_m, o) => String.fromCharCode(parseInt(o, 8)))
100
- .replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\r/g, '\r')
101
- .replace(/\\\\/g, '\\').replace(/\\(['"])/g, '$1'),
102
- );
103
- }
104
-
105
- // Extract $(…) and `…` command-substitution bodies so each is re-scanned by
106
- // isBlockedCommand (e.g. eval $(printf 'rm -rf ~')).
107
- function _extractSubstitutionBodies(s) {
108
- if (typeof s !== 'string') return [];
109
- const out = [];
110
- const re = /\$\(([^()]*(?:\([^()]*\)[^()]*)*)\)|`([^`]*)`/g;
111
- let m;
112
- while ((m = re.exec(s)) !== null) {
113
- const body = m[1] != null ? m[1] : m[2];
114
- if (body && body.trim()) out.push(body);
115
- }
116
- return out;
117
- }
118
-
119
- // Combined injection-aware block targets: decoded form + substitution bodies
120
- // (and their decoded forms). Used on BOTH the persistent and stateless paths.
121
- export function _injectionBlockTargets(cmd) {
122
- return injectionBlockTargets(cmd);
123
- }
124
-
125
87
  function _prefixPowerShellUtf8(command) {
126
88
  const prefix = '[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; $OutputEncoding=[System.Text.Encoding]::UTF8;';
127
89
  const text = String(command || '');
128
90
  return text.trimStart().startsWith(prefix) ? text : `${prefix}\n${text}`;
129
91
  }
130
92
 
131
- const _unquoteSpansForPolicy = (s) => s.replace(/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/g, (m) => m.slice(1, -1));
132
-
133
- // Same normalized + decoded target set as hard-block (strip/unquote/shell -c/PS).
134
- export { buildBashPolicyScanTargets } from '../bash-policy-scan.mjs';
135
-
136
93
  export function getDedupedDestructiveWarnings(command) {
137
94
  const seenMsg = new Set();
138
95
  const warnings = [];
@@ -17,7 +17,7 @@ export const BUILTIN_TOOLS = [
17
17
  name: 'read',
18
18
  title: 'Mixdog Read',
19
19
  annotations: { title: 'Mixdog Read', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: false },
20
- description: 'Read known file path(s). Use line+context for small windows. Batch paths.',
20
+ description: 'Read known file path(s). Prefer grep content_with_context or code_graph anchors first. Window with numeric offset+limit only. Batch paths/regions as real arrays. Dirs use list.',
21
21
  inputSchema: {
22
22
  type: 'object',
23
23
  properties: {
@@ -26,14 +26,26 @@ export const BUILTIN_TOOLS = [
26
26
  { type: 'string' },
27
27
  {
28
28
  type: 'array',
29
- items: { type: 'string' },
29
+ items: {
30
+ anyOf: [
31
+ { type: 'string' },
32
+ {
33
+ type: 'object',
34
+ properties: {
35
+ path: { type: 'string' },
36
+ offset: { type: 'number', minimum: 0 },
37
+ limit: { type: 'number', minimum: 1 },
38
+ },
39
+ },
40
+ ],
41
+ },
30
42
  minItems: 1,
31
43
  },
32
44
  ],
33
- description: 'File path or array. Dirs use list.',
45
+ description: 'File path, path[], or {path,offset,limit}[] region objects. Pass arrays directly; JSON strings are legacy recovery only. Dirs use list.',
34
46
  },
35
- line: { type: 'number', minimum: 1, description: 'Anchor line.' },
36
- context: { type: 'number', minimum: 0, maximum: 200, description: 'Lines around line; max 200.' },
47
+ offset: { type: 'number', minimum: 0, description: 'Numeric lines to skip before reading; 0 starts at line 1. Continue with offset:N.' },
48
+ limit: { type: 'number', minimum: 1, description: 'Numeric max lines to return after offset.' },
37
49
  },
38
50
  },
39
51
  },
@@ -53,7 +65,10 @@ export const BUILTIN_TOOLS = [
53
65
  {
54
66
  name: 'open_config',
55
67
  title: 'Open Config UI',
56
- annotations: { title: 'Open Config UI', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
68
+ // agentHidden: a worker/reviewer session has no business popping the
69
+ // settings UI on the user's machine; it also wastes schema bytes on
70
+ // every agent request. Lead keeps it.
71
+ annotations: { title: 'Open Config UI', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, agentHidden: true },
57
72
  description: 'Open settings UI; returns URL. No params.',
58
73
  inputSchema: { type: 'object', properties: {}, additionalProperties: false },
59
74
  },
@@ -61,7 +76,7 @@ export const BUILTIN_TOOLS = [
61
76
  name: 'shell',
62
77
  title: 'Mixdog Shell',
63
78
  annotations: { title: 'Mixdog Shell', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, compressible: true },
64
- description: `Run shell for git/build/test/run. Not for source browsing; use code_graph/grep/read/list/glob. Set shell: powershell or bash. ${TOOL_ASYNC_EXECUTION_CONTRACT}`,
79
+ description: `Run shell to CHANGE state or RUN programs (git/build/test/run). Never to inspect the filesystem — reading, listing, searching, or checking existence go through the dedicated tools, never a shell command. Set shell: powershell or bash. ${TOOL_ASYNC_EXECUTION_CONTRACT}`,
65
80
  inputSchema: {
66
81
  type: 'object',
67
82
  properties: {
@@ -101,7 +116,7 @@ export const BUILTIN_TOOLS = [
101
116
  name: 'grep',
102
117
  title: 'Mixdog Grep',
103
118
  annotations: { title: 'Mixdog Grep', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
104
- description: 'Search text/logs/regex.',
119
+ description: 'Search file contents by text/regex in a known scope. Use files_with_matches/count for broad anchors, content_with_context for narrow code answers. One concept → one grep.',
105
120
  inputSchema: {
106
121
  type: 'object',
107
122
  properties: {
@@ -110,19 +125,20 @@ export const BUILTIN_TOOLS = [
110
125
  { type: 'string' },
111
126
  { type: 'array', items: { type: 'string' }, minItems: 1 },
112
127
  ],
113
- description: 'Text/regex. Array = OR.',
128
+ description: 'Text/regex. Put synonyms in pattern[] as OR in ONE grep; no serial rewording or equivalent repeats.',
114
129
  },
115
- path: { type: 'string', description: 'File or directory.' },
130
+ path: { type: 'string', description: 'Known narrowest file/dir; broad scopes return paths first, then refine from returned paths.' },
116
131
  glob: {
117
132
  anyOf: [
118
133
  { type: 'string' },
119
134
  { type: 'array', items: { type: 'string' }, minItems: 1 },
120
135
  ],
121
- description: 'Optional narrowing glob(s).',
136
+ description: 'Narrow in same grep; no follow-up grep for equivalent scope changes.',
122
137
  },
123
- output_mode: { type: 'string', enum: ['files_with_matches', 'content', 'count'], description: 'Output mode.' },
124
- head_limit: { type: 'number', description: 'Max lines.' },
125
- offset: { type: 'number', description: 'Skip lines.' },
138
+ output_mode: { type: 'string', enum: ['content_with_context', 'content', 'files_with_matches', 'count'], description: 'Broad scope: files_with_matches/count. Narrow scope: content_with_context; answer from it, skip read unless span is not shown.' },
139
+ context: { type: 'number', minimum: 0, description: 'Lines before/after each match; keep bounded.' },
140
+ head_limit: { type: 'number', minimum: 0, description: 'Max output lines; keep small.' },
141
+ offset: { type: 'number', minimum: 0, description: 'Skip output lines for paging.' },
126
142
  },
127
143
  required: [],
128
144
  },
@@ -131,7 +147,7 @@ export const BUILTIN_TOOLS = [
131
147
  name: 'glob',
132
148
  title: 'Mixdog Glob',
133
149
  annotations: { title: 'Mixdog Glob', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
134
- description: 'Find files by glob.',
150
+ description: 'Find files by exact glob. Unknown path/name uses find.',
135
151
  inputSchema: {
136
152
  type: 'object',
137
153
  properties: {
@@ -140,7 +156,7 @@ export const BUILTIN_TOOLS = [
140
156
  { type: 'string' },
141
157
  { type: 'array', items: { type: 'string' }, minItems: 1 },
142
158
  ],
143
- description: 'Glob pattern(s).',
159
+ description: 'Exact glob pattern(s).',
144
160
  },
145
161
  path: {
146
162
  anyOf: [
@@ -159,11 +175,11 @@ export const BUILTIN_TOOLS = [
159
175
  name: 'find',
160
176
  title: 'Mixdog Find Files',
161
177
  annotations: { title: 'Mixdog Find Files', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
162
- description: 'Fuzzy-find files by partial path/name. Returns paths.',
178
+ description: 'Find files by partial path/name. Exact structure uses glob. Returns verified paths.',
163
179
  inputSchema: {
164
180
  type: 'object',
165
181
  properties: {
166
- query: { type: 'string', description: 'Partial path/name words.' },
182
+ query: { type: 'string', description: 'Partial path/name words; not file contents.' },
167
183
  path: { type: 'string', description: 'Base directory.' },
168
184
  head_limit: { type: 'number', description: 'Max paths.' },
169
185
  },
@@ -5,3 +5,13 @@ export function formatMtime(mtimeMs) {
5
5
  if (!mtimeMs) return '-';
6
6
  return new Date(mtimeMs).toISOString().slice(0, 19).replace('T', ' ');
7
7
  }
8
+
9
+ // Human-readable size column for list/find. Directories have no meaningful
10
+ // byte size (render '-'); files show KB so the model can spot a large file
11
+ // before reading it. Sub-1KB files round up to 1KB.
12
+ export function formatListSize(type, bytes) {
13
+ if (type !== 'file') return '-';
14
+ const n = Number(bytes) || 0;
15
+ if (n >= 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)}MB`;
16
+ return `${Math.max(1, Math.round(n / 1024))}KB`;
17
+ }
@@ -26,7 +26,8 @@ import {
26
26
  NOISE_DIR_NAMES,
27
27
  walkDir,
28
28
  } from './glob-walk.mjs';
29
- import { formatMtime } from './list-formatting.mjs';
29
+ import { formatMtime, formatListSize } from './list-formatting.mjs';
30
+ import { TOOL_OUTPUT_MAX_BYTES } from './tool-output-limit.mjs';
30
31
  import { runRg } from './rg-runner.mjs';
31
32
  import { fuzzyRank } from './fuzzy-match.mjs';
32
33
  import { assertPathReachable } from './fs-reachability.mjs';
@@ -169,7 +170,7 @@ export async function executeListTool(args, workDir, options = {}) {
169
170
  }
170
171
  }
171
172
  const lines = sliced.map(r =>
172
- `${normalizeOutputPath(r.path)}\t${r.type}\t${r.size}\t${formatMtime(r.mtimeMs)}`);
173
+ `${normalizeOutputPath(r.path)}\t${r.type}\t${formatListSize(r.type, r.size)}\t${formatMtime(r.mtimeMs)}`);
173
174
  if (windowed.length > sliced.length) lines.push(`... [entries ${offset + 1}-${offset + sliced.length} of ${rows.length}; pass offset:${offset + sliced.length} to continue]`);
174
175
  if (truncatedByCap) lines.push(`... walk truncated at ${LIST_ABSOLUTE_CAP} rows or ${LIST_WALK_TIMEOUT_MS}ms timeout; narrow the path or lower depth for a complete listing`);
175
176
  let emptyMsg = '(empty directory)';
@@ -282,7 +283,7 @@ export async function executeTreeTool(args, workDir, options = {}) {
282
283
  const totalLabel = body.length >= gatherCap ? `${body.length}+` : `${body.length}`;
283
284
  outLines.push(`... [entries ${offset + 1}-${offset + sliced.length} of ${totalLabel}; pass offset:${offset + sliced.length} to continue]`);
284
285
  }
285
- const TREE_OUTPUT_CHAR_CAP = 50_000;
286
+ const TREE_OUTPUT_CHAR_CAP = TOOL_OUTPUT_MAX_BYTES;
286
287
  let out = outLines.join('\n');
287
288
  let outputCharTruncated = false;
288
289
  if (out.length > TREE_OUTPUT_CHAR_CAP) {
@@ -603,7 +604,7 @@ export async function executeFindFilesTool(args, workDir, options = {}) {
603
604
  const windowed = offset > 0 ? matches.slice(offset) : matches;
604
605
  const sliced = headLimit > 0 ? windowed.slice(0, headLimit) : windowed;
605
606
  const lines = sliced.map(m =>
606
- `${normalizeOutputPath(m.path)}\t${m.size}\t${formatMtime(m.mtimeMs)}`);
607
+ `${normalizeOutputPath(m.path)}\t${formatListSize('file', m.size)}\t${formatMtime(m.mtimeMs)}`);
607
608
  if (windowed.length > sliced.length) lines.push(`... [entries ${offset + 1}-${offset + sliced.length} of ${matches.length}; pass offset:${offset + sliced.length} to continue]`);
608
609
  if (rgStdoutTruncated) lines.push('... [warning] rg stdout truncated at 20MB cap; results incomplete');
609
610
  if (rgStdoutPartial) lines.push('... [warning] rg exit 2 (partial results); listing may be incomplete');
@@ -220,5 +220,20 @@ export function coerceShapeFlex(value) {
220
220
  if (typeof parsed === 'string') return parsed;
221
221
  } catch {}
222
222
  }
223
+ // A single {path,offset,limit}-shaped object JSON string (e.g. a model
224
+ // emitting `path: "{\"path\":\"x\",\"offset\":10}"` instead of a real
225
+ // object) is a lossless single-entry batch. Wrap it in an array so the
226
+ // downstream object-array batch path (read-tool.mjs) picks it up instead
227
+ // of falling through to the plain-string path guard, which would
228
+ // misdetect the raw JSON text as a literal (and often invalid) filename.
229
+ if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
230
+ try {
231
+ const parsed = JSON.parse(trimmed);
232
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)
233
+ && typeof (parsed.path ?? parsed.file_path) === 'string') {
234
+ return [parsed];
235
+ }
236
+ } catch {}
237
+ }
223
238
  return value;
224
239
  }
@@ -1,21 +1,11 @@
1
1
  import { existsSync } from 'fs';
2
2
  import { normalizeInputPath, resolveAgainstCwd } from './path-utils.mjs';
3
- import { parseLineLimitArg } from './read-formatting.mjs';
4
-
5
- const READ_LINE_CONTEXT_DEFAULT = 20;
6
- const READ_LINE_CONTEXT_MAX = 200;
7
3
 
8
4
  export function parseReadLineNumberArg(value) {
9
5
  const n = Number(value);
10
6
  return Number.isFinite(n) && n > 0 ? Math.trunc(n) : null;
11
7
  }
12
8
 
13
- export function parseReadLineContextArg(value) {
14
- const n = Number(value);
15
- if (!Number.isFinite(n)) return READ_LINE_CONTEXT_DEFAULT;
16
- return Math.min(READ_LINE_CONTEXT_MAX, Math.max(0, Math.trunc(n)));
17
- }
18
-
19
9
  export function parseReadPathLineSpec(rawPath) {
20
10
  if (typeof rawPath !== 'string' || rawPath.length === 0) return null;
21
11
  const text = rawPath.trim();
@@ -51,7 +41,6 @@ export function normalizePathAndStripLineCoordinate(rawPath, workDir) {
51
41
 
52
42
  export function normaliseReadLineWindowArgs(inputArgs, workDir) {
53
43
  const args = { ...inputArgs };
54
- let lineNo = parseReadLineNumberArg(args.line);
55
44
  let pathLineRange = null;
56
45
  if (typeof args.path === 'string' && args.path) {
57
46
  const spec = resolveExistingPathLineCoordinate(args.path, workDir);
@@ -61,45 +50,21 @@ export function normaliseReadLineWindowArgs(inputArgs, workDir) {
61
50
  return args;
62
51
  }
63
52
  args.path = spec.path;
64
- if (!lineNo) lineNo = spec.lineNo;
65
53
  if (spec.endLine) pathLineRange = { startLine: spec.lineNo, endLine: spec.endLine };
54
+ else pathLineRange = { startLine: spec.lineNo, endLine: spec.lineNo };
66
55
  }
67
56
  }
68
57
  const isFullMode = !args.mode || args.mode === 'full';
69
- // line= and offset= are ALTERNATIVE window anchors. Silently preferring
70
- // offset used to discard line/context and (with a bare low offset) fall
71
- // through to a whole-file smart-truncate dump — the exact read explosion
72
- // a 7-line `line+context` request was trying to avoid. Make it a hard
73
- // conflict instead.
74
- if (isFullMode && lineNo && args.offset !== undefined && args.offset !== null) {
75
- args._invertedRangeError = `Error: conflicting window args — both line (${lineNo}) and offset (${args.offset}) were given; line/context and offset/limit are alternative window forms, pass only one`;
76
- return args;
77
- }
78
- if (isFullMode && lineNo) {
79
- if (pathLineRange && args.context === undefined && (args.limit === undefined || args.limit === null)) {
80
- args.offset = Math.max(0, pathLineRange.startLine - 1);
58
+ // Public Read args are offset/limit only. Keep a narrow private
59
+ // compatibility path for file#Lx/file:line strings by converting them into
60
+ // offset/limit; do not interpret line/context fields here.
61
+ if (isFullMode && pathLineRange && args.offset === undefined) {
62
+ args.offset = Math.max(0, pathLineRange.startLine - 1);
63
+ if (args.limit === undefined) {
81
64
  args.limit = Math.max(1, pathLineRange.endLine - pathLineRange.startLine + 1);
82
- } else {
83
- const contextExplicit = args.context !== undefined && args.context !== null;
84
- const limitExplicit = args.limit !== undefined && args.limit !== null;
85
- const context = parseReadLineContextArg(args.context);
86
- if (limitExplicit && !contextExplicit) {
87
- // Explicit limit, no explicit context: anchor the window AT the
88
- // requested line so it is always included. (Was: offset centered
89
- // by the default context, which a small limit then truncated to
90
- // exclude the very line the caller asked for.)
91
- args.offset = Math.max(0, lineNo - 1);
92
- args.limit = parseLineLimitArg(args.limit, (context * 2) + 1);
93
- } else {
94
- const limit = limitExplicit
95
- ? parseLineLimitArg(args.limit, (context * 2) + 1)
96
- : (context * 2) + 1;
97
- args.offset = Math.max(0, lineNo - context - 1);
98
- args.limit = limit;
99
- }
100
65
  }
101
- delete args.line;
102
- delete args.context;
103
66
  }
67
+ delete args.line;
68
+ delete args.context;
104
69
  return args;
105
70
  }
@@ -1,4 +1,5 @@
1
1
  import { SMART_READ_MAX_BYTES } from './read-formatting.mjs';
2
+ import { TOOL_OUTPUT_MAX_BYTES } from './tool-output-limit.mjs';
2
3
 
3
4
  // Read tool caps.
4
5
  //
@@ -28,7 +29,7 @@ import { SMART_READ_MAX_BYTES } from './read-formatting.mjs';
28
29
  // cleanly without a tokenizer.
29
30
  export const READ_MAX_SIZE_BYTES = 10 * 1024 * 1024;
30
31
  export const READ_WHOLE_FILE_MAX_BYTES = 256 * 1024;
31
- export const READ_MAX_OUTPUT_BYTES = 30_000;
32
+ export const READ_MAX_OUTPUT_BYTES = TOOL_OUTPUT_MAX_BYTES;
32
33
  export const READ_STREAM_RANGE_MIN_BYTES = 128 * 1024;
33
34
  export const READ_SMART_STREAM_MIN_BYTES = SMART_READ_MAX_BYTES;
34
35
  export const READ_BATCH_RANGE_COALESCE_GAP_LINES = 256;
@@ -1,9 +1,18 @@
1
1
  import { mergeReadRanges } from './read-ranges.mjs';
2
+ import { TOOL_OUTPUT_MAX_BYTES } from './tool-output-limit.mjs';
2
3
 
3
- export const SMART_READ_MAX_BYTES = 30 * 1024;
4
- export const SMART_READ_MAX_LINES = 600;
5
- export const SMART_READ_HEAD_LINES = 200;
6
- export const SMART_READ_TAIL_LINES = 100;
4
+ // Smart-truncate cap: a no-window read returns the file until this cap, past
5
+ // which head+tail are shown and the model pages the rest with offset (footer
6
+ // says how). Byte budget is the shared TOOL_OUTPUT_MAX_BYTES; line/head/tail
7
+ // stay read-specific. Env-overridable for bench: MIXDOG_READ_MAX_LINES/_HEAD/_TAIL.
8
+ function _readEnvInt(name, fallback) {
9
+ const v = parseInt(process.env[name], 10);
10
+ return Number.isFinite(v) && v > 0 ? v : fallback;
11
+ }
12
+ export const SMART_READ_MAX_BYTES = TOOL_OUTPUT_MAX_BYTES;
13
+ export const SMART_READ_MAX_LINES = _readEnvInt('MIXDOG_READ_MAX_LINES', 2000);
14
+ export const SMART_READ_HEAD_LINES = _readEnvInt('MIXDOG_READ_HEAD_LINES', 1200);
15
+ export const SMART_READ_TAIL_LINES = _readEnvInt('MIXDOG_READ_TAIL_LINES', 400);
7
16
  // Only the genuinely large full reads warrant the anti-re-read advisory; below
8
17
  // this the smart-truncate path (30 KB) already caps normal reads, so a 16 KB
9
18
  // floor mostly fired on full:true mid-size reads where the advisory was pure
@@ -49,8 +49,7 @@ function _guardedReadError(p, helpers) {
49
49
  }
50
50
 
51
51
  // Reachability preflight for EVERY read shape (scalar / array / reads[]). MUST
52
- // run before any sync FS — including line-coordinate disambiguation (existsSync
53
- // in readPathStringGuardError / normaliseReadLineWindowArgs) and the image
52
+ // run before any sync FS — including path normalization and the image
54
53
  // stat/read. A dead mount would otherwise freeze the event loop, defeating even
55
54
  // the 630s dispatch ceiling.
56
55
  async function _readReachPreflight(rawPath, workDir, helpers) {
@@ -58,10 +57,9 @@ async function _readReachPreflight(rawPath, workDir, helpers) {
58
57
  normalizeInputPath, resolveAgainstCwd,
59
58
  } = helpers;
60
59
  // A guarded path (UNC/SMB, Windows device, ADS, /dev/* block) must be
61
- // REJECTED here, not skipped: skipping would let the later sync existsSync
62
- // line-coordinate disambiguation (normaliseReadLineWindowArgs /
63
- // readPathStringGuardError) touch it and trigger NTLM/raw-device access or
64
- // hang. Reject up front with the same message the inline guards emit.
60
+ // REJECTED here, not skipped: skipping would let the later sync guard/open
61
+ // path touch it and trigger NTLM/raw-device access or hang. Reject up front
62
+ // with the same message the inline guards emit.
65
63
  // normalizeInputPath FIRST (FS-pure) so we stat the same path the real read
66
64
  // opens (e.g. /mnt/z/... -> Z:\...). Reachability is per-mount/dir, so the
67
65
  // line-coordinate strip only needs to land in the right directory — exact
@@ -113,7 +111,7 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
113
111
  }
114
112
  args.path = coerceShapeFlex(args.path);
115
113
  // Reachability preflight up front (all shapes) — before readPathStringGuardError /
116
- // normaliseReadLineWindowArgs / image stat, all of which can touch sync FS.
114
+ // image stat, all of which can touch sync FS.
117
115
  {
118
116
  const _reErr = await _readReachPreflight(args.path, workDir, helpers);
119
117
  if (_reErr) return _reErr;
@@ -186,8 +184,6 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
186
184
  }
187
185
  }
188
186
  if (r?.limit !== undefined) entry.limit = r.limit;
189
- if (r?.line !== undefined) entry.line = r.line;
190
- if (r?.context !== undefined) entry.context = r.context;
191
187
  if (r?.full !== undefined) entry.full = r.full;
192
188
  entry = normaliseReadLineWindowArgs(entry, workDir);
193
189
  return entry;
@@ -235,8 +231,6 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
235
231
  if (args.n !== undefined) entry.n = args.n;
236
232
  if (args.offset !== undefined) entry.offset = args.offset;
237
233
  if (args.limit !== undefined) entry.limit = args.limit;
238
- if (args.line !== undefined) entry.line = args.line;
239
- if (args.context !== undefined) entry.context = args.context;
240
234
  if (args.full !== undefined) entry.full = args.full;
241
235
  entry = normaliseReadLineWindowArgs(entry, workDir);
242
236
  return entry;
@@ -464,14 +458,13 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
464
458
  if (span.note) args._symbolReadNote = `symbol ${sym}: ${span.note}`;
465
459
  }
466
460
  }
467
- // A window (offset/limit/line or a path:line coordinate) beats a glance
461
+ // A window (offset/limit or a path:line coordinate) beats a glance
468
462
  // mode (head/tail/summary), which would otherwise read from a file end and
469
- // silently drop the window. Drop the glance mode BEFORE line-window
470
- // normalization so a line / path:line coordinate is actually converted to
471
- // offset/limit (normaliseReadLineWindowArgs only converts when no mode is
472
- // set). count/hex are not text-window ops and keep their mode.
463
+ // silently drop the window. Drop the glance mode BEFORE path:line
464
+ // compatibility normalization so any path coordinate is converted to
465
+ // offset/limit. count/hex are not text-window ops and keep their mode.
473
466
  {
474
- const _win = args.offset != null || args.limit != null || args.line != null || hasLineCoordinate(args.path);
467
+ const _win = args.offset != null || args.limit != null || hasLineCoordinate(args.path);
475
468
  if (_win && (args.mode === 'head' || args.mode === 'tail' || args.mode === 'summary')) {
476
469
  args = { ...args, mode: undefined };
477
470
  }