mixdog 0.9.17 → 0.9.19

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 (129) hide show
  1. package/package.json +3 -2
  2. package/scripts/build-runtime-windows.ps1 +242 -242
  3. package/scripts/output-style-smoke.mjs +2 -2
  4. package/scripts/recall-bench-cases.json +11 -0
  5. package/scripts/recall-bench.mjs +91 -2
  6. package/scripts/recall-usecase-cases.json +1 -1
  7. package/scripts/smoke-runtime-negative.ps1 +106 -106
  8. package/scripts/tool-efficiency-diag.mjs +5 -2
  9. package/scripts/tool-smoke.mjs +101 -27
  10. package/src/agents/debugger/AGENT.md +3 -3
  11. package/src/agents/heavy-worker/AGENT.md +7 -10
  12. package/src/agents/maintainer/AGENT.md +1 -2
  13. package/src/agents/reviewer/AGENT.md +1 -2
  14. package/src/agents/worker/AGENT.md +5 -8
  15. package/src/defaults/agents.json +3 -0
  16. package/src/mixdog-session-runtime.mjs +23 -6
  17. package/src/rules/agent/00-core.md +4 -3
  18. package/src/rules/agent/30-explorer.md +53 -22
  19. package/src/rules/lead/02-channels.md +3 -3
  20. package/src/rules/lead/lead-tool.md +3 -2
  21. package/src/rules/shared/01-tool.md +24 -29
  22. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +1 -1
  23. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
  24. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +24 -3
  25. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +3 -3
  26. package/src/runtime/agent/orchestrator/session/context-utils.mjs +2 -2
  27. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +250 -35
  28. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +6 -4
  29. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +198 -6
  30. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -2
  31. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +13 -15
  32. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +6 -1
  33. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +45 -3
  34. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -2
  35. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +195 -3
  36. package/src/runtime/agent/orchestrator/tools/builtin.mjs +17 -1
  37. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +38 -0
  38. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -4
  39. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +1 -1
  40. package/src/runtime/channels/backends/discord-attachments.mjs +2 -2
  41. package/src/runtime/channels/backends/discord-gateway.mjs +26 -3
  42. package/src/runtime/channels/backends/discord.mjs +139 -7
  43. package/src/runtime/channels/index.mjs +290 -76
  44. package/src/runtime/channels/lib/crash-log.mjs +21 -3
  45. package/src/runtime/channels/lib/inbound-routing.mjs +19 -12
  46. package/src/runtime/channels/lib/memory-client.mjs +32 -14
  47. package/src/runtime/channels/lib/output-forwarder.mjs +156 -14
  48. package/src/runtime/channels/lib/owner-heartbeat.mjs +13 -4
  49. package/src/runtime/channels/lib/runtime-paths.mjs +48 -1
  50. package/src/runtime/channels/lib/tool-dispatch.mjs +17 -7
  51. package/src/runtime/channels/lib/voice-transcription.mjs +4 -2
  52. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +8 -2
  53. package/src/runtime/memory/lib/memory-recall-store.mjs +182 -9
  54. package/src/runtime/memory/lib/query-handlers.mjs +36 -4
  55. package/src/runtime/memory/lib/recall-format.mjs +106 -6
  56. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  57. package/src/runtime/shared/atomic-file.mjs +10 -4
  58. package/src/runtime/shared/background-tasks.mjs +4 -2
  59. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  60. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  61. package/src/runtime/shared/tool-surface.mjs +30 -1
  62. package/src/session-runtime/config-lifecycle.mjs +1 -1
  63. package/src/session-runtime/cwd-plugins.mjs +46 -3
  64. package/src/session-runtime/mcp-glue.mjs +24 -3
  65. package/src/session-runtime/output-styles.mjs +44 -10
  66. package/src/session-runtime/workflow.mjs +16 -1
  67. package/src/standalone/channel-worker.mjs +88 -7
  68. package/src/standalone/explore-tool.mjs +1 -1
  69. package/src/tui/App.jsx +57 -77
  70. package/src/tui/app/channel-pickers.mjs +45 -0
  71. package/src/tui/app/slash-commands.mjs +0 -1
  72. package/src/tui/app/slash-dispatch.mjs +0 -16
  73. package/src/tui/app/transcript-window.mjs +66 -1
  74. package/src/tui/app/use-mouse-input.mjs +9 -2
  75. package/src/tui/app/use-prompt-handlers.mjs +7 -94
  76. package/src/tui/app/use-transcript-scroll.mjs +5 -1
  77. package/src/tui/app/use-transcript-window.mjs +74 -8
  78. package/src/tui/components/PromptInput.jsx +33 -64
  79. package/src/tui/components/ToolExecution.jsx +2 -2
  80. package/src/tui/dist/index.mjs +4744 -4806
  81. package/src/tui/engine.mjs +109 -20
  82. package/src/tui/lib/voice-setup.mjs +166 -0
  83. package/src/tui/paste-attachments.mjs +12 -5
  84. package/src/tui/prompt-history-store.mjs +125 -12
  85. package/scripts/bench/cache-probe-tasks.json +0 -8
  86. package/scripts/bench/lead-review-tasks-r3.json +0 -20
  87. package/scripts/bench/lead-review-tasks.json +0 -20
  88. package/scripts/bench/r4-mixed-tasks.json +0 -20
  89. package/scripts/bench/r5-orchestrated-task.json +0 -7
  90. package/scripts/bench/review-tasks.json +0 -20
  91. package/scripts/bench/round-codex.json +0 -114
  92. package/scripts/bench/round-mixdog-lead-r3.json +0 -269
  93. package/scripts/bench/round-mixdog-lead.json +0 -269
  94. package/scripts/bench/round-mixdog.json +0 -126
  95. package/scripts/bench/round-r10-bigsample.json +0 -679
  96. package/scripts/bench/round-r11-codexalign.json +0 -257
  97. package/scripts/bench/round-r13-clientmeta.json +0 -464
  98. package/scripts/bench/round-r14-betafeatures.json +0 -466
  99. package/scripts/bench/round-r15-fulldefault.json +0 -462
  100. package/scripts/bench/round-r16-sessionid.json +0 -466
  101. package/scripts/bench/round-r17-wirebytes.json +0 -456
  102. package/scripts/bench/round-r18-prewarm.json +0 -468
  103. package/scripts/bench/round-r19-clean.json +0 -472
  104. package/scripts/bench/round-r20-prewarm-clean.json +0 -475
  105. package/scripts/bench/round-r21-delta-retry.json +0 -473
  106. package/scripts/bench/round-r22-full-probe.json +0 -693
  107. package/scripts/bench/round-r23-itemprobe.json +0 -701
  108. package/scripts/bench/round-r24-shapefix.json +0 -677
  109. package/scripts/bench/round-r25-serial.json +0 -464
  110. package/scripts/bench/round-r26-parallel3.json +0 -671
  111. package/scripts/bench/round-r27-parallel10.json +0 -894
  112. package/scripts/bench/round-r28-parallel10-stagger.json +0 -882
  113. package/scripts/bench/round-r29-parallel10-stagger166.json +0 -886
  114. package/scripts/bench/round-r30-instid.json +0 -253
  115. package/scripts/bench/round-r31-upgradeprobe.json +0 -256
  116. package/scripts/bench/round-r32-vs-codex-lead.json +0 -254
  117. package/scripts/bench/round-r33-vs-codex-codex.json +0 -115
  118. package/scripts/bench/round-r34-orchestrated.json +0 -120
  119. package/scripts/bench/round-r35-orchestrated-codex.json +0 -61
  120. package/scripts/bench/round-r36-orchestrated-capped.json +0 -128
  121. package/scripts/bench/round-r4-codex.json +0 -114
  122. package/scripts/bench/round-r4-mixed.json +0 -225
  123. package/scripts/bench/round-r5-gpt-lead.json +0 -259
  124. package/scripts/bench/round-r6-codex.json +0 -114
  125. package/scripts/bench/round-r6-solo.json +0 -257
  126. package/scripts/bench/round-r7-full.json +0 -254
  127. package/scripts/bench/round-r8-fulldefault.json +0 -255
  128. package/src/tui/components/prompt-input/voice-indicator.mjs +0 -39
  129. package/src/tui/lib/voice-recorder.mjs +0 -469
@@ -113,13 +113,87 @@ function scoreTopNContains(lines, substrings, n) {
113
113
  return { perSubstring, hitAtN, mrr, n };
114
114
  }
115
115
 
116
- function evaluateCase(kase, { count, ms, isError }, quality) {
116
+ // Parse leading "[YYYY-MM-DD HH:MM]" timestamps from result lines and check
117
+ // they are non-increasing (newest-first). Returns null when fewer than 2
118
+ // lines carry a parseable timestamp (nothing to order).
119
+ function scoreRecencyOrdered(lines) {
120
+ const tsRe = /^\s*\[(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2})/;
121
+ const headerRe = /^\s*##\s/;
122
+ const parse = (raw) => Date.parse(raw.replace(' ', 'T'));
123
+ // Non-increasing check within a single ordered list of {raw,ts}. Returns
124
+ // the first {prev,cur} pair where cur is newer than prev, else null.
125
+ const firstBreak = (stamps) => {
126
+ for (let i = 1; i < stamps.length; i++) {
127
+ if (Number.isFinite(stamps[i].ts) && Number.isFinite(stamps[i - 1].ts) && stamps[i].ts > stamps[i - 1].ts) {
128
+ return { prev: stamps[i - 1].raw, cur: stamps[i].raw };
129
+ }
130
+ }
131
+ return null;
132
+ };
133
+ const hasSessions = lines.some((l) => headerRe.test(l));
134
+ if (!hasSessions) {
135
+ // Ungrouped: single global non-increasing check over all timestamped lines.
136
+ const stamps = [];
137
+ for (const line of lines) {
138
+ const m = tsRe.exec(line);
139
+ if (m) stamps.push({ raw: m[1], ts: parse(m[1]) });
140
+ }
141
+ if (stamps.length < 2) return { parsed: stamps.length, groups: 0, ordered: true, firstViolation: null };
142
+ const b = firstBreak(stamps);
143
+ return { parsed: stamps.length, groups: 0, ordered: b === null, firstViolation: b };
144
+ }
145
+ // Session-grouped: partition timestamped lines by their "## session" header.
146
+ // Timestamps only compared WITHIN a group; groups themselves must descend by
147
+ // their first line's timestamp.
148
+ const groups = [];
149
+ let cur = null;
150
+ for (const line of lines) {
151
+ if (headerRe.test(line)) { cur = { stamps: [] }; groups.push(cur); continue; }
152
+ const m = tsRe.exec(line);
153
+ if (m && cur) cur.stamps.push({ raw: m[1], ts: parse(m[1]) });
154
+ }
155
+ const nonEmpty = groups.filter((g) => g.stamps.length);
156
+ const parsed = nonEmpty.reduce((s, g) => s + g.stamps.length, 0);
157
+ let firstViolation = null;
158
+ for (const g of nonEmpty) {
159
+ const b = firstBreak(g.stamps);
160
+ if (b) { firstViolation = { ...b, scope: 'within-session' }; break; }
161
+ }
162
+ if (!firstViolation) {
163
+ const heads = nonEmpty.map((g) => g.stamps[0]);
164
+ const b = firstBreak(heads);
165
+ if (b) firstViolation = { ...b, scope: 'across-sessions' };
166
+ }
167
+ return { parsed, groups: nonEmpty.length, ordered: firstViolation === null, firstViolation };
168
+ }
169
+
170
+ // Score expect.allContain: every result line must contain at least one of the
171
+ // given substrings (case-insensitive). Returns offending lines (matching none).
172
+ // Use for negative cases where rows legitimately mention the term.
173
+ function scoreAllContain(lines, substrings) {
174
+ const needles = substrings.map((s) => String(s || '').toLowerCase()).filter(Boolean);
175
+ const offenders = lines.filter((line) => {
176
+ const l = line.toLowerCase();
177
+ return !needles.some((n) => l.includes(n));
178
+ });
179
+ return { needles: substrings, offenders, ok: offenders.length === 0 };
180
+ }
181
+
182
+ function evaluateCase(kase, { count, ms, isError }, quality, recency, allContain) {
117
183
  const warnings = [];
118
184
  if (isError) warnings.push('error result');
119
185
  if (ms > WARN_LATENCY_MS) warnings.push(`latency ${ms}ms > ${WARN_LATENCY_MS}ms`);
120
186
  if ((kase.expect === 'browse' || kase.expect === 'idlookup') && count === 0) {
121
187
  warnings.push('0 results for a browse/id-lookup case (expected data present)');
122
188
  }
189
+ if (kase.expect === 'empty' && count > 0) {
190
+ warnings.push(`expected empty but got ${count} result(s) — possible filler/unrelated match`);
191
+ }
192
+ if (allContain) {
193
+ for (const line of allContain.offenders) {
194
+ warnings.push(`allContain miss: result line contains none of [${allContain.needles.join(', ')}]: "${line}"`);
195
+ }
196
+ }
123
197
  if (quality) {
124
198
  for (const p of quality.perSubstring) {
125
199
  if (!p.hit) {
@@ -129,6 +203,10 @@ function evaluateCase(kase, { count, ms, isError }, quality) {
129
203
  }
130
204
  }
131
205
  }
206
+ if (recency && !recency.ordered && recency.firstViolation) {
207
+ const v = recency.firstViolation;
208
+ warnings.push(`recencyOrdered violation (${v.scope || 'global'}): ${v.cur} newer than prior ${v.prev}`);
209
+ }
132
210
  const status = warnings.length ? 'WARN' : 'PASS';
133
211
  return { status, warnings };
134
212
  }
@@ -156,7 +234,10 @@ async function runCase(memoryModule, kase) {
156
234
  const topNContains = expectObj && Array.isArray(expectObj.topNContains) ? expectObj.topNContains : null;
157
235
  const cutoffN = expectObj && Number.isInteger(expectObj.topN) ? expectObj.topN : 5;
158
236
  const quality = topNContains ? scoreTopNContains(resultLines(text), topNContains, cutoffN) : null;
159
- const evalResult = evaluateCase({ ...kase, expect: expectKind }, { count, ms, isError }, quality);
237
+ const recency = expectObj && expectObj.recencyOrdered ? scoreRecencyOrdered(resultLines(text)) : null;
238
+ const allContainNeedles = expectObj && Array.isArray(expectObj.allContain) ? expectObj.allContain : null;
239
+ const allContain = allContainNeedles ? scoreAllContain(resultLines(text), allContainNeedles) : null;
240
+ const evalResult = evaluateCase({ ...kase, expect: expectKind }, { count, ms, isError }, quality, recency, allContain);
160
241
  return {
161
242
  id: kase.id,
162
243
  label: kase.label,
@@ -167,6 +248,8 @@ async function runCase(memoryModule, kase) {
167
248
  errMsg,
168
249
  top3: topN(text, 3),
169
250
  quality,
251
+ recency,
252
+ allContain,
170
253
  status: evalResult.status,
171
254
  warnings: evalResult.warnings,
172
255
  };
@@ -182,6 +265,9 @@ function printCase(row) {
182
265
  process.stdout.write(` substr "${p.needle}" -> rank ${p.rank ?? 'none'}${p.hit ? '' : ' (miss)'}\n`);
183
266
  }
184
267
  }
268
+ if (row.recency) {
269
+ process.stdout.write(` recency: ${row.recency.ordered ? 'ordered' : 'OUT-OF-ORDER'} (${row.recency.parsed} timestamped lines)\n`);
270
+ }
185
271
  if (row.top3.length) {
186
272
  for (const line of row.top3) process.stdout.write(` - ${line}\n`);
187
273
  } else {
@@ -218,6 +304,7 @@ function printSummary(rows) {
218
304
  async function main() {
219
305
  const casesPath = argValue('cases', DEFAULT_CASES_PATH);
220
306
  const jsonMode = hasFlag('json');
307
+ const strict = hasFlag('strict');
221
308
  const cases = loadCases(casesPath);
222
309
 
223
310
  let memoryModule;
@@ -262,6 +349,8 @@ async function main() {
262
349
 
263
350
  const hardErrors = rows.filter((r) => r.isError);
264
351
  if (hardErrors.length) process.exitCode = 1;
352
+ // --strict: any WARN row fails the run. Default behavior (errors-only) unchanged.
353
+ if (strict && rows.some((r) => r.status === 'WARN')) process.exitCode = 1;
265
354
  }
266
355
 
267
356
  main().catch((e) => {
@@ -15,4 +15,4 @@
15
15
  { "id": "uc5-yesterday", "label": "UC5 calendar yesterday", "args": { "period": "yesterday", "limit": 10 }, "expect": "browse" },
16
16
  { "id": "uc5-thisweek", "label": "UC5 calendar this_week", "args": { "period": "this_week", "limit": 10 }, "expect": "browse" },
17
17
  { "id": "uc6-category", "label": "UC6 category decision 7d", "args": { "period": "7d", "category": "decision", "limit": 10 }, "expect": "browse" }
18
- ]
18
+ ]
@@ -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
+ }
@@ -66,7 +66,10 @@ for (const target of ['heavy-worker', 'worker']) {
66
66
  }
67
67
  if (n === 'grep' && ['content', 'content_with_context'].includes(String(a.output_mode || ''))) {
68
68
  grepContent++;
69
- if (a['-C'] != null || a['-A'] != null || a['-B'] != null) grepContentWithCtx++;
69
+ // Implicit context: output_mode content_with_context carries context
70
+ // without explicit -C/-A/-B flags.
71
+ if (a['-C'] != null || a['-A'] != null || a['-B'] != null
72
+ || String(a.output_mode) === 'content_with_context') grepContentWithCtx++;
70
73
  const gpath = typeof a.path === 'string' ? a.path : JSON.stringify(a.path || '');
71
74
  for (let j = i + 1; j < Math.min(i + 4, tools.length); j++) {
72
75
  const u = tools[j];
@@ -85,4 +88,4 @@ for (const target of ['heavy-worker', 'worker']) {
85
88
  console.log(` single-call batches: ${batch1}/${batchTot} (${pct(batch1, batchTot)}%)`);
86
89
  console.log(` content greps with -C/-A/-B: ${grepContentWithCtx}/${grepContent} (${pct(grepContentWithCtx, grepContent)}%) grep->read follow-ups: ${grepThenRead}`);
87
90
  console.log(` same-path re-reads >=3x: ${rereads}`);
88
- }
91
+ }
@@ -508,8 +508,8 @@ if (!/^read 2\b/m.test(String(readRegionBatchOut))
508
508
  || (String(readRegionBatchOut).match(/scripts\/smoke\.mjs \[full\] \[ok\]/g) || []).length < 2
509
509
  || !/1→import \{ spawnSync \}/.test(String(readRegionBatchOut))
510
510
  || !/3→import \{ fileURLToPath \}/.test(String(readRegionBatchOut))
511
- || !/pass offset:2 to continue/.test(String(readRegionBatchOut))
512
- || !/pass offset:4 to continue/.test(String(readRegionBatchOut))) {
511
+ || !/(pass offset:2 to continue|ONE window: offset:2, limit:\d+)/.test(String(readRegionBatchOut))
512
+ || !/(pass offset:4 to continue|ONE window: offset:4, limit:\d+)/.test(String(readRegionBatchOut))) {
513
513
  throw new Error(`read region batch must preserve both requested spans:\n${readRegionBatchOut}`);
514
514
  }
515
515
 
@@ -534,6 +534,46 @@ if (readStringifiedLineErr || readStringifiedLineArgs.path[0].offset !== 7 || re
534
534
  throw new Error(`read guard must losslessly convert legacy line/context inside stringified arrays to offset/limit: err=${readStringifiedLineErr} args=${JSON.stringify(readStringifiedLineArgs)}`);
535
535
  }
536
536
 
537
+ // Absorb shape 1: region array + top-level offset/limit → top-level becomes
538
+ // the default window for regions that lack their own; no hard error.
539
+ const readRegionPlusTopLevelArgs = {
540
+ path: [{ path: 'scripts/smoke.mjs', offset: 3, limit: 4 }, { path: 'scripts/smoke.mjs' }],
541
+ offset: 0,
542
+ limit: 2,
543
+ };
544
+ const readRegionPlusTopLevelErr = validateBuiltinArgs('read', readRegionPlusTopLevelArgs);
545
+ if (readRegionPlusTopLevelErr
546
+ || 'offset' in readRegionPlusTopLevelArgs || 'limit' in readRegionPlusTopLevelArgs
547
+ || readRegionPlusTopLevelArgs.path[0].offset !== 3 || readRegionPlusTopLevelArgs.path[0].limit !== 4
548
+ || readRegionPlusTopLevelArgs.path[1].offset !== 0 || readRegionPlusTopLevelArgs.path[1].limit !== 2) {
549
+ throw new Error(`read guard must absorb region-array + top-level offset/limit: err=${readRegionPlusTopLevelErr} args=${JSON.stringify(readRegionPlusTopLevelArgs)}`);
550
+ }
551
+
552
+ // Absorb shape 2: parallel offset/limit as JSON-stringified arrays with path[]
553
+ // → zipped into per-file region objects (pairwise recovery), no int error.
554
+ const readZipWindowArgs = {
555
+ path: ['scripts/smoke.mjs', 'scripts/smoke.mjs'],
556
+ offset: '[0, 5]',
557
+ limit: '[2, 3]',
558
+ };
559
+ const readZipWindowErr = validateBuiltinArgs('read', readZipWindowArgs);
560
+ if (readZipWindowErr || !Array.isArray(readZipWindowArgs.path)
561
+ || readZipWindowArgs.path[0].offset !== 0 || readZipWindowArgs.path[0].limit !== 2
562
+ || readZipWindowArgs.path[1].offset !== 5 || readZipWindowArgs.path[1].limit !== 3
563
+ || 'offset' in readZipWindowArgs || 'limit' in readZipWindowArgs) {
564
+ throw new Error(`read guard must zip stringified offset/limit arrays onto path[]: err=${readZipWindowErr} args=${JSON.stringify(readZipWindowArgs)}`);
565
+ }
566
+
567
+ // Absorb shape 3: code_graph file/files as a JSON-stringified array → parsed to
568
+ // a real array before lookup (dispatched into files[]).
569
+ const cgStringifiedFileArgs = { mode: 'symbols', file: JSON.stringify(['a.mjs', 'b.mjs']) };
570
+ const cgStringifiedFileErr = validateBuiltinArgs('code_graph', cgStringifiedFileArgs);
571
+ if (cgStringifiedFileErr || 'file' in cgStringifiedFileArgs
572
+ || !Array.isArray(cgStringifiedFileArgs.files)
573
+ || cgStringifiedFileArgs.files[0] !== 'a.mjs' || cgStringifiedFileArgs.files[1] !== 'b.mjs') {
574
+ throw new Error(`code_graph guard must parse JSON-stringified file array: err=${cgStringifiedFileErr} args=${JSON.stringify(cgStringifiedFileArgs)}`);
575
+ }
576
+
537
577
  const graphOut = await executeCodeGraphTool('code_graph', {
538
578
  mode: 'symbols',
539
579
  file: 'scripts/smoke.mjs',
@@ -549,6 +589,17 @@ if (!/# symbol_search executeBuiltinTool\b/.test(String(graphSymbolBatchOut)) ||
549
589
  throw new Error(`code_graph symbol_search symbols[] batch execution failed:\n${graphSymbolBatchOut}`);
550
590
  }
551
591
 
592
+ // Absorb shape 3 (real dispatch): file as a JSON-stringified array batches per
593
+ // file instead of hitting "file not found: [...]".
594
+ const graphStringifiedFileOut = await executeCodeGraphTool('code_graph', {
595
+ mode: 'symbols',
596
+ file: JSON.stringify(['scripts/smoke.mjs']),
597
+ }, root);
598
+ if (/file not found/.test(String(graphStringifiedFileOut))
599
+ || !/binding|spawnSync|symbol/i.test(String(graphStringifiedFileOut))) {
600
+ throw new Error(`code_graph must parse JSON-stringified file array before lookup:\n${graphStringifiedFileOut}`);
601
+ }
602
+
552
603
  const graphMissingFileOut = await executeCodeGraphTool('code_graph', {
553
604
  mode: 'symbols',
554
605
  file: 'src/runtime/loop.mjs',
@@ -634,6 +685,33 @@ if (!/^Error[\s:[]/.test(String(shellTimeoutOut)) || !/\[timeout: 500ms\b/.test(
634
685
  throw new Error(`bash timeout must be milliseconds and classified as Error:\n${shellTimeoutOut}`);
635
686
  }
636
687
 
688
+ // Auto-promotion: a sync foreground command still running past the (soft)
689
+ // promotion budget is detached into a tracked background task and returns the
690
+ // same task_id envelope as explicit async — the caller never pre-chose async.
691
+ // Shrink the budget via MIXDOG_SHELL_AUTO_BACKGROUND_MS so the smoke stays fast.
692
+ const _priorAutoBgBudget = process.env.MIXDOG_SHELL_AUTO_BACKGROUND_MS;
693
+ process.env.MIXDOG_SHELL_AUTO_BACKGROUND_MS = '800';
694
+ let shellAutoPromoteOut;
695
+ try {
696
+ shellAutoPromoteOut = await executeBuiltinTool('shell', {
697
+ command: 'Start-Sleep -Seconds 6; Write-Output tool-smoke-autopromote-done',
698
+ cwd: root,
699
+ timeout: 30_000,
700
+ shell: 'powershell',
701
+ }, root);
702
+ } finally {
703
+ if (_priorAutoBgBudget === undefined) delete process.env.MIXDOG_SHELL_AUTO_BACKGROUND_MS;
704
+ else process.env.MIXDOG_SHELL_AUTO_BACKGROUND_MS = _priorAutoBgBudget;
705
+ }
706
+ if (!/auto-backgrounded/i.test(String(shellAutoPromoteOut)) || !/task_id:\s*\S+/i.test(String(shellAutoPromoteOut))) {
707
+ throw new Error(`shell auto-promotion must return a background task envelope (task_id + auto-backgrounded):\n${shellAutoPromoteOut}`);
708
+ }
709
+ // Clean up the promoted job so it doesn't outlive the smoke as an orphan.
710
+ const _autoPromoteTaskId = (/task_id:\s*(\S+)/i.exec(String(shellAutoPromoteOut)) || [])[1];
711
+ if (_autoPromoteTaskId) {
712
+ try { await executeBuiltinTool('task', { action: 'cancel', task_id: _autoPromoteTaskId }, root); } catch {}
713
+ }
714
+
637
715
  const legacyEscapedAlternationErr = validateBuiltinArgs('grep', { pattern: 'state\\.items\\.map\\|items\\.map', path: root });
638
716
  if (legacyEscapedAlternationErr) {
639
717
  throw new Error(`grep legacy \\| alternation should be accepted: ${legacyEscapedAlternationErr}`);
@@ -1217,8 +1295,10 @@ setInternalToolsProvider({
1217
1295
  if (workerToolNames.includes('tool_search')) {
1218
1296
  throw new Error(`agent session schema must not expose deferred tool_search: ${workerToolNames.join(', ')}`);
1219
1297
  }
1220
- if (workerToolNames.includes('shell')) {
1221
- throw new Error(`read-write agent session schema must not expose shell: ${workerToolNames.join(', ')}`);
1298
+ for (const name of ['shell', 'task']) {
1299
+ if (!workerToolNames.includes(name)) {
1300
+ throw new Error(`read-write agent session schema must expose ${name} for self-verification: ${workerToolNames.join(', ')}`);
1301
+ }
1222
1302
  }
1223
1303
  for (const name of ['skills_list', 'skill_view', 'skill_execute']) {
1224
1304
  if (workerToolNames.includes(name)) {
@@ -1266,7 +1346,7 @@ setInternalToolsProvider({
1266
1346
  const fullTools = (fullAgentSession.tools || []).map((tool) => tool?.name).filter(Boolean);
1267
1347
  const publicExploreTools = (publicExploreSession.tools || []).map((tool) => tool?.name).filter(Boolean);
1268
1348
  const expectedReadTools = ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'explore', 'search', 'web_fetch', 'Skill'];
1269
- const expectedWriteTools = ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'apply_patch', 'explore', 'search', 'web_fetch', 'Skill'];
1349
+ const expectedWriteTools = ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'apply_patch', 'shell', 'task', 'explore', 'search', 'web_fetch', 'Skill'];
1270
1350
  if (JSON.stringify(readTools) !== JSON.stringify(expectedReadTools)) {
1271
1351
  throw new Error(`read agent schema must be fixed allow-list: expected=${expectedReadTools.join(', ')} actual=${readTools.join(', ')}`);
1272
1352
  }
@@ -1276,20 +1356,17 @@ setInternalToolsProvider({
1276
1356
  if (readTools.includes('tool_search') || writeTools.includes('tool_search')) {
1277
1357
  throw new Error(`agent session fixed schemas must omit tool_search: read=${readTools.join(', ')} write=${writeTools.join(', ')}`);
1278
1358
  }
1279
- if (readTools.includes('shell') || writeTools.includes('shell')) {
1280
- throw new Error(`read/read-write agent schemas must omit shell: read=${readTools.join(', ')} write=${writeTools.join(', ')}`);
1359
+ if (readTools.includes('shell')) {
1360
+ throw new Error(`read agent schema must omit shell: read=${readTools.join(', ')}`);
1281
1361
  }
1282
1362
  for (const name of ['shell', 'apply_patch', 'task']) {
1283
1363
  if (readTools.includes(name)) {
1284
1364
  throw new Error(`read agent schema must omit non-read tool ${name}: read=${readTools.join(', ')}`);
1285
1365
  }
1286
1366
  }
1287
- for (const name of ['apply_patch', 'task']) {
1288
- if (name === 'apply_patch' && !writeTools.includes(name)) {
1289
- throw new Error(`read-write agent schema must preserve apply_patch: write=${writeTools.join(', ')}`);
1290
- }
1291
- if (name !== 'apply_patch' && writeTools.includes(name)) {
1292
- throw new Error(`read-write agent schema must omit non-edit tool ${name}: write=${writeTools.join(', ')}`);
1367
+ for (const name of ['apply_patch', 'shell', 'task']) {
1368
+ if (!writeTools.includes(name)) {
1369
+ throw new Error(`read-write agent schema must preserve ${name}: write=${writeTools.join(', ')}`);
1293
1370
  }
1294
1371
  }
1295
1372
  for (const name of ['memory', 'recall', 'reply']) {
@@ -1334,7 +1411,7 @@ setInternalToolsProvider({
1334
1411
  try {
1335
1412
  const resumed = await resumeSession(resumeAgentSession.id, 'full');
1336
1413
  const resumedTools = (resumed?.tools || []).map((tool) => tool?.name).filter(Boolean);
1337
- const expectedWriteTools = ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'apply_patch', 'explore', 'search', 'web_fetch', 'Skill'];
1414
+ const expectedWriteTools = ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'apply_patch', 'shell', 'task', 'explore', 'search', 'web_fetch', 'Skill'];
1338
1415
  if (JSON.stringify(resumedTools) !== JSON.stringify(expectedWriteTools)) {
1339
1416
  throw new Error(`resumed read-write agent schema must keep fixed allow-list: expected=${expectedWriteTools.join(', ')} actual=${resumedTools.join(', ')}`);
1340
1417
  }
@@ -1522,7 +1599,7 @@ const readPathDescription = readPathSchema.description || '';
1522
1599
  if (!/File path/i.test(readPathDescription) || !/\{path,offset,limit\}\[\]/i.test(readPathDescription) || !/Pass arrays directly/i.test(readPathDescription) || !/legacy recovery only/i.test(readPathDescription)) {
1523
1600
  throw new Error('read schema must keep directory-vs-file guidance');
1524
1601
  }
1525
- if (!/Dirs use list/i.test((BUILTIN_TOOLS.find((tool) => tool.name === 'read')?.description) || '')) {
1602
+ if (!/Not for directory listing/i.test((BUILTIN_TOOLS.find((tool) => tool.name === 'read')?.description) || '')) {
1526
1603
  throw new Error('read description must keep directory-vs-file guidance');
1527
1604
  }
1528
1605
  const readTool = BUILTIN_TOOLS.find((tool) => tool.name === 'read');
@@ -1533,7 +1610,7 @@ const readArrayItemAnyOf = readArraySchema?.items?.anyOf || [];
1533
1610
  if (!readArrayItemAnyOf.some((entry) => entry?.type === 'object' && entry?.properties?.offset && entry?.properties?.limit)) {
1534
1611
  throw new Error('read schema must expose array-of-region objects for batched spans');
1535
1612
  }
1536
- if (/line\+context/i.test(readDescription) || !/numeric offset\+limit/i.test(readDescription) || !/Batch paths\/regions as real arrays/i.test(readDescription) || !/grep content_with_context or code_graph anchors/i.test(readDescription)) {
1613
+ if (/line\+context/i.test(readDescription) || !/numeric offset\+limit/i.test(readDescription) || !/Batch paths\/regions as real arrays/i.test(readDescription)) {
1537
1614
  throw new Error('read description must expose offset/limit as the single window form');
1538
1615
  }
1539
1616
  if (readProps.line || readProps.context) {
@@ -1596,9 +1673,6 @@ if (codeGraphSymbolSearchErr) {
1596
1673
  if (!/code structure\/flow/i.test(codeGraphDescription) || !/symbols\/references\/calls\/deps/i.test(codeGraphDescription)) {
1597
1674
  throw new Error('code_graph description must stay structure-oriented and name its symbol modes');
1598
1675
  }
1599
- if (!/before text grep/i.test(codeGraphDescription)) {
1600
- throw new Error('code_graph description must steer symbol/caller lookups to structure lookup before grep');
1601
- }
1602
1676
  if (!/repo-local/i.test(codeGraphDescription) || !/NOT web search|not web/i.test(codeGraphDescription)) {
1603
1677
  throw new Error('code_graph description must mark itself repo-local (not web search)');
1604
1678
  }
@@ -1853,23 +1927,23 @@ const grepPathDescription = grepTool?.inputSchema?.properties?.path?.description
1853
1927
  const grepGlobDescription = grepTool?.inputSchema?.properties?.glob?.description || '';
1854
1928
  const grepOutputModeDescription = grepTool?.inputSchema?.properties?.output_mode?.description || '';
1855
1929
  const grepHeadLimitDescription = grepTool?.inputSchema?.properties?.head_limit?.description || '';
1856
- if (!/synonyms in pattern\[\]/i.test(grepPatternDescription) || !/OR in ONE grep/i.test(grepPatternDescription) || !/serial rewording/i.test(grepPatternDescription) || !/equivalent repeats/i.test(grepPatternDescription) || !/Narrowest file\/dir/i.test(grepPathDescription) || !/broad scopes return paths first/i.test(grepPathDescription) || !/refine from returned paths/i.test(grepPathDescription)) {
1930
+ if (!/Array = variants in one call/i.test(grepPatternDescription) || !/Known file or dir/i.test(grepPathDescription)) {
1857
1931
  throw new Error('grep schema must keep compact pattern/path guidance');
1858
1932
  }
1859
- if (!/same grep/i.test(grepGlobDescription) || !/no follow-up grep/i.test(grepGlobDescription) || !/equivalent scope changes/i.test(grepGlobDescription)) {
1860
- throw new Error('grep glob schema must steer narrowing into the same grep call');
1933
+ if (!/narrow scope/i.test(grepGlobDescription)) {
1934
+ throw new Error('grep glob schema must describe scope narrowing');
1861
1935
  }
1862
- if (!/Broad scope: files_with_matches\/count/i.test(grepOutputModeDescription) || !/Narrow scope: content_with_context/i.test(grepOutputModeDescription) || !/answer from/i.test(grepOutputModeDescription) || !/skip read/i.test(grepOutputModeDescription) || !/span is not shown/i.test(grepOutputModeDescription)) {
1863
- throw new Error('grep output_mode schema must steer content_with_context away from follow-up reads');
1936
+ if (!/files_with_matches\/count/i.test(grepOutputModeDescription) || !/content_with_context/i.test(grepOutputModeDescription)) {
1937
+ throw new Error('grep output_mode schema must name its output shapes');
1864
1938
  }
1865
- if (grepTool?.inputSchema?.properties?.head_limit?.minimum !== 0 || !/keep small/i.test(grepHeadLimitDescription)) {
1939
+ if (grepTool?.inputSchema?.properties?.head_limit?.minimum !== 0 || !/Max results/i.test(grepHeadLimitDescription)) {
1866
1940
  throw new Error('grep head_limit schema must keep locator caps explicit');
1867
1941
  }
1868
1942
  if (grepTool?.inputSchema?.properties?.type) {
1869
1943
  throw new Error('grep type schema must stay hidden; prefer glob for extension narrowing');
1870
1944
  }
1871
- if (!/code flow before text search/i.test(codeGraphProps.mode?.description || '') || !/one anchor is enough/i.test(codeGraphProps.symbols?.description || '') || !/one call/i.test(codeGraphProps.symbols?.description || '')) {
1872
- throw new Error('code_graph schema fields must steer away from repeated lookup loops');
1945
+ if (!/Repo-local/i.test(codeGraphProps.mode?.description || '') || !/one call/i.test(codeGraphProps.symbols?.description || '')) {
1946
+ throw new Error('code_graph schema fields must stay compact and repo-local');
1873
1947
  }
1874
1948
 
1875
1949
  const longToolSearchText = compactToolSearchDescription(`${patchDescription}\n${patchDescription}`);
@@ -8,7 +8,7 @@ Root-cause analysis agent.
8
8
 
9
9
  Smallest confirmed cause chain before fixes. Return likely cause, evidence
10
10
  (`file:line`), smallest next check/fix. Mark confirmed facts vs inferences;
11
- avoid broad speculation. Fragments only; no narration, no report bloat.
11
+ avoid broad speculation.
12
12
 
13
- Converge, don't sweep: when new reads stop adding evidence, report the best
14
- cause chain so far — a partial/blocked report is a valid completion.
13
+ Converge, don't sweep: when new evidence stops accruing, report the best
14
+ cause chain so far.