mixdog 0.9.26 → 0.9.28
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 +7 -2
- package/scripts/arg-guard-test.mjs +68 -0
- package/scripts/compacted-placeholder-scrub-test.mjs +77 -0
- package/src/app.mjs +14 -0
- package/src/cli.mjs +40 -4
- package/src/rules/shared/01-tool.md +3 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +30 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +14 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
- package/src/runtime/memory/tool-defs.mjs +3 -3
- package/src/runtime/shared/staged-install-worker.mjs +14 -0
- package/src/runtime/shared/staged-update.mjs +530 -0
- package/src/runtime/shared/update-checker.mjs +1 -1
- package/src/session-runtime/lifecycle-api.mjs +9 -11
- package/src/session-runtime/runtime-core.mjs +61 -40
- package/src/standalone/agent-tool/tool-def.mjs +2 -2
- package/src/tui/App.jsx +2 -1
- package/src/tui/app/transcript-window.mjs +26 -6
- package/src/tui/components/Spinner.jsx +5 -2
- package/src/tui/components/StatusLine.jsx +9 -9
- package/src/tui/components/ToolExecution.jsx +34 -59
- package/src/tui/display-width.mjs +8 -4
- package/src/tui/dist/index.mjs +545 -425
- package/src/tui/engine/notification-plan.mjs +5 -1
- package/src/tui/hooks/useSharedTick.mjs +103 -0
- package/src/tui/index.jsx +2 -1
- package/src/tui/markdown/format-token.mjs +46 -8
- package/src/tui/markdown/render-ansi.mjs +24 -1
- package/src/tui/markdown/stream-fence.mjs +60 -0
- package/src/tui/markdown/streaming-markdown.mjs +22 -1
- package/src/workflows/default/WORKFLOW.md +12 -8
- package/vendor/ink/build/display-width.js +5 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixdog",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.28",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Standalone mixdog coding-agent CLI/TUI workspace.",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"!scripts/bench/",
|
|
30
30
|
"!scripts/recall-bench-*.txt",
|
|
31
31
|
"src/",
|
|
32
|
+
"!src/tui/dev",
|
|
32
33
|
"vendor/"
|
|
33
34
|
],
|
|
34
35
|
"scripts": {
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
"smoke:freevars": "node scripts/freevar-smoke.mjs",
|
|
49
50
|
"smoke:logguard": "node scripts/log-writer-guard-smoke.mjs",
|
|
50
51
|
"test:toolcall": "node --test scripts/toolcall-args-test.mjs",
|
|
52
|
+
"test:placeholder": "node --test scripts/compacted-placeholder-scrub-test.mjs",
|
|
51
53
|
"test:providers": "node --test scripts/provider-toolcall-test.mjs",
|
|
52
54
|
"test:atomiclock": "node --test scripts/atomic-lock-tryonce-test.mjs",
|
|
53
55
|
"failures": "node scripts/tool-failures.mjs",
|
|
@@ -61,7 +63,10 @@
|
|
|
61
63
|
"bench:recall": "node scripts/recall-bench.mjs",
|
|
62
64
|
"audit:models": "node scripts/model-catalog-audit.mjs",
|
|
63
65
|
"patch:replay": "node scripts/patch-replay.mjs",
|
|
64
|
-
"build:tui": "node scripts/build-tui.mjs"
|
|
66
|
+
"build:tui": "node scripts/build-tui.mjs",
|
|
67
|
+
"release:patch": "npm run smoke && npm version patch -m \"mixdog v%s\" && git push --follow-tags origin main",
|
|
68
|
+
"release:minor": "npm run smoke && npm version minor -m \"mixdog v%s\" && git push --follow-tags origin main",
|
|
69
|
+
"release:major": "npm run smoke && npm version major -m \"mixdog v%s\" && git push --follow-tags origin main"
|
|
65
70
|
},
|
|
66
71
|
"engines": {
|
|
67
72
|
"node": ">=22.0.0"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Regression tests for validateBuiltinArgs numeric coercion + over-max clamp:
|
|
2
|
+
// - integer-shaped strings ("5") coerce to numbers instead of erroring
|
|
3
|
+
// - over-max integers clamp to the cap instead of erroring
|
|
4
|
+
// - truly non-numeric / below-min input keeps the existing error
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import { validateBuiltinArgs } from '../src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs';
|
|
8
|
+
|
|
9
|
+
test('read offset/limit as numeric strings coerce to numbers', () => {
|
|
10
|
+
const a = { path: 'x.mjs', offset: '5', limit: '40' };
|
|
11
|
+
assert.equal(validateBuiltinArgs('read', a), null);
|
|
12
|
+
assert.equal(a.offset, 5);
|
|
13
|
+
assert.equal(a.limit, 40);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('read line/context as numeric strings coerce and window', () => {
|
|
17
|
+
const a = { path: 'x.mjs', line: '100', context: '5' };
|
|
18
|
+
assert.equal(validateBuiltinArgs('read', a), null);
|
|
19
|
+
assert.equal(a.offset, 94);
|
|
20
|
+
assert.equal(a.limit, 11);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('grep head_limit/offset/-C as numeric strings coerce', () => {
|
|
24
|
+
const a = { pattern: 'x', head_limit: '3', offset: '10', '-C': '2' };
|
|
25
|
+
assert.equal(validateBuiltinArgs('grep', a), null);
|
|
26
|
+
assert.equal(a.head_limit, 3);
|
|
27
|
+
assert.equal(a.offset, 10);
|
|
28
|
+
assert.equal(a['-C'], 2);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('list/find/glob head_limit as numeric string coerces', () => {
|
|
32
|
+
const l = { path: '.', head_limit: '5' };
|
|
33
|
+
assert.equal(validateBuiltinArgs('list', l), null);
|
|
34
|
+
assert.equal(l.head_limit, 5);
|
|
35
|
+
const f = { query: 'x', head_limit: '5' };
|
|
36
|
+
assert.equal(validateBuiltinArgs('find', f), null);
|
|
37
|
+
assert.equal(f.head_limit, 5);
|
|
38
|
+
const g = { pattern: '*.mjs', head_limit: '5' };
|
|
39
|
+
assert.equal(validateBuiltinArgs('glob', g), null);
|
|
40
|
+
assert.equal(g.head_limit, 5);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('over-max integer clamps to the cap instead of erroring', () => {
|
|
44
|
+
// read n cap is 0..10000; a huge value clamps down instead of erroring.
|
|
45
|
+
const a = { path: 'x.mjs', n: 999999999 };
|
|
46
|
+
assert.equal(validateBuiltinArgs('read', a), null);
|
|
47
|
+
assert.equal(a.n, 10000);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('over-max numeric-string clamps too', () => {
|
|
51
|
+
const a = { path: 'x.mjs', n: '999999999' };
|
|
52
|
+
assert.equal(validateBuiltinArgs('read', a), null);
|
|
53
|
+
assert.equal(a.n, 10000);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('non-numeric string still errors', () => {
|
|
57
|
+
assert.match(validateBuiltinArgs('read', { path: 'x.mjs', limit: 'soon' }), /must be an integer/);
|
|
58
|
+
assert.match(validateBuiltinArgs('list', { path: '.', head_limit: 'soon' }), /finite integer/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('below-min (negative) still errors', () => {
|
|
62
|
+
assert.match(validateBuiltinArgs('read', { path: 'x.mjs', offset: -1 }), /must be >= 0/);
|
|
63
|
+
assert.match(validateBuiltinArgs('list', { path: '.', head_limit: '-2' }), /must be >= 0/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('fractional numeric string is not coerced (still errors)', () => {
|
|
67
|
+
assert.match(validateBuiltinArgs('read', { path: 'x.mjs', limit: '3.5' }), /must be an integer/);
|
|
68
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Regression test for the pre-send compacted-placeholder invariant: no
|
|
3
|
+
// provider-visible assistant toolCall may ship a `[mixdog compacted …]`
|
|
4
|
+
// placeholder body that the model could copy back as apply_patch input.
|
|
5
|
+
// scrubCompactedPlaceholderToolCalls is the single enforcement point invoked
|
|
6
|
+
// by repairTranscriptBeforeProviderSend right before provider.send.
|
|
7
|
+
import test from 'node:test';
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
import {
|
|
10
|
+
compactToolCallsForHistory,
|
|
11
|
+
scrubCompactedPlaceholderToolCalls,
|
|
12
|
+
} from '../src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs';
|
|
13
|
+
import { repairTranscriptBeforeProviderSend } from '../src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs';
|
|
14
|
+
|
|
15
|
+
// A patch body longer than the body limit compacts to a marker-alone string.
|
|
16
|
+
const BIG_PATCH = '*** Begin Patch\n' + '+x\n'.repeat(2000) + '*** End Patch';
|
|
17
|
+
|
|
18
|
+
function assistantWithCompactedPatch(id = 'call_1') {
|
|
19
|
+
const calls = [{ id, name: 'apply_patch', arguments: { patch: BIG_PATCH, base_path: '/repo' } }];
|
|
20
|
+
return { role: 'assistant', content: '', toolCalls: compactToolCallsForHistory(calls) };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test('compact leaves a placeholder patch body (precondition)', () => {
|
|
24
|
+
const msg = assistantWithCompactedPatch();
|
|
25
|
+
assert.match(msg.toolCalls[0].arguments.patch, /^\[mixdog compacted /);
|
|
26
|
+
assert.equal(msg.toolCalls[0].arguments.base_path, '/repo');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('scrub drops the placeholder patch key, keeps other args', () => {
|
|
30
|
+
const msg = assistantWithCompactedPatch();
|
|
31
|
+
scrubCompactedPlaceholderToolCalls([msg]);
|
|
32
|
+
assert.equal('patch' in msg.toolCalls[0].arguments, false);
|
|
33
|
+
assert.equal(msg.toolCalls[0].arguments.base_path, '/repo');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('scrub is recursive across nested batch body args', () => {
|
|
37
|
+
const calls = [{
|
|
38
|
+
id: 'call_2', name: 'edit',
|
|
39
|
+
arguments: { edits: [{ path: 'a.js', old_string: BIG_PATCH, new_string: 'ok' }] },
|
|
40
|
+
}];
|
|
41
|
+
const msg = { role: 'assistant', content: '', toolCalls: compactToolCallsForHistory(calls) };
|
|
42
|
+
assert.match(msg.toolCalls[0].arguments.edits[0].old_string, /^\[mixdog compacted /);
|
|
43
|
+
scrubCompactedPlaceholderToolCalls([msg]);
|
|
44
|
+
const edit = msg.toolCalls[0].arguments.edits[0];
|
|
45
|
+
assert.equal('old_string' in edit, false);
|
|
46
|
+
assert.equal(edit.path, 'a.js');
|
|
47
|
+
assert.equal(edit.new_string, 'ok');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('scrub leaves real (restored) patch bodies untouched', () => {
|
|
51
|
+
const real = '*** Begin Patch\n@@\n-a\n+b\n*** End Patch';
|
|
52
|
+
const msg = { role: 'assistant', content: '', toolCalls: [
|
|
53
|
+
{ id: 'call_3', name: 'apply_patch', arguments: { patch: real } },
|
|
54
|
+
] };
|
|
55
|
+
scrubCompactedPlaceholderToolCalls([msg]);
|
|
56
|
+
assert.equal(msg.toolCalls[0].arguments.patch, real);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('scrub ignores non-assistant / non-toolCall messages', () => {
|
|
60
|
+
const msgs = [
|
|
61
|
+
{ role: 'user', content: 'hi' },
|
|
62
|
+
{ role: 'tool', content: 'x', toolCallId: 'call_1' },
|
|
63
|
+
{ role: 'assistant', content: 'text only' },
|
|
64
|
+
];
|
|
65
|
+
assert.doesNotThrow(() => scrubCompactedPlaceholderToolCalls(msgs));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('repairTranscriptBeforeProviderSend enforces the invariant end-to-end', () => {
|
|
69
|
+
const msgs = [
|
|
70
|
+
{ role: 'user', content: 'do it' },
|
|
71
|
+
assistantWithCompactedPatch('call_9'),
|
|
72
|
+
{ role: 'tool', content: 'applied', toolCallId: 'call_9' },
|
|
73
|
+
];
|
|
74
|
+
repairTranscriptBeforeProviderSend(msgs, null);
|
|
75
|
+
const asst = msgs.find((m) => m.role === 'assistant');
|
|
76
|
+
assert.equal('patch' in asst.toolCalls[0].arguments, false);
|
|
77
|
+
});
|
package/src/app.mjs
CHANGED
|
@@ -166,6 +166,20 @@ export async function run(argv = []) {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
// Default: the canonical React/Ink TUI over the mixdog session runtime.
|
|
169
|
+
// DEV convenience (opt-in via MIXDOG_TUI_DEV): rebuild the JSX bundle from
|
|
170
|
+
// source before launch so local edits reflect without a manual build. The
|
|
171
|
+
// dev module is excluded from the published package ("files" negation) and
|
|
172
|
+
// esbuild is a devDependency, so this is a no-op fallback on any install.
|
|
173
|
+
if (process.env.MIXDOG_TUI_DEV) {
|
|
174
|
+
try {
|
|
175
|
+
const { rebuildTuiFromSource } = await import('./tui/dev/jit-rebuild.mjs');
|
|
176
|
+
await rebuildTuiFromSource();
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (process.env.MIXDOG_TUI_DEV_VERBOSE) {
|
|
179
|
+
process.stderr.write(`mixdog[tui-dev]: rebuild skipped — ${err?.message ?? err}\n`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
169
183
|
const bundle = join(__dirname, 'tui', 'dist', 'index.mjs');
|
|
170
184
|
if (!existsSync(bundle)) {
|
|
171
185
|
process.stderr.write(
|
package/src/cli.mjs
CHANGED
|
@@ -1,10 +1,46 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { performPendingSwap, registerLiveSession } from './runtime/shared/staged-update.mjs';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
const argv = process.argv.slice(2);
|
|
7
|
+
|
|
8
|
+
// Pre-import self-update swap: BEFORE any runtime module is imported, if a
|
|
9
|
+
// completed staged newer version exists and no other mixdog session is live,
|
|
10
|
+
// atomically swap the global package dir into place. Doing it here (and only
|
|
11
|
+
// here) means the process goes on to load the NEW files, and a live session
|
|
12
|
+
// never has its files overwritten mid-run. Any obstacle → false, run current.
|
|
13
|
+
let swapped = false;
|
|
14
|
+
try { swapped = performPendingSwap(); } catch { swapped = false; }
|
|
15
|
+
|
|
16
|
+
// Register this process in the live-session refcount BEFORE any runtime module
|
|
17
|
+
// loads, so a concurrently-launching mixdog's liveness check can see us and
|
|
18
|
+
// defer its own swap. Unregister is hooked on process 'exit' (and on graceful
|
|
19
|
+
// close). Harmless for the swap decision above — our own pid is excluded.
|
|
20
|
+
try { registerLiveSession(); } catch { /* advisory refcount only */ }
|
|
21
|
+
|
|
22
|
+
async function main() {
|
|
23
|
+
// If we actually swapped, re-exec a fresh node process so the new package
|
|
24
|
+
// loads with a clean module cache (no stale pre-swap modules mixed in). The
|
|
25
|
+
// env guard prevents re-swap / relaunch loops. Foreground + inherited stdio
|
|
26
|
+
// keeps the interactive TUI intact. Best-effort: if the re-exec spawn fails,
|
|
27
|
+
// fall through and run in-place.
|
|
28
|
+
if (swapped && !process.env.MIXDOG_SWAP_REEXEC) {
|
|
29
|
+
try {
|
|
30
|
+
const { spawnSync } = await import('node:child_process');
|
|
31
|
+
const r = spawnSync(process.execPath, [fileURLToPath(import.meta.url), ...argv], {
|
|
32
|
+
stdio: 'inherit',
|
|
33
|
+
env: { ...process.env, MIXDOG_SWAP_REEXEC: '1' },
|
|
34
|
+
});
|
|
35
|
+
if (!r.error) return Number.isInteger(r.status) ? r.status : 0;
|
|
36
|
+
} catch { /* fall through to in-place run */ }
|
|
37
|
+
}
|
|
38
|
+
const { run } = await import('./app.mjs');
|
|
39
|
+
return await run(argv);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
main().then((code) => {
|
|
43
|
+
process.exit(Number.isInteger(code) ? code : 0);
|
|
8
44
|
}).catch((error) => {
|
|
9
45
|
process.stderr.write(`${error?.stack || error?.message || String(error)}\n`);
|
|
10
46
|
process.exit(1);
|
|
@@ -36,3 +36,6 @@
|
|
|
36
36
|
- Don't mix `apply_patch` with shell or other state-changing calls in one
|
|
37
37
|
turn; batch independent-file patches in one turn, then verify them all in
|
|
38
38
|
ONE shell call the next.
|
|
39
|
+
- Consecutive single `shell` or `apply_patch` turns are a batching miss
|
|
40
|
+
unless output-dependent: chain shell commands with `;`/`&&` or call in
|
|
41
|
+
parallel; put all known edits in ONE patch.
|
|
@@ -349,7 +349,9 @@ export function traceAgentBatch({ sessionId, toolCallCount }) {
|
|
|
349
349
|
appendAgentTrace({
|
|
350
350
|
sessionId,
|
|
351
351
|
kind: 'batch',
|
|
352
|
-
tool_call_count
|
|
352
|
+
// trace_events has no tool_call_count column — top-level unknown
|
|
353
|
+
// fields are dropped at insert time, so carry it in payload (jsonb).
|
|
354
|
+
payload: { tool_call_count: toolCallCount },
|
|
353
355
|
});
|
|
354
356
|
}
|
|
355
357
|
|
|
@@ -151,3 +151,33 @@ export function dropCompactedBodyArgsForId(assistantMsg, callId) {
|
|
|
151
151
|
if (!tc || !tc.arguments || typeof tc.arguments !== 'object') return;
|
|
152
152
|
tc.arguments = _dropCompactedPlaceholders(tc.arguments);
|
|
153
153
|
}
|
|
154
|
+
|
|
155
|
+
// PRE-SEND INVARIANT. The per-call paths above run during tool-batch
|
|
156
|
+
// processing: a SUCCESS drops its placeholder body (dropCompactedBodyArgsForId),
|
|
157
|
+
// a FAILURE restores the full original (restoreToolCallBodyForId). But those are
|
|
158
|
+
// gated per call id and several loop paths never reach them — dedup /
|
|
159
|
+
// repeat-failure early-continue, the iteration-cap refusal stub, or a call whose
|
|
160
|
+
// id never matched. Any of those can leave a `[mixdog compacted …]` placeholder
|
|
161
|
+
// sitting in a provider-visible assistant toolCall, which the model then copies
|
|
162
|
+
// back verbatim as new apply_patch input. Enforce the invariant once, right
|
|
163
|
+
// before provider.send: sweep EVERY assistant toolCall arguments tree and drop
|
|
164
|
+
// any placeholder-valued key at any depth (batch shapes like edits[].old_string
|
|
165
|
+
// carry nested compacted bodies too).
|
|
166
|
+
//
|
|
167
|
+
// This does NOT re-inline full bodies, so it never raises history token cost. A
|
|
168
|
+
// failed call whose body was restored upstream holds real patch content (it no
|
|
169
|
+
// longer starts with the compacted marker), so the sweep leaves it untouched —
|
|
170
|
+
// the full body stays available exactly where the failed call needs it. The
|
|
171
|
+
// downstream apply_patch guard (isCompactedPlaceholderPatch) remains as a
|
|
172
|
+
// backstop for any placeholder a model synthesizes from prose.
|
|
173
|
+
export function scrubCompactedPlaceholderToolCalls(messages) {
|
|
174
|
+
if (!Array.isArray(messages)) return messages;
|
|
175
|
+
for (const msg of messages) {
|
|
176
|
+
if (!msg || msg.role !== 'assistant' || !Array.isArray(msg.toolCalls)) continue;
|
|
177
|
+
for (const tc of msg.toolCalls) {
|
|
178
|
+
if (!tc || !tc.arguments || typeof tc.arguments !== 'object') continue;
|
|
179
|
+
tc.arguments = _dropCompactedPlaceholders(tc.arguments);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return messages;
|
|
183
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// strip any trailing/orphaned tool_use|tool_result so provider.send sees a
|
|
6
6
|
// valid transcript instead of leaking the 400 to the user.
|
|
7
7
|
import { sanitizeToolPairs } from '../context-utils.mjs';
|
|
8
|
+
import { scrubCompactedPlaceholderToolCalls } from './stored-tool-args.mjs';
|
|
8
9
|
|
|
9
10
|
// Transcript pairing guard. Anthropic 400-rejects when an assistant message
|
|
10
11
|
// ends with tool_use blocks and the next message isn't tool results for
|
|
@@ -97,5 +98,9 @@ export function repairTranscriptBeforeProviderSend(messages, sessionId = null) {
|
|
|
97
98
|
messages.push(...sanitized);
|
|
98
99
|
}
|
|
99
100
|
_ensureTranscriptPairing(messages, sessionId);
|
|
101
|
+
// Pre-send invariant: no provider-visible assistant toolCall may carry a
|
|
102
|
+
// compacted `[mixdog compacted …]` placeholder body that looks like
|
|
103
|
+
// submittable patch input. Runs after pairing repair, still before send.
|
|
104
|
+
scrubCompactedPlaceholderToolCalls(messages);
|
|
100
105
|
return messages;
|
|
101
106
|
}
|
|
@@ -278,9 +278,16 @@ function checkIntInRange(a, field, min, max, opts = {}) {
|
|
|
278
278
|
if (value < min) {
|
|
279
279
|
return `Error: builtin arg "${field}" must be >= ${min} (got ${value})`;
|
|
280
280
|
}
|
|
281
|
+
// Over-max is clamped to the cap instead of erroring: an over-large
|
|
282
|
+
// integer is an unambiguous "as much as allowed" request, not a shape
|
|
283
|
+
// violation worth a retry turn (mirrors the soft-cap clamp above).
|
|
284
|
+
// Under-min still errors — a negative where >=0 is required is a real
|
|
285
|
+
// mistake, not a saturating intent.
|
|
281
286
|
if (value > max) {
|
|
282
|
-
|
|
287
|
+
a[field] = max;
|
|
288
|
+
return null;
|
|
283
289
|
}
|
|
290
|
+
a[field] = value;
|
|
284
291
|
return null;
|
|
285
292
|
}
|
|
286
293
|
|
|
@@ -670,6 +677,8 @@ function guardList(a) {
|
|
|
670
677
|
return `Error: list arg "pattern" must be string or string[] (got ${describeType(a.pattern)})`;
|
|
671
678
|
}
|
|
672
679
|
if (hasOwn(a, 'head_limit') && a.head_limit !== undefined && a.head_limit !== null) {
|
|
680
|
+
const coerced = coerceIntegerString(a.head_limit);
|
|
681
|
+
if (coerced !== null) a.head_limit = coerced;
|
|
673
682
|
if (!isFiniteInt(a.head_limit)) {
|
|
674
683
|
return `Error: list arg "head_limit" must be a finite integer (got ${describeType(a.head_limit)})`;
|
|
675
684
|
}
|
|
@@ -695,6 +704,8 @@ function guardFind(a) {
|
|
|
695
704
|
return `Error: find arg "path" must be a string (got ${describeType(a.path)})`;
|
|
696
705
|
}
|
|
697
706
|
if (hasOwn(a, 'head_limit') && a.head_limit !== undefined && a.head_limit !== null) {
|
|
707
|
+
const coerced = coerceIntegerString(a.head_limit);
|
|
708
|
+
if (coerced !== null) a.head_limit = coerced;
|
|
698
709
|
if (!isFiniteInt(a.head_limit)) {
|
|
699
710
|
return `Error: find arg "head_limit" must be a finite integer (got ${describeType(a.head_limit)})`;
|
|
700
711
|
}
|
|
@@ -734,6 +745,8 @@ function guardGlob(a) {
|
|
|
734
745
|
}
|
|
735
746
|
}
|
|
736
747
|
if (hasOwn(a, 'head_limit') && a.head_limit !== undefined && a.head_limit !== null) {
|
|
748
|
+
const coerced = coerceIntegerString(a.head_limit);
|
|
749
|
+
if (coerced !== null) a.head_limit = coerced;
|
|
737
750
|
if (!isFiniteInt(a.head_limit)) {
|
|
738
751
|
return `Error: glob arg "head_limit" must be a finite integer (got ${describeType(a.head_limit)})`;
|
|
739
752
|
}
|
|
@@ -67,7 +67,7 @@ export const BUILTIN_TOOLS = [
|
|
|
67
67
|
name: 'shell',
|
|
68
68
|
title: 'Mixdog Shell',
|
|
69
69
|
annotations: { title: 'Mixdog Shell', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, compressible: true },
|
|
70
|
-
description: `Run programs or change system state. Set shell: powershell or bash. Not for reading, listing, or searching files. ${TOOL_ASYNC_EXECUTION_CONTRACT}`,
|
|
70
|
+
description: `Run programs or change system state. Set shell: powershell or bash. Not for reading, listing, or searching files. Batch independent commands: ;/&& in one call, or parallel calls in the same turn. ${TOOL_ASYNC_EXECUTION_CONTRACT}`,
|
|
71
71
|
inputSchema: {
|
|
72
72
|
type: 'object',
|
|
73
73
|
properties: {
|
|
@@ -9,7 +9,7 @@ export const CODE_GRAPH_TOOL_DEFS = [
|
|
|
9
9
|
properties: {
|
|
10
10
|
mode: { type: 'string', enum: ['overview', 'imports', 'dependents', 'related', 'impact', 'symbols', 'find_symbol', 'symbol_search', 'search', 'references', 'callers'], description: 'Repo-local graph operation.' },
|
|
11
11
|
files: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Source file(s); array fans out per file. `file` alias too.' },
|
|
12
|
-
symbols: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Identifier(s)/keyword(s); array batches in one call. `symbol` alias too.' },
|
|
12
|
+
symbols: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Identifier(s)/keyword(s); array batches in one call. `symbol` alias too. Required for callers/callees/references/find_symbol/symbol_search.' },
|
|
13
13
|
symbol: { type: 'string', description: 'Singular alias for symbols.' },
|
|
14
14
|
file: { type: 'string', description: 'Singular alias for files.' },
|
|
15
15
|
body: { type: 'boolean', description: 'Include body.' },
|
|
@@ -26,7 +26,7 @@ export const PATCH_TOOL_DEFS = [
|
|
|
26
26
|
name: 'apply_patch',
|
|
27
27
|
title: 'Mixdog Apply Patch',
|
|
28
28
|
annotations: { title: 'Mixdog Apply Patch', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false, compressible: false, compressibleLossless: true },
|
|
29
|
-
description: 'Apply file patches. Prefer V4A envelope with exact current context.',
|
|
29
|
+
description: 'Apply file patches. Prefer V4A envelope with exact current context. Batch independent edits into ONE patch (multiple file blocks).',
|
|
30
30
|
freeformDescription: APPLY_PATCH_FREEFORM_DESCRIPTION,
|
|
31
31
|
freeform: {
|
|
32
32
|
type: 'grammar',
|
|
@@ -15,8 +15,8 @@ export const TOOL_DEFS = [
|
|
|
15
15
|
action: { type: 'string', enum: ['core','status'], description: 'Operation.' },
|
|
16
16
|
op: { type: 'string', enum: ['add','edit','delete','list','candidates','promote','dismiss'], description: 'Mutation op. candidates/promote/dismiss drive core-memory proposal approval.' },
|
|
17
17
|
id: { type: 'number', description: 'Exact memory id.' },
|
|
18
|
-
element: { type: 'string', description: 'Memory key/title.' },
|
|
19
|
-
summary: { type: 'string', description: 'Memory content.' },
|
|
18
|
+
element: { type: 'string', description: 'Memory key/title. Max 40 chars.' },
|
|
19
|
+
summary: { type: 'string', description: 'Memory content: 1 fact, 1-2 sentences, max 100 chars.' },
|
|
20
20
|
category: { type: 'string', enum: ['rule','constraint','decision','fact','goal','preference','task','issue'], description: 'Category.' },
|
|
21
21
|
status: { type: 'string', enum: ['pending','active','archived'], description: 'Lifecycle status.' },
|
|
22
22
|
limit: { type: 'number', description: 'Max rows/items.' },
|
|
@@ -30,7 +30,7 @@ export const TOOL_DEFS = [
|
|
|
30
30
|
name: 'recall',
|
|
31
31
|
title: 'Recall',
|
|
32
32
|
annotations: { title: 'Recall', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
33
|
-
description: 'Retrieve stored memory/session history.
|
|
33
|
+
description: 'Retrieve stored memory/session history. Call when a task ties to prior work — resumes, continuations, or references to earlier decisions/messages. Do not call as a reflexive pre-step, to verify a just-made decision, or before storing memory. Query is topic/semantic search, not regex. Patterns: period:"last" for previous conversation; sessionId without query for current session; period:"3h"/"30m"/"24h" for recent; YYYY-MM-DD, date ranges, or HH:MM~HH:MM for time windows; id for exact follow-up.',
|
|
34
34
|
inputSchema: {
|
|
35
35
|
type: 'object',
|
|
36
36
|
properties: {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* staged-install-worker.mjs — detached background entry for staging a mixdog
|
|
4
|
+
* self-update. Spawned (shell-less, hidden) by spawnStagedInstall() so the
|
|
5
|
+
* npm install + relocate + verify + marker-write survive the launching session
|
|
6
|
+
* quitting mid-install. Does its work and exits; the swap happens on a later
|
|
7
|
+
* clean launch via performPendingSwap().
|
|
8
|
+
*/
|
|
9
|
+
import { runStagedInstall } from './staged-update.mjs';
|
|
10
|
+
|
|
11
|
+
const version = process.argv[2] || process.env.MIXDOG_STAGE_VERSION || '';
|
|
12
|
+
runStagedInstall(version)
|
|
13
|
+
.then((r) => process.exit(r?.ok || r?.alreadyStaged || r?.inProgress ? 0 : 1))
|
|
14
|
+
.catch(() => process.exit(1));
|