mixdog 0.9.19 → 0.9.20

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 (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -29
  3. package/package.json +3 -2
  4. package/scripts/build-runtime-windows.ps1 +242 -242
  5. package/scripts/build-tui.mjs +6 -0
  6. package/scripts/hook-bus-test.mjs +8 -0
  7. package/scripts/log-writer-guard-smoke.mjs +131 -0
  8. package/scripts/reactive-compact-persist-smoke.mjs +22 -6
  9. package/scripts/recall-bench-cases.json +0 -1
  10. package/scripts/recall-quality-cases.json +1 -2
  11. package/scripts/recall-usecase-cases.json +1 -1
  12. package/scripts/session-ingest-compaction-smoke.mjs +241 -0
  13. package/scripts/smoke-runtime-negative.ps1 +106 -106
  14. package/scripts/tool-efficiency-diag.mjs +1 -1
  15. package/scripts/tool-smoke.mjs +150 -45
  16. package/src/help.mjs +2 -5
  17. package/src/lib/mixdog-debug.cjs +13 -0
  18. package/src/mixdog-session-runtime.mjs +7 -3328
  19. package/src/rules/lead/02-channels.md +3 -3
  20. package/src/runtime/agent/orchestrator/context/collect.mjs +33 -9
  21. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +663 -0
  22. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +6 -0
  23. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +153 -0
  24. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +49 -0
  25. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -1
  26. package/src/runtime/agent/orchestrator/session/loop.mjs +8 -1860
  27. package/src/runtime/agent/orchestrator/session/manager/agent-runtime-singleton.mjs +29 -0
  28. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +686 -0
  29. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +60 -12
  30. package/src/runtime/agent/orchestrator/session/manager/env-utils.mjs +8 -0
  31. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +159 -0
  32. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +143 -0
  33. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +268 -0
  34. package/src/runtime/agent/orchestrator/session/manager/provider-cache-key.mjs +22 -0
  35. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +26 -0
  36. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +124 -0
  37. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +258 -0
  38. package/src/runtime/agent/orchestrator/session/manager/session-errors.mjs +20 -0
  39. package/src/runtime/agent/orchestrator/session/manager/session-id.mjs +9 -0
  40. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +475 -0
  41. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +23 -0
  42. package/src/runtime/agent/orchestrator/session/manager.mjs +88 -2285
  43. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +440 -0
  44. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +153 -0
  45. package/src/runtime/agent/orchestrator/session/store.mjs +4 -1
  46. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +642 -0
  47. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +23 -3
  48. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +108 -2
  49. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +15 -3
  50. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -0
  51. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +24 -2
  52. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
  53. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +72 -8
  54. package/src/runtime/agent/orchestrator/tools/shell-policy-danger-target.mjs +5 -1
  55. package/src/runtime/channels/index.mjs +6 -2183
  56. package/src/runtime/channels/lib/inbound-handler.mjs +328 -0
  57. package/src/runtime/channels/lib/interaction-handlers.mjs +260 -0
  58. package/src/runtime/channels/lib/network-retry.mjs +23 -0
  59. package/src/runtime/channels/lib/owned-runtime.mjs +547 -0
  60. package/src/runtime/channels/lib/transcript-binding.mjs +336 -0
  61. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +39 -2
  62. package/src/runtime/channels/lib/worker-bootstrap.mjs +97 -0
  63. package/src/runtime/channels/lib/worker-ipc.mjs +201 -0
  64. package/src/runtime/channels/lib/worker-main.mjs +771 -0
  65. package/src/runtime/memory/index.mjs +73 -1725
  66. package/src/runtime/memory/lib/embedding-provider.mjs +4 -2
  67. package/src/runtime/memory/lib/embedding-worker.mjs +47 -6
  68. package/src/runtime/memory/lib/http-router.mjs +772 -0
  69. package/src/runtime/memory/lib/memory-action-handlers.mjs +901 -0
  70. package/src/runtime/memory/lib/memory-embed.mjs +28 -7
  71. package/src/runtime/memory/lib/memory-recall-store.mjs +4 -4
  72. package/src/runtime/memory/lib/query-handlers.mjs +2 -2
  73. package/src/runtime/memory/lib/session-ingest-runtime.mjs +401 -0
  74. package/src/session-runtime/boot-profile.mjs +36 -0
  75. package/src/session-runtime/channel-config-api.mjs +70 -0
  76. package/src/session-runtime/context-status.mjs +181 -0
  77. package/src/session-runtime/env.mjs +17 -0
  78. package/src/session-runtime/lifecycle-api.mjs +242 -0
  79. package/src/session-runtime/model-route-api.mjs +198 -0
  80. package/src/session-runtime/provider-auth-api.mjs +135 -0
  81. package/src/session-runtime/resource-api.mjs +282 -0
  82. package/src/session-runtime/runtime-core.mjs +2046 -0
  83. package/src/session-runtime/session-turn-api.mjs +274 -0
  84. package/src/session-runtime/tool-catalog.mjs +18 -264
  85. package/src/session-runtime/tool-defs.mjs +2 -2
  86. package/src/session-runtime/workflow-agents-api.mjs +238 -0
  87. package/src/standalone/agent-tool.mjs +2 -2
  88. package/src/standalone/channel-worker.mjs +4 -0
  89. package/src/standalone/memory-runtime-proxy.mjs +56 -6
  90. package/src/tui/App.jsx +39 -28
  91. package/src/tui/app/core-memory-picker.mjs +1 -1
  92. package/src/tui/app/use-mouse-input.mjs +6 -0
  93. package/src/tui/app/use-transcript-scroll.mjs +77 -3
  94. package/src/tui/dist/index.mjs +1990 -1527
  95. package/src/tui/engine/context-state.mjs +145 -0
  96. package/src/tui/engine/prompt-history.mjs +27 -0
  97. package/src/tui/engine/session-api-ext.mjs +478 -0
  98. package/src/tui/engine/session-api.mjs +545 -0
  99. package/src/tui/engine/session-flow.mjs +485 -0
  100. package/src/tui/engine/turn.mjs +1078 -0
  101. package/src/tui/engine.mjs +68 -2620
  102. package/src/tui/index.jsx +7 -0
  103. package/vendor/ink/build/ink.js +16 -1
  104. package/vendor/ink/build/output.js +30 -4
  105. package/vendor/ink/build/render.js +5 -0
  106. package/src/workflows/sequential/WORKFLOW.md +0 -51
@@ -1,242 +1,242 @@
1
- # build-runtime-windows.ps1 — Build PostgreSQL 16 + pgvector runtime on Windows.
2
- # Uses windows-2022 GHA runner's preinstalled PostgreSQL 16 (consistent
3
- # pg_config + postgres.exe from same package) — avoids EDB zip's split-version
4
- # packaging bug that linked pgvector against PG 14 ABI.
5
- # Builds pgvector from source via MSVC/nmake, then assembles a self-contained
6
- # runtime tree (bin + lib + share at root). Final smoke: initdb + CREATE
7
- # EXTENSION vector + distance query.
8
- # Produces: dist\mixdog-runtime-win32-x64-pg{pgver}-pgvector{vecver}.tar.gz
9
-
10
- $ErrorActionPreference = 'Stop'
11
-
12
- $PG_VERSION = '16.4'
13
- $PGVECTOR_VERSION = '0.8.2'
14
- $TARGET_OS = $env:TARGET_OS ?? 'win32'
15
- $TARGET_ARCH = $env:TARGET_ARCH ?? 'x64'
16
-
17
- # Auto-detect highest preinstalled PG ≥ 16 OR install via chocolatey.
18
- $PgInstallRoot = 'C:\Program Files\PostgreSQL'
19
-
20
- function Find-PgRoot {
21
- if (-not (Test-Path $PgInstallRoot)) { return $null }
22
- $cands = Get-ChildItem $PgInstallRoot -Directory -ErrorAction SilentlyContinue |
23
- Where-Object { $_.Name -match '^(\d+)' } |
24
- Sort-Object { [int]([regex]::Match($_.Name, '^(\d+)').Groups[1].Value) } -Descending
25
- foreach ($c in $cands) {
26
- $major = [int]([regex]::Match($c.Name, '^(\d+)').Groups[1].Value)
27
- if ($major -ge 16 -and (Test-Path "$($c.FullName)\bin\pg_config.exe")) {
28
- return $c.FullName
29
- }
30
- }
31
- return $null
32
- }
33
-
34
- $PgRoot = Find-PgRoot
35
- if (-not $PgRoot) {
36
- Write-Host "==> No preinstalled PG ≥ 16 found. Installing via chocolatey..."
37
- if (Test-Path $PgInstallRoot) {
38
- Write-Host "Existing PG dirs (none usable):"
39
- Get-ChildItem $PgInstallRoot -Directory -ErrorAction SilentlyContinue | Select-Object Name
40
- }
41
- choco install postgresql16 --version=16.4.0 --params '/Password:postgres' -y --no-progress 2>&1 | Out-Host
42
- if ($LASTEXITCODE -ne 0) { Write-Error "choco install postgresql16 failed (exit $LASTEXITCODE)"; exit 1 }
43
- $PgRoot = Find-PgRoot
44
- if (-not $PgRoot) {
45
- Write-Error "ASSERT FAILED: chocolatey install completed but PG ≥ 16 still not found under $PgInstallRoot"
46
- Get-ChildItem $PgInstallRoot -Directory -ErrorAction SilentlyContinue | Select-Object Name
47
- exit 1
48
- }
49
- }
50
-
51
- $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
52
- $RootDir = (Resolve-Path "$ScriptDir\..").Path
53
- $BuildDir = "$RootDir\build\runtime-win32-$TARGET_ARCH"
54
- $DistDir = "$RootDir\dist"
55
- $RuntimeDir = "$BuildDir\runtime"
56
-
57
- $PgBin = "$PgRoot\bin"
58
- $PgConfig = "$PgBin\pg_config.exe"
59
-
60
- $OutputName = "mixdog-runtime-${TARGET_OS}-${TARGET_ARCH}-pg${PG_VERSION}-pgvector${PGVECTOR_VERSION}.tar.gz"
61
-
62
- Write-Host "==> Using preinstalled PG: $PgRoot"
63
- & $PgConfig --version
64
- $RealVersion = (& $PgConfig --version) -replace 'PostgreSQL ', ''
65
- Write-Host " pg_config reports version: $RealVersion"
66
-
67
- if (Test-Path $RuntimeDir) { Remove-Item -Recurse -Force $RuntimeDir }
68
- New-Item -ItemType Directory -Force -Path $BuildDir, $DistDir,
69
- "$RuntimeDir\bin", "$RuntimeDir\lib", "$RuntimeDir\share" | Out-Null
70
-
71
- Write-Host "==> Cloning pgvector $PGVECTOR_VERSION"
72
- $PgVectorDir = "$BuildDir\pgvector"
73
- $VectorDllBuilt = "$PgVectorDir\vector.dll"
74
-
75
- if (Test-Path $VectorDllBuilt) {
76
- Write-Host " Cache hit: vector.dll already built at $VectorDllBuilt"
77
- } else {
78
- if (Test-Path $PgVectorDir) { Remove-Item -Recurse -Force $PgVectorDir }
79
- git clone --branch "v$PGVECTOR_VERSION" --depth 1 `
80
- https://github.com/pgvector/pgvector.git $PgVectorDir
81
-
82
- Write-Host "==> Building pgvector (MSVC/nmake against system PG 16)"
83
- Push-Location $PgVectorDir
84
- try {
85
- $VsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
86
- $VcVarsAll = & $VsWhere -latest -find 'VC\Auxiliary\Build\vcvarsall.bat' 2>$null | Select-Object -First 1
87
- if (-not $VcVarsAll) {
88
- Write-Error "vswhere could not locate vcvarsall.bat — Visual Studio Build Tools required."
89
- exit 1
90
- }
91
- # CRITICAL: prepend $PgRoot\bin to PATH so any bare `pg_config` call
92
- # inside Makefile.win resolves to PG 16 — runner has PG 14/15
93
- # preinstalled and would otherwise win the PATH lookup, producing
94
- # PG14-ABI vector.dll that fails to load in our PG 16 postgres.exe.
95
- # Set in PowerShell so cmd /c inherits; setting inside the cmd batch
96
- # via %PATH% loses vcvarsall's additions due to parse-time expansion.
97
- $env:PATH = "$PgRoot\bin;$env:PATH"
98
- $env:PGROOT = $PgRoot
99
- $BuildCmd = "`"$VcVarsAll`" amd64 && nmake /F Makefile.win PG_CONFIG=`"$PgConfig`""
100
- cmd /c $BuildCmd
101
- if ($LASTEXITCODE -ne 0) {
102
- Write-Error "pgvector nmake build failed (exit $LASTEXITCODE)"
103
- exit 1
104
- }
105
- } finally {
106
- Pop-Location
107
- }
108
- }
109
-
110
- Write-Host "==> Assembling runtime layout — copy bin (.exe + .dll), lib, share from $PgRoot"
111
- # Copy ALL .exe + .dll from PG bin so postgres.exe + libpq.dll + libcrypto/libssl/
112
- # libintl/libiconv/icu*/libxml2/libxslt/libwinpthread/libecpg/libpgtypes etc. all
113
- # ship together. PG 16 install layout puts these all in bin\.
114
- Copy-Item "$PgBin\*.exe","$PgBin\*.dll" "$RuntimeDir\bin\" -Force
115
-
116
- # lib\: PG extension modules (incl. contrib like pgcrypto.dll). vector.dll
117
- # placed here below — PG looks for $libdir/<ext>.dll which resolves to lib\.
118
- Copy-Item -Recurse -Force "$PgRoot\lib\*" "$RuntimeDir\lib\" -ErrorAction SilentlyContinue
119
- # share\: extension SQL/control, locale, timezone data, conf samples.
120
- Copy-Item -Recurse -Force "$PgRoot\share\*" "$RuntimeDir\share\" -ErrorAction SilentlyContinue
121
-
122
- Write-Host "==> Manually staging pgvector artifacts (avoid pg_config-derived install paths)"
123
- $RuntimeExtDir = "$RuntimeDir\share\extension"
124
- New-Item -ItemType Directory -Force -Path $RuntimeExtDir | Out-Null
125
- Copy-Item "$PgVectorDir\vector.dll" "$RuntimeDir\lib\" -Force
126
- Copy-Item "$PgVectorDir\vector.control" $RuntimeExtDir -Force
127
- Copy-Item "$PgVectorDir\sql\vector--*.sql" $RuntimeExtDir -Force
128
-
129
- Write-Host "==> Asserting runtime layout"
130
- $VectorControl = "$RuntimeDir\share\extension\vector.control"
131
- if (-not (Test-Path $VectorControl)) { Write-Error "ASSERT FAILED: $VectorControl not found"; exit 1 }
132
- $VectorSql = "$RuntimeDir\share\extension\vector--$PGVECTOR_VERSION.sql"
133
- if (-not (Test-Path $VectorSql)) { Write-Error "ASSERT FAILED: $VectorSql not found"; exit 1 }
134
- if (-not (Test-Path "$RuntimeDir\lib\vector.dll")) {
135
- Write-Error "ASSERT FAILED: vector.dll not found in lib\"
136
- exit 1
137
- }
138
- Write-Host " PASS runtime layout"
139
-
140
- # Licenses
141
- if (Test-Path "$PgRoot\doc\postgresql\COPYRIGHT") {
142
- Copy-Item "$PgRoot\doc\postgresql\COPYRIGHT" "$RuntimeDir\LICENSE.postgresql" -Force
143
- } elseif (Test-Path "$PgRoot\doc\COPYRIGHT") {
144
- Copy-Item "$PgRoot\doc\COPYRIGHT" "$RuntimeDir\LICENSE.postgresql" -Force
145
- }
146
- if (Test-Path "$PgVectorDir\LICENSE") {
147
- Copy-Item "$PgVectorDir\LICENSE" "$RuntimeDir\LICENSE.pgvector" -Force
148
- }
149
-
150
- Write-Host "==> Self-contained smoke test (initdb + CREATE EXTENSION vector + distance query)"
151
- & "$RuntimeDir\bin\postgres.exe" --version
152
- if ($LASTEXITCODE -ne 0) { Write-Error "FAIL: postgres.exe --version exit $LASTEXITCODE"; exit 1 }
153
-
154
- $SmokeData = "$BuildDir\smoke-pgdata"
155
- $SmokeLog = "$BuildDir\smoke-pg.log"
156
- $SmokePort = 55899
157
- if (Test-Path $SmokeData) { Remove-Item -Recurse -Force $SmokeData }
158
-
159
- & "$RuntimeDir\bin\initdb.exe" -D $SmokeData --username=postgres --auth-local=trust --no-locale -E UTF8 | Out-Null
160
- if ($LASTEXITCODE -ne 0) { Write-Error "FAIL: initdb"; exit 1 }
161
-
162
- & "$RuntimeDir\bin\pg_ctl.exe" -D $SmokeData -o "-p $SmokePort -h 127.0.0.1" -l $SmokeLog -w start
163
- if ($LASTEXITCODE -ne 0) { Write-Error "FAIL: pg_ctl start (see $SmokeLog)"; Get-Content $SmokeLog | Select-Object -Last 30; exit 1 }
164
-
165
- try {
166
- & "$RuntimeDir\bin\psql.exe" -h 127.0.0.1 -p $SmokePort -U postgres -d postgres -c "CREATE EXTENSION vector;" | Out-Null
167
- if ($LASTEXITCODE -ne 0) { throw "CREATE EXTENSION vector failed" }
168
- $ExtV = & "$RuntimeDir\bin\psql.exe" -h 127.0.0.1 -p $SmokePort -U postgres -d postgres -tAc "SELECT extversion FROM pg_extension WHERE extname='vector';"
169
- $Dist = & "$RuntimeDir\bin\psql.exe" -h 127.0.0.1 -p $SmokePort -U postgres -d postgres -tAc "SELECT '[1,2,3]'::vector <-> '[1,2,4]'::vector;"
170
- Write-Host " vector extension version: $ExtV"
171
- Write-Host " distance query result: $Dist"
172
- if ($ExtV.Trim() -ne $PGVECTOR_VERSION) {
173
- Write-Error "FAIL: extversion='$ExtV' expected='$PGVECTOR_VERSION'"
174
- exit 1
175
- }
176
- Write-Host " PASS smoke (extension load + vector distance)"
177
- }
178
- finally {
179
- & "$RuntimeDir\bin\pg_ctl.exe" -D $SmokeData -m fast stop 2>$null | Out-Null
180
- Remove-Item -Recurse -Force $SmokeData -ErrorAction SilentlyContinue
181
- }
182
-
183
- Write-Host "==> Creating tarball: $OutputName"
184
- $DistDirFwd = $DistDir.Replace('\', '/')
185
- $RuntimeDirFwd = $RuntimeDir.Replace('\', '/')
186
- & tar -czf "$DistDirFwd/$OutputName" -C "$RuntimeDirFwd" .
187
- if ($LASTEXITCODE -ne 0) { Write-Error "tar failed (exit $LASTEXITCODE)"; exit 1 }
188
-
189
- Write-Host "==> Generating sha256 sidecar"
190
- Push-Location $DistDir
191
- $Hash = (Get-FileHash -Algorithm SHA256 $OutputName).Hash.ToLower()
192
- "$Hash $OutputName" | Out-File -Encoding ascii "${OutputName}.sha256"
193
- Pop-Location
194
-
195
- # ---------------------------------------------------------------------------
196
- # Phase A: Re-smoke from EXTRACTED tarball with hostile env (cleared PATH,
197
- # only system32). Catches false-pass where the build host has VC redist /
198
- # preinstalled DLLs that the tarball might be missing.
199
- # ---------------------------------------------------------------------------
200
- Write-Host "==> Re-smoke from extracted tarball (hostile env)"
201
- $ExtractDir = "$BuildDir\extract-smoke"
202
- if (Test-Path $ExtractDir) { Remove-Item -Recurse -Force $ExtractDir }
203
- New-Item -ItemType Directory -Force -Path $ExtractDir | Out-Null
204
- & tar -xzf "$DistDirFwd/$OutputName" -C ($ExtractDir.Replace('\','/'))
205
- if ($LASTEXITCODE -ne 0) { Write-Error "extract failed"; exit 1 }
206
-
207
- $ExtractData = "$ExtractDir\extract-pgdata"
208
- $ExtractLog = "$ExtractDir\extract-pg.log"
209
- $ExtractPort = 55898
210
-
211
- # Snapshot current env, then strip to minimal Windows PATH (no PG14/15/16
212
- # preinstalled bin, no chocolatey, no MSVC tools).
213
- $SavedPath = $env:PATH
214
- $SavedPgRoot = $env:PGROOT
215
- $env:PATH = "$env:SystemRoot\System32;$env:SystemRoot"
216
- $env:PGROOT = $null
217
- $env:PGDATA = $null
218
-
219
- try {
220
- & "$ExtractDir\bin\postgres.exe" --version
221
- if ($LASTEXITCODE -ne 0) { throw "FAIL: postgres --version under hostile env" }
222
- & "$ExtractDir\bin\initdb.exe" -D $ExtractData --username=postgres --auth-local=trust --no-locale -E UTF8 | Out-Null
223
- if ($LASTEXITCODE -ne 0) { throw "FAIL: initdb under hostile env" }
224
- & "$ExtractDir\bin\pg_ctl.exe" -D $ExtractData -o "-p $ExtractPort -h 127.0.0.1" -l $ExtractLog -w start
225
- if ($LASTEXITCODE -ne 0) { Get-Content $ExtractLog | Select-Object -Last 30; throw "FAIL: pg_ctl start under hostile env" }
226
- try {
227
- & "$ExtractDir\bin\psql.exe" -h 127.0.0.1 -p $ExtractPort -U postgres -d postgres -c "CREATE EXTENSION vector;" | Out-Null
228
- if ($LASTEXITCODE -ne 0) { throw "FAIL: CREATE EXTENSION vector under hostile env" }
229
- $ExtV2 = & "$ExtractDir\bin\psql.exe" -h 127.0.0.1 -p $ExtractPort -U postgres -d postgres -tAc "SELECT extversion FROM pg_extension WHERE extname='vector';"
230
- if ($ExtV2.Trim() -ne $PGVECTOR_VERSION) { throw "FAIL: extracted-smoke extversion='$ExtV2'" }
231
- Write-Host " PASS extracted-tarball smoke (hostile env)"
232
- } finally {
233
- & "$ExtractDir\bin\pg_ctl.exe" -D $ExtractData -m fast stop 2>$null | Out-Null
234
- }
235
- } finally {
236
- $env:PATH = $SavedPath
237
- $env:PGROOT = $SavedPgRoot
238
- Remove-Item -Recurse -Force $ExtractDir -ErrorAction SilentlyContinue
239
- }
240
-
241
- Write-Host "==> Done: $DistDir\$OutputName"
242
- Get-Item "$DistDir\$OutputName" | Select-Object Name, Length
1
+ # build-runtime-windows.ps1 — Build PostgreSQL 16 + pgvector runtime on Windows.
2
+ # Uses windows-2022 GHA runner's preinstalled PostgreSQL 16 (consistent
3
+ # pg_config + postgres.exe from same package) — avoids EDB zip's split-version
4
+ # packaging bug that linked pgvector against PG 14 ABI.
5
+ # Builds pgvector from source via MSVC/nmake, then assembles a self-contained
6
+ # runtime tree (bin + lib + share at root). Final smoke: initdb + CREATE
7
+ # EXTENSION vector + distance query.
8
+ # Produces: dist\mixdog-runtime-win32-x64-pg{pgver}-pgvector{vecver}.tar.gz
9
+
10
+ $ErrorActionPreference = 'Stop'
11
+
12
+ $PG_VERSION = '16.4'
13
+ $PGVECTOR_VERSION = '0.8.2'
14
+ $TARGET_OS = $env:TARGET_OS ?? 'win32'
15
+ $TARGET_ARCH = $env:TARGET_ARCH ?? 'x64'
16
+
17
+ # Auto-detect highest preinstalled PG ≥ 16 OR install via chocolatey.
18
+ $PgInstallRoot = 'C:\Program Files\PostgreSQL'
19
+
20
+ function Find-PgRoot {
21
+ if (-not (Test-Path $PgInstallRoot)) { return $null }
22
+ $cands = Get-ChildItem $PgInstallRoot -Directory -ErrorAction SilentlyContinue |
23
+ Where-Object { $_.Name -match '^(\d+)' } |
24
+ Sort-Object { [int]([regex]::Match($_.Name, '^(\d+)').Groups[1].Value) } -Descending
25
+ foreach ($c in $cands) {
26
+ $major = [int]([regex]::Match($c.Name, '^(\d+)').Groups[1].Value)
27
+ if ($major -ge 16 -and (Test-Path "$($c.FullName)\bin\pg_config.exe")) {
28
+ return $c.FullName
29
+ }
30
+ }
31
+ return $null
32
+ }
33
+
34
+ $PgRoot = Find-PgRoot
35
+ if (-not $PgRoot) {
36
+ Write-Host "==> No preinstalled PG ≥ 16 found. Installing via chocolatey..."
37
+ if (Test-Path $PgInstallRoot) {
38
+ Write-Host "Existing PG dirs (none usable):"
39
+ Get-ChildItem $PgInstallRoot -Directory -ErrorAction SilentlyContinue | Select-Object Name
40
+ }
41
+ choco install postgresql16 --version=16.4.0 --params '/Password:postgres' -y --no-progress 2>&1 | Out-Host
42
+ if ($LASTEXITCODE -ne 0) { Write-Error "choco install postgresql16 failed (exit $LASTEXITCODE)"; exit 1 }
43
+ $PgRoot = Find-PgRoot
44
+ if (-not $PgRoot) {
45
+ Write-Error "ASSERT FAILED: chocolatey install completed but PG ≥ 16 still not found under $PgInstallRoot"
46
+ Get-ChildItem $PgInstallRoot -Directory -ErrorAction SilentlyContinue | Select-Object Name
47
+ exit 1
48
+ }
49
+ }
50
+
51
+ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
52
+ $RootDir = (Resolve-Path "$ScriptDir\..").Path
53
+ $BuildDir = "$RootDir\build\runtime-win32-$TARGET_ARCH"
54
+ $DistDir = "$RootDir\dist"
55
+ $RuntimeDir = "$BuildDir\runtime"
56
+
57
+ $PgBin = "$PgRoot\bin"
58
+ $PgConfig = "$PgBin\pg_config.exe"
59
+
60
+ $OutputName = "mixdog-runtime-${TARGET_OS}-${TARGET_ARCH}-pg${PG_VERSION}-pgvector${PGVECTOR_VERSION}.tar.gz"
61
+
62
+ Write-Host "==> Using preinstalled PG: $PgRoot"
63
+ & $PgConfig --version
64
+ $RealVersion = (& $PgConfig --version) -replace 'PostgreSQL ', ''
65
+ Write-Host " pg_config reports version: $RealVersion"
66
+
67
+ if (Test-Path $RuntimeDir) { Remove-Item -Recurse -Force $RuntimeDir }
68
+ New-Item -ItemType Directory -Force -Path $BuildDir, $DistDir,
69
+ "$RuntimeDir\bin", "$RuntimeDir\lib", "$RuntimeDir\share" | Out-Null
70
+
71
+ Write-Host "==> Cloning pgvector $PGVECTOR_VERSION"
72
+ $PgVectorDir = "$BuildDir\pgvector"
73
+ $VectorDllBuilt = "$PgVectorDir\vector.dll"
74
+
75
+ if (Test-Path $VectorDllBuilt) {
76
+ Write-Host " Cache hit: vector.dll already built at $VectorDllBuilt"
77
+ } else {
78
+ if (Test-Path $PgVectorDir) { Remove-Item -Recurse -Force $PgVectorDir }
79
+ git clone --branch "v$PGVECTOR_VERSION" --depth 1 `
80
+ https://github.com/pgvector/pgvector.git $PgVectorDir
81
+
82
+ Write-Host "==> Building pgvector (MSVC/nmake against system PG 16)"
83
+ Push-Location $PgVectorDir
84
+ try {
85
+ $VsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
86
+ $VcVarsAll = & $VsWhere -latest -find 'VC\Auxiliary\Build\vcvarsall.bat' 2>$null | Select-Object -First 1
87
+ if (-not $VcVarsAll) {
88
+ Write-Error "vswhere could not locate vcvarsall.bat — Visual Studio Build Tools required."
89
+ exit 1
90
+ }
91
+ # CRITICAL: prepend $PgRoot\bin to PATH so any bare `pg_config` call
92
+ # inside Makefile.win resolves to PG 16 — runner has PG 14/15
93
+ # preinstalled and would otherwise win the PATH lookup, producing
94
+ # PG14-ABI vector.dll that fails to load in our PG 16 postgres.exe.
95
+ # Set in PowerShell so cmd /c inherits; setting inside the cmd batch
96
+ # via %PATH% loses vcvarsall's additions due to parse-time expansion.
97
+ $env:PATH = "$PgRoot\bin;$env:PATH"
98
+ $env:PGROOT = $PgRoot
99
+ $BuildCmd = "`"$VcVarsAll`" amd64 && nmake /F Makefile.win PG_CONFIG=`"$PgConfig`""
100
+ cmd /c $BuildCmd
101
+ if ($LASTEXITCODE -ne 0) {
102
+ Write-Error "pgvector nmake build failed (exit $LASTEXITCODE)"
103
+ exit 1
104
+ }
105
+ } finally {
106
+ Pop-Location
107
+ }
108
+ }
109
+
110
+ Write-Host "==> Assembling runtime layout — copy bin (.exe + .dll), lib, share from $PgRoot"
111
+ # Copy ALL .exe + .dll from PG bin so postgres.exe + libpq.dll + libcrypto/libssl/
112
+ # libintl/libiconv/icu*/libxml2/libxslt/libwinpthread/libecpg/libpgtypes etc. all
113
+ # ship together. PG 16 install layout puts these all in bin\.
114
+ Copy-Item "$PgBin\*.exe","$PgBin\*.dll" "$RuntimeDir\bin\" -Force
115
+
116
+ # lib\: PG extension modules (incl. contrib like pgcrypto.dll). vector.dll
117
+ # placed here below — PG looks for $libdir/<ext>.dll which resolves to lib\.
118
+ Copy-Item -Recurse -Force "$PgRoot\lib\*" "$RuntimeDir\lib\" -ErrorAction SilentlyContinue
119
+ # share\: extension SQL/control, locale, timezone data, conf samples.
120
+ Copy-Item -Recurse -Force "$PgRoot\share\*" "$RuntimeDir\share\" -ErrorAction SilentlyContinue
121
+
122
+ Write-Host "==> Manually staging pgvector artifacts (avoid pg_config-derived install paths)"
123
+ $RuntimeExtDir = "$RuntimeDir\share\extension"
124
+ New-Item -ItemType Directory -Force -Path $RuntimeExtDir | Out-Null
125
+ Copy-Item "$PgVectorDir\vector.dll" "$RuntimeDir\lib\" -Force
126
+ Copy-Item "$PgVectorDir\vector.control" $RuntimeExtDir -Force
127
+ Copy-Item "$PgVectorDir\sql\vector--*.sql" $RuntimeExtDir -Force
128
+
129
+ Write-Host "==> Asserting runtime layout"
130
+ $VectorControl = "$RuntimeDir\share\extension\vector.control"
131
+ if (-not (Test-Path $VectorControl)) { Write-Error "ASSERT FAILED: $VectorControl not found"; exit 1 }
132
+ $VectorSql = "$RuntimeDir\share\extension\vector--$PGVECTOR_VERSION.sql"
133
+ if (-not (Test-Path $VectorSql)) { Write-Error "ASSERT FAILED: $VectorSql not found"; exit 1 }
134
+ if (-not (Test-Path "$RuntimeDir\lib\vector.dll")) {
135
+ Write-Error "ASSERT FAILED: vector.dll not found in lib\"
136
+ exit 1
137
+ }
138
+ Write-Host " PASS runtime layout"
139
+
140
+ # Licenses
141
+ if (Test-Path "$PgRoot\doc\postgresql\COPYRIGHT") {
142
+ Copy-Item "$PgRoot\doc\postgresql\COPYRIGHT" "$RuntimeDir\LICENSE.postgresql" -Force
143
+ } elseif (Test-Path "$PgRoot\doc\COPYRIGHT") {
144
+ Copy-Item "$PgRoot\doc\COPYRIGHT" "$RuntimeDir\LICENSE.postgresql" -Force
145
+ }
146
+ if (Test-Path "$PgVectorDir\LICENSE") {
147
+ Copy-Item "$PgVectorDir\LICENSE" "$RuntimeDir\LICENSE.pgvector" -Force
148
+ }
149
+
150
+ Write-Host "==> Self-contained smoke test (initdb + CREATE EXTENSION vector + distance query)"
151
+ & "$RuntimeDir\bin\postgres.exe" --version
152
+ if ($LASTEXITCODE -ne 0) { Write-Error "FAIL: postgres.exe --version exit $LASTEXITCODE"; exit 1 }
153
+
154
+ $SmokeData = "$BuildDir\smoke-pgdata"
155
+ $SmokeLog = "$BuildDir\smoke-pg.log"
156
+ $SmokePort = 55899
157
+ if (Test-Path $SmokeData) { Remove-Item -Recurse -Force $SmokeData }
158
+
159
+ & "$RuntimeDir\bin\initdb.exe" -D $SmokeData --username=postgres --auth-local=trust --no-locale -E UTF8 | Out-Null
160
+ if ($LASTEXITCODE -ne 0) { Write-Error "FAIL: initdb"; exit 1 }
161
+
162
+ & "$RuntimeDir\bin\pg_ctl.exe" -D $SmokeData -o "-p $SmokePort -h 127.0.0.1" -l $SmokeLog -w start
163
+ if ($LASTEXITCODE -ne 0) { Write-Error "FAIL: pg_ctl start (see $SmokeLog)"; Get-Content $SmokeLog | Select-Object -Last 30; exit 1 }
164
+
165
+ try {
166
+ & "$RuntimeDir\bin\psql.exe" -h 127.0.0.1 -p $SmokePort -U postgres -d postgres -c "CREATE EXTENSION vector;" | Out-Null
167
+ if ($LASTEXITCODE -ne 0) { throw "CREATE EXTENSION vector failed" }
168
+ $ExtV = & "$RuntimeDir\bin\psql.exe" -h 127.0.0.1 -p $SmokePort -U postgres -d postgres -tAc "SELECT extversion FROM pg_extension WHERE extname='vector';"
169
+ $Dist = & "$RuntimeDir\bin\psql.exe" -h 127.0.0.1 -p $SmokePort -U postgres -d postgres -tAc "SELECT '[1,2,3]'::vector <-> '[1,2,4]'::vector;"
170
+ Write-Host " vector extension version: $ExtV"
171
+ Write-Host " distance query result: $Dist"
172
+ if ($ExtV.Trim() -ne $PGVECTOR_VERSION) {
173
+ Write-Error "FAIL: extversion='$ExtV' expected='$PGVECTOR_VERSION'"
174
+ exit 1
175
+ }
176
+ Write-Host " PASS smoke (extension load + vector distance)"
177
+ }
178
+ finally {
179
+ & "$RuntimeDir\bin\pg_ctl.exe" -D $SmokeData -m fast stop 2>$null | Out-Null
180
+ Remove-Item -Recurse -Force $SmokeData -ErrorAction SilentlyContinue
181
+ }
182
+
183
+ Write-Host "==> Creating tarball: $OutputName"
184
+ $DistDirFwd = $DistDir.Replace('\', '/')
185
+ $RuntimeDirFwd = $RuntimeDir.Replace('\', '/')
186
+ & tar -czf "$DistDirFwd/$OutputName" -C "$RuntimeDirFwd" .
187
+ if ($LASTEXITCODE -ne 0) { Write-Error "tar failed (exit $LASTEXITCODE)"; exit 1 }
188
+
189
+ Write-Host "==> Generating sha256 sidecar"
190
+ Push-Location $DistDir
191
+ $Hash = (Get-FileHash -Algorithm SHA256 $OutputName).Hash.ToLower()
192
+ "$Hash $OutputName" | Out-File -Encoding ascii "${OutputName}.sha256"
193
+ Pop-Location
194
+
195
+ # ---------------------------------------------------------------------------
196
+ # Phase A: Re-smoke from EXTRACTED tarball with hostile env (cleared PATH,
197
+ # only system32). Catches false-pass where the build host has VC redist /
198
+ # preinstalled DLLs that the tarball might be missing.
199
+ # ---------------------------------------------------------------------------
200
+ Write-Host "==> Re-smoke from extracted tarball (hostile env)"
201
+ $ExtractDir = "$BuildDir\extract-smoke"
202
+ if (Test-Path $ExtractDir) { Remove-Item -Recurse -Force $ExtractDir }
203
+ New-Item -ItemType Directory -Force -Path $ExtractDir | Out-Null
204
+ & tar -xzf "$DistDirFwd/$OutputName" -C ($ExtractDir.Replace('\','/'))
205
+ if ($LASTEXITCODE -ne 0) { Write-Error "extract failed"; exit 1 }
206
+
207
+ $ExtractData = "$ExtractDir\extract-pgdata"
208
+ $ExtractLog = "$ExtractDir\extract-pg.log"
209
+ $ExtractPort = 55898
210
+
211
+ # Snapshot current env, then strip to minimal Windows PATH (no PG14/15/16
212
+ # preinstalled bin, no chocolatey, no MSVC tools).
213
+ $SavedPath = $env:PATH
214
+ $SavedPgRoot = $env:PGROOT
215
+ $env:PATH = "$env:SystemRoot\System32;$env:SystemRoot"
216
+ $env:PGROOT = $null
217
+ $env:PGDATA = $null
218
+
219
+ try {
220
+ & "$ExtractDir\bin\postgres.exe" --version
221
+ if ($LASTEXITCODE -ne 0) { throw "FAIL: postgres --version under hostile env" }
222
+ & "$ExtractDir\bin\initdb.exe" -D $ExtractData --username=postgres --auth-local=trust --no-locale -E UTF8 | Out-Null
223
+ if ($LASTEXITCODE -ne 0) { throw "FAIL: initdb under hostile env" }
224
+ & "$ExtractDir\bin\pg_ctl.exe" -D $ExtractData -o "-p $ExtractPort -h 127.0.0.1" -l $ExtractLog -w start
225
+ if ($LASTEXITCODE -ne 0) { Get-Content $ExtractLog | Select-Object -Last 30; throw "FAIL: pg_ctl start under hostile env" }
226
+ try {
227
+ & "$ExtractDir\bin\psql.exe" -h 127.0.0.1 -p $ExtractPort -U postgres -d postgres -c "CREATE EXTENSION vector;" | Out-Null
228
+ if ($LASTEXITCODE -ne 0) { throw "FAIL: CREATE EXTENSION vector under hostile env" }
229
+ $ExtV2 = & "$ExtractDir\bin\psql.exe" -h 127.0.0.1 -p $ExtractPort -U postgres -d postgres -tAc "SELECT extversion FROM pg_extension WHERE extname='vector';"
230
+ if ($ExtV2.Trim() -ne $PGVECTOR_VERSION) { throw "FAIL: extracted-smoke extversion='$ExtV2'" }
231
+ Write-Host " PASS extracted-tarball smoke (hostile env)"
232
+ } finally {
233
+ & "$ExtractDir\bin\pg_ctl.exe" -D $ExtractData -m fast stop 2>$null | Out-Null
234
+ }
235
+ } finally {
236
+ $env:PATH = $SavedPath
237
+ $env:PGROOT = $SavedPgRoot
238
+ Remove-Item -Recurse -Force $ExtractDir -ErrorAction SilentlyContinue
239
+ }
240
+
241
+ Write-Host "==> Done: $DistDir\$OutputName"
242
+ Get-Item "$DistDir\$OutputName" | Select-Object Name, Length
@@ -44,6 +44,12 @@ await build({
44
44
  // Only `ink` is redirected to Mixdog's checked-in renderer instead of
45
45
  // node_modules/ink.
46
46
  packages: 'external',
47
+ // Bundled CJS helpers (e.g. src/lib/mixdog-debug.cjs) compile to esbuild's
48
+ // __require shim, which throws "Dynamic require of ..." in plain ESM.
49
+ // Provide a real module-scope require so those requires resolve at runtime.
50
+ banner: {
51
+ js: "import { createRequire as __mixdogCreateRequire } from 'node:module';\nconst require = __mixdogCreateRequire(import.meta.url);",
52
+ },
47
53
  external: [
48
54
  '../vendor/*',
49
55
  '../../vendor/*',
@@ -142,6 +142,14 @@ console.log(JSON.stringify({ hookSpecificOutput: {
142
142
  },
143
143
  });
144
144
 
145
+ // Untrusted project: executable project hooks must be neutered (security gate).
146
+ const busUntrusted = createStandaloneHookBus({ dataDir: join(root, 'data-untrusted') });
147
+ const blocked = await busUntrusted.beforeTool({ sessionId: 'sess_test', cwd: root, name: 'shell', args: { command: 'echo ok' } });
148
+ assert.equal(blocked, null);
149
+
150
+ // Trusted project: user-level trustedProjects list re-enables project command hooks.
151
+ mkdirSync(join(root, 'data-mixdog'), { recursive: true });
152
+ writeJson(join(root, 'data-mixdog', 'config.json'), { trustedProjects: [root] });
145
153
  const busWithMixdog = createStandaloneHookBus({ dataDir: join(root, 'data-mixdog') });
146
154
  const denied = await busWithMixdog.beforeTool({ sessionId: 'sess_test', cwd: root, name: 'shell', args: { command: 'echo ok' } });
147
155
  assert.equal(denied.action, 'deny');
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scripts/log-writer-guard-smoke.mjs — unbounded `.log` writer regression gate.
4
+ *
5
+ * A new `.log` file name that lands in src/ without being covered by one of the
6
+ * bounded-writer contracts is an unbounded-growth footgun: the sibling-GC in
7
+ * mixdog-debug.cjs only prunes names it knows (canonical set + stale-sibling
8
+ * regexes), and the worker-boot rotation loop only rotates its explicit list.
9
+ * Any other steady `.log` append grows without limit.
10
+ *
11
+ * This scans src/ for string-literal `.log` file names (the targets that feed
12
+ * appendFile/appendFileSync/createWriteStream) and fails when a name is not:
13
+ * - in the canonical set (parsed live from mixdog-debug.cjs), or
14
+ * - in the worker-boot rotation list (parsed live from worker-bootstrap.mjs), or
15
+ * - matched by the dynamic / per-PID sibling patterns, or
16
+ * - in the explicit in-script allowlist below.
17
+ *
18
+ * Run: node scripts/log-writer-guard-smoke.mjs (or `npm run smoke:logguard`)
19
+ * Exit: 0 = every writer covered, 1 = uncovered `.log` name(s) found.
20
+ */
21
+ import { readFileSync, readdirSync, statSync } from 'node:fs';
22
+ import { dirname, join, relative } from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
24
+
25
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
26
+ const SRC = join(ROOT, 'src');
27
+ // `dist` = built TUI bundle (generated from src/tui/*); vendor is third-party.
28
+ const SKIP_DIRS = new Set(['node_modules', 'dist', '.git', 'vendor']);
29
+ const SCAN_EXT = /\.(mjs|cjs|js|jsx)$/;
30
+
31
+ // `.log` file-name targets. Two shapes, both bounded to real string content:
32
+ // STATIC — a quoted/backtick literal (`'boot.log'`, `"logs\\a.log"`). The
33
+ // `+` stem keeps us off the bare `.endsWith('.log')` guards.
34
+ // TEMPLATE — a `${...}` interpolation immediately followed by a static `.log`
35
+ // tail (`${jobId}.stdout.log`, `${root}.log`). The dynamic prefix
36
+ // is unverifiable, so the trailing literal (down to bare `.log`)
37
+ // must be covered/allowlisted or it fails.
38
+ // Separators (`/` and `\`) are captured; basename() reduces to the file name.
39
+ const STATIC_RE = /['"`]([\w.\-/\\]+\.log)\b/g;
40
+ const TEMPLATE_RE = /\$\{[^}]*\}([\w.\-/\\]*\.log)\b/g;
41
+
42
+ /** Last path segment, splitting on both POSIX and Windows separators. */
43
+ function basename(p) {
44
+ const parts = p.split(/[\\/]/);
45
+ return parts[parts.length - 1];
46
+ }
47
+
48
+ /** Parse a `new Set([...])` / array of quoted `.log` names out of a source file. */
49
+ function parseLogNames(file, anchor) {
50
+ let src;
51
+ try { src = readFileSync(file, 'utf8'); } catch { return []; }
52
+ const at = src.indexOf(anchor);
53
+ if (at === -1) return [];
54
+ const tail = src.slice(at, at + 2000);
55
+ const end = tail.indexOf(']');
56
+ const block = end === -1 ? tail : tail.slice(0, end);
57
+ return [...block.matchAll(/['"]([\w.\-]+\.log)['"]/g)].map((m) => m[1]);
58
+ }
59
+
60
+ // ── Covered names ────────────────────────────────────────────────────
61
+ // Parsed live so a name added to either contract auto-covers here.
62
+ const canonical = new Set(
63
+ parseLogNames(join(SRC, 'lib/mixdog-debug.cjs'), 'CANONICAL_PLUGIN_LOG_NAMES'),
64
+ );
65
+ const rotation = new Set(
66
+ parseLogNames(join(SRC, 'runtime/channels/lib/worker-bootstrap.mjs'), '_rotLog of ['),
67
+ );
68
+
69
+ // Explicitly bounded / non-plugin-dir writers that live outside the sibling-GC
70
+ // contract by design (size-rotated in place, per-job spill, one-shot probes).
71
+ const ALLOWLIST = new Set([
72
+ 'session-start-critical.log', // size-bounded via rotateBoundedLog()
73
+ 'mixdog-ws-upgrade-probe.log', // one-shot WS upgrade probe dump
74
+ '.stdout.log', // shell-jobs per-jobId spill (`${jobId}.stdout.log`)
75
+ '.stderr.log', // shell-jobs per-jobId spill (`${jobId}.stderr.log`)
76
+ ]);
77
+
78
+ // Dynamic / per-PID sibling names (never literal, but keep the contract explicit).
79
+ const DYNAMIC_RE = [
80
+ /^(channels|memory)-worker\.\d+\.\d+\.log$/,
81
+ /-worker\.\d+\.\d+\.log$/,
82
+ /^mcp-debug\.\d+\.\d+\.log$/,
83
+ /^supervisor\.\d+\.log$/,
84
+ ];
85
+
86
+ function isCovered(name) {
87
+ return (
88
+ canonical.has(name) ||
89
+ rotation.has(name) ||
90
+ ALLOWLIST.has(name) ||
91
+ DYNAMIC_RE.some((re) => re.test(name))
92
+ );
93
+ }
94
+
95
+ function* walk(dir) {
96
+ let entries;
97
+ try { entries = readdirSync(dir); } catch { return; }
98
+ for (const name of entries) {
99
+ if (SKIP_DIRS.has(name)) continue;
100
+ const p = join(dir, name);
101
+ if (statSync(p).isDirectory()) yield* walk(p);
102
+ else if (SCAN_EXT.test(name)) yield p;
103
+ }
104
+ }
105
+
106
+ const offenders = [];
107
+ let scanned = 0;
108
+ for (const file of walk(SRC)) {
109
+ scanned++;
110
+ const lines = readFileSync(file, 'utf8').split('\n');
111
+ for (let i = 0; i < lines.length; i++) {
112
+ for (const re of [STATIC_RE, TEMPLATE_RE]) {
113
+ for (const m of lines[i].matchAll(re)) {
114
+ const name = basename(m[1]);
115
+ if (isCovered(name)) continue;
116
+ offenders.push(`${relative(ROOT, file)}:${i + 1} — uncovered \`.log\` writer target \`${name}\``);
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ if (offenders.length > 0) {
123
+ console.error('log-writer-guard: FAIL — uncovered unbounded `.log` writer(s):');
124
+ for (const line of offenders) console.error(` ${line}`);
125
+ console.error(
126
+ `log-writer-guard: ${offenders.length} offender(s). Add the name to CANONICAL_PLUGIN_LOG_NAMES ` +
127
+ '(mixdog-debug.cjs), the worker-boot rotation list, or the ALLOWLIST in this script.',
128
+ );
129
+ process.exit(1);
130
+ }
131
+ console.log(`log-writer-guard: ok — ${scanned} files scanned, every \`.log\` writer covered`);