mixdog 0.9.9 → 0.9.11
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.
- package/package.json +1 -1
- package/scripts/agent-tag-reuse-smoke.mjs +12 -8
- package/scripts/bench/cache-probe-tasks.json +8 -0
- package/scripts/bench/lead-review-tasks-r3.json +1 -1
- package/scripts/bench/r4-mixed-tasks.json +1 -1
- package/scripts/bench/review-tasks.json +1 -1
- package/scripts/build-runtime-windows.ps1 +242 -242
- package/scripts/recall-usecase-cases.json +1 -1
- package/scripts/smoke-runtime-negative.ps1 +106 -106
- package/scripts/tool-smoke.mjs +109 -0
- package/src/defaults/mixdog-config.template.json +1 -0
- package/src/mixdog-session-runtime.mjs +8 -0
- package/src/rules/agent/30-explorer.md +18 -20
- package/src/rules/lead/02-channels.md +3 -3
- package/src/runtime/agent/orchestrator/config.mjs +30 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +160 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +25 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +24 -9
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +18 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +79 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/manager.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +1 -22
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +26 -3
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +29 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +17 -16
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +30 -4
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +333 -14
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +129 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +58 -18
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +56 -83
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +26 -56
- package/src/runtime/channels/lib/webhook/deliveries.mjs +4 -1
- package/src/session-runtime/settings-api.mjs +13 -0
- package/src/session-runtime/tool-catalog.mjs +34 -7
- package/src/standalone/agent-tool/render.mjs +2 -0
- package/src/standalone/agent-tool.mjs +28 -6
- package/src/standalone/explore-tool.mjs +2 -2
- package/src/standalone/memory-runtime-proxy.mjs +85 -21
- package/src/tui/App.jsx +20 -1
- package/src/tui/app/extension-pickers.mjs +6 -3
- package/src/tui/dist/index.mjs +28 -4
- package/src/tui/engine.mjs +2 -0
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { readdirSync } from 'fs';
|
|
2
2
|
import { basename, relative } from 'path';
|
|
3
3
|
import {
|
|
4
|
+
coerceReadFamilyPathArg,
|
|
4
5
|
extractGlobBaseDirectory,
|
|
5
6
|
hasGlobMagic,
|
|
6
7
|
normalizeInputPath,
|
|
7
8
|
normalizeOutputPath,
|
|
8
9
|
resolveAgainstCwd,
|
|
9
10
|
} from './path-utils.mjs';
|
|
11
|
+
import { tryReadFamilyEnoentRedirect } from './search-path-diagnostics.mjs';
|
|
10
12
|
import { normalizeErrorMessage } from './path-diagnostics.mjs';
|
|
11
13
|
import { isUncPath, isWindowsDevicePath, hasUnsafeWin32Component } from './device-paths.mjs';
|
|
12
14
|
import {
|
|
@@ -37,6 +39,19 @@ const LIST_WALK_TIMEOUT_MS = 20_000;
|
|
|
37
39
|
const LIST_ABSOLUTE_CAP = 50_000;
|
|
38
40
|
|
|
39
41
|
/** undefined / invalid / negative → defaultCap; 0 = no page cap (absolute caps still apply). */
|
|
42
|
+
async function readFamilyPathEnoentOrError(workDir, fullPath, inputPath, args, options, err, rerunTool) {
|
|
43
|
+
const redirected = await tryReadFamilyEnoentRedirect({
|
|
44
|
+
workDir,
|
|
45
|
+
resolvedPath: fullPath,
|
|
46
|
+
requestedPath: inputPath,
|
|
47
|
+
errCode: err?.code,
|
|
48
|
+
options,
|
|
49
|
+
rerun: (target, opts) => rerunTool({ ...args, path: target }, workDir, opts),
|
|
50
|
+
});
|
|
51
|
+
if (redirected) return redirected;
|
|
52
|
+
return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
40
55
|
function normalizeListHeadLimit(raw, defaultCap) {
|
|
41
56
|
if (raw === undefined || raw === null || raw === '') return defaultCap;
|
|
42
57
|
const n = Number(raw);
|
|
@@ -60,6 +75,7 @@ function listGuardPath(p) {
|
|
|
60
75
|
}
|
|
61
76
|
|
|
62
77
|
export async function executeListTool(args, workDir, options = {}) {
|
|
78
|
+
args.path = coerceReadFamilyPathArg(args.path, workDir);
|
|
63
79
|
if (Array.isArray(args.path)) {
|
|
64
80
|
const list = [...new Set(args.path.map((p) => (typeof p === 'string' ? p.trim() : '')).filter((p) => p.length > 0))];
|
|
65
81
|
const capped = list.length > 10;
|
|
@@ -120,7 +136,9 @@ export async function executeListTool(args, workDir, options = {}) {
|
|
|
120
136
|
catch (err) { return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`; }
|
|
121
137
|
let st;
|
|
122
138
|
try { st = getCachedReadOnlyStat(fullPath); }
|
|
123
|
-
catch (err) {
|
|
139
|
+
catch (err) {
|
|
140
|
+
return await readFamilyPathEnoentOrError(workDir, fullPath, inputPath, args, options, err, executeListTool);
|
|
141
|
+
}
|
|
124
142
|
if (!st.isDirectory()) return `Error: not a directory — ${normalizeOutputPath(fullPath)}`;
|
|
125
143
|
|
|
126
144
|
const rows = [];
|
|
@@ -251,7 +269,9 @@ export async function executeTreeTool(args, workDir, options = {}) {
|
|
|
251
269
|
catch (err) { return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`; }
|
|
252
270
|
let st;
|
|
253
271
|
try { st = getCachedReadOnlyStat(fullPath); }
|
|
254
|
-
catch (err) {
|
|
272
|
+
catch (err) {
|
|
273
|
+
return await readFamilyPathEnoentOrError(workDir, fullPath, inputPath, args, options, err, executeListTool);
|
|
274
|
+
}
|
|
255
275
|
if (!st.isDirectory()) return `Error: not a directory — ${normalizeOutputPath(fullPath)}`;
|
|
256
276
|
const lines = [`${normalizeOutputPath(fullPath)}/`];
|
|
257
277
|
const prefixStack = [''];
|
|
@@ -407,6 +427,7 @@ export async function executeFuzzyFindTool(args, workDir, options = {}) {
|
|
|
407
427
|
}
|
|
408
428
|
|
|
409
429
|
export async function executeFindFilesTool(args, workDir, options = {}) {
|
|
430
|
+
args.path = coerceReadFamilyPathArg(args.path, workDir);
|
|
410
431
|
args.path = normalizeInputPath(args.path);
|
|
411
432
|
let inputPath = args.path || '.';
|
|
412
433
|
let namePattern = typeof args.name === 'string' ? args.name : null;
|
|
@@ -511,7 +532,9 @@ export async function executeFindFilesTool(args, workDir, options = {}) {
|
|
|
511
532
|
catch (err) { return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`; }
|
|
512
533
|
let rootStat;
|
|
513
534
|
try { rootStat = getCachedReadOnlyStat(fullPath); }
|
|
514
|
-
catch (err) {
|
|
535
|
+
catch (err) {
|
|
536
|
+
return await readFamilyPathEnoentOrError(workDir, fullPath, inputPath, args, options, err, executeFindFilesTool);
|
|
537
|
+
}
|
|
515
538
|
if (!rootStat.isDirectory()) return `Error: not a directory — ${normalizeOutputPath(fullPath)}`;
|
|
516
539
|
|
|
517
540
|
const matches = [];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { homedir } from 'os';
|
|
2
2
|
import { isAbsolute, relative, resolve } from 'path';
|
|
3
|
-
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { realpathSync, statSync } from 'node:fs';
|
|
4
4
|
import { isWSL } from '../../../../shared/wsl.mjs';
|
|
5
5
|
|
|
6
6
|
// Restore the on-disk casing of a path (win32 only). rg relativizes candidate
|
|
@@ -237,3 +237,31 @@ export function coerceShapeFlex(value) {
|
|
|
237
237
|
}
|
|
238
238
|
return value;
|
|
239
239
|
}
|
|
240
|
+
|
|
241
|
+
// JSON-stringified path arrays (path:"[]") and empty arrays mean "search cwd".
|
|
242
|
+
// Bracket-shaped strings that exist on disk (e.g. a literal `[x]` directory) are
|
|
243
|
+
// left untouched — JSON reinterpretation only runs after a stat miss.
|
|
244
|
+
export function coerceReadFamilyPathArg(path, workDir = null) {
|
|
245
|
+
if (path === undefined || path === null || path === '') return path;
|
|
246
|
+
if (typeof path === 'string' && typeof workDir === 'string' && workDir) {
|
|
247
|
+
const trimmed = path.trim();
|
|
248
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
249
|
+
try {
|
|
250
|
+
const resolved = resolveAgainstCwd(normalizeInputPath(trimmed), workDir);
|
|
251
|
+
statSync(resolved);
|
|
252
|
+
return path;
|
|
253
|
+
} catch { /* not a literal bracket path — allow JSON coercion */ }
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const coerced = coerceShapeFlex(path);
|
|
257
|
+
if (!Array.isArray(coerced)) return coerced;
|
|
258
|
+
const list = coerced
|
|
259
|
+
// Region objects ({path,offset,limit}) must pass through untouched —
|
|
260
|
+
// read's batch dispatcher consumes them. Only strings are trimmed.
|
|
261
|
+
.map((p) => (typeof p === 'string' ? p.trim() : p))
|
|
262
|
+
.filter((p) => (typeof p === 'string' ? p.length > 0 : (p && typeof p === 'object')));
|
|
263
|
+
if (list.length === 0) return '.';
|
|
264
|
+
// Collapse to scalar only for a lone string; a lone region object must
|
|
265
|
+
// stay an array so read's object-batch dispatcher handles it.
|
|
266
|
+
return list.length === 1 && typeof list[0] === 'string' ? list[0] : list;
|
|
267
|
+
}
|
|
@@ -3,7 +3,7 @@ import * as fsPromises from 'fs/promises';
|
|
|
3
3
|
import { readFile } from 'fs/promises';
|
|
4
4
|
import { extname } from 'path';
|
|
5
5
|
import { normalizeInputPath } from './path-utils.mjs';
|
|
6
|
-
import {
|
|
6
|
+
import { buildNotFoundHint, tryReadFamilyEnoentRedirect } from './search-path-diagnostics.mjs';
|
|
7
7
|
import { getReadSnapshot } from './read-snapshot-runtime.mjs';
|
|
8
8
|
import { snapshotCoversFullFile, statMatchesSnapshot } from './snapshot-helpers.mjs';
|
|
9
9
|
import { formatBinaryReadPreview } from './binary-file.mjs';
|
|
@@ -202,24 +202,25 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
|
|
|
202
202
|
}
|
|
203
203
|
if (!st) {
|
|
204
204
|
const err = _statErr;
|
|
205
|
+
const redirected = await tryReadFamilyEnoentRedirect({
|
|
206
|
+
workDir,
|
|
207
|
+
resolvedPath: fullPath,
|
|
208
|
+
requestedPath: filePath,
|
|
209
|
+
errCode: err?.code,
|
|
210
|
+
options,
|
|
211
|
+
rerun: (target, opts) => executeSingleReadTool(
|
|
212
|
+
{ ...args, path: target },
|
|
213
|
+
workDir,
|
|
214
|
+
readStateScope,
|
|
215
|
+
opts,
|
|
216
|
+
helpers,
|
|
217
|
+
),
|
|
218
|
+
});
|
|
219
|
+
if (redirected) return redirected;
|
|
205
220
|
const similar = findSimilarFile(fullPath);
|
|
206
221
|
let hint = similar ? ` Did you mean "${normalizeOutputPath(similar)}"?` : '';
|
|
207
|
-
// Right-name / wrong-directory miss: findSimilarFile only checks the
|
|
208
|
-
// same dir. Locate the basename elsewhere in the tree and name the real
|
|
209
|
-
// path(s) directly — the route a model would otherwise reconstruct with
|
|
210
|
-
// a grep/glob storm.
|
|
211
222
|
if (!similar) {
|
|
212
|
-
|
|
213
|
-
// stats directly (finds vendor/ paths the basename BFS skips).
|
|
214
|
-
const suffixHit = findBySuffixStrip(workDir, fullPath);
|
|
215
|
-
if (suffixHit) {
|
|
216
|
-
hint = ` Not found at this path; the same filename exists at: "${normalizeOutputPath(suffixHit)}". Read that path directly.`;
|
|
217
|
-
} else {
|
|
218
|
-
const elsewhere = findFileByBasename(workDir, fullPath);
|
|
219
|
-
if (elsewhere.length) {
|
|
220
|
-
hint = ` Not found at this path; the same filename exists at: ${elsewhere.map((p) => `"${normalizeOutputPath(p)}"`).join(', ')}. Read that path directly.`;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
+
hint = buildNotFoundHint(workDir, fullPath, 'Read', err?.code);
|
|
223
224
|
}
|
|
224
225
|
const _rawMsg = err instanceof Error ? err.message : String(err);
|
|
225
226
|
const _safeMsg = normalizeErrorMessage(_rawMsg, workDir);
|
|
@@ -5,6 +5,7 @@ import { readEntryCoalescedDiskWindow } from './read-batch.mjs';
|
|
|
5
5
|
import { readPathStringGuardError } from './read-open.mjs';
|
|
6
6
|
import { parseReadLineNumberArg } from './read-args.mjs';
|
|
7
7
|
import { assertPathsReachable } from './fs-reachability.mjs';
|
|
8
|
+
import { coerceReadFamilyPathArg } from './path-utils.mjs';
|
|
8
9
|
|
|
9
10
|
function hasLineCoordinate(path) {
|
|
10
11
|
return typeof path === 'string' && /(?:#L\d+|:\d+(?:-\d+)?(?::|$))/i.test(path);
|
|
@@ -109,7 +110,7 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
|
|
|
109
110
|
args.offset = Math.trunc(ccOffset) - 1;
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
|
-
args.path =
|
|
113
|
+
args.path = coerceReadFamilyPathArg(args.path, workDir);
|
|
113
114
|
// Reachability preflight up front (all shapes) — before readPathStringGuardError /
|
|
114
115
|
// image stat, all of which can touch sync FS.
|
|
115
116
|
{
|
|
@@ -30,6 +30,7 @@ export function buildGrepCacheKey(parts) {
|
|
|
30
30
|
fileType,
|
|
31
31
|
onlyMatching,
|
|
32
32
|
pcre2,
|
|
33
|
+
withFilename,
|
|
33
34
|
} = parts;
|
|
34
35
|
return [
|
|
35
36
|
'grep',
|
|
@@ -48,6 +49,7 @@ export function buildGrepCacheKey(parts) {
|
|
|
48
49
|
onlyMatching ? 'o1' : 'o0',
|
|
49
50
|
Array.isArray(fileType) ? fileType.join('\x01') : (fileType || ''),
|
|
50
51
|
pcre2 ? 'p1' : 'p0',
|
|
52
|
+
withFilename ? 'H1' : 'H0',
|
|
51
53
|
].join('|');
|
|
52
54
|
}
|
|
53
55
|
|
|
@@ -67,6 +69,7 @@ export function buildGrepRgArgs(parts) {
|
|
|
67
69
|
onlyMatching,
|
|
68
70
|
fixedStrings = false,
|
|
69
71
|
pcre2 = false,
|
|
72
|
+
withFilename = false,
|
|
70
73
|
} = parts;
|
|
71
74
|
// `--hidden`: search dotfiles/dot-dirs (.github, .mixdog) that
|
|
72
75
|
// rg skips by default. The DEFAULT_IGNORE_GLOBS below still exclude .git and
|
|
@@ -78,6 +81,7 @@ export function buildGrepRgArgs(parts) {
|
|
|
78
81
|
rgArgs.push('--count');
|
|
79
82
|
} else {
|
|
80
83
|
rgArgs.push('--no-heading');
|
|
84
|
+
if (withFilename) rgArgs.push('-H');
|
|
81
85
|
if (showLineNumbers) rgArgs.push('--line-number');
|
|
82
86
|
if (beforeN !== null) rgArgs.push('-B', String(beforeN));
|
|
83
87
|
if (afterN !== null) rgArgs.push('-A', String(afterN));
|
|
@@ -64,12 +64,38 @@ export function stripEmbeddedPathQuotes(p) {
|
|
|
64
64
|
// not-found codes: EACCES/EPERM etc. keep their real failure semantics and
|
|
65
65
|
// must not trigger same-basename guidance or the BFS scan.
|
|
66
66
|
const NOT_FOUND_CODES = new Set(['ENOENT', 'ENOTDIR']);
|
|
67
|
+
export function resolveUniqueEnoentRedirect(workDir, missingPath, errCode = 'ENOENT') {
|
|
68
|
+
if (!NOT_FOUND_CODES.has(String(errCode || 'ENOENT'))) return null;
|
|
69
|
+
const suffixHit = findBySuffixStrip(workDir, missingPath);
|
|
70
|
+
if (suffixHit) return suffixHit;
|
|
71
|
+
const elsewhere = findFileByBasename(workDir, missingPath);
|
|
72
|
+
if (elsewhere.length === 1) return elsewhere[0];
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function redirectedFromPrefix(requestedPath) {
|
|
77
|
+
return `[redirected from ${normalizeOutputPath(requestedPath)}]\n`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function tryReadFamilyEnoentRedirect({
|
|
81
|
+
workDir,
|
|
82
|
+
resolvedPath,
|
|
83
|
+
requestedPath,
|
|
84
|
+
errCode,
|
|
85
|
+
options,
|
|
86
|
+
rerun,
|
|
87
|
+
}) {
|
|
88
|
+
if (options?._enoentRedirectFrom) return null;
|
|
89
|
+
const target = resolveUniqueEnoentRedirect(workDir, resolvedPath, errCode);
|
|
90
|
+
if (!target) return null;
|
|
91
|
+
const body = await rerun(target, { ...options, _enoentRedirectFrom: resolvedPath });
|
|
92
|
+
const shown = requestedPath ?? resolvedPath;
|
|
93
|
+
return redirectedFromPrefix(shown) + body;
|
|
94
|
+
}
|
|
95
|
+
|
|
67
96
|
export function buildNotFoundHint(workDir, missingPath, actionVerb, errCode = 'ENOENT') {
|
|
68
97
|
if (!NOT_FOUND_CODES.has(String(errCode || 'ENOENT'))) return '';
|
|
69
|
-
|
|
70
|
-
if (suffixHit) {
|
|
71
|
-
return ` Not found at this path; the same filename exists at: "${normalizeOutputPath(suffixHit)}". ${actionVerb} that path directly.`;
|
|
72
|
-
}
|
|
98
|
+
if (resolveUniqueEnoentRedirect(workDir, missingPath, errCode)) return '';
|
|
73
99
|
const elsewhere = findFileByBasename(workDir, missingPath);
|
|
74
100
|
if (elsewhere.length) {
|
|
75
101
|
return ` Not found at this path; the same filename exists at: ${elsewhere.map((p) => `"${normalizeOutputPath(p)}"`).join(', ')}. ${actionVerb} that path directly.`;
|