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.
Files changed (49) hide show
  1. package/package.json +1 -1
  2. package/scripts/agent-tag-reuse-smoke.mjs +12 -8
  3. package/scripts/bench/cache-probe-tasks.json +8 -0
  4. package/scripts/bench/lead-review-tasks-r3.json +1 -1
  5. package/scripts/bench/r4-mixed-tasks.json +1 -1
  6. package/scripts/bench/review-tasks.json +1 -1
  7. package/scripts/build-runtime-windows.ps1 +242 -242
  8. package/scripts/recall-usecase-cases.json +1 -1
  9. package/scripts/smoke-runtime-negative.ps1 +106 -106
  10. package/scripts/tool-smoke.mjs +109 -0
  11. package/src/defaults/mixdog-config.template.json +1 -0
  12. package/src/mixdog-session-runtime.mjs +8 -0
  13. package/src/rules/agent/30-explorer.md +18 -20
  14. package/src/rules/lead/02-channels.md +3 -3
  15. package/src/runtime/agent/orchestrator/config.mjs +30 -0
  16. package/src/runtime/agent/orchestrator/context/collect.mjs +160 -0
  17. package/src/runtime/agent/orchestrator/mcp/client.mjs +25 -1
  18. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +24 -9
  19. package/src/runtime/agent/orchestrator/providers/gemini.mjs +18 -2
  20. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +23 -0
  21. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +9 -0
  22. package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +79 -0
  23. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +7 -1
  24. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +9 -2
  25. package/src/runtime/agent/orchestrator/session/manager.mjs +3 -3
  26. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +5 -5
  27. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +1 -22
  28. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +26 -3
  29. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +29 -1
  30. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +17 -16
  31. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -1
  32. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +4 -0
  33. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +30 -4
  34. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +333 -14
  35. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +129 -2
  36. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +58 -18
  37. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +56 -83
  38. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +26 -56
  39. package/src/runtime/channels/lib/webhook/deliveries.mjs +4 -1
  40. package/src/session-runtime/settings-api.mjs +13 -0
  41. package/src/session-runtime/tool-catalog.mjs +34 -7
  42. package/src/standalone/agent-tool/render.mjs +2 -0
  43. package/src/standalone/agent-tool.mjs +28 -6
  44. package/src/standalone/explore-tool.mjs +2 -2
  45. package/src/standalone/memory-runtime-proxy.mjs +85 -21
  46. package/src/tui/App.jsx +20 -1
  47. package/src/tui/app/extension-pickers.mjs +6 -3
  48. package/src/tui/dist/index.mjs +28 -4
  49. package/src/tui/engine.mjs +2 -0
@@ -1,106 +1,106 @@
1
- # smoke-runtime-negative.ps1 — Negative-path tests against released win32-x64
2
- # runtime tarball. Validates: download, sha256, corrupt-tarball, fresh-extract
3
- # boot, double-init refusal, hostile-env survival.
4
- #
5
- # Env: TAG / OS / ARCH / RUNTIME_RELEASE_REPOSITORY (default tribgames/mixdog)
6
-
7
- $ErrorActionPreference = 'Stop'
8
-
9
- $Tag = if ($env:TAG) { $env:TAG } else { 'runtime-v0.4.0' }
10
- $Os = if ($env:OS) { $env:OS } else { 'win32' }
11
- $Arch = if ($env:ARCH) { $env:ARCH } else { 'x64' }
12
- $ReleaseRepo = if ($env:RUNTIME_RELEASE_REPOSITORY) { $env:RUNTIME_RELEASE_REPOSITORY } else { 'tribgames/mixdog' }
13
- $PgVer = '16.4'
14
- $PgvectorVer = '0.8.2'
15
-
16
- $Asset = "mixdog-runtime-${Os}-${Arch}-pg${PgVer}-pgvector${PgvectorVer}.tar.gz"
17
- $Url = "https://github.com/${ReleaseRepo}/releases/download/${Tag}/${Asset}"
18
-
19
- $Work = New-Item -ItemType Directory -Force -Path "$env:TEMP\smoke-$([guid]::NewGuid().ToString('N'))" | Select-Object -ExpandProperty FullName
20
-
21
- try {
22
- Write-Host "==> Negative-path smoke for ${Os}-${Arch} from $Url"
23
-
24
- Write-Host "==> Test 1: HEAD reaches release asset"
25
- $resp = Invoke-WebRequest -Uri $Url -Method Head -UseBasicParsing -ErrorAction Stop
26
- Write-Host " HTTP $($resp.StatusCode)"
27
-
28
- Write-Host "==> Test 2: full download + sha256"
29
- $TarPath = Join-Path $Work $Asset
30
- Invoke-WebRequest -Uri $Url -OutFile $TarPath -UseBasicParsing
31
- $sha = (Get-FileHash -Algorithm SHA256 $TarPath).Hash.ToLower()
32
- Write-Host " sha256=$sha"
33
-
34
- Write-Host "==> Test 3: corrupt tarball — flip a byte and ensure tar errors"
35
- $CorruptPath = "$Work\corrupt.tar.gz"
36
- Copy-Item $TarPath $CorruptPath
37
- $bytes = [byte[]]::new(1); $rand = [System.Random]::new(); $rand.NextBytes($bytes)
38
- $stream = [System.IO.File]::OpenWrite($CorruptPath)
39
- $stream.Position = 102400
40
- $stream.Write($bytes, 0, 1)
41
- $stream.Close()
42
- $CorruptDir = "$Work\corrupt"
43
- New-Item -ItemType Directory -Force -Path $CorruptDir | Out-Null
44
- $TarProc = Start-Process -FilePath tar -ArgumentList @('-xzf', $CorruptPath, '-C', $CorruptDir.Replace('\','/')) -Wait:$false -PassThru -WindowStyle Hidden
45
- if (-not $TarProc.WaitForExit(15000)) {
46
- Stop-Process -Id $TarProc.Id -Force -ErrorAction SilentlyContinue
47
- Write-Host " PASS: corrupted tarball extraction timed out and was stopped"
48
- } elseif ($TarProc.ExitCode -eq 0) {
49
- Write-Host " WARN: corrupted tarball extracted without error (tar gzip recovery)"
50
- } else {
51
- Write-Host " PASS: corrupted tarball rejected by tar (exit $($TarProc.ExitCode))"
52
- }
53
-
54
- Write-Host "==> Test 4: fresh-extract boot (extension load + distance query)"
55
- $FreshDir = "$Work\fresh"
56
- New-Item -ItemType Directory -Force -Path $FreshDir | Out-Null
57
- & tar -xzf $TarPath -C ($FreshDir.Replace('\','/'))
58
- if ($LASTEXITCODE -ne 0) { throw "fresh extract failed" }
59
-
60
- $PgBin = "$FreshDir\bin"
61
- $Data = "$FreshDir\pgdata"
62
- $Log = "$FreshDir\pg.log"
63
- $Port = 55897
64
-
65
- # Hostile env: minimal PATH (System32 only), no PGROOT/PGDATA
66
- $SavedPath = $env:PATH
67
- $SavedPgRoot = $env:PGROOT
68
- $env:PATH = "$env:SystemRoot\System32;$env:SystemRoot"
69
- $env:PGROOT = $null
70
- $env:PGDATA = $null
71
-
72
- try {
73
- & "$PgBin\postgres.exe" --version
74
- if ($LASTEXITCODE -ne 0) { throw "FAIL: postgres --version" }
75
- & "$PgBin\initdb.exe" -D $Data --username=postgres --auth-local=trust --no-locale -E UTF8 | Out-Null
76
- if ($LASTEXITCODE -ne 0) { throw "FAIL: initdb" }
77
- & "$PgBin\pg_ctl.exe" -D $Data -o "-p $Port -h 127.0.0.1" -l $Log -w start
78
- if ($LASTEXITCODE -ne 0) { Get-Content $Log | Select-Object -Last 30; throw "FAIL: pg_ctl start" }
79
-
80
- try {
81
- & "$PgBin\psql.exe" -h 127.0.0.1 -p $Port -U postgres -d postgres -c "CREATE EXTENSION vector;" | Out-Null
82
- if ($LASTEXITCODE -ne 0) { throw "FAIL: CREATE EXTENSION" }
83
- $ExtV = & "$PgBin\psql.exe" -h 127.0.0.1 -p $Port -U postgres -d postgres -tAc "SELECT extversion FROM pg_extension WHERE extname='vector';"
84
- if ($ExtV.Trim() -ne $PgvectorVer) { throw "FAIL: extversion='$ExtV' expected='$PgvectorVer'" }
85
- Write-Host " PASS: fresh-extract boot + vector extension"
86
- } finally {
87
- & "$PgBin\pg_ctl.exe" -D $Data -m fast stop 2>$null | Out-Null
88
- }
89
-
90
- Write-Host "==> Test 5: second initdb on initialized dir refused"
91
- $rc = (Start-Process -FilePath "$PgBin\initdb.exe" -ArgumentList "-D",$Data,"-U","postgres" -Wait -PassThru -NoNewWindow).ExitCode
92
- if ($rc -eq 0) {
93
- Write-Host " WARN: second initdb succeeded (unexpected)"
94
- } else {
95
- Write-Host " PASS: second initdb refused (exit $rc)"
96
- }
97
- } finally {
98
- $env:PATH = $SavedPath
99
- $env:PGROOT = $SavedPgRoot
100
- }
101
-
102
- Write-Host "==> All negative-path smokes: PASS"
103
- }
104
- finally {
105
- Remove-Item -Recurse -Force $Work -ErrorAction SilentlyContinue
106
- }
1
+ # smoke-runtime-negative.ps1 — Negative-path tests against released win32-x64
2
+ # runtime tarball. Validates: download, sha256, corrupt-tarball, fresh-extract
3
+ # boot, double-init refusal, hostile-env survival.
4
+ #
5
+ # Env: TAG / OS / ARCH / RUNTIME_RELEASE_REPOSITORY (default tribgames/mixdog)
6
+
7
+ $ErrorActionPreference = 'Stop'
8
+
9
+ $Tag = if ($env:TAG) { $env:TAG } else { 'runtime-v0.4.0' }
10
+ $Os = if ($env:OS) { $env:OS } else { 'win32' }
11
+ $Arch = if ($env:ARCH) { $env:ARCH } else { 'x64' }
12
+ $ReleaseRepo = if ($env:RUNTIME_RELEASE_REPOSITORY) { $env:RUNTIME_RELEASE_REPOSITORY } else { 'tribgames/mixdog' }
13
+ $PgVer = '16.4'
14
+ $PgvectorVer = '0.8.2'
15
+
16
+ $Asset = "mixdog-runtime-${Os}-${Arch}-pg${PgVer}-pgvector${PgvectorVer}.tar.gz"
17
+ $Url = "https://github.com/${ReleaseRepo}/releases/download/${Tag}/${Asset}"
18
+
19
+ $Work = New-Item -ItemType Directory -Force -Path "$env:TEMP\smoke-$([guid]::NewGuid().ToString('N'))" | Select-Object -ExpandProperty FullName
20
+
21
+ try {
22
+ Write-Host "==> Negative-path smoke for ${Os}-${Arch} from $Url"
23
+
24
+ Write-Host "==> Test 1: HEAD reaches release asset"
25
+ $resp = Invoke-WebRequest -Uri $Url -Method Head -UseBasicParsing -ErrorAction Stop
26
+ Write-Host " HTTP $($resp.StatusCode)"
27
+
28
+ Write-Host "==> Test 2: full download + sha256"
29
+ $TarPath = Join-Path $Work $Asset
30
+ Invoke-WebRequest -Uri $Url -OutFile $TarPath -UseBasicParsing
31
+ $sha = (Get-FileHash -Algorithm SHA256 $TarPath).Hash.ToLower()
32
+ Write-Host " sha256=$sha"
33
+
34
+ Write-Host "==> Test 3: corrupt tarball — flip a byte and ensure tar errors"
35
+ $CorruptPath = "$Work\corrupt.tar.gz"
36
+ Copy-Item $TarPath $CorruptPath
37
+ $bytes = [byte[]]::new(1); $rand = [System.Random]::new(); $rand.NextBytes($bytes)
38
+ $stream = [System.IO.File]::OpenWrite($CorruptPath)
39
+ $stream.Position = 102400
40
+ $stream.Write($bytes, 0, 1)
41
+ $stream.Close()
42
+ $CorruptDir = "$Work\corrupt"
43
+ New-Item -ItemType Directory -Force -Path $CorruptDir | Out-Null
44
+ $TarProc = Start-Process -FilePath tar -ArgumentList @('-xzf', $CorruptPath, '-C', $CorruptDir.Replace('\','/')) -Wait:$false -PassThru -WindowStyle Hidden
45
+ if (-not $TarProc.WaitForExit(15000)) {
46
+ Stop-Process -Id $TarProc.Id -Force -ErrorAction SilentlyContinue
47
+ Write-Host " PASS: corrupted tarball extraction timed out and was stopped"
48
+ } elseif ($TarProc.ExitCode -eq 0) {
49
+ Write-Host " WARN: corrupted tarball extracted without error (tar gzip recovery)"
50
+ } else {
51
+ Write-Host " PASS: corrupted tarball rejected by tar (exit $($TarProc.ExitCode))"
52
+ }
53
+
54
+ Write-Host "==> Test 4: fresh-extract boot (extension load + distance query)"
55
+ $FreshDir = "$Work\fresh"
56
+ New-Item -ItemType Directory -Force -Path $FreshDir | Out-Null
57
+ & tar -xzf $TarPath -C ($FreshDir.Replace('\','/'))
58
+ if ($LASTEXITCODE -ne 0) { throw "fresh extract failed" }
59
+
60
+ $PgBin = "$FreshDir\bin"
61
+ $Data = "$FreshDir\pgdata"
62
+ $Log = "$FreshDir\pg.log"
63
+ $Port = 55897
64
+
65
+ # Hostile env: minimal PATH (System32 only), no PGROOT/PGDATA
66
+ $SavedPath = $env:PATH
67
+ $SavedPgRoot = $env:PGROOT
68
+ $env:PATH = "$env:SystemRoot\System32;$env:SystemRoot"
69
+ $env:PGROOT = $null
70
+ $env:PGDATA = $null
71
+
72
+ try {
73
+ & "$PgBin\postgres.exe" --version
74
+ if ($LASTEXITCODE -ne 0) { throw "FAIL: postgres --version" }
75
+ & "$PgBin\initdb.exe" -D $Data --username=postgres --auth-local=trust --no-locale -E UTF8 | Out-Null
76
+ if ($LASTEXITCODE -ne 0) { throw "FAIL: initdb" }
77
+ & "$PgBin\pg_ctl.exe" -D $Data -o "-p $Port -h 127.0.0.1" -l $Log -w start
78
+ if ($LASTEXITCODE -ne 0) { Get-Content $Log | Select-Object -Last 30; throw "FAIL: pg_ctl start" }
79
+
80
+ try {
81
+ & "$PgBin\psql.exe" -h 127.0.0.1 -p $Port -U postgres -d postgres -c "CREATE EXTENSION vector;" | Out-Null
82
+ if ($LASTEXITCODE -ne 0) { throw "FAIL: CREATE EXTENSION" }
83
+ $ExtV = & "$PgBin\psql.exe" -h 127.0.0.1 -p $Port -U postgres -d postgres -tAc "SELECT extversion FROM pg_extension WHERE extname='vector';"
84
+ if ($ExtV.Trim() -ne $PgvectorVer) { throw "FAIL: extversion='$ExtV' expected='$PgvectorVer'" }
85
+ Write-Host " PASS: fresh-extract boot + vector extension"
86
+ } finally {
87
+ & "$PgBin\pg_ctl.exe" -D $Data -m fast stop 2>$null | Out-Null
88
+ }
89
+
90
+ Write-Host "==> Test 5: second initdb on initialized dir refused"
91
+ $rc = (Start-Process -FilePath "$PgBin\initdb.exe" -ArgumentList "-D",$Data,"-U","postgres" -Wait -PassThru -NoNewWindow).ExitCode
92
+ if ($rc -eq 0) {
93
+ Write-Host " WARN: second initdb succeeded (unexpected)"
94
+ } else {
95
+ Write-Host " PASS: second initdb refused (exit $rc)"
96
+ }
97
+ } finally {
98
+ $env:PATH = $SavedPath
99
+ $env:PGROOT = $SavedPgRoot
100
+ }
101
+
102
+ Write-Host "==> All negative-path smokes: PASS"
103
+ }
104
+ finally {
105
+ Remove-Item -Recurse -Force $Work -ErrorAction SilentlyContinue
106
+ }
@@ -345,6 +345,23 @@ const grepOut = await executeBuiltinTool('grep', {
345
345
  }, root);
346
346
  assertOk('grep', grepOut, /smoke\.mjs/);
347
347
 
348
+ const grepBracketPathOut = await executeBuiltinTool('grep', {
349
+ pattern: 'tool-smoke',
350
+ path: '[]',
351
+ glob: '*.mjs',
352
+ head_limit: 5,
353
+ }, root);
354
+ assertOk('grep path [] coerces to cwd', grepBracketPathOut, /tool-smoke\.mjs/);
355
+
356
+ const grepRedirectOut = await executeBuiltinTool('grep', {
357
+ pattern: 'assertOk',
358
+ path: 'bogus/wrong/prefix/scripts/tool-smoke.mjs',
359
+ head_limit: 3,
360
+ }, root);
361
+ if (!/^\[redirected from/.test(grepRedirectOut) || !/assertOk/.test(grepRedirectOut)) {
362
+ throw new Error(`grep ENOENT should auto-redirect on unique suffix hit:\n${grepRedirectOut.slice(0, 800)}`);
363
+ }
364
+
348
365
  const redundantAllFilesGlobGrepOut = await executeBuiltinTool('grep', {
349
366
  pattern: 'standalone mixdog CLI/TUI coding agent',
350
367
  glob: '**/*',
@@ -367,6 +384,98 @@ const explicitSrcGlobOut = await executeBuiltinTool('glob', {
367
384
  }, root);
368
385
  assertOk('glob explicit src', explicitSrcGlobOut, /src[\\/].*engine\.mjs/i);
369
386
 
387
+ const globPathOnlyOut = await executeBuiltinTool('glob', {
388
+ path: 'scripts',
389
+ head_limit: 8,
390
+ }, root);
391
+ assertOk('glob path-only default *', globPathOnlyOut, /tool-smoke\.mjs/i);
392
+
393
+ const grepNoPatternGlobOut = await executeBuiltinTool('grep', {
394
+ path: 'scripts',
395
+ glob: 'tool-smoke.mjs',
396
+ head_limit: 5,
397
+ }, root);
398
+ assertOk('grep without pattern routes to glob', grepNoPatternGlobOut, /tool-smoke\.mjs/i);
399
+
400
+ const grepManyPatterns = [
401
+ ...Array.from({ length: 20 }, (_, i) => `__tool_smoke_miss_${i}__`),
402
+ 'tool-smoke',
403
+ ];
404
+ const grepManyPatternsOut = await executeBuiltinTool('grep', {
405
+ pattern: grepManyPatterns,
406
+ path: 'scripts',
407
+ glob: '*.mjs',
408
+ head_limit: 5,
409
+ }, root);
410
+ if (/exceeds the 20-pattern cap/i.test(String(grepManyPatternsOut))) {
411
+ throw new Error(`grep should chunk >20 patterns instead of error:\n${grepManyPatternsOut.slice(0, 400)}`);
412
+ }
413
+ assertOk('grep >20 pattern chunk merge', grepManyPatternsOut, /tool-smoke\.mjs/i);
414
+ if (!/\[pattern set split into 2 chunks\]/.test(String(grepManyPatternsOut))) {
415
+ throw new Error(`grep >20 patterns should emit chunk advisory:\n${grepManyPatternsOut.slice(0, 400)}`);
416
+ }
417
+
418
+ function grepCountTotalMatches(body) {
419
+ const m = String(body).match(/\[total (\d+) match/i);
420
+ return m ? Number(m[1]) : null;
421
+ }
422
+
423
+ const grepCountSingleOut = await executeBuiltinTool('grep', {
424
+ pattern: 'assertOk',
425
+ path: 'scripts/tool-smoke.mjs',
426
+ output_mode: 'count',
427
+ }, root);
428
+ const singleCountTotal = grepCountTotalMatches(grepCountSingleOut);
429
+ if (singleCountTotal == null || singleCountTotal < 1) {
430
+ throw new Error(`grep count baseline failed:\n${grepCountSingleOut.slice(0, 400)}`);
431
+ }
432
+
433
+ const grepCountOverlapPatterns = [
434
+ ...Array.from({ length: 19 }, (_, i) => `__count_overlap_a_${i}__`),
435
+ 'assertOk',
436
+ ...Array.from({ length: 19 }, (_, i) => `__count_overlap_b_${i}__`),
437
+ 'assertOk',
438
+ ];
439
+ const grepCountOverlapOut = await executeBuiltinTool('grep', {
440
+ pattern: grepCountOverlapPatterns,
441
+ path: 'scripts/tool-smoke.mjs',
442
+ output_mode: 'count',
443
+ }, root);
444
+ const overlapCountTotal = grepCountTotalMatches(grepCountOverlapOut);
445
+ if (overlapCountTotal !== singleCountTotal) {
446
+ throw new Error(
447
+ `chunked count must not double-count overlapping lines (single=${singleCountTotal} overlap=${overlapCountTotal}):\n${grepCountOverlapOut.slice(0, 600)}`,
448
+ );
449
+ }
450
+
451
+ const grepChunkContextPatterns = [
452
+ ...Array.from({ length: 20 }, (_, i) => `__ctx_chunk_miss_${i}__`),
453
+ 'grepCountTotalMatches',
454
+ ];
455
+ const grepChunkContextOut = await executeBuiltinTool('grep', {
456
+ pattern: grepChunkContextPatterns,
457
+ path: 'scripts/tool-smoke.mjs',
458
+ '-C': 1,
459
+ head_limit: 30,
460
+ }, root);
461
+ if (!/\[pattern set split into 2 chunks\]/.test(String(grepChunkContextOut))) {
462
+ throw new Error(`chunked -C grep should split patterns:\n${grepChunkContextOut.slice(0, 500)}`);
463
+ }
464
+ if (!/tool-smoke\.mjs:\d+:/.test(String(grepChunkContextOut))) {
465
+ throw new Error(`chunked -C must emit path-prefixed match lines:\n${grepChunkContextOut.slice(0, 800)}`);
466
+ }
467
+ if (!/tool-smoke\.mjs-\d+-/.test(String(grepChunkContextOut))) {
468
+ throw new Error(`chunked -C must keep path-prefixed context lines:\n${grepChunkContextOut.slice(0, 800)}`);
469
+ }
470
+ const ctxBodyLines = String(grepChunkContextOut).split('\n').filter((l) => l && !/^\[/.test(l) && !/^\(no matches\)/.test(l));
471
+ const orphanLineOnlyContext = ctxBodyLines.some((l) => /^\d+-/.test(l));
472
+ if (orphanLineOnlyContext) {
473
+ throw new Error(`chunked -C must not leave line-only context orphans:\n${grepChunkContextOut.slice(0, 800)}`);
474
+ }
475
+ if (!/function grepCountTotalMatches/.test(String(grepChunkContextOut))) {
476
+ throw new Error(`chunked -C should include match span:\n${grepChunkContextOut.slice(0, 800)}`);
477
+ }
478
+
370
479
  const findOut = await executeBuiltinTool('find', {
371
480
  query: 'tool smoke',
372
481
  path: '.',
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "outputStyle": "default",
3
+ "mcpServers": {},
3
4
  "channels": {
4
5
  "promptInjection": {
5
6
  "mode": "hook",
@@ -818,6 +818,10 @@ export async function createMixdogSessionRuntime({
818
818
  }
819
819
 
820
820
  function skillToolContent(name) {
821
+ if (typeof contextMod.isSkillDisabled === 'function' && contextMod.isSkillDisabled(name)) {
822
+ const label = String(name || '').trim() || 'skill';
823
+ return `Error: skill "${label}" is disabled`;
824
+ }
821
825
  const skill = skillContent(name);
822
826
  // Return the general tool envelope so the main/Lead session behaves the
823
827
  // same as agent-loop sessions: the model-visible tool_result is the short
@@ -1596,6 +1600,9 @@ export async function createMixdogSessionRuntime({
1596
1600
  // Channels are opt-in: only boot the worker when this session started in (or
1597
1601
  // was toggled into) remote mode. Non-remote sessions never contend for the
1598
1602
  // channel; see startRemote()/stopRemote() and the `/remote` toggle.
1603
+ // `remote.autoStart` in mixdog-config.json makes every session claim remote
1604
+ // at boot (same force-takeover semantics as `mixdog --remote` / `/remote`).
1605
+ if (!remoteEnabled && config?.remote?.autoStart === true) remoteEnabled = true;
1599
1606
  if (remoteEnabled) startRemote();
1600
1607
 
1601
1608
  function contextStatusCacheKeyFor({ messages, tools }) {
@@ -1657,6 +1664,7 @@ export async function createMixdogSessionRuntime({
1657
1664
  getRoute: () => route,
1658
1665
  getSession: () => session,
1659
1666
  getRemoteEnabled: () => remoteEnabled,
1667
+ adoptConfig,
1660
1668
  saveConfigAndAdopt,
1661
1669
  scheduleBackendSave,
1662
1670
  cfgMod,
@@ -6,29 +6,27 @@ kind: retrieval
6
6
 
7
7
  # Role: explorer
8
8
 
9
- You are a one-shot locator, not a researcher. You find WHERE, never WHY or
10
- HOW: no reading code to understand it, no explaining behaviour, no tracing
11
- logic across files. Coordinates are the entire deliverable.
9
+ You are a one-shot locator, not a researcher. You find WHERE, never WHY
10
+ no understanding code, explaining behaviour, or tracing logic. Coordinates
11
+ are the entire deliverable.
12
12
 
13
- Default shape: ONE tool turn, then the answer. Turn 1 is ONE maximal batch:
14
- a single `grep` carrying ALL literal strings/errors/identifier guesses
15
- (content mode, small `-C`), plus `code_graph`/`find`/`glob` in the same
16
- turn. Fire everything at once; never probe one tool at a time. Any second
17
- turn means turn 1 under-batched.
13
+ Default shape: ONE maximal-batch turn, then the answer. Fire everything at
14
+ once: a single `grep` whose pattern[] carries ALL literal strings/errors/
15
+ identifier guesses synonyms, casings, variants — plus `code_graph`/`find`/
16
+ `glob` in the SAME turn when useful. Width is free, turns are not.
18
17
 
19
- Answer straight from search output: grep/code_graph hits already carry
20
- `path:line` — NEVER spend a turn on `read` just to confirm or polish an
21
- anchor you already have. Prefer user-visible strings and identifiers.
22
- `dist`/generated hits are leads, not answers: trace them back to source.
18
+ Answer straight from search output: hits already carry `path:line` — never
19
+ spend a turn on `read` to confirm or polish an anchor. `dist`/generated
20
+ hits are leads, not answers: trace them to source.
23
21
 
24
- Budget: 5 tool turns; coordinates only, zero analysis. The FIRST
25
- credible `path:line` ends the search — answer in that same turn; do not
26
- verify, refine, or collect extras. A plausible anchor now beats a perfect
27
- anchor later: mark weak ones `?` instead of checking them. Turns 2-5 exist
28
- ONLY for a zero-anchor turn 1: derive new tokens from returned paths/names
29
- or widen scope (no repeated synonyms), batching ALL remaining candidates in
30
- one shot. Past turn 5 you are wasting the caller's time: answer with
31
- best-so-far anchors or `EXPLORATION_FAILED`.
22
+ Budget: 5 turns of maximal batches. Credible `path:line` on screen means the
23
+ search is OVER — answer in that turn; no verifying or collecting extras;
24
+ mark weak anchors `?`. Turns 2-5 exist ONLY for a zero-anchor turn 1: derive
25
+ new tokens from returned paths/names or widen scope, packing ALL candidates
26
+ into the next batch. Concept queries: grep implementation vocabulary
27
+ (file/dir names, symbol fragments, config keys). `EXPLORATION_FAILED` only
28
+ after all 5 turns found zero anchors giving up early is as wrong as
29
+ over-searching; at turn 5 answer best-so-far.
32
30
 
33
31
  Answer format, nothing else:
34
32
  - up to 5 lines: `path:line — symbol/name — short reason` (append `?` if weak)
@@ -1,3 +1,3 @@
1
- # Channels
2
-
3
- - Channel features are handled by the runtime.
1
+ # Channels
2
+
3
+ - Channel features are handled by the runtime.
@@ -81,6 +81,16 @@ export function normalizeProfileConfig(value = {}) {
81
81
  return { title, language };
82
82
  }
83
83
 
84
+ /** Persisted `skills.disabled` name list (deduped, trimmed, sorted). */
85
+ export function normalizeSkillsConfig(value = {}) {
86
+ const raw = value && typeof value === 'object' ? value : {};
87
+ const disabled = Array.isArray(raw.disabled)
88
+ ? [...new Set(raw.disabled.map((n) => String(n).trim()).filter(Boolean))]
89
+ : [];
90
+ disabled.sort((a, b) => a.localeCompare(b));
91
+ return { disabled };
92
+ }
93
+
84
94
  // Look up the catalog entry for a stored language id (defaults to 'system').
85
95
  export function profileLanguageEntry(languageId) {
86
96
  const id = String(languageId || 'system');
@@ -435,6 +445,7 @@ export function loadConfig(options = {}) {
435
445
  workflow: raw.workflow && typeof raw.workflow === 'object' ? { active: String(raw.workflow.active || 'default') } : { active: 'default' },
436
446
  agentMaintenance: { enabled: true, interval: '1h', ...raw.agentMaintenance },
437
447
  profile: normalizeProfileConfig(raw.profile),
448
+ skills: normalizeSkillsConfig(raw.skills),
438
449
  // No idleMs default here: absent idleMs means "provider default"
439
450
  // (config-helpers normalizeAutoClearConfig custom:false path).
440
451
  // Injecting a fixed 1h would mark every config custom:true and
@@ -467,6 +478,7 @@ export function loadConfig(options = {}) {
467
478
  workflow: { active: 'default' },
468
479
  agentMaintenance: { enabled: true, interval: '1h' },
469
480
  profile: normalizeProfileConfig(null),
481
+ skills: normalizeSkillsConfig(null),
470
482
  autoClear: { enabled: true },
471
483
  compaction: {},
472
484
  trajectory: { enabled: true },
@@ -493,6 +505,23 @@ export function loadConfig(options = {}) {
493
505
  * via persistAgentConfig((current) => ({ ...current, <field> })) so a
494
506
  * concurrent instance's edits are not reverted.
495
507
  */
508
+ /** In-lock patch of `skills.disabled` only (avoids whole-config lost-update). */
509
+ export function patchSkillsDisabled(disabledNames) {
510
+ const names = disabledNames instanceof Set
511
+ ? [...disabledNames]
512
+ : (Array.isArray(disabledNames) ? disabledNames : []);
513
+ const nextSkills = normalizeSkillsConfig({ disabled: names });
514
+ persistAgentConfig((current) => {
515
+ const cur = { ...current };
516
+ const target = (cur.agent && cur.agent.providers)
517
+ ? (cur.agent = { ...cur.agent })
518
+ : cur;
519
+ target.skills = nextSkills;
520
+ return cur;
521
+ });
522
+ return nextSkills;
523
+ }
524
+
496
525
  export function saveConfig(config) {
497
526
  // Strip ephemeral defaults from providers but preserve any unknown
498
527
  // per-provider subkey so future schema additions round-trip through the
@@ -544,6 +573,7 @@ export function saveConfig(config) {
544
573
  workflow: config.workflow || { active: 'default' },
545
574
  agentMaintenance: config.agentMaintenance || {},
546
575
  profile: normalizeProfileConfig(config.profile),
576
+ skills: normalizeSkillsConfig(config.skills),
547
577
  autoClear: config.autoClear || {},
548
578
  compaction: config.compaction || {},
549
579
  trajectory: config.trajectory || {},