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.
Files changed (36) hide show
  1. package/package.json +7 -2
  2. package/scripts/arg-guard-test.mjs +68 -0
  3. package/scripts/compacted-placeholder-scrub-test.mjs +77 -0
  4. package/src/app.mjs +14 -0
  5. package/src/cli.mjs +40 -4
  6. package/src/rules/shared/01-tool.md +3 -0
  7. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +3 -1
  8. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +30 -0
  9. package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +5 -0
  10. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +14 -1
  11. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
  12. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
  13. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
  14. package/src/runtime/memory/tool-defs.mjs +3 -3
  15. package/src/runtime/shared/staged-install-worker.mjs +14 -0
  16. package/src/runtime/shared/staged-update.mjs +530 -0
  17. package/src/runtime/shared/update-checker.mjs +1 -1
  18. package/src/session-runtime/lifecycle-api.mjs +9 -11
  19. package/src/session-runtime/runtime-core.mjs +61 -40
  20. package/src/standalone/agent-tool/tool-def.mjs +2 -2
  21. package/src/tui/App.jsx +2 -1
  22. package/src/tui/app/transcript-window.mjs +26 -6
  23. package/src/tui/components/Spinner.jsx +5 -2
  24. package/src/tui/components/StatusLine.jsx +9 -9
  25. package/src/tui/components/ToolExecution.jsx +34 -59
  26. package/src/tui/display-width.mjs +8 -4
  27. package/src/tui/dist/index.mjs +545 -425
  28. package/src/tui/engine/notification-plan.mjs +5 -1
  29. package/src/tui/hooks/useSharedTick.mjs +103 -0
  30. package/src/tui/index.jsx +2 -1
  31. package/src/tui/markdown/format-token.mjs +46 -8
  32. package/src/tui/markdown/render-ansi.mjs +24 -1
  33. package/src/tui/markdown/stream-fence.mjs +60 -0
  34. package/src/tui/markdown/streaming-markdown.mjs +22 -1
  35. package/src/workflows/default/WORKFLOW.md +12 -8
  36. 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.26",
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 { run } from './app.mjs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { performPendingSwap, registerLiveSession } from './runtime/shared/staged-update.mjs';
4
5
 
5
- run(process.argv.slice(2)).then((code) => {
6
- const exitCode = Number.isInteger(code) ? code : 0;
7
- process.exit(exitCode);
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: toolCallCount,
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
- return `Error: builtin arg "${field}" must be <= ${max} (got ${value})`;
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. Use only to check prior conversation or past events: resumes, prior work, earlier decisions or 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.',
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));