mixdog 0.7.11 → 0.7.13

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 (67) hide show
  1. package/.claude-plugin/marketplace.json +5 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +28 -74
  4. package/README.md +193 -249
  5. package/bin/statusline-launcher.mjs +5 -1
  6. package/bin/statusline-lib.mjs +14 -6
  7. package/bin/statusline.mjs +14 -6
  8. package/bun.lock +128 -3
  9. package/defaults/hidden-roles.json +3 -0
  10. package/defaults/user-workflow.json +1 -2
  11. package/defaults/user-workflow.md +5 -1
  12. package/hooks/lib/settings-loader.cjs +4 -3
  13. package/hooks/pre-tool-subagent.cjs +7 -2
  14. package/hooks/session-start.cjs +52 -24
  15. package/lib/mixdog-debug.cjs +163 -0
  16. package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
  17. package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
  18. package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
  19. package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
  20. package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
  21. package/package.json +9 -2
  22. package/scripts/builtin-utils-smoke.mjs +14 -8
  23. package/scripts/bump.mjs +80 -0
  24. package/scripts/doctor.mjs +8 -3
  25. package/scripts/ensure-deps.mjs +2 -2
  26. package/scripts/mutation-io-smoke.mjs +17 -1
  27. package/scripts/permission-eval-smoke.mjs +18 -1
  28. package/scripts/run-mcp.mjs +65 -9
  29. package/scripts/statusline-launcher-smoke.mjs +2 -2
  30. package/scripts/webhook-selfheal-smoke.mjs +1 -3
  31. package/server-main.mjs +57 -3
  32. package/setup/install.mjs +574 -574
  33. package/setup/launch-core.mjs +0 -1
  34. package/setup/setup-server.mjs +90 -35
  35. package/setup/setup.html +44 -11
  36. package/skills/setup/SKILL.md +12 -2
  37. package/src/agent/index.mjs +1 -1
  38. package/src/agent/orchestrator/config.mjs +58 -6
  39. package/src/agent/orchestrator/providers/model-catalog.mjs +1 -1
  40. package/src/agent/orchestrator/providers/openai-oauth.mjs +9 -2
  41. package/src/agent/orchestrator/providers/openai-ws.mjs +23 -0
  42. package/src/agent/orchestrator/session/loop.mjs +3 -3
  43. package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +6 -2
  44. package/src/agent/orchestrator/tools/bash-session.mjs +1 -0
  45. package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
  46. package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +29 -6
  47. package/src/agent/orchestrator/tools/builtin/list-tool.mjs +8 -4
  48. package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +29 -8
  49. package/src/agent/orchestrator/tools/builtin.mjs +5 -2
  50. package/src/agent/orchestrator/tools/cwd-tool.mjs +17 -17
  51. package/src/agent/orchestrator/tools/graph-manifest.json +11 -11
  52. package/src/agent/orchestrator/tools/patch-manifest.json +11 -11
  53. package/src/agent/tool-defs.mjs +1 -1
  54. package/src/channels/index.mjs +39 -9
  55. package/src/channels/lib/event-queue.mjs +24 -1
  56. package/src/channels/lib/hook-pipe-server.mjs +21 -8
  57. package/src/channels/lib/webhook.mjs +159 -20
  58. package/src/memory/index.mjs +5 -1
  59. package/src/memory/lib/core-memory-store.mjs +1 -1
  60. package/src/memory/lib/memory-cycle1.mjs +8 -4
  61. package/src/memory/lib/memory-cycle2.mjs +1 -1
  62. package/src/memory/lib/memory-cycle3.mjs +1 -1
  63. package/src/memory/lib/memory-recall-store.mjs +27 -10
  64. package/src/search/lib/backends/openai-oauth.mjs +6 -2
  65. package/src/search/lib/cache.mjs +55 -7
  66. package/tools.json +2 -2
  67. package/scripts/test-config-rmw-restore.mjs +0 -122
@@ -1,4 +1,4 @@
1
- import { readdirSync } from 'fs';
1
+ import { readdir } from 'fs/promises';
2
2
  import { join } from 'path';
3
3
 
4
4
  // Glob-to-RegExp compiler for name filters used by find_files and the
@@ -125,21 +125,34 @@ export const NOISE_DIR_NAMES = new Set([
125
125
  '.gradle', 'coverage',
126
126
  ]);
127
127
 
128
+ export const MAX_WALK_ENTRIES = 200_000;
129
+
128
130
  // Unified directory walk used by list / tree / find_files. The visitor
129
131
  // callback owns the "should I record this entry?" decision; returning
130
132
  // literal false aborts the whole walk.
131
133
  // `onWarn(dir, err)` (optional) is invoked for any readdir failure so
132
134
  // callers can surface skipped paths instead of silently dropping them.
133
- export function walkDir(root, { hidden = false, maxDepth = Infinity, visit, sort, excludeDirNames, onWarn } = {}) {
135
+ export async function walkDir(root, { hidden = false, maxDepth = Infinity, visit, sort, excludeDirNames, onWarn, maxEntries = MAX_WALK_ENTRIES, signal } = {}) {
134
136
  // Windows filesystems are case-insensitive — match exclusion names the
135
137
  // same way so e.g. Node_Modules is pruned like node_modules.
136
138
  const _exclCI = process.platform === 'win32' && excludeDirNames && excludeDirNames.size > 0
137
139
  ? new Set([...excludeDirNames].map((n) => n.toLowerCase()))
138
140
  : null;
139
- const _walk = (dir, depth) => {
141
+ let truncated = false;
142
+ const cap = maxEntries;
143
+ let entriesVisited = 0;
144
+ const _walk = async (dir, depth) => {
145
+ if (signal?.aborted) {
146
+ truncated = true;
147
+ return false;
148
+ }
149
+ if (entriesVisited >= cap) {
150
+ truncated = true;
151
+ return false;
152
+ }
140
153
  if (depth > maxDepth) return true;
141
154
  let entries;
142
- try { entries = readdirSync(dir, { withFileTypes: true }); }
155
+ try { entries = await readdir(dir, { withFileTypes: true }); }
143
156
  catch (err) {
144
157
  if (typeof onWarn === 'function') {
145
158
  try { onWarn(dir, err); } catch { /* warning sink must not abort */ }
@@ -155,16 +168,26 @@ export function walkDir(root, { hidden = false, maxDepth = Infinity, visit, sort
155
168
  if (sort) entries.sort(sort);
156
169
  const total = entries.length;
157
170
  for (let i = 0; i < total; i++) {
171
+ if (signal?.aborted) {
172
+ truncated = true;
173
+ return false;
174
+ }
175
+ if (entriesVisited >= cap) {
176
+ truncated = true;
177
+ return false;
178
+ }
179
+ entriesVisited += 1;
158
180
  const ent = entries[i];
159
181
  const entPath = join(dir, ent.name);
160
182
  const ctx = { depth, index: i, total, isLast: i === total - 1 };
161
183
  const cont = visit(ent, entPath, ctx);
162
184
  if (cont === false) return false;
163
185
  if (ent.isDirectory()) {
164
- if (_walk(entPath, depth + 1) === false) return false;
186
+ if ((await _walk(entPath, depth + 1)) === false) return false;
165
187
  }
166
188
  }
167
189
  return true;
168
190
  };
169
- _walk(root, 1);
191
+ await _walk(root, 1);
192
+ return { truncated, entriesVisited };
170
193
  }
@@ -111,10 +111,11 @@ export async function executeListTool(args, workDir, options = {}) {
111
111
  // never hit either bound, so normal behavior is unchanged.
112
112
  let truncatedByCap = false;
113
113
  const walkDeadline = Date.now() + LIST_WALK_TIMEOUT_MS;
114
- walkDir(fullPath, {
114
+ await walkDir(fullPath, {
115
115
  hidden,
116
116
  maxDepth: depth,
117
117
  excludeDirNames: includeNoise ? null : NOISE_DIR_NAMES,
118
+ signal: options.signal,
118
119
  visit: (ent, entPath) => {
119
120
  if (Date.now() > walkDeadline) { truncatedByCap = true; return false; }
120
121
  const isDir = ent.isDirectory();
@@ -235,10 +236,11 @@ export async function executeTreeTool(args, workDir, options = {}) {
235
236
  const lines = [`${normalizeOutputPath(fullPath)}/`];
236
237
  const prefixStack = [''];
237
238
  const TREE_BRANCH_LINE_CAP = 500;
238
- walkDir(fullPath, {
239
+ await walkDir(fullPath, {
239
240
  hidden,
240
241
  maxDepth: depth,
241
242
  excludeDirNames: includeNoise ? null : NOISE_DIR_NAMES,
243
+ signal: options.signal,
242
244
  sort: (a, b) => {
243
245
  const ad = a.isDirectory(), bd = b.isDirectory();
244
246
  if (ad !== bd) return ad ? -1 : 1;
@@ -500,10 +502,11 @@ export async function executeFindFilesTool(args, workDir, options = {}) {
500
502
  if (!handledByRgFiles && useBatchedStat) {
501
503
  const candidates = [];
502
504
  const walkDeadline1 = Date.now() + FIND_WALK_TIMEOUT_MS;
503
- walkDir(fullPath, {
505
+ await walkDir(fullPath, {
504
506
  hidden,
505
507
  maxDepth: depth ?? Infinity,
506
508
  excludeDirNames: includeNoise ? null : NOISE_DIR_NAMES,
509
+ signal: options.signal,
507
510
  visit: (ent, entPath) => {
508
511
  if (Date.now() > walkDeadline1) { truncatedByCap = true; return false; }
509
512
  const isDir = ent.isDirectory();
@@ -532,10 +535,11 @@ export async function executeFindFilesTool(args, workDir, options = {}) {
532
535
  const effectiveTypeFilter = sizeFiltered && typeFilter === 'any' ? 'file' : typeFilter;
533
536
  const candidates = [];
534
537
  const walkDeadline2 = Date.now() + FIND_WALK_TIMEOUT_MS;
535
- walkDir(fullPath, {
538
+ await walkDir(fullPath, {
536
539
  hidden,
537
540
  maxDepth: depth ?? Infinity,
538
541
  excludeDirNames: includeNoise ? null : NOISE_DIR_NAMES,
542
+ signal: options.signal,
539
543
  visit: (ent, entPath) => {
540
544
  if (Date.now() > walkDeadline2) { truncatedByCap = true; return false; }
541
545
  const isDir = ent.isDirectory();
@@ -19,21 +19,35 @@ export function nativeEditMode() {
19
19
  return String(process.env.MIXDOG_EDIT_NATIVE || 'auto').toLowerCase();
20
20
  }
21
21
 
22
- export function nativeEditBinPath() {
22
+ function nativeEditBinCandidate() {
23
23
  const override = process.env.MIXDOG_EDIT_NATIVE_BIN || process.env.MIXDOG_PATCH_NATIVE_BIN;
24
- if (override) return override;
25
- if (existsSync(NATIVE_EDIT_DEFAULT_BIN)) return NATIVE_EDIT_DEFAULT_BIN;
26
- return findCachedPatchBinary(getPluginData()) || NATIVE_EDIT_DEFAULT_BIN;
24
+ if (override) return { path: override, kind: 'override' };
25
+ if (existsSync(NATIVE_EDIT_DEFAULT_BIN)) return { path: NATIVE_EDIT_DEFAULT_BIN, kind: 'local' };
26
+ const cached = findCachedPatchBinary(getPluginData());
27
+ if (cached) return { path: cached, kind: 'cached' };
28
+ return { path: NATIVE_EDIT_DEFAULT_BIN, kind: 'missing' };
29
+ }
30
+
31
+ export function nativeEditBinPath() {
32
+ return nativeEditBinCandidate().path;
27
33
  }
28
34
 
29
35
  export function nativeEditShouldAttempt({ editSnapshot, oldStr, newStr, preloadedContent, preloadedRawBuf }) {
30
36
  const mode = nativeEditMode();
31
37
  if (/^(0|false|no|off|js|legacy)$/i.test(mode)) return false;
32
- if (!existsSync(nativeEditBinPath())) return false;
38
+ const forcedNative = /^(1|true|yes|on|native)$/i.test(mode);
39
+ const candidate = nativeEditBinCandidate();
40
+ if (!existsSync(candidate.path)) return false;
41
+ // Cached release prebuilds are guaranteed valid for apply_patch, but older
42
+ // manifests (currently v0.6.5 in clean CI) predate the EDIT server protocol.
43
+ // In auto mode, native edit is only an acceleration, so require either a
44
+ // local cargo build or an explicit override. If a user forces native mode,
45
+ // still try the cached binary and surface any protocol failure.
46
+ if (candidate.kind === 'cached' && !forcedNative) return false;
33
47
  if (!snapshotCoversFullFile(editSnapshot)) return false;
34
48
  if (preloadedContent !== null || preloadedRawBuf !== null) return false;
35
49
  if (typeof oldStr !== 'string' || oldStr.length === 0 || typeof newStr !== 'string') return false;
36
- if (/^(1|true|yes|on|native)$/i.test(mode)) return true;
50
+ if (forcedNative) return true;
37
51
  // auto: the persistent server removed per-call spawn cost, so route edits to
38
52
  // native edit2 by default (B3). Same-size edits keep the JS in-place partial
39
53
  // write, which rewrites bytes in place instead of the whole file.
@@ -44,6 +58,7 @@ export function nativeEditShouldAttempt({ editSnapshot, oldStr, newStr, preloade
44
58
  }
45
59
 
46
60
  export async function runNativeExactEdit({ fullPath, oldStr, newStr, replaceAll, signal = null }) {
61
+ const forcedNative = /^(1|true|yes|on|native)$/i.test(nativeEditMode());
47
62
  if (signal?.aborted) {
48
63
  return { ok: false, fallback: false, error: signal.reason?.message || signal.reason || 'native edit aborted' };
49
64
  }
@@ -82,8 +97,14 @@ export async function runNativeExactEdit({ fullPath, oldStr, newStr, replaceAll,
82
97
  }
83
98
  const msg = String(err?.message || err);
84
99
  // Tier misses and not-found map to a JS fallback; transport/spawn errors
85
- // also fall back so a server hiccup never blocks an edit.
86
- const fallback = /old_string (?:not found|found \d+ times)|not valid UTF-8|no exact match|not found|server/i.test(msg);
100
+ // also fall back so a server hiccup never blocks an edit. Older cached
101
+ // mixdog-patch binaries (for example the v0.6.5 release prebuilds used
102
+ // by clean CI before a local cargo build exists) support APPLY but not
103
+ // the EDIT server protocol, and answer EDIT with the APPLY parser's
104
+ // "bad header" error. In auto mode that means "native edit unavailable",
105
+ // not "the edit is invalid", so fall through to the JS editor. When the
106
+ // user explicitly forces native mode, keep surfacing the native failure.
107
+ const fallback = !forcedNative && /old_string (?:not found|found \d+ times)|not valid UTF-8|no exact match|not found|server|bad header|bad edit header/i.test(msg);
87
108
  return { ok: false, fallback, error: msg };
88
109
  }
89
110
  }
@@ -361,6 +361,9 @@ function capToolOutput(result, options = {}) {
361
361
  }
362
362
 
363
363
  export async function executeBuiltinTool(name, args, cwd, options = {}) {
364
+ if (options.abortSignal && !options.signal) {
365
+ options = { ...options, signal: options.abortSignal };
366
+ }
364
367
  const toolName = canonicalizeBuiltinToolName(name);
365
368
  const argError = validateBuiltinArgs(toolName, args);
366
369
  if (argError) return argError;
@@ -414,9 +417,9 @@ export async function executeBuiltinTool(name, args, cwd, options = {}) {
414
417
  case 'list':
415
418
  return executeListTool(args, workDir, options);
416
419
  case 'tree':
417
- return executeTreeTool(args, workDir);
420
+ return executeTreeTool(args, workDir, options);
418
421
  case 'find_files':
419
- return executeFindFilesTool(args, workDir);
422
+ return executeFindFilesTool(args, workDir, options);
420
423
  case 'head':
421
424
  return executeHeadTool(args, workDir, readStateScope, _readModeHelpers);
422
425
  case 'tail':
@@ -58,15 +58,14 @@ const PRUNED_DIR_NAMES = new Set([...NOISE_DIR_NAMES, ...EXTRA_NOISE_DIR_NAMES])
58
58
 
59
59
  let _listCache = null
60
60
 
61
- // Per-scan set used by the visitor to mark repos whose subtree must not
62
- // be descended. walkDir does not natively support "stop descent into
63
- // this dir but keep the walk going", so we approximate by checking
64
- // ancestry on every visit and bailing early when the entry lives under
65
- // a previously-recorded repo.
66
- const _recordedRepos = new Set()
67
-
68
- function _scanReposUnderFiltered(root) {
69
- _recordedRepos.clear()
61
+ async function _scanReposUnderFiltered(root, signal) {
62
+ // Per-scan set used by the visitor to mark repos whose subtree must not be
63
+ // descended. walkDir does not natively support "stop descent into this dir
64
+ // but keep the walk going", so we approximate by checking ancestry on every
65
+ // visit and bailing early when the entry lives under a previously-recorded
66
+ // repo. MUST be local per-call: walkDir is async, so a module-global set
67
+ // would race across concurrent/background scans suspended in `await readdir`.
68
+ const recordedRepos = new Set()
70
69
  const repos = []
71
70
  if (!root || !existsSync(root)) return repos
72
71
  try {
@@ -79,17 +78,18 @@ function _scanReposUnderFiltered(root) {
79
78
  return repos
80
79
  }
81
80
 
82
- walkDir(root, {
81
+ await walkDir(root, {
83
82
  hidden: true,
84
83
  maxDepth: 4,
85
84
  excludeDirNames: PRUNED_DIR_NAMES,
85
+ signal,
86
86
  visit: (ent, entPath) => {
87
87
  if (!ent.isDirectory()) return
88
88
  // Skip subtree of a previously-recorded repo. walkDir invokes
89
89
  // visit before recursing, so flipping ent.isDirectory() here
90
90
  // would not stop descent. Instead, check ancestry on every
91
91
  // visit and bail early.
92
- for (const recorded of _recordedRepos) {
92
+ for (const recorded of recordedRepos) {
93
93
  if (entPath === recorded || entPath.startsWith(recorded + '/') || entPath.startsWith(recorded + '\\')) {
94
94
  return
95
95
  }
@@ -97,7 +97,7 @@ function _scanReposUnderFiltered(root) {
97
97
  try {
98
98
  if (existsSync(`${entPath}/.git`)) {
99
99
  repos.push(entPath)
100
- _recordedRepos.add(entPath)
100
+ recordedRepos.add(entPath)
101
101
  }
102
102
  } catch { /* ignore stat races */ }
103
103
  },
@@ -151,7 +151,7 @@ function _authoredByIdentity(repoPath, emails) {
151
151
  }
152
152
  }
153
153
 
154
- function _buildRepoList(includeAll = false) {
154
+ async function _buildRepoList(includeAll = false, signal) {
155
155
  const cfg = _cwdConfig()
156
156
  const user = rawUserCwd()
157
157
  const roots = []
@@ -169,7 +169,7 @@ function _buildRepoList(includeAll = false) {
169
169
 
170
170
  const repoSet = new Map() // canonical → original path
171
171
  for (const root of roots) {
172
- for (const repo of _scanReposUnderFiltered(root)) {
172
+ for (const repo of await _scanReposUnderFiltered(root, signal)) {
173
173
  const key = _canonicalSessionCwd(repo)
174
174
  if (!repoSet.has(key)) repoSet.set(key, repo)
175
175
  }
@@ -257,9 +257,9 @@ export async function executeCwdTool(name, args, callerCwd, opts = {}) {
257
257
  if (action === 'list') {
258
258
  // `all:true` bypasses the owned-project filter and transient prune — a
259
259
  // raw scan of every .git repo under the roots. Not cached (rare).
260
- if (args?.all === true) return _formatList(_buildRepoList(true))
260
+ if (args?.all === true) return _formatList(await _buildRepoList(true, opts.signal))
261
261
  if (args?.refresh === true) _listCache = null
262
- if (!_listCache) _listCache = _buildRepoList(false)
262
+ if (!_listCache) _listCache = await _buildRepoList(false, opts.signal)
263
263
  return _formatList(_listCache)
264
264
  }
265
265
 
@@ -293,6 +293,6 @@ export const CWD_TOOL_DEFS = [
293
293
  // and unref'd so it never blocks startup or keeps the process alive.
294
294
  try {
295
295
  setTimeout(() => {
296
- try { if (!_listCache) _listCache = _buildRepoList(false) } catch { /* best-effort */ }
296
+ try { if (!_listCache) _buildRepoList(false).then((r) => { _listCache = r }) } catch { /* best-effort */ }
297
297
  }, 1000).unref?.()
298
298
  } catch { /* environments without setTimeout */ }
@@ -1,26 +1,26 @@
1
1
  {
2
- "version": "0.6.5",
2
+ "version": "0.7.13",
3
3
  "_comment": "Rewritten by .github/workflows/graph-release.yml on each tagged release. assets maps platformKey (process.platform-process.arch, e.g. win32-x64, linux-x64, darwin-arm64) to { url, sha256 } of the mixdog-graph binary on the GitHub release. A local cargo build under native/mixdog-graph/target/release always takes precedence at runtime. (v0.5.236 entries were filled manually after CI's commit step hit detached HEAD; the workflow now checks out ref: main so future releases self-update.)",
4
4
  "assets": {
5
5
  "darwin-arm64": {
6
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.6.5/mixdog-graph-darwin-arm64",
7
- "sha256": "7016c273a07d19ca9e2f56e8fa7f273fdd40fc41bdc7fef206bf23e31a21a736"
6
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-graph-darwin-arm64",
7
+ "sha256": "75bfdd200b2f8553b72dc877ec2637208f581800083d1ee5f9caf33f87792bf7"
8
8
  },
9
9
  "darwin-x64": {
10
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.6.5/mixdog-graph-darwin-x64",
11
- "sha256": "d076e97da4420f49a6c726bc088a3321e2e7f6a9bfb32d39162c8c53045cfcdb"
10
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-graph-darwin-x64",
11
+ "sha256": "04742fbb4cbe09bb76943f312ee129c05814543e7bc9d37e1241fb4e65b97137"
12
12
  },
13
13
  "linux-arm64": {
14
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.6.5/mixdog-graph-linux-arm64",
15
- "sha256": "74754562b3c080868738c032c5b6e0e13bc53d7a5277002176b036f8d6681f39"
14
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-graph-linux-arm64",
15
+ "sha256": "4b3edcd7be1ffec7184c48fe6bc7d6bce42f2ea67d4709f44d4402e6b48564f2"
16
16
  },
17
17
  "linux-x64": {
18
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.6.5/mixdog-graph-linux-x64",
19
- "sha256": "0d8e8bbdd49b18746ed3f972fc3719731a1143ee03ac9e6d86586788b0b431f8"
18
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-graph-linux-x64",
19
+ "sha256": "4394bb7884a8706dd6a4eea55f8755c76ba584cd02248863802d94acc3e1413c"
20
20
  },
21
21
  "win32-x64": {
22
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.6.5/mixdog-graph-win32-x64.exe",
23
- "sha256": "1a671558e5a5f13c7429ff9987a46ad72a71e52241e200d2da820a13d7cbdae7"
22
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-graph-win32-x64.exe",
23
+ "sha256": "2acf1fc9040cfc78a643daa477b15c8eaa0f520fd8addd1cf6f2f8cddd5c914c"
24
24
  }
25
25
  }
26
26
  }
@@ -1,26 +1,26 @@
1
1
  {
2
- "version": "0.6.5",
2
+ "version": "0.7.13",
3
3
  "_comment": "Rewritten by .github/workflows/patch-release.yml on each tagged release. assets maps platformKey (process.platform-process.arch, e.g. win32-x64, linux-x64, darwin-arm64) to { url, sha256 } of the mixdog-patch binary on the GitHub release. A local cargo build under native/mixdog-patch/target/release always takes precedence; otherwise the binary is fetched per this manifest into the data dir (apply is native-only — no JS apply engine).",
4
4
  "assets": {
5
5
  "darwin-arm64": {
6
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.6.5/mixdog-patch-darwin-arm64",
7
- "sha256": "d37afb583cd597a9599ea9feac76a853d215ffaadc2bb2b54cf71ef28848f7ae"
6
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-patch-darwin-arm64",
7
+ "sha256": "836a0b60a443b0a6a8c1bbe24d15a79ed70ee92c2f0fbc05374c4e9ed2536415"
8
8
  },
9
9
  "darwin-x64": {
10
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.6.5/mixdog-patch-darwin-x64",
11
- "sha256": "bc4dad6a7fdc2fcdfceb850bcaae43757ae304740ca503be955eb57cf8cd07e3"
10
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-patch-darwin-x64",
11
+ "sha256": "cab10c4e1e8b72d3958241dffdff764712ed74f4861d105bafa8258961215c98"
12
12
  },
13
13
  "linux-arm64": {
14
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.6.5/mixdog-patch-linux-arm64",
15
- "sha256": "ebe3fd45aaed0f383f7b7940733fd141dd0b93c4c44e08fbffab10a3e43b788c"
14
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-patch-linux-arm64",
15
+ "sha256": "a90c32ce3417a7d853f2723f82f3613cf2cd030fe885cf710cfc9f8e4b193264"
16
16
  },
17
17
  "linux-x64": {
18
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.6.5/mixdog-patch-linux-x64",
19
- "sha256": "627289a3b5c0156bc299d4ff7563e4e4536a5a5bf6329e7c9151e811c795928f"
18
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-patch-linux-x64",
19
+ "sha256": "0fea40ab98acd35bfb47515756024d1882a2abbaddce8a0b51642d20ac405577"
20
20
  },
21
21
  "win32-x64": {
22
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.6.5/mixdog-patch-win32-x64.exe",
23
- "sha256": "4282546719a8c149597d3afe21eaf31aab3079a371876bf7f91ff0bccbe316b2"
22
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.13/mixdog-patch-win32-x64.exe",
23
+ "sha256": "24b3c2b2b95a2e3b15e0e089ca41297056be7c12df8c4c7524fa91a4833b1f68"
24
24
  }
25
25
  }
26
26
  }
@@ -18,7 +18,7 @@ export const TOOL_DEFS = [
18
18
  title: 'Explore',
19
19
  aiWrapped: true,
20
20
  annotations: { title: 'Explore', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
21
- description: 'Read-only codebase EXPLORATION — fact-finding only: locate/map where and how things are implemented, for open-ended/unknown scope (for a known or partial identifier use code_graph; recall=memory, search=web). NOT a reviewer/auditor: explorers LOCATE and DESCRIBE code, never judge it — bug/quality/risk claims in explore output are UNVERIFIED leads; verify them (reviewer role or direct reads) before acting on or reporting them. Shape every query as a LOCATION/INVENTORY question ("where is X handled", "which files implement Y", "what does Z read/write") — NEVER a verdict question ("is X correct/missing/inconsistent?", "are there gaps/bugs?"); the judgment stays with the caller, applied to the coordinates explore returns. Query shaping (one-line, one topic per item, decompose a multi-part brief) is specified on the query parameter — follow it. Fan-out runs items in parallel; wall-clock = the slowest item. LEAD: default background:true (answer pushed via channel, avoids the 120s sync cap). BRIDGE WORKERS run it sync and SHOULD prefer it for a tree-wide enumeration or broad/unanchored exploration — ONE call offloads the whole sweep into a sub-agent instead of a long grep/code_graph storm; a bounded/known-anchor lookup stays a direct code_graph/grep call.',
21
+ description: 'Read-only codebase EXPLORATION — fact-finding only: locate/map where and how things are implemented, for open-ended/unknown scope (for a known or partial identifier use code_graph; recall=memory, search=web). NOT a reviewer/auditor: explorers LOCATE and DESCRIBE code, never judge it — bug/quality/risk claims in explore output are UNVERIFIED leads; verify them (reviewer role or direct reads) before acting on or reporting them. Shape every query as a LOCATION/INVENTORY question ("where is X handled", "which files implement Y", "what does Z read/write") — NEVER a verdict question ("is X correct/missing/inconsistent?", "are there gaps/bugs?"); the judgment stays with the caller, applied to the coordinates explore returns. Query shaping rules are on the query parameter — follow them. Fan-out runs items in parallel; wall-clock = the slowest item. LEAD: default background:true (answer pushed via channel, avoids the 120s sync cap). BRIDGE WORKERS run it sync and SHOULD prefer it for a tree-wide enumeration or broad/unanchored exploration — ONE call offloads the whole sweep into a sub-agent instead of a long grep/code_graph storm; a bounded/known-anchor lookup stays a direct code_graph/grep call.',
22
22
  inputSchema: {
23
23
  type: 'object',
24
24
  properties: {
@@ -183,10 +183,18 @@ const _bootLogEarly = path.join(
183
183
  process.env.CLAUDE_PLUGIN_DATA || path.join(os.tmpdir(), "mixdog"),
184
184
  "boot.log"
185
185
  );
186
+ const {
187
+ isMixdogDebugEnabled: isMixdogDebug,
188
+ pruneStalePluginDataLogSiblings,
189
+ appendSessionStartCriticalLog,
190
+ DEFAULT_STALE_LOG_SIBLING_MAX,
191
+ } = _require("../../lib/mixdog-debug.cjs");
186
192
  // One-shot log rotation at worker boot (10 MB threshold, .1 suffix overwrite).
187
- try { if (fs.statSync(_bootLogEarly).size > 10 * 1024 * 1024) fs.renameSync(_bootLogEarly, _bootLogEarly + '.1') } catch {}
188
- fs.appendFileSync(_bootLogEarly, `[${localTimestamp()}] bootstrap start pid=${process.pid}
193
+ if (isMixdogDebug()) {
194
+ try { if (fs.statSync(_bootLogEarly).size > 10 * 1024 * 1024) fs.renameSync(_bootLogEarly, _bootLogEarly + '.1') } catch {}
195
+ fs.appendFileSync(_bootLogEarly, `[${localTimestamp()}] bootstrap start pid=${process.pid}
189
196
  `);
197
+ }
190
198
  const _bootLog = path.join(DATA_DIR, "boot.log");
191
199
  let config = loadConfig();
192
200
  let backend = createBackend(config);
@@ -238,6 +246,11 @@ try {
238
246
  try { if (_now - fs.statSync(_p).mtimeMs > _STALE_SESSION_TTL_MS) fs.unlinkSync(_p); } catch {}
239
247
  }
240
248
  } catch {}
249
+ // Count-based cap: drop oldest *.log siblings when plugin-data accumulates
250
+ // hundreds of per-process files (doctor warns above 300).
251
+ try {
252
+ pruneStalePluginDataLogSiblings(DATA_DIR, DEFAULT_STALE_LOG_SIBLING_MAX);
253
+ } catch {}
241
254
 
242
255
  // ── Buffered drop-trace writer (channels/index) ──────────────────────────────
243
256
  // Flushes every 1 s OR when buffer reaches 64 KB — whichever fires first.
@@ -2030,8 +2043,12 @@ backend.onInteraction = (interaction) => {
2030
2043
  const [, uuid, action] = match;
2031
2044
  const access = config.access;
2032
2045
  if (!access) {
2033
- fs.appendFileSync(_bootLog, `[${localTimestamp()}] perm interaction dropped: no access config
2034
- `);
2046
+ const _permDropLine = `[${localTimestamp()}] perm interaction dropped: no access config\n`;
2047
+ if (isMixdogDebug()) {
2048
+ fs.appendFileSync(_bootLog, _permDropLine);
2049
+ } else {
2050
+ appendSessionStartCriticalLog(DATA_DIR, `[channels] ${_permDropLine}`);
2051
+ }
2035
2052
  return;
2036
2053
  }
2037
2054
  if (access.allowFrom?.length > 0 && !access.allowFrom.includes(interaction.userId)) {
@@ -2566,7 +2583,18 @@ async function handleToolCall(name, args, signal) {
2566
2583
  }
2567
2584
  case "reload_config": {
2568
2585
  await reloadRuntimeConfig();
2569
- result = { content: [{ type: "text", text: "config reloaded \u2014 schedules, webhooks, and events re-registered" }] };
2586
+ // Extend reload to the agent module so providers/presets/maintenance
2587
+ // hot-reload on the same call (dynamic import: agent/index.mjs does not
2588
+ // import channels, so this stays acyclic and tolerant of load order).
2589
+ let agentReloadMsg = "";
2590
+ try {
2591
+ const { reloadAgentConfig } = await import("../agent/index.mjs");
2592
+ await reloadAgentConfig("reload_config tool");
2593
+ agentReloadMsg = ", agent providers/presets/maintenance";
2594
+ } catch (err) {
2595
+ process.stderr.write(`[reload_config] agent reload failed: ${err?.message || String(err)}\n`);
2596
+ }
2597
+ result = { content: [{ type: "text", text: `config reloaded — schedules, webhooks, events${agentReloadMsg} re-registered` }] };
2570
2598
  break;
2571
2599
  }
2572
2600
  case "inject_command": {
@@ -3016,12 +3044,14 @@ async function stop() {
3016
3044
  return false;
3017
3045
  };
3018
3046
  _channelFlagDetected = detectChannelFlag();
3019
- fs.appendFileSync(_bootLog, `[${localTimestamp()}] channelFlag: ${_channelFlagDetected}
3020
- `);
3047
+ if (isMixdogDebug()) {
3048
+ fs.appendFileSync(_bootLog, `[${localTimestamp()}] channelFlag: ${_channelFlagDetected}\n`);
3049
+ if (_channelFlagDetected) {
3050
+ fs.appendFileSync(_bootLog, `[${localTimestamp()}] channel mode detected — bridge auto-activated\n`);
3051
+ }
3052
+ }
3021
3053
  if (_channelFlagDetected) {
3022
3054
  channelBridgeActive = true;
3023
- fs.appendFileSync(_bootLog, `[${localTimestamp()}] channel mode detected \u2014 bridge auto-activated
3024
- `);
3025
3055
  }
3026
3056
  writeBridgeState(channelBridgeActive);
3027
3057
  const previousOwner = readActiveInstance();
@@ -1,4 +1,4 @@
1
- import { readdirSync, readFileSync, existsSync as fsExistsSync } from "fs";
1
+ import { readdirSync, readFileSync, existsSync as fsExistsSync, statSync, unlinkSync } from "fs";
2
2
  import { join } from "path";
3
3
  import { DATA_DIR } from "./config.mjs";
4
4
  import { ensureDir } from "./state-file.mjs";
@@ -7,6 +7,27 @@ import { renameWithRetrySync, writeJsonAtomicSync } from "../../shared/atomic-fi
7
7
  const QUEUE_DIR = join(DATA_DIR, "events", "queue");
8
8
  const IN_PROGRESS_DIR = join(DATA_DIR, "events", "in-progress");
9
9
  const PROCESSED_DIR = join(DATA_DIR, "events", "processed");
10
+ const PROCESSED_DIR_MAX_ENTRIES = 200;
11
+ function pruneProcessedDir() {
12
+ try {
13
+ if (!fsExistsSync(PROCESSED_DIR)) return;
14
+ const names = readdirSync(PROCESSED_DIR);
15
+ if (names.length <= PROCESSED_DIR_MAX_ENTRIES) return;
16
+ const ranked = [];
17
+ for (const name of names) {
18
+ try {
19
+ const st = statSync(join(PROCESSED_DIR, name));
20
+ ranked.push({ name, mtime: st.mtimeMs });
21
+ } catch {}
22
+ }
23
+ ranked.sort((a, b) => b.mtime - a.mtime);
24
+ for (let i = PROCESSED_DIR_MAX_ENTRIES; i < ranked.length; i++) {
25
+ try {
26
+ unlinkSync(join(PROCESSED_DIR, ranked[i].name));
27
+ } catch {}
28
+ }
29
+ } catch {}
30
+ }
10
31
  function finiteInt(value, { min, max, def }) {
11
32
  const n = Number(value);
12
33
  if (!Number.isFinite(n)) return def;
@@ -290,6 +311,7 @@ ${p.item.prompt}`).join("\n\n")}`;
290
311
  const fromQueue = join(QUEUE_DIR, file);
291
312
  const src = this.existsSync(fromInProgress) ? fromInProgress : fromQueue;
292
313
  renameWithRetrySync(src, join(PROCESSED_DIR, `${status}-${file}`));
314
+ pruneProcessedDir();
293
315
  } catch {
294
316
  }
295
317
  }
@@ -297,6 +319,7 @@ ${p.item.prompt}`).join("\n\n")}`;
297
319
  try {
298
320
  ensureDir(PROCESSED_DIR);
299
321
  renameWithRetrySync(join(IN_PROGRESS_DIR, file), join(PROCESSED_DIR, `${status}-${file}`));
322
+ pruneProcessedDir();
300
323
  } catch {
301
324
  }
302
325
  }
@@ -24,6 +24,11 @@ import { request as httpsRequest } from 'node:https'
24
24
  import { createRequire } from 'node:module'
25
25
 
26
26
  const moduleRequire = createRequire(import.meta.url)
27
+ const {
28
+ isMixdogDebugEnabled,
29
+ pruneStalePluginDataLogSiblings,
30
+ DEFAULT_STALE_LOG_SIBLING_MAX,
31
+ } = moduleRequire('../../../lib/mixdog-debug.cjs')
27
32
 
28
33
  // IPC transport path. Windows uses a named pipe (`\\.\pipe\…`); Unix uses a
29
34
  // Unix domain socket under XDG_RUNTIME_DIR (or /tmp as fallback). Node's
@@ -57,9 +62,13 @@ const POLL_INTERVAL_MS = 2000
57
62
  const SUBAGENT_TIMEOUT_MS = 120_000
58
63
  const DEFAULT_DISPATCH_TIMEOUT_MS = 15_000
59
64
  const SESSION_START_MEMORY_DISPATCH_TIMEOUT_MS = 125_000
60
- const SESSION_START_TRACE_ENABLED =
61
- process.env.MIXDOG_DEBUG_SESSION_START === '1' ||
62
- process.env.MIXDOG_DEBUG_SESSION_START === 'true'
65
+ const MIXDOG_DEBUG_ENABLED = isMixdogDebugEnabled()
66
+ let _hookPipeLogsPruned = false
67
+
68
+ function hookPipeDebugStderr(line) {
69
+ if (!MIXDOG_DEBUG_ENABLED) return
70
+ try { process.stderr.write(line) } catch {}
71
+ }
63
72
 
64
73
  let _started = false
65
74
  let _server = null
@@ -82,13 +91,17 @@ function formatError(err) {
82
91
  }
83
92
 
84
93
  function traceSessionStart(message) {
85
- if (!SESSION_START_TRACE_ENABLED) return
94
+ if (!MIXDOG_DEBUG_ENABLED) return
86
95
  const line = `[${new Date().toISOString()}] [hook-pipe][session-start] ${message}\n`
87
96
  try { process.stderr.write(line) } catch {}
88
97
  try {
89
98
  const dataDir = process.env.CLAUDE_PLUGIN_DATA ||
90
99
  join(homedir(), '.claude', 'plugins', 'data', 'mixdog-trib-plugin')
91
100
  mkdirSync(dataDir, { recursive: true })
101
+ if (!_hookPipeLogsPruned) {
102
+ _hookPipeLogsPruned = true
103
+ pruneStalePluginDataLogSiblings(dataDir, DEFAULT_STALE_LOG_SIBLING_MAX)
104
+ }
92
105
  appendFileSync(join(dataDir, 'session-start.log'), line)
93
106
  } catch {}
94
107
  }
@@ -320,10 +333,10 @@ async function handlePreToolSubagent(payload) {
320
333
  }
321
334
  const route = routeMod.shouldRoutePermissionToDiscord()
322
335
  if (route.route !== 'discord') {
323
- process.stderr.write(`[hook-pipe] pre-tool-subagent discord-route=off agent=${agentIdRaw || 'unknown'} tool=${toolName} reason=${route.reason || 'inactive'}\n`)
336
+ hookPipeDebugStderr(`[hook-pipe] pre-tool-subagent discord-route=off agent=${agentIdRaw || 'unknown'} tool=${toolName} reason=${route.reason || 'inactive'}\n`)
324
337
  return null
325
338
  }
326
- process.stderr.write(`[hook-pipe] pre-tool-subagent discord-route=on agent=${agentIdRaw || 'unknown'} tool=${toolName}\n`)
339
+ hookPipeDebugStderr(`[hook-pipe] pre-tool-subagent discord-route=on agent=${agentIdRaw || 'unknown'} tool=${toolName}\n`)
327
340
 
328
341
  let getDiscordToken
329
342
  try {
@@ -715,7 +728,7 @@ export function startHookPipeServer() {
715
728
  _server.on('error', (err) => {
716
729
  const msg = String(err?.message || err || '')
717
730
  if (err?.code === 'EADDRINUSE' || msg.includes('EADDRINUSE') || msg.includes('Failed to listen')) {
718
- process.stderr.write(`[hook-pipe] ${PIPE_PATH} already owned by a peer daemon; standby for hook IPC\n`)
731
+ hookPipeDebugStderr(`[hook-pipe] ${PIPE_PATH} already owned by a peer daemon; standby for hook IPC\n`)
719
732
  _server = null
720
733
  _started = false
721
734
  return
@@ -727,7 +740,7 @@ export function startHookPipeServer() {
727
740
  try {
728
741
  _server.listen(PIPE_PATH, () => {
729
742
  _started = true
730
- process.stderr.write(`[hook-pipe] listening on ${PIPE_PATH}\n`)
743
+ hookPipeDebugStderr(`[hook-pipe] listening on ${PIPE_PATH}\n`)
731
744
  })
732
745
  } catch (err) {
733
746
  process.stderr.write(`[hook-pipe] listen failed: ${err?.message || err}\n`)