mixdog 0.9.23 → 0.9.24

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 (84) hide show
  1. package/package.json +1 -1
  2. package/scripts/boot-smoke.mjs +1 -1
  3. package/scripts/build-runtime-windows.ps1 +242 -242
  4. package/scripts/channel-daemon-smoke.mjs +327 -9
  5. package/scripts/channel-daemon-stub.mjs +12 -1
  6. package/scripts/debounced-skills-async-save-test.mjs +57 -0
  7. package/scripts/explore-bench-tmp.mjs +17 -0
  8. package/scripts/find-fuzzy-hidden-test.mjs +145 -0
  9. package/scripts/mcp-grace-deferred-test.mjs +149 -0
  10. package/scripts/recall-usecase-cases.json +1 -1
  11. package/scripts/smoke-runtime-negative.ps1 +106 -106
  12. package/scripts/tool-efficiency-diag.mjs +1 -1
  13. package/scripts/tool-smoke.mjs +38 -30
  14. package/src/rules/agent/30-explorer.md +6 -0
  15. package/src/rules/lead/02-channels.md +3 -3
  16. package/src/rules/shared/01-tool.md +11 -4
  17. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +76 -1
  18. package/src/runtime/agent/orchestrator/config.mjs +33 -7
  19. package/src/runtime/agent/orchestrator/context/collect.mjs +43 -8
  20. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +39 -0
  21. package/src/runtime/agent/orchestrator/mcp/client.mjs +145 -31
  22. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +38 -1
  23. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +39 -1
  24. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +24 -7
  25. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +11 -1
  26. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +7 -3
  27. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +40 -6
  28. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +2 -1
  29. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +13 -5
  30. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +62 -24
  31. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -6
  32. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +20 -0
  33. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +34 -3
  34. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +220 -27
  35. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +61 -0
  36. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +43 -16
  37. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +54 -5
  38. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +97 -54
  39. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +49 -31
  40. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +35 -2
  41. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +70 -21
  42. package/src/runtime/channels/lib/crash-log.mjs +4 -2
  43. package/src/runtime/channels/lib/output-forwarder.mjs +2 -1
  44. package/src/runtime/channels/lib/owned-runtime.mjs +65 -180
  45. package/src/runtime/channels/lib/owner-heartbeat.mjs +9 -13
  46. package/src/runtime/channels/lib/runtime-paths.mjs +6 -6
  47. package/src/runtime/channels/lib/tool-dispatch.mjs +9 -17
  48. package/src/runtime/channels/lib/tool-format.mjs +7 -2
  49. package/src/runtime/channels/lib/worker-main.mjs +9 -28
  50. package/src/runtime/memory/lib/query-handlers.mjs +4 -1
  51. package/src/runtime/memory/lib/recall-format.mjs +7 -3
  52. package/src/runtime/memory/tool-defs.mjs +1 -1
  53. package/src/runtime/shared/atomic-file.mjs +130 -2
  54. package/src/runtime/shared/background-tasks.mjs +1 -1
  55. package/src/runtime/shared/config.mjs +53 -1
  56. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  57. package/src/runtime/shared/tool-surface.mjs +19 -0
  58. package/src/runtime/shared/update-checker.mjs +3 -0
  59. package/src/runtime/shared/user-data-guard.mjs +66 -0
  60. package/src/session-runtime/config-lifecycle.mjs +175 -15
  61. package/src/session-runtime/mcp-glue.mjs +30 -0
  62. package/src/session-runtime/runtime-core.mjs +91 -7
  63. package/src/session-runtime/session-turn-api.mjs +42 -16
  64. package/src/session-runtime/tool-catalog.mjs +44 -0
  65. package/src/standalone/channel-admin.mjs +32 -3
  66. package/src/standalone/channel-daemon-client.mjs +3 -1
  67. package/src/standalone/channel-daemon-transport.mjs +202 -8
  68. package/src/standalone/channel-daemon.mjs +54 -17
  69. package/src/standalone/channel-worker.mjs +18 -7
  70. package/src/standalone/explore-tool.mjs +87 -15
  71. package/src/tui/App.jsx +2 -2
  72. package/src/tui/components/StatusLine.jsx +3 -3
  73. package/src/tui/components/ToolExecution.jsx +14 -2
  74. package/src/tui/components/TranscriptItem.jsx +1 -1
  75. package/src/tui/dist/index.mjs +209 -44
  76. package/src/tui/engine/agent-job-feed.mjs +5 -0
  77. package/src/tui/engine/notification-plan.mjs +5 -0
  78. package/src/tui/engine/session-api.mjs +6 -1
  79. package/src/tui/engine/tool-card-results.mjs +14 -5
  80. package/src/tui/engine/turn.mjs +9 -2
  81. package/src/tui/engine.mjs +31 -12
  82. package/src/ui/statusline-agents.mjs +36 -0
  83. package/src/ui/statusline.mjs +15 -5
  84. package/src/runtime/channels/lib/seat-lock.mjs +0 -196
@@ -0,0 +1,149 @@
1
+ // Regression tests for turn-time deferred-manifest construction (claude-code
2
+ // style). An MCP server that finishes its handshake BETWEEN session-create and
3
+ // the user's FIRST send is folded — on the first turn, synchronously, with no
4
+ // boot await — into the INITIAL <available-deferred-tools> manifest (BP1) and
5
+ // pre-marked announced, so the first-turn tool-catalog reconcile emits NO late
6
+ // <system-reminder> for it. A server that connects AFTER the first turn keeps
7
+ // the append-only late-reminder path. Unit-style: exercise the tool-catalog
8
+ // exports directly (no runtime, no spawn) the way session-turn-api/runtime-core
9
+ // wire them.
10
+ import test from 'node:test';
11
+ import assert from 'node:assert/strict';
12
+ import {
13
+ applyDeferredToolSurface,
14
+ refreshInitialDeferredMcpSurface,
15
+ reconcileDeferredMcpToolCatalog,
16
+ } from '../src/session-runtime/tool-catalog.mjs';
17
+
18
+ function baseSession() {
19
+ return {
20
+ provider: 'legacy',
21
+ tools: [{ name: 'read', description: 'read a file' }, { name: 'grep', description: 'search' }],
22
+ messages: [{ role: 'system', content: 'BASE PROMPT' }],
23
+ };
24
+ }
25
+
26
+ // A freshly-created session: the create-time surface is baked WITHOUT any MCP
27
+ // (server still mid-handshake at create). `shell` is a deferred (non-active)
28
+ // standalone tool so BP1 carries a manifest block even before MCP arrives.
29
+ function createdSession() {
30
+ const session = baseSession();
31
+ applyDeferredToolSurface(session, 'full', [{ name: 'shell', description: 'run a command' }], { provider: 'legacy' });
32
+ return session;
33
+ }
34
+
35
+ function systemContent(session) {
36
+ return session.messages.find((m) => m.role === 'system').content;
37
+ }
38
+
39
+ const mcpTool = { name: 'mcp__unity__get_scene', description: 'Return the active Unity scene graph.' };
40
+ const mcpTool2 = { name: 'mcp__unity__run_tests', description: 'Run the Unity test runner.' };
41
+
42
+ // Mirror the session-turn-api first-turn gate: refresh runs ONCE, only for a
43
+ // session flagged fresh (deferredInitialRefreshPending) at create; a resumed
44
+ // session (reloaded transcript, flag absent) takes the late-reconcile path and
45
+ // its already-baked BP1 is never rebuilt or re-announced.
46
+ function firstTurnGate(session, liveMcp) {
47
+ if (session.deferredInitialRefreshPending) {
48
+ session.deferredInitialRefreshPending = false;
49
+ return refreshInitialDeferredMcpSurface(session, liveMcp) ? 'refreshed' : 'refresh-noop';
50
+ }
51
+ return 'late';
52
+ }
53
+
54
+ test('MCP connecting between session-create and first send lands in the INITIAL manifest, not a late reminder', () => {
55
+ const session = createdSession();
56
+ assert.ok(!systemContent(session).includes('mcp__unity__get_scene'), 'MCP absent from create-time BP1');
57
+
58
+ // First-turn refresh: the live registry now includes the connected server.
59
+ const changed = refreshInitialDeferredMcpSurface(session, [mcpTool]);
60
+ assert.equal(changed, true, 'first-turn refresh folded the newly-connected MCP tool');
61
+
62
+ const sys = systemContent(session);
63
+ assert.ok(sys.includes('<available-deferred-tools>'), 'manifest block present');
64
+ assert.ok(sys.includes('mcp__unity__get_scene'), 'MCP tool in the INITIAL manifest');
65
+ assert.ok(session.deferredAnnouncedTools.includes('mcp__unity__get_scene'), 'MCP tool pre-marked announced');
66
+
67
+ let enqueued = null;
68
+ const result = reconcileDeferredMcpToolCatalog(session, [mcpTool], {
69
+ enqueue: (text) => { enqueued = text; return true; },
70
+ });
71
+ assert.equal(result, null, 'no late-tool announcement for a first-turn-folded tool');
72
+ assert.equal(enqueued, null, 'nothing enqueued for a first-turn-folded tool');
73
+ });
74
+
75
+ test('an MCP server connecting AFTER the first turn is still announced via the late reminder', () => {
76
+ const session = createdSession();
77
+ // First turn: no MCP connected yet — nothing to fold.
78
+ assert.equal(refreshInitialDeferredMcpSurface(session, []), false, 'no live MCP => first-turn no-op');
79
+ assert.ok(!(session.deferredAnnouncedTools || []).includes('mcp__unity__get_scene'), 'MCP not pre-announced');
80
+
81
+ // A later turn: the server has since connected → late path fires.
82
+ let enqueued = null;
83
+ const result = reconcileDeferredMcpToolCatalog(session, [mcpTool], {
84
+ enqueue: (text) => { enqueued = text; return true; },
85
+ });
86
+ assert.deepEqual(result, ['mcp__unity__get_scene'], 'late tool announced');
87
+ assert.ok(enqueued && enqueued.includes('mcp__unity__get_scene'), 'late reminder enqueued');
88
+ assert.ok(enqueued.includes('connected after this session started'), 'reminder carries the late-tool sentinel');
89
+ });
90
+
91
+ test('recreated session (MCP already connected at create) seeds its manifest, no late re-announce', () => {
92
+ // Recreate/reset path: createCurrentSession folds the live MCP tools into the
93
+ // surface at create time, so a cwd-change recreate seeds its BP1 directly and
94
+ // never needs the first-turn refresh.
95
+ const session = baseSession();
96
+ applyDeferredToolSurface(session, 'full', [{ name: 'shell', description: 'run a command' }, mcpTool], { provider: 'legacy' });
97
+ assert.ok(systemContent(session).includes('mcp__unity__get_scene'), 'MCP in recreated BP1');
98
+ assert.ok(session.deferredAnnouncedTools.includes('mcp__unity__get_scene'), 'recreated MCP pre-marked announced');
99
+
100
+ let enqueued = null;
101
+ const result = reconcileDeferredMcpToolCatalog(session, [mcpTool], {
102
+ enqueue: (text) => { enqueued = text; return true; },
103
+ });
104
+ assert.equal(result, null, 'no late announcement on recreate');
105
+ assert.equal(enqueued, null, 'nothing enqueued on recreate');
106
+ });
107
+
108
+ test('first-turn refresh is idempotent: a re-render with no new MCP is a no-op and keeps ONE manifest block', () => {
109
+ const session = createdSession();
110
+ assert.equal(refreshInitialDeferredMcpSurface(session, [mcpTool]), true, 'first fold applies');
111
+ const once = systemContent(session);
112
+ assert.equal(refreshInitialDeferredMcpSurface(session, [mcpTool]), false, 'no genuinely-new MCP => no-op');
113
+ const twice = systemContent(session);
114
+ assert.equal(once, twice, 'BP1 byte-identical on re-render');
115
+ assert.equal((twice.match(/<available-deferred-tools>/g) || []).length, 1, 'exactly one manifest block (no duplicate)');
116
+ });
117
+
118
+ test('a second newly-connected MCP tool re-renders BP1 in place (both listed, still one block)', () => {
119
+ const session = createdSession();
120
+ refreshInitialDeferredMcpSurface(session, [mcpTool]);
121
+ assert.equal(refreshInitialDeferredMcpSurface(session, [mcpTool, mcpTool2]), true, 're-fold applies the new tool');
122
+ const sys = systemContent(session);
123
+ assert.ok(sys.includes('mcp__unity__get_scene') && sys.includes('mcp__unity__run_tests'), 'both MCP tools listed');
124
+ assert.equal((sys.match(/<available-deferred-tools>/g) || []).length, 1, 'still exactly one block (rebuilt in place)');
125
+ assert.ok(session.deferredAnnouncedTools.includes('mcp__unity__run_tests'), 'the new tool is pre-announced too');
126
+ });
127
+
128
+ test('a resumed session (no fresh flag, prior baked BP1) is NOT refreshed on its next turn', () => {
129
+ // A prior run baked BP1 with the MCP tool; resume reloads that transcript.
130
+ const session = baseSession();
131
+ applyDeferredToolSurface(session, 'full', [{ name: 'shell', description: 'run a command' }, mcpTool], { provider: 'legacy' });
132
+ const before = systemContent(session);
133
+ const announcedBefore = [...session.deferredAnnouncedTools];
134
+ // Resume path never sets the per-session fresh flag.
135
+ assert.equal(session.deferredInitialRefreshPending, undefined, 'resumed session carries no fresh flag');
136
+ const route = firstTurnGate(session, [mcpTool, mcpTool2]);
137
+ assert.equal(route, 'late', 'resumed session takes the late-reconcile path, not the initial refresh');
138
+ assert.equal(systemContent(session), before, 'BP1 untouched on resume (no rebuild)');
139
+ assert.deepEqual([...session.deferredAnnouncedTools], announcedBefore, 'announced set unchanged on resume');
140
+ });
141
+
142
+ test('a fresh session (flagged) is refreshed exactly once, then falls to the late path', () => {
143
+ const session = createdSession();
144
+ session.deferredInitialRefreshPending = true;
145
+ assert.equal(firstTurnGate(session, [mcpTool]), 'refreshed', 'fresh session refreshes on its first turn');
146
+ assert.equal(session.deferredInitialRefreshPending, false, 'fresh flag consumed (one-shot)');
147
+ assert.ok(session.deferredAnnouncedTools.includes('mcp__unity__get_scene'), 'first-turn MCP pre-announced');
148
+ assert.equal(firstTurnGate(session, [mcpTool2]), 'late', 'second turn no longer refreshes');
149
+ });
@@ -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
+ }
@@ -88,4 +88,4 @@ for (const target of ['heavy-worker', 'worker']) {
88
88
  console.log(` single-call batches: ${batch1}/${batchTot} (${pct(batch1, batchTot)}%)`);
89
89
  console.log(` content greps with -C/-A/-B: ${grepContentWithCtx}/${grepContent} (${pct(grepContentWithCtx, grepContent)}%) grep->read follow-ups: ${grepThenRead}`);
90
90
  console.log(` same-path re-reads >=3x: ${rereads}`);
91
- }
91
+ }
@@ -1050,32 +1050,28 @@ const prevChannelSingleton = process.env.MIXDOG_CHANNEL_SINGLETON;
1050
1050
  const prevChannelWorkerProcess = process.env.MIXDOG_CHANNEL_WORKER_PROCESS;
1051
1051
  const prevRuntimeRoot = process.env.MIXDOG_RUNTIME_ROOT;
1052
1052
  const prevEnvOut = process.env.SMOKE_CHANNEL_ENV_OUT;
1053
+ const prevDaemonEntry = process.env.MIXDOG_CHANNEL_DAEMON_ENTRY;
1053
1054
  try {
1054
- const entry = join(channelWorkerTmp, 'entry.mjs');
1055
+ // Daemon-mode worker env coverage: start() spawn-or-attaches the machine
1056
+ // -global daemon (the stub daemon entry — no Discord token) instead of
1057
+ // forking `entry`, so assert the flags on the SPAWNED DAEMON's env (the stub
1058
+ // dumps them to SMOKE_CHANNEL_ENV_OUT). The old fork-path env assertion died
1059
+ // with the fork path itself; full flip/attach coverage lives in
1060
+ // scripts/channel-daemon-smoke.mjs.
1061
+ const stubEntry = join(root, 'scripts', 'channel-daemon-stub.mjs');
1055
1062
  const dataDir = join(channelWorkerTmp, 'data');
1056
1063
  const runtimeDir = join(channelWorkerTmp, 'runtime');
1057
1064
  const envOut = join(channelWorkerTmp, 'env.json');
1058
1065
  mkdirSync(dataDir, { recursive: true });
1059
1066
  mkdirSync(runtimeDir, { recursive: true });
1060
- writeFileSync(entry, `
1061
- import { writeFileSync } from 'node:fs';
1062
- writeFileSync(process.env.SMOKE_CHANNEL_ENV_OUT, JSON.stringify({
1063
- cliOwned: process.env.MIXDOG_CLI_OWNED,
1064
- daemon: process.env.MIXDOG_CHANNEL_DAEMON,
1065
- }));
1066
- process.send?.({ type: 'ready' });
1067
- process.on('message', (msg) => {
1068
- if (msg?.type === 'shutdown') process.exit(0);
1069
- });
1070
- setInterval(() => {}, 10000);
1071
- `);
1072
1067
  process.env.MIXDOG_CHANNEL_DAEMON = '1';
1073
1068
  process.env.MIXDOG_CHANNEL_SINGLETON = '1';
1074
1069
  process.env.MIXDOG_CHANNEL_WORKER_PROCESS = '1';
1075
1070
  process.env.MIXDOG_RUNTIME_ROOT = runtimeDir;
1076
1071
  process.env.SMOKE_CHANNEL_ENV_OUT = envOut;
1072
+ process.env.MIXDOG_CHANNEL_DAEMON_ENTRY = stubEntry;
1077
1073
  channelEnvWorker = createStandaloneChannelWorker({
1078
- entry,
1074
+ entry: stubEntry,
1079
1075
  rootDir: root,
1080
1076
  dataDir,
1081
1077
  cwd: root,
@@ -1100,6 +1096,11 @@ setInterval(() => {}, 10000);
1100
1096
  else process.env.MIXDOG_RUNTIME_ROOT = prevRuntimeRoot;
1101
1097
  if (prevEnvOut == null) delete process.env.SMOKE_CHANNEL_ENV_OUT;
1102
1098
  else process.env.SMOKE_CHANNEL_ENV_OUT = prevEnvOut;
1099
+ if (prevDaemonEntry == null) delete process.env.MIXDOG_CHANNEL_DAEMON_ENTRY;
1100
+ else process.env.MIXDOG_CHANNEL_DAEMON_ENTRY = prevDaemonEntry;
1101
+ // Detach only ends OUR attachment; the stub daemon self-shuts after its
1102
+ // client-grace window. Give it that window before deleting its tmp root.
1103
+ await new Promise((resolveWait) => setTimeout(resolveWait, 700));
1103
1104
  rmSync(channelWorkerTmp, { recursive: true, force: true });
1104
1105
  }
1105
1106
 
@@ -1191,10 +1192,10 @@ if (EXPLORE_TOOL.annotations?.agentHidden === true) {
1191
1192
  }
1192
1193
  }
1193
1194
  const exploreProps = EXPLORE_TOOL.inputSchema?.properties || {};
1194
- if (!/Repo anchor locator/i.test(EXPLORE_TOOL.description || '') || !/broad\/uncertain/i.test(EXPLORE_TOOL.description || '') || !/independent targets/i.test(EXPLORE_TOOL.description || '') || (EXPLORE_TOOL.description || '').length > 90) {
1195
- throw new Error('explore description must stay compact and anchor-oriented');
1195
+ if (!/broad\/uncertain/i.test(EXPLORE_TOOL.description || '') || !/machine-wide/i.test(EXPLORE_TOOL.description || '') || !/independent targets/i.test(EXPLORE_TOOL.description || '') || (EXPLORE_TOOL.description || '').length > 600) {
1196
+ throw new Error('explore description must keep the locator + facet fan-out contract');
1196
1197
  }
1197
- if (!/Narrow locator query/i.test(exploreProps.query?.description || '') || !/independent targets/i.test(exploreProps.query?.description || '') || !/Project\/root/i.test(exploreProps.cwd?.description || '')) {
1198
+ if (!/Narrow locator query/i.test(exploreProps.query?.description || '') || !/independent facets/i.test(exploreProps.query?.description || '') || !/Project\/root/i.test(exploreProps.cwd?.description || '')) {
1198
1199
  throw new Error('explore schema must stay compact and preserve query/cwd shape');
1199
1200
  }
1200
1201
  const normalizedExplore = normalizeExploreQueries('["where is model selection?"," ","which file owns agent async?"]');
@@ -1412,7 +1413,11 @@ setInternalToolsProvider({
1412
1413
  const writeTools = (writeAgentSession.tools || []).map((tool) => tool?.name).filter(Boolean);
1413
1414
  const fullTools = (fullAgentSession.tools || []).map((tool) => tool?.name).filter(Boolean);
1414
1415
  const publicExploreTools = (publicExploreSession.tools || []).map((tool) => tool?.name).filter(Boolean);
1415
- const expectedReadTools = ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'explore', 'search', 'web_fetch', 'Skill'];
1416
+ // Read-role AGENT sessions carry shell/task so review/debug agents can run
1417
+ // their own verification (build/test); the plain readonly preset (public
1418
+ // explore role) still omits them.
1419
+ const expectedReadTools = ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'shell', 'task', 'explore', 'search', 'web_fetch', 'Skill'];
1420
+ const expectedPublicReadTools = ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'explore', 'search', 'web_fetch', 'Skill'];
1416
1421
  const expectedWriteTools = ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'apply_patch', 'shell', 'task', 'explore', 'search', 'web_fetch', 'Skill'];
1417
1422
  if (JSON.stringify(readTools) !== JSON.stringify(expectedReadTools)) {
1418
1423
  throw new Error(`read agent schema must be fixed allow-list: expected=${expectedReadTools.join(', ')} actual=${readTools.join(', ')}`);
@@ -1423,12 +1428,15 @@ setInternalToolsProvider({
1423
1428
  if (readTools.includes('load_tool') || writeTools.includes('load_tool')) {
1424
1429
  throw new Error(`agent session fixed schemas must omit load_tool: read=${readTools.join(', ')} write=${writeTools.join(', ')}`);
1425
1430
  }
1426
- if (readTools.includes('shell')) {
1427
- throw new Error(`read agent schema must omit shell: read=${readTools.join(', ')}`);
1431
+ if (readTools.includes('apply_patch')) {
1432
+ throw new Error(`read agent schema must omit apply_patch: read=${readTools.join(', ')}`);
1428
1433
  }
1429
- for (const name of ['shell', 'apply_patch', 'task']) {
1430
- if (readTools.includes(name)) {
1431
- throw new Error(`read agent schema must omit non-read tool ${name}: read=${readTools.join(', ')}`);
1434
+ for (const name of ['shell', 'task']) {
1435
+ if (!readTools.includes(name)) {
1436
+ throw new Error(`read agent schema must carry verification tool ${name}: read=${readTools.join(', ')}`);
1437
+ }
1438
+ if (publicExploreTools.includes(name)) {
1439
+ throw new Error(`public explore role must omit ${name}: explore=${publicExploreTools.join(', ')}`);
1432
1440
  }
1433
1441
  }
1434
1442
  for (const name of ['apply_patch', 'shell', 'task']) {
@@ -1450,13 +1458,13 @@ setInternalToolsProvider({
1450
1458
  if (!fullTools.includes('explore')) {
1451
1459
  throw new Error(`full agent schema must expose explore: full=${fullTools.join(', ')}`);
1452
1460
  }
1453
- // Unified-shard policy (v0.9.13): the explore wrapper stays IN the schema
1454
- // for every read role including the explore agent itself — so the
1455
- // read-only bundle is bit-identical across roles (one cache group).
1456
- // Recursion is broken at call time in pre-dispatch-deny.mjs via
1457
- // recursiveWrapperToolNameForPublicAgent, not by schema stripping.
1458
- if (JSON.stringify(publicExploreTools) !== JSON.stringify(expectedReadTools)) {
1459
- throw new Error(`public explore role must ship the shared read-only bundle (incl. explore): expected=${expectedReadTools.join(', ')} actual=${publicExploreTools.join(', ')}`);
1461
+ // The explore wrapper stays IN the schema for every read role — including
1462
+ // the explore agent itself. Recursion is broken at call time in
1463
+ // pre-dispatch-deny.mjs via recursiveWrapperToolNameForPublicAgent, not by
1464
+ // schema stripping. (Read AGENT sessions add shell/task on top, so the
1465
+ // public explore bundle is its own cache group now.)
1466
+ if (JSON.stringify(publicExploreTools) !== JSON.stringify(expectedPublicReadTools)) {
1467
+ throw new Error(`public explore role must ship the readonly bundle (incl. explore): expected=${expectedPublicReadTools.join(', ')} actual=${publicExploreTools.join(', ')}`);
1460
1468
  }
1461
1469
  if (recursiveWrapperToolNameForPublicAgent('explore') !== 'explore') {
1462
1470
  throw new Error('call-time anti-recursion must map public explore agent to its own wrapper tool');
@@ -22,6 +22,12 @@ synonyms, library/domain names), plus `find` with name fragments, plus `code_gra
22
22
  symbol_search when the query names an identifier. A single-pattern,
23
23
  single-tool first turn is a defect.
24
24
 
25
+ A turn-1 broad grep MUST set `output_mode:"files_with_matches"` — an
26
+ unscoped `content`/`content_with_context` scan reads every match body and is
27
+ a defect (a full-content scan across the tree costs seconds). Use
28
+ `content_with_context` ONLY against a `path` that appeared in an earlier
29
+ result THIS session, and always with `head_limit`.
30
+
25
31
  Search tokens are CODE tokens: first translate natural-language or
26
32
  non-English queries into probable English identifiers (e.g. "최대 루프 반복
27
33
  횟수" → maxLoop, loop-policy, iterations). Grep non-ASCII text only when
@@ -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.
@@ -4,12 +4,19 @@
4
4
  prior result. Merge equivalent variants/scopes into ONE call wherever the
5
5
  schema accepts arrays (`pattern[]`, `path[]`, `symbols[]`, `query[]`).
6
6
  - Route by what is already known: known symbol/relation → `code_graph`;
7
- exact text in a known scope → `grep`; unknown location or concept-level
8
- question `explore`; name fragment → `find`; exact name pattern → `glob`;
9
- known directory → `list`; known file/region → `read`.
7
+ exact text in a known scope → `grep`; unknown location, machine-wide/
8
+ out-of-repo whereabouts, or concept-level question → `explore` (which uses
9
+ the hardened `find` internally); name fragment → `find`; exact name pattern
10
+ → `glob`; known directory → `list`; known file/region → `read`.
11
+ - `explore` fan-out: at task start, decompose what the task needs to know
12
+ into independent facets (implementation site, config/load path, tests,
13
+ error origin, ...) and send them as ONE `query[]` call — facets run in
14
+ parallel. Never fan out rephrasings of the same target; on
15
+ EXPLORATION_FAILED, retry once with changed tokens.
10
16
  - Valid anchors come from user input or tool output in this session; locate
11
17
  anything else with `find` before `grep`/`read`. On ENOENT the next call is
12
- `find` on the basename — never a retried guess.
18
+ `find` on the basename — never a retried guess; and never guess an absolute
19
+ path outside the project — `find` from a verified broad root instead.
13
20
  - Retrieval stops when evidence covers the deliverable: single-answer tasks
14
21
  end at the first sufficient anchor; enumeration tasks (review, audit) end
15
22
  when the stated scope is covered. Never re-verify a hit already on screen;