mixdog 0.7.1

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 (404) hide show
  1. package/.claude-plugin/marketplace.json +31 -0
  2. package/.claude-plugin/plugin.json +20 -0
  3. package/.gitattributes +34 -0
  4. package/.mcp.json +14 -0
  5. package/ARCHITECTURE.md +77 -0
  6. package/CHANGELOG.md +7 -0
  7. package/CONTRIBUTING.md +45 -0
  8. package/DATA-FLOW.md +79 -0
  9. package/LICENSE +21 -0
  10. package/README.md +389 -0
  11. package/SECURITY.md +138 -0
  12. package/UNINSTALL.md +112 -0
  13. package/agents/maintenance.md +5 -0
  14. package/agents/memory-classification.md +30 -0
  15. package/agents/scheduler-task.md +18 -0
  16. package/agents/webhook-handler.md +27 -0
  17. package/agents/worker.md +24 -0
  18. package/bin/bridge +133 -0
  19. package/bin/statusline-launcher.mjs +78 -0
  20. package/bin/statusline-lib.mjs +550 -0
  21. package/bin/statusline.mjs +607 -0
  22. package/bun.lock +802 -0
  23. package/commands/config.md +16 -0
  24. package/commands/doctor.md +13 -0
  25. package/commands/setup.md +17 -0
  26. package/defaults/cycle3-review-prompt.md +90 -0
  27. package/defaults/hidden-roles.json +65 -0
  28. package/defaults/memory-chunk-prompt.md +63 -0
  29. package/defaults/memory-promote-prompt.md +135 -0
  30. package/defaults/mixdog-config.template.json +27 -0
  31. package/defaults/user-workflow.json +8 -0
  32. package/defaults/user-workflow.md +12 -0
  33. package/hooks/hooks.json +73 -0
  34. package/hooks/lib/active-instance.cjs +77 -0
  35. package/hooks/lib/permission-evaluator.cjs +411 -0
  36. package/hooks/lib/permission-route.cjs +63 -0
  37. package/hooks/lib/permission-rules.cjs +170 -0
  38. package/hooks/lib/settings-loader.cjs +116 -0
  39. package/hooks/post-tool-use.cjs +84 -0
  40. package/hooks/pre-mcp-sandbox.cjs +158 -0
  41. package/hooks/pre-tool-subagent.cjs +253 -0
  42. package/hooks/session-start.cjs +1372 -0
  43. package/hooks/turn-timer.cjs +82 -0
  44. package/lib/claude-md-writer.cjs +386 -0
  45. package/lib/config-cjs.cjs +61 -0
  46. package/lib/hook-pipe-path.cjs +10 -0
  47. package/lib/keychain-cjs.cjs +263 -0
  48. package/lib/plugin-paths.cjs +61 -0
  49. package/lib/rules-builder.cjs +241 -0
  50. package/lib/text-utils.cjs +61 -0
  51. package/native/README.md +117 -0
  52. package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
  53. package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
  54. package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
  55. package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
  56. package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
  57. package/package.json +107 -0
  58. package/prompts/code-review.txt +16 -0
  59. package/prompts/security-audit.txt +17 -0
  60. package/rules/bridge/00-common.md +39 -0
  61. package/rules/bridge/20-skip-protocol.md +18 -0
  62. package/rules/bridge/30-explorer.md +33 -0
  63. package/rules/bridge/40-cycle1-agent.md +52 -0
  64. package/rules/bridge/41-cycle2-agent.md +62 -0
  65. package/rules/bridge/42-cycle3-agent.md +44 -0
  66. package/rules/lead/00-tool-lead.md +61 -0
  67. package/rules/lead/01-general.md +23 -0
  68. package/rules/lead/02-channels.md +49 -0
  69. package/rules/lead/03-team.md +27 -0
  70. package/rules/lead/04-workflow.md +20 -0
  71. package/rules/shared/00-language.md +14 -0
  72. package/rules/shared/01-tool.md +138 -0
  73. package/scripts/bootstrap.mjs +184 -0
  74. package/scripts/bridge-unify-smoke.mjs +308 -0
  75. package/scripts/build-runtime-linux.sh +348 -0
  76. package/scripts/build-runtime-macos.sh +217 -0
  77. package/scripts/build-runtime-windows.ps1 +242 -0
  78. package/scripts/builtin-utils-smoke.mjs +392 -0
  79. package/scripts/check-json.mjs +45 -0
  80. package/scripts/check-syntax-changed.mjs +102 -0
  81. package/scripts/check-syntax.mjs +58 -0
  82. package/scripts/code-graph-batch.test.mjs +33 -0
  83. package/scripts/config-preserve-smoke.mjs +180 -0
  84. package/scripts/doctor.mjs +484 -0
  85. package/scripts/edit-normalize-fuzz.mjs +130 -0
  86. package/scripts/edit-normalize-smoke.mjs +401 -0
  87. package/scripts/edit-operation-smoke.mjs +369 -0
  88. package/scripts/edit2-smoke.mjs +63 -0
  89. package/scripts/fuzzy-e2e.mjs +28 -0
  90. package/scripts/fuzzy-smoke.mjs +26 -0
  91. package/scripts/generate-runtime-manifest.mjs +166 -0
  92. package/scripts/guard-smoke.mjs +66 -0
  93. package/scripts/hidden-role-schema-smoke.mjs +162 -0
  94. package/scripts/hook-routing-smoke.mjs +29 -0
  95. package/scripts/inject-input.ps1 +204 -0
  96. package/scripts/io-complex-smoke.mjs +667 -0
  97. package/scripts/io-explore-bench.mjs +424 -0
  98. package/scripts/io-guardrails-smoke.mjs +205 -0
  99. package/scripts/io-mini-bench-baseline.json +11 -0
  100. package/scripts/io-mini-bench.mjs +216 -0
  101. package/scripts/io-route-harness.mjs +933 -0
  102. package/scripts/io-telemetry-report.mjs +691 -0
  103. package/scripts/mutation-bench.mjs +564 -0
  104. package/scripts/mutation-io-smoke.mjs +1081 -0
  105. package/scripts/native-patch-bridge-smoke.mjs +288 -0
  106. package/scripts/native-patch-smoke.mjs +304 -0
  107. package/scripts/patch-interior-context-smoke.mjs +49 -0
  108. package/scripts/patch-newline-utf8-smoke.mjs +157 -0
  109. package/scripts/perf-hook-smoke.mjs +71 -0
  110. package/scripts/permission-eval-smoke.mjs +426 -0
  111. package/scripts/prep-patch.mjs +53 -0
  112. package/scripts/prep-shim.mjs +96 -0
  113. package/scripts/provider-cache-smoke.mjs +687 -0
  114. package/scripts/report-runtime-health.mjs +132 -0
  115. package/scripts/run-mcp.mjs +1547 -0
  116. package/scripts/salvage-v4a-shatter.test.mjs +58 -0
  117. package/scripts/scoped-cache-io-smoke.mjs +103 -0
  118. package/scripts/shell-policy-round3-smoke.mjs +46 -0
  119. package/scripts/smoke-runtime-negative.ps1 +100 -0
  120. package/scripts/smoke-runtime-negative.sh +95 -0
  121. package/scripts/stall-policy-smoke.mjs +50 -0
  122. package/scripts/start-memory-worker.mjs +23 -0
  123. package/scripts/statusline-launcher-smoke.mjs +82 -0
  124. package/scripts/stress-atomic-write.mjs +1028 -0
  125. package/scripts/test-config-rmw-restore.mjs +122 -0
  126. package/scripts/test-fault-inject.mjs +164 -0
  127. package/scripts/test-large-file.mjs +174 -0
  128. package/scripts/tool-edge-smoke.mjs +209 -0
  129. package/scripts/uninstall.mjs +201 -0
  130. package/scripts/webhook-selfheal-smoke.mjs +29 -0
  131. package/scripts/write-overwrite-guard-smoke.mjs +56 -0
  132. package/server-main.mjs +3055 -0
  133. package/server.mjs +468 -0
  134. package/setup/config-merge.mjs +254 -0
  135. package/setup/install.mjs +120 -0
  136. package/setup/launch-core.mjs +507 -0
  137. package/setup/launch.mjs +101 -0
  138. package/setup/setup-server.mjs +3206 -0
  139. package/setup/setup.html +3693 -0
  140. package/skills/retro-skill-proposer/SKILL.md +92 -0
  141. package/skills/schedule-add/SKILL.md +77 -0
  142. package/skills/setup/SKILL.md +346 -0
  143. package/skills/webhook-add/SKILL.md +81 -0
  144. package/src/agent/bridge-stall-watchdog.mjs +337 -0
  145. package/src/agent/index.mjs +2138 -0
  146. package/src/agent/orchestrator/activity-bus.mjs +38 -0
  147. package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +1010 -0
  148. package/src/agent/orchestrator/bridge-retry.mjs +220 -0
  149. package/src/agent/orchestrator/bridge-trace.mjs +583 -0
  150. package/src/agent/orchestrator/cache-mtime.mjs +58 -0
  151. package/src/agent/orchestrator/config.mjs +358 -0
  152. package/src/agent/orchestrator/context/collect.mjs +651 -0
  153. package/src/agent/orchestrator/dispatch-persist.mjs +549 -0
  154. package/src/agent/orchestrator/drain-registry.mjs +50 -0
  155. package/src/agent/orchestrator/explore-validator.mjs +8 -0
  156. package/src/agent/orchestrator/internal-roles.mjs +118 -0
  157. package/src/agent/orchestrator/internal-tools.mjs +88 -0
  158. package/src/agent/orchestrator/jobs.mjs +116 -0
  159. package/src/agent/orchestrator/mcp/client.mjs +364 -0
  160. package/src/agent/orchestrator/providers/anthropic-betas.mjs +21 -0
  161. package/src/agent/orchestrator/providers/anthropic-oauth.mjs +1745 -0
  162. package/src/agent/orchestrator/providers/anthropic.mjs +437 -0
  163. package/src/agent/orchestrator/providers/gemini.mjs +1175 -0
  164. package/src/agent/orchestrator/providers/grok-oauth.mjs +782 -0
  165. package/src/agent/orchestrator/providers/model-catalog.mjs +241 -0
  166. package/src/agent/orchestrator/providers/openai-compat.mjs +1467 -0
  167. package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +1890 -0
  168. package/src/agent/orchestrator/providers/openai-oauth.mjs +1307 -0
  169. package/src/agent/orchestrator/providers/openai-ws.mjs +104 -0
  170. package/src/agent/orchestrator/providers/registry.mjs +192 -0
  171. package/src/agent/orchestrator/providers/retry-classifier.mjs +325 -0
  172. package/src/agent/orchestrator/session/abort-lookup.mjs +13 -0
  173. package/src/agent/orchestrator/session/cache/post-edit-marks.mjs +42 -0
  174. package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
  175. package/src/agent/orchestrator/session/cache/read-cache.mjs +319 -0
  176. package/src/agent/orchestrator/session/cache/scoped-cache-outcome.mjs +11 -0
  177. package/src/agent/orchestrator/session/cache/scoped-cache.mjs +361 -0
  178. package/src/agent/orchestrator/session/cache/util.mjs +49 -0
  179. package/src/agent/orchestrator/session/loop.mjs +1478 -0
  180. package/src/agent/orchestrator/session/manager.mjs +1975 -0
  181. package/src/agent/orchestrator/session/read-dedup.mjs +6 -0
  182. package/src/agent/orchestrator/session/result-classification.mjs +65 -0
  183. package/src/agent/orchestrator/session/save-session-worker.mjs +18 -0
  184. package/src/agent/orchestrator/session/store.mjs +624 -0
  185. package/src/agent/orchestrator/session/stream-watchdog.mjs +130 -0
  186. package/src/agent/orchestrator/session/tool-result-offload.mjs +166 -0
  187. package/src/agent/orchestrator/session/trim.mjs +491 -0
  188. package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +115 -0
  189. package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +327 -0
  190. package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +150 -0
  191. package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +228 -0
  192. package/src/agent/orchestrator/smart-bridge/index.mjs +215 -0
  193. package/src/agent/orchestrator/smart-bridge/profiles.mjs +37 -0
  194. package/src/agent/orchestrator/smart-bridge/registry.mjs +348 -0
  195. package/src/agent/orchestrator/smart-bridge/session-builder.mjs +116 -0
  196. package/src/agent/orchestrator/stall-policy.mjs +195 -0
  197. package/src/agent/orchestrator/tool-loop-guard.mjs +75 -0
  198. package/src/agent/orchestrator/tools/bash-policy-scan.mjs +77 -0
  199. package/src/agent/orchestrator/tools/bash-session.mjs +721 -0
  200. package/src/agent/orchestrator/tools/builtin/advisory-lock.mjs +171 -0
  201. package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +455 -0
  202. package/src/agent/orchestrator/tools/builtin/atomic-write.mjs +236 -0
  203. package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +480 -0
  204. package/src/agent/orchestrator/tools/builtin/binary-file.mjs +76 -0
  205. package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +256 -0
  206. package/src/agent/orchestrator/tools/builtin/cache-layers.mjs +386 -0
  207. package/src/agent/orchestrator/tools/builtin/cwd-utils.mjs +37 -0
  208. package/src/agent/orchestrator/tools/builtin/device-paths.mjs +154 -0
  209. package/src/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +292 -0
  210. package/src/agent/orchestrator/tools/builtin/diff-utils.mjs +109 -0
  211. package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +58 -0
  212. package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +240 -0
  213. package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +113 -0
  214. package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +74 -0
  215. package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +242 -0
  216. package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +211 -0
  217. package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +1364 -0
  218. package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +126 -0
  219. package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +141 -0
  220. package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +194 -0
  221. package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +60 -0
  222. package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +168 -0
  223. package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +173 -0
  224. package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +48 -0
  225. package/src/agent/orchestrator/tools/builtin/fs-reachability.mjs +48 -0
  226. package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +99 -0
  227. package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +170 -0
  228. package/src/agent/orchestrator/tools/builtin/grep-formatting.mjs +113 -0
  229. package/src/agent/orchestrator/tools/builtin/hash-utils.mjs +6 -0
  230. package/src/agent/orchestrator/tools/builtin/list-formatting.mjs +7 -0
  231. package/src/agent/orchestrator/tools/builtin/list-tool.mjs +593 -0
  232. package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +89 -0
  233. package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +300 -0
  234. package/src/agent/orchestrator/tools/builtin/open-config-tool.mjs +26 -0
  235. package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +152 -0
  236. package/src/agent/orchestrator/tools/builtin/path-locks.mjs +35 -0
  237. package/src/agent/orchestrator/tools/builtin/path-utils.mjs +201 -0
  238. package/src/agent/orchestrator/tools/builtin/read-args.mjs +103 -0
  239. package/src/agent/orchestrator/tools/builtin/read-batch.mjs +172 -0
  240. package/src/agent/orchestrator/tools/builtin/read-constants.mjs +40 -0
  241. package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
  242. package/src/agent/orchestrator/tools/builtin/read-image-resize.mjs +189 -0
  243. package/src/agent/orchestrator/tools/builtin/read-image.mjs +88 -0
  244. package/src/agent/orchestrator/tools/builtin/read-lines.mjs +12 -0
  245. package/src/agent/orchestrator/tools/builtin/read-mode-tool.mjs +455 -0
  246. package/src/agent/orchestrator/tools/builtin/read-open.mjs +190 -0
  247. package/src/agent/orchestrator/tools/builtin/read-range-index.mjs +271 -0
  248. package/src/agent/orchestrator/tools/builtin/read-ranges.mjs +26 -0
  249. package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +728 -0
  250. package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +173 -0
  251. package/src/agent/orchestrator/tools/builtin/read-special-files.mjs +268 -0
  252. package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
  253. package/src/agent/orchestrator/tools/builtin/read-tool.mjs +530 -0
  254. package/src/agent/orchestrator/tools/builtin/read-windows.mjs +107 -0
  255. package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +196 -0
  256. package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +422 -0
  257. package/src/agent/orchestrator/tools/builtin/search-builders.mjs +158 -0
  258. package/src/agent/orchestrator/tools/builtin/search-tool.mjs +869 -0
  259. package/src/agent/orchestrator/tools/builtin/shell-analysis.mjs +653 -0
  260. package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +936 -0
  261. package/src/agent/orchestrator/tools/builtin/shell-output.mjs +36 -0
  262. package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +214 -0
  263. package/src/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +143 -0
  264. package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +206 -0
  265. package/src/agent/orchestrator/tools/builtin/snapshot-validation.mjs +98 -0
  266. package/src/agent/orchestrator/tools/builtin/text-stats.mjs +69 -0
  267. package/src/agent/orchestrator/tools/builtin/windows-roots.mjs +23 -0
  268. package/src/agent/orchestrator/tools/builtin/write-tool.mjs +401 -0
  269. package/src/agent/orchestrator/tools/builtin.mjs +500 -0
  270. package/src/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +39 -0
  271. package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
  272. package/src/agent/orchestrator/tools/code-graph.mjs +4095 -0
  273. package/src/agent/orchestrator/tools/cwd-tool.mjs +298 -0
  274. package/src/agent/orchestrator/tools/destructive-warning.mjs +323 -0
  275. package/src/agent/orchestrator/tools/edit-normalize.mjs +603 -0
  276. package/src/agent/orchestrator/tools/env-scrub.mjs +100 -0
  277. package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +144 -0
  278. package/src/agent/orchestrator/tools/graph-manifest.json +26 -0
  279. package/src/agent/orchestrator/tools/host-input.mjs +204 -0
  280. package/src/agent/orchestrator/tools/mutation-content-cache.mjs +67 -0
  281. package/src/agent/orchestrator/tools/mutation-planner.mjs +75 -0
  282. package/src/agent/orchestrator/tools/next-call-utils.mjs +48 -0
  283. package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +133 -0
  284. package/src/agent/orchestrator/tools/patch-manifest.json +26 -0
  285. package/src/agent/orchestrator/tools/patch-tool-defs.mjs +20 -0
  286. package/src/agent/orchestrator/tools/patch.mjs +2754 -0
  287. package/src/agent/orchestrator/tools/progress-message.mjs +118 -0
  288. package/src/agent/orchestrator/tools/result-compression.mjs +279 -0
  289. package/src/agent/orchestrator/tools/shell-command.mjs +865 -0
  290. package/src/agent/orchestrator/tools/shell-exec-policy.mjs +89 -0
  291. package/src/agent/orchestrator/tools/shell-policy-danger-target.mjs +27 -0
  292. package/src/agent/orchestrator/tools/shell-policy-imports.mjs +7 -0
  293. package/src/agent/orchestrator/tools/shell-policy.mjs +345 -0
  294. package/src/agent/orchestrator/tools/shell-snapshot.mjs +313 -0
  295. package/src/agent/orchestrator/workflow-store.mjs +93 -0
  296. package/src/agent/tool-defs.mjs +103 -0
  297. package/src/channels/backends/discord.mjs +784 -0
  298. package/src/channels/data/voice-runtime-manifest.json +138 -0
  299. package/src/channels/index.mjs +3229 -0
  300. package/src/channels/lib/cli-worker-host.mjs +12 -0
  301. package/src/channels/lib/config-lock.mjs +13 -0
  302. package/src/channels/lib/config.mjs +292 -0
  303. package/src/channels/lib/drop-trace.mjs +71 -0
  304. package/src/channels/lib/event-pipeline.mjs +81 -0
  305. package/src/channels/lib/event-queue.mjs +345 -0
  306. package/src/channels/lib/executor.mjs +168 -0
  307. package/src/channels/lib/format.mjs +188 -0
  308. package/src/channels/lib/holidays.mjs +138 -0
  309. package/src/channels/lib/hook-pipe-server.mjs +802 -0
  310. package/src/channels/lib/interaction-workflows.mjs +184 -0
  311. package/src/channels/lib/memory-client.mjs +149 -0
  312. package/src/channels/lib/output-forwarder.mjs +765 -0
  313. package/src/channels/lib/runtime-paths.mjs +479 -0
  314. package/src/channels/lib/scheduler.mjs +723 -0
  315. package/src/channels/lib/session-control.mjs +36 -0
  316. package/src/channels/lib/session-discovery.mjs +103 -0
  317. package/src/channels/lib/settings.mjs +11 -0
  318. package/src/channels/lib/state-file.mjs +68 -0
  319. package/src/channels/lib/status-snapshot.mjs +219 -0
  320. package/src/channels/lib/tool-format.mjs +140 -0
  321. package/src/channels/lib/transcript-discovery.mjs +195 -0
  322. package/src/channels/lib/voice-runtime-fetcher.mjs +734 -0
  323. package/src/channels/lib/webhook.mjs +1179 -0
  324. package/src/channels/lib/whisper-server.mjs +477 -0
  325. package/src/channels/tool-defs.mjs +170 -0
  326. package/src/daemon/host.mjs +118 -0
  327. package/src/daemon/mcp-transport.mjs +47 -0
  328. package/src/daemon/session.mjs +100 -0
  329. package/src/daemon/thin-client.mjs +71 -0
  330. package/src/daemon/transport.mjs +163 -0
  331. package/src/memory/data/runtime-manifest.json +40 -0
  332. package/src/memory/index.mjs +3305 -0
  333. package/src/memory/lib/agent-ipc.mjs +93 -0
  334. package/src/memory/lib/bridge-trace-queries.mjs +120 -0
  335. package/src/memory/lib/core-memory-store.mjs +330 -0
  336. package/src/memory/lib/embedding-provider.mjs +269 -0
  337. package/src/memory/lib/embedding-worker.mjs +323 -0
  338. package/src/memory/lib/llm-worker-host.mjs +17 -0
  339. package/src/memory/lib/memory-cycle.mjs +11 -0
  340. package/src/memory/lib/memory-cycle1.mjs +641 -0
  341. package/src/memory/lib/memory-cycle2.mjs +1284 -0
  342. package/src/memory/lib/memory-cycle3.mjs +540 -0
  343. package/src/memory/lib/memory-embed.mjs +299 -0
  344. package/src/memory/lib/memory-extraction.mjs +5 -0
  345. package/src/memory/lib/memory-maintenance-store.mjs +32 -0
  346. package/src/memory/lib/memory-ops-policy.mjs +190 -0
  347. package/src/memory/lib/memory-recall-id-patch.mjs +15 -0
  348. package/src/memory/lib/memory-recall-read-query.mjs +7 -0
  349. package/src/memory/lib/memory-recall-scope-filter.mjs +63 -0
  350. package/src/memory/lib/memory-recall-store.mjs +621 -0
  351. package/src/memory/lib/memory-retrievers.mjs +112 -0
  352. package/src/memory/lib/memory-score.mjs +71 -0
  353. package/src/memory/lib/memory-text-utils.mjs +58 -0
  354. package/src/memory/lib/memory.mjs +412 -0
  355. package/src/memory/lib/model-profile.mjs +85 -0
  356. package/src/memory/lib/pg/adapter.mjs +308 -0
  357. package/src/memory/lib/pg/process.mjs +360 -0
  358. package/src/memory/lib/pg/supervisor.mjs +396 -0
  359. package/src/memory/lib/project-id-resolver.mjs +86 -0
  360. package/src/memory/lib/runtime-fetcher.mjs +442 -0
  361. package/src/memory/lib/trace-store.mjs +728 -0
  362. package/src/memory/tool-defs.mjs +79 -0
  363. package/src/search/index.mjs +1173 -0
  364. package/src/search/lib/backends/anthropic-oauth.mjs +98 -0
  365. package/src/search/lib/backends/exa.mjs +50 -0
  366. package/src/search/lib/backends/firecrawl.mjs +61 -0
  367. package/src/search/lib/backends/gemini-api.mjs +83 -0
  368. package/src/search/lib/backends/grok-oauth.mjs +86 -0
  369. package/src/search/lib/backends/index.mjs +150 -0
  370. package/src/search/lib/backends/openai-api.mjs +144 -0
  371. package/src/search/lib/backends/openai-oauth.mjs +98 -0
  372. package/src/search/lib/backends/openai-web-search.mjs +76 -0
  373. package/src/search/lib/backends/tavily.mjs +55 -0
  374. package/src/search/lib/backends/xai-api.mjs +113 -0
  375. package/src/search/lib/cache.mjs +131 -0
  376. package/src/search/lib/config.mjs +192 -0
  377. package/src/search/lib/formatter.mjs +115 -0
  378. package/src/search/lib/provider-usage.mjs +67 -0
  379. package/src/search/lib/providers.mjs +47 -0
  380. package/src/search/lib/search-intent.mjs +109 -0
  381. package/src/search/lib/setup-handler.mjs +261 -0
  382. package/src/search/lib/state.mjs +201 -0
  383. package/src/search/lib/web-tools.mjs +1207 -0
  384. package/src/search/tool-defs.mjs +83 -0
  385. package/src/setup/defender-exclusion.mjs +183 -0
  386. package/src/shared/abort-controller.mjs +15 -0
  387. package/src/shared/atomic-file.mjs +420 -0
  388. package/src/shared/config.mjs +350 -0
  389. package/src/shared/daemon-recycle.mjs +108 -0
  390. package/src/shared/disable-claude-builtins.mjs +88 -0
  391. package/src/shared/err-text.mjs +12 -0
  392. package/src/shared/llm/cost.mjs +66 -0
  393. package/src/shared/llm/http-agent.mjs +123 -0
  394. package/src/shared/llm/index.mjs +41 -0
  395. package/src/shared/llm/pid-cleanup.mjs +27 -0
  396. package/src/shared/llm/usage-log.mjs +47 -0
  397. package/src/shared/plugin-paths.mjs +58 -0
  398. package/src/shared/schedules-store.mjs +70 -0
  399. package/src/shared/seed.mjs +119 -0
  400. package/src/shared/user-cwd.mjs +213 -0
  401. package/src/shared/user-data-guard.mjs +238 -0
  402. package/src/status/aggregator.mjs +584 -0
  403. package/src/status/server.mjs +413 -0
  404. package/tools.json +1653 -0
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env node
2
+ // bridge-unify-smoke.mjs — arg-routing checks for the unified `bridge` tool.
3
+ //
4
+ // Exercises the type-discriminator routing and tag-registry guard rails on the
5
+ // fast-fail branches (no provider/model needed): list on an empty store,
6
+ // send/close against an unknown tag, spawn missing role, and the type-less
7
+ // (default spawn) backward-compat path. Does NOT spin a real worker.
8
+
9
+ import { tmpdir } from 'os';
10
+ import { join, dirname } from 'path';
11
+ import { fileURLToPath } from 'url';
12
+ import { mkdirSync, writeFileSync, rmSync } from 'fs';
13
+
14
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
15
+ process.env.CLAUDE_PLUGIN_ROOT ||= ROOT;
16
+ // MUST be unconditional (`=`, not `||=`): when the smoke runs inside a real
17
+ // mixdog environment CLAUDE_PLUGIN_DATA already points at the LIVE plugin data
18
+ // dir, so `||=` would write the synthetic `sess_<pid>_<scenario>_<ts>` fixtures
19
+ // straight into the production session store. Those fixtures (provider/model
20
+ // `none`, closed:false, no `owner`) slip past both sweeps and linger forever in
21
+ // `bridge list` / the statusline. Force an isolated per-pid temp dir, always.
22
+ const _SMOKE_DATA_DIR = join(tmpdir(), `mixdog-bridge-unify-smoke-${process.pid}`);
23
+ process.env.CLAUDE_PLUGIN_DATA = _SMOKE_DATA_DIR;
24
+ process.env.MIXDOG_SKIP_USER_DATA_BACKUP = '1';
25
+
26
+ const { handleToolCall } = await import('../src/agent/index.mjs');
27
+ // updateSessionStage drives the in-memory runtime stage the send busy-guard
28
+ // consults when session.status is still unset — exercised by the startup-window
29
+ // race case (#1) below.
30
+ const { updateSessionStage, drainPendingMessages } = await import('../src/agent/orchestrator/session/manager.mjs');
31
+ // initProviders lets the cold-tag respawn case register a network-free dummy
32
+ // provider so the spawn path passes its synchronous provider check and returns
33
+ // a detached jobId (the real API call is deferred to the IIFE, never reached).
34
+ const { initProviders } = await import('../src/agent/orchestrator/providers/registry.mjs');
35
+
36
+ // Write a minimal on-disk session fixture so the unified `bridge type=send`
37
+ // path (which resolves a raw sess_ id via getSession/loadSession) can be
38
+ // exercised without spinning a real provider-backed worker. The send handler's
39
+ // busy-guard reads session.status BEFORE calling askSession, so a fixture is
40
+ // enough to assert the resumability guard rails (bugs #1/#2).
41
+ const _sessionsDir = join(process.env.CLAUDE_PLUGIN_DATA, 'sessions');
42
+ function writeSessionFixture(id, status) {
43
+ mkdirSync(_sessionsDir, { recursive: true });
44
+ const now = Date.now();
45
+ const session = {
46
+ id,
47
+ status,
48
+ closed: false,
49
+ generation: 0,
50
+ provider: 'none', // unknown provider → askSession fails fast (no hang)
51
+ model: 'none',
52
+ messages: [],
53
+ tools: [],
54
+ createdAt: now,
55
+ updatedAt: now,
56
+ };
57
+ writeFileSync(join(_sessionsDir, `${id}.json`), JSON.stringify(session));
58
+ return id;
59
+ }
60
+
61
+ let passed = 0;
62
+ let failed = 0;
63
+ function check(name, fn) {
64
+ return Promise.resolve()
65
+ .then(fn)
66
+ .then(() => { passed++; console.log(` PASS ${name}`); })
67
+ .catch((e) => { failed++; console.log(` FAIL ${name}\n ${e?.message || e}`); });
68
+ }
69
+
70
+ // Tool results are MCP content blocks: { content: [{ type:'text', text }], isError? }.
71
+ function textOf(res) {
72
+ if (res && Array.isArray(res.content)) {
73
+ return res.content.map((c) => (c && typeof c.text === 'string' ? c.text : '')).join('');
74
+ }
75
+ return typeof res === 'string' ? res : JSON.stringify(res);
76
+ }
77
+ function assert(cond, msg) { if (!cond) throw new Error(msg || 'assertion failed'); }
78
+
79
+ await check('type=list on empty store → "No active sessions."', async () => {
80
+ const res = await handleToolCall('bridge', { type: 'list' }, {});
81
+ const t = textOf(res);
82
+ assert(/No active sessions/.test(t), `unexpected: ${t}`);
83
+ });
84
+
85
+ await check('type=send with unknown tag → clear error', async () => {
86
+ const res = await handleToolCall('bridge', { type: 'send', tag: 'nope1', message: 'hi' }, {});
87
+ const t = textOf(res);
88
+ assert(res.isError === true, 'expected isError');
89
+ assert(/not found \(may have been reaped after 5min\) — include a role to respawn fresh/.test(t), `unexpected: ${t}`);
90
+ });
91
+
92
+ await check('type=send unknown tag WITH role → respawns fresh (jobId)', async () => {
93
+ // Register a network-free dummy provider so the spawn path passes its
94
+ // synchronous provider check. The bench escape hatch (provider+model) builds
95
+ // an ad-hoc preset so no user-workflow.json role mapping is needed. spawn
96
+ // returns the jobId synchronously; the real API call (which would fail on the
97
+ // dummy key) is deferred to the detached IIFE and never asserted here.
98
+ await initProviders({ anthropic: { enabled: true, apiKey: 'dummy-smoke-key' } });
99
+ const res = await handleToolCall('bridge', { type: 'send', tag: 'respawn1', role: 'worker', provider: 'anthropic', model: 'claude-3-haiku', message: 'hi' }, {});
100
+ const t = textOf(res);
101
+ assert(res.isError !== true, `expected success, got: ${t}`);
102
+ assert(/"jobId"\s*:\s*"bridge_/.test(t), `expected a jobId, got: ${t}`);
103
+ });
104
+
105
+ await check('type=send missing message → error', async () => {
106
+ const res = await handleToolCall('bridge', { type: 'send', tag: 'x' }, {});
107
+ assert(res.isError === true, 'expected isError');
108
+ });
109
+
110
+ await check('type=close with unknown tag → error', async () => {
111
+ const res = await handleToolCall('bridge', { type: 'close', tag: 'nope2' }, {});
112
+ const t = textOf(res);
113
+ assert(res.isError === true, 'expected isError');
114
+ assert(/does not map to a live session/.test(t), `unexpected: ${t}`);
115
+ });
116
+
117
+ await check('type=close missing tag → error', async () => {
118
+ const res = await handleToolCall('bridge', { type: 'close' }, {});
119
+ assert(res.isError === true, 'expected isError');
120
+ });
121
+
122
+ await check('unknown type → error', async () => {
123
+ const res = await handleToolCall('bridge', { type: 'bogus', role: 'worker' }, {});
124
+ const t = textOf(res);
125
+ assert(res.isError === true, 'expected isError');
126
+ assert(/unknown type/.test(t), `unexpected: ${t}`);
127
+ });
128
+
129
+ await check('default (no type) + no role → spawn path "role is required"', async () => {
130
+ const res = await handleToolCall('bridge', {}, {});
131
+ const t = textOf(res);
132
+ assert(res.isError === true, 'expected isError');
133
+ assert(/role is required/.test(t), `unexpected: ${t}`);
134
+ });
135
+
136
+ await check('type=spawn + role but no prompt/message → prompt resolution error', async () => {
137
+ const res = await handleToolCall('bridge', { type: 'spawn', role: 'nonexistent-role-xyz' }, {});
138
+ const t = textOf(res);
139
+ assert(res.isError === true, 'expected isError');
140
+ // Either prompt-missing or role-not-found surfaces — both prove routing into spawn.
141
+ assert(/prompt|role|provide exactly one/.test(t), `unexpected: ${t}`);
142
+ });
143
+
144
+ // --- Resumability guard rails (bugs #1/#2) ---------------------------------
145
+ // These drive the unified `bridge type=send` path against an on-disk session
146
+ // fixture addressed by raw sess_ id (the tag→sessionId registry is module
147
+ // private; raw-id passthrough exercises the same handler code).
148
+
149
+ await check('type=send while worker running → queued (not rejected)', async () => {
150
+ // pendingMessages pattern: a send during the in-flight turn must NOT become
151
+ // the first user message and must NOT be rejected — it enqueues and drains
152
+ // after the current turn. A non-terminal status trips the queue branch
153
+ // before askSession.
154
+ const sid = writeSessionFixture(`sess_${process.pid}_running_${Date.now()}`, 'running');
155
+ const res = await handleToolCall('bridge', { type: 'send', sessionId: sid, message: 'race' }, {});
156
+ const t = textOf(res);
157
+ assert(res.isError !== true, `expected success (queued), got: ${t}`);
158
+ assert(/"queued"\s*:\s*true/.test(t), `expected queued:true, got: ${t}`);
159
+ assert(!/still starting|retry shortly/.test(t), `must not reject a busy send anymore: ${t}`);
160
+ // The message must actually be parked on the pending queue.
161
+ const drained = drainPendingMessages(sid);
162
+ assert(drained.length === 1 && drained[0] === 'race', `expected ['race'] queued, got: ${JSON.stringify(drained)}`);
163
+ });
164
+
165
+ await check('type=send to non-terminal (connecting) status → queued', async () => {
166
+ // Same branch, mid-startup stage label — queues instead of rejecting. Since
167
+ // queuing preserves order, the connecting/startup window can queue safely:
168
+ // the queued send runs after the worker's first turn.
169
+ const sid = writeSessionFixture(`sess_${process.pid}_connecting_${Date.now()}`, 'connecting');
170
+ const res = await handleToolCall('bridge', { type: 'send', sessionId: sid, message: 'race' }, {});
171
+ const t = textOf(res);
172
+ assert(res.isError !== true, `expected success (queued), got: ${t}`);
173
+ assert(/"queued"\s*:\s*true/.test(t), `expected queued:true, got: ${t}`);
174
+ assert(!/still starting|retry shortly/.test(t), `must not reject a connecting send anymore: ${t}`);
175
+ });
176
+
177
+ await check('type=send to idle session → returns a jobId synchronously (detached, no inline response)', async () => {
178
+ // Bug #1 + async detach: once the initial turn has settled to a terminal
179
+ // status the send must NOT be rejected by the busy-guard — it passes the
180
+ // guard, cancels the pending 120s reap, and (now) returns IMMEDIATELY with a
181
+ // detached jobId instead of blocking on askSession. The provider failure on
182
+ // the dummy session now surfaces asynchronously via notifyFn, NOT in this
183
+ // synchronous return value.
184
+ const sid = writeSessionFixture(`sess_${process.pid}_idle_${Date.now()}`, 'idle');
185
+ const res = await handleToolCall('bridge', { type: 'send', sessionId: sid, message: 'resume' }, {});
186
+ const t = textOf(res);
187
+ assert(res.isError !== true, `expected a synchronous jobId return (not an error), got: ${t}`);
188
+ // Must be the immediate detached return, NOT the busy-guard.
189
+ assert(!/still starting|retry shortly/.test(t), `guard wrongly rejected an idle resume: ${t}`);
190
+ assert(/bridge_\d+_/.test(t), `expected a detached jobId in the sync return, got: ${t}`);
191
+ assert(/"detached":\s*true/.test(t), `expected detached:true in the sync return, got: ${t}`);
192
+ // The resume reply is delivered later via dispatch_result/notifyFn — the
193
+ // synchronous return must NOT carry an inline response/usage payload.
194
+ assert(!/"response"/.test(t), `send must not return an inline response now: ${t}`);
195
+ });
196
+
197
+ await check('type=send during startup window (tag bound, status not yet running) → queued', async () => {
198
+ // Bug #1 (round 2): spawn binds the tag and retry rebinds it BEFORE the
199
+ // session status is written 'running' / the runtime entry reaches askSession.
200
+ // A send landing in that gap must NOT slip through as the first user turn.
201
+ // The runtime stage is marked 'connecting' at the moment the tag is bound, so
202
+ // the queue branch's active-status set catches the window even though
203
+ // session.status is still unwritten — the send is QUEUED and drained after
204
+ // the worker's first turn, preserving order. Simulate by writing a fixture
205
+ // with NO status, then setting the runtime stage to 'connecting' the way the
206
+ // spawn/retry bind now does.
207
+ const sid = writeSessionFixture(`sess_${process.pid}_startwin_${Date.now()}`, undefined);
208
+ updateSessionStage(sid, 'connecting');
209
+ const res = await handleToolCall('bridge', { type: 'send', sessionId: sid, message: 'race' }, {});
210
+ const t = textOf(res);
211
+ assert(res.isError !== true, `expected success (queued), got: ${t}`);
212
+ assert(/"queued"\s*:\s*true/.test(t), `expected queued:true, got: ${t}`);
213
+ assert(!/still starting|retry shortly/.test(t), `startup-window send must queue, not reject: ${t}`);
214
+ });
215
+
216
+ await check('failed resume re-arms reap → session stays usable for a follow-up send', async () => {
217
+ // Bug #2 (round 2): send cancels the pending reap before resuming, but the
218
+ // reap was previously re-armed only on the success path — a failed resumed
219
+ // turn left the reap canceled, so the idle/error session was never terminally
220
+ // reaped. The fix re-arms _scheduleBridgeReap in a finally (idempotent). The
221
+ // reap timer is module-private, so assert the observable consequence: after a
222
+ // FIRST resume that fails on the dummy provider, a SECOND send to the same
223
+ // session still passes the busy-guard and reaches askSession (the failed
224
+ // resume left the session addressable + the reap re-armed, not orphaned).
225
+ const sid = writeSessionFixture(`sess_${process.pid}_failresume_${Date.now()}`, 'idle');
226
+ const first = await handleToolCall('bridge', { type: 'send', sessionId: sid, message: 'resume-1' }, {});
227
+ const t1 = textOf(first);
228
+ assert(first.isError !== true, `expected first resume to detach (jobId), got: ${t1}`);
229
+ assert(!/still starting|retry shortly/.test(t1), `first resume wrongly guard-rejected: ${t1}`);
230
+ assert(/bridge_\d+_/.test(t1), `expected first resume jobId, got: ${t1}`);
231
+ // The detached resume fails on the dummy provider in the background and
232
+ // re-arms the reap in its finally. Yield so that async tail can settle.
233
+ await new Promise((r) => setTimeout(r, 50));
234
+ // Re-mark idle (real path: failed turn flips status to 'idle'/'error'; the
235
+ // fixture write here restores a terminal status so the second send is guarded
236
+ // the same way) and re-send.
237
+ writeSessionFixture(sid, 'idle');
238
+ const second = await handleToolCall('bridge', { type: 'send', sessionId: sid, message: 'resume-2' }, {});
239
+ const t2 = textOf(second);
240
+ assert(second.isError !== true, `expected second resume to detach (jobId), got: ${t2}`);
241
+ assert(!/still starting|retry shortly/.test(t2), `second resume wrongly guard-rejected (session orphaned by failed resume?): ${t2}`);
242
+ assert(/bridge_\d+_/.test(t2), `expected second resume jobId (session addressable + reap re-armed, not orphaned), got: ${t2}`);
243
+ });
244
+
245
+ await check('type=send in post-askSession/pre-idle gap (persisted running, runtime done) → NOT stranded', async () => {
246
+ // Bug #2: spawn keeps persisted status 'running' until AFTER the completion
247
+ // emit (updateSessionStatus(idle) runs OUTSIDE askSession, past the drain
248
+ // point). A send landing after askSession returned but before status flips
249
+ // to 'idle' used to see persisted 'running' and ENQUEUE — but askSession had
250
+ // already drained and returned, so nothing consumed it: stranded + out of
251
+ // order. The busy-guard now prefers the RUNTIME terminal stage over the
252
+ // stale persisted 'running', so the send does a fresh detached resume
253
+ // instead of queuing into the void. Simulate: persisted status 'running'
254
+ // (not yet flipped), runtime stage 'done' (markSessionDone ran before the
255
+ // turn-loop drain).
256
+ const sid = writeSessionFixture(`sess_${process.pid}_gap_${Date.now()}`, 'running');
257
+ updateSessionStage(sid, 'done');
258
+ const res = await handleToolCall('bridge', { type: 'send', sessionId: sid, message: 'gap' }, {});
259
+ const t = textOf(res);
260
+ assert(res.isError !== true, `expected a detached resume, got: ${t}`);
261
+ assert(!/"queued"\s*:\s*true/.test(t), `must NOT enqueue in the post-askSession/pre-idle gap (would strand): ${t}`);
262
+ assert(/bridge_\d+_/.test(t), `expected a detached jobId (fresh resume), got: ${t}`);
263
+ // Nothing parked on the queue — proves the message was not stranded.
264
+ assert(drainPendingMessages(sid).length === 0, 'queue must be empty (message not stranded)');
265
+ });
266
+
267
+ await check('type=send raw MISSING sess_ id WITH role → respawns fresh (jobId), not a dead-session resume', async () => {
268
+ // Bug #5: a raw `sess_...` id that does NOT resolve to a LIVE session must
269
+ // fall into the reaped/unknown branch — with a role present, that means a
270
+ // fresh respawn (spawn path), NOT an async askSession launched against a
271
+ // missing/closed session. The id below is never written to disk, so
272
+ // _isLiveSession() rejects it and the precheck flips bridgeType→spawn.
273
+ await initProviders({ anthropic: { enabled: true, apiKey: 'dummy-smoke-key' } });
274
+ const missing = `sess_${process.pid}_ghost_${Date.now()}`;
275
+ const res = await handleToolCall('bridge', { type: 'send', sessionId: missing, role: 'worker', provider: 'anthropic', model: 'claude-3-haiku', message: 'hi' }, {});
276
+ const t = textOf(res);
277
+ assert(res.isError !== true, `expected a fresh respawn, got: ${t}`);
278
+ assert(/"jobId"\s*:\s*"bridge_/.test(t), `expected a jobId (fresh spawn), got: ${t}`);
279
+ // A fresh spawn allocates a NEW session id — it must not echo the dead id.
280
+ assert(!t.includes(missing), `respawn must not resume the missing/dead session id: ${t}`);
281
+ });
282
+
283
+ await check('multiple sends to a running worker → queued FIFO, drained in order', async () => {
284
+ // Order preservation: two sends landing during the same in-flight turn must
285
+ // queue in arrival order and drain in that order (the askSession turn loop
286
+ // shifts them off the front as follow-up turns). queueDepth reflects the
287
+ // growing queue.
288
+ const sid = writeSessionFixture(`sess_${process.pid}_fifo_${Date.now()}`, 'running');
289
+ const r1 = await handleToolCall('bridge', { type: 'send', sessionId: sid, message: 'first' }, {});
290
+ const r2 = await handleToolCall('bridge', { type: 'send', sessionId: sid, message: 'second' }, {});
291
+ const t1 = textOf(r1); const t2 = textOf(r2);
292
+ assert(/"queued"\s*:\s*true/.test(t1) && /"queued"\s*:\s*true/.test(t2), `both sends must queue, got: ${t1} | ${t2}`);
293
+ assert(/"queueDepth"\s*:\s*1/.test(t1), `first queued send should report queueDepth 1, got: ${t1}`);
294
+ assert(/"queueDepth"\s*:\s*2/.test(t2), `second queued send should report queueDepth 2, got: ${t2}`);
295
+ // Drain mirrors what askSession does after each turn — FIFO order.
296
+ const drained = drainPendingMessages(sid);
297
+ assert(drained.length === 2 && drained[0] === 'first' && drained[1] === 'second',
298
+ `expected ['first','second'] in order, got: ${JSON.stringify(drained)}`);
299
+ // Queue is emptied after drain.
300
+ assert(drainPendingMessages(sid).length === 0, 'queue must be empty after drain');
301
+ });
302
+
303
+ console.log(`\nbridge-unify-smoke: ${passed} passed, ${failed} failed`);
304
+ // Remove the isolated data dir so no fixture survives the run. Belt-and-braces
305
+ // with the unconditional CLAUDE_PLUGIN_DATA override above: even the isolated
306
+ // temp store should not accumulate per-pid dirs across repeated smoke runs.
307
+ try { rmSync(_SMOKE_DATA_DIR, { recursive: true, force: true }); } catch { /* best-effort */ }
308
+ process.exit(failed === 0 ? 0 : 1);
@@ -0,0 +1,348 @@
1
+ #!/usr/bin/env bash
2
+ # build-runtime-linux.sh — Build self-contained PostgreSQL 16 + pgvector runtime on Linux.
3
+ # Designed to run inside ubuntu:20.04 container (glibc 2.31 floor) so produced
4
+ # binaries run on every distro from Ubuntu 20.04 / Debian 11 / RHEL 8 forward.
5
+ # Targets the runner's native arch (x64 or arm64).
6
+ # Produces: dist/mixdog-runtime-linux-{arch}-pg{pgver}-pgvector{vecver}.tar.gz
7
+ # Bundles foreign dyn deps via ldd transitive closure (binaries + ALL extension
8
+ # modules under lib/postgresql/) + patchelf rpath rewrite. Final smoke: initdb,
9
+ # pg_ctl start, CREATE EXTENSION vector, distance query, stop.
10
+
11
+ set -euo pipefail
12
+
13
+ PG_VERSION="16.4"
14
+ PGVECTOR_VERSION="0.8.2"
15
+ TARGET_OS="${TARGET_OS:-linux}"
16
+ TARGET_ARCH="${TARGET_ARCH:-x64}"
17
+
18
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+ ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
20
+ BUILD_DIR="$ROOT_DIR/build/runtime-linux-$TARGET_ARCH"
21
+ STAGE_DIR="$BUILD_DIR/stage"
22
+ DIST_DIR="$ROOT_DIR/dist"
23
+ RUNTIME_DIR="$BUILD_DIR/runtime"
24
+
25
+ OUTPUT_NAME="mixdog-runtime-${TARGET_OS}-${TARGET_ARCH}-pg${PG_VERSION}-pgvector${PGVECTOR_VERSION}.tar.gz"
26
+
27
+ mkdir -p "$BUILD_DIR" "$STAGE_DIR" "$DIST_DIR" "$RUNTIME_DIR"/{bin,lib,share}
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Build deps. SUDO=sudo iff non-root; inside ubuntu:20.04 container we are root.
31
+ # ---------------------------------------------------------------------------
32
+ SUDO=""
33
+ if [[ "$(id -u)" -ne 0 ]]; then SUDO="sudo"; fi
34
+ # Export rather than inline-assign before $SUDO: when SUDO is empty (root),
35
+ # `$SUDO DEBIAN_FRONTEND=... apt-get` would parse the env-assignment as a
36
+ # command name and fail with "command not found".
37
+ export DEBIAN_FRONTEND=noninteractive
38
+
39
+ echo "==> Installing build dependencies"
40
+ $SUDO apt-get update -qq
41
+ $SUDO apt-get install -y --no-install-recommends \
42
+ build-essential libreadline-dev zlib1g-dev libssl-dev \
43
+ libicu-dev libxml2-dev libzstd-dev liblz4-dev \
44
+ pkg-config curl git ca-certificates patchelf file \
45
+ bsdmainutils
46
+
47
+ if [[ -x "$STAGE_DIR/bin/postgres" ]]; then
48
+ echo "==> Cache hit: PG already built at $STAGE_DIR — skipping configure/make"
49
+ unset TARGET_OS TARGET_ARCH
50
+ else
51
+ echo "==> Downloading PostgreSQL $PG_VERSION source"
52
+ cd "$BUILD_DIR"
53
+ if [[ ! -f "postgresql-${PG_VERSION}.tar.gz" ]]; then
54
+ curl -fsSL "https://ftp.postgresql.org/pub/source/v${PG_VERSION}/postgresql-${PG_VERSION}.tar.gz" \
55
+ -o "postgresql-${PG_VERSION}.tar.gz"
56
+ fi
57
+ rm -rf "postgresql-${PG_VERSION}"
58
+ tar xzf "postgresql-${PG_VERSION}.tar.gz"
59
+
60
+ echo "==> Configuring PostgreSQL"
61
+ cd "postgresql-${PG_VERSION}"
62
+ ./configure \
63
+ --prefix="$STAGE_DIR" \
64
+ --without-perl \
65
+ --without-python \
66
+ --without-tcl \
67
+ --with-openssl \
68
+ --with-libxml \
69
+ --with-icu \
70
+ --with-readline \
71
+ --enable-thread-safety \
72
+ CFLAGS="-O2"
73
+
74
+ echo "==> Building PostgreSQL"
75
+ # PG Makefile.global has its own TARGET_ARCH var; env-passed TARGET_ARCH=x64
76
+ # would collide as a make-variable override and leak into compile commands.
77
+ unset TARGET_OS TARGET_ARCH
78
+ make -j"$(nproc)"
79
+ make install
80
+ make -C contrib/pgcrypto install
81
+ fi
82
+
83
+ PG_CONFIG="$STAGE_DIR/bin/pg_config"
84
+ export PATH="$STAGE_DIR/bin:$PATH"
85
+
86
+ echo "==> Stripping PostgreSQL binaries"
87
+ find "$STAGE_DIR" -name '*.so*' -type f -exec strip --strip-debug {} \; 2>/dev/null || true
88
+ find "$STAGE_DIR/bin" -type f -exec strip --strip-all {} \; 2>/dev/null || true
89
+
90
+ if [[ -f "$STAGE_DIR/lib/postgresql/vector.so" ]]; then
91
+ echo "==> Cache hit: pgvector already installed — skipping clone/build"
92
+ else
93
+ echo "==> Cloning pgvector $PGVECTOR_VERSION"
94
+ cd "$BUILD_DIR"
95
+ rm -rf pgvector
96
+ git clone --branch "v${PGVECTOR_VERSION}" --depth 1 \
97
+ https://github.com/pgvector/pgvector.git pgvector
98
+
99
+ echo "==> Building pgvector"
100
+ cd pgvector
101
+ make PG_CONFIG="$PG_CONFIG" -j"$(nproc)"
102
+ make PG_CONFIG="$PG_CONFIG" install
103
+ fi
104
+
105
+ echo "==> Assembling runtime layout"
106
+ rm -rf "$RUNTIME_DIR"
107
+ mkdir -p "$RUNTIME_DIR"/{bin,lib,share}
108
+ cp -a "$STAGE_DIR/bin/postgres" "$RUNTIME_DIR/bin/"
109
+ cp -a "$STAGE_DIR/bin/pg_ctl" "$RUNTIME_DIR/bin/"
110
+ cp -a "$STAGE_DIR/bin/pg_dump" "$RUNTIME_DIR/bin/"
111
+ cp -a "$STAGE_DIR/bin/pg_restore" "$RUNTIME_DIR/bin/"
112
+ cp -a "$STAGE_DIR/bin/psql" "$RUNTIME_DIR/bin/"
113
+ cp -a "$STAGE_DIR/bin/initdb" "$RUNTIME_DIR/bin/"
114
+
115
+ cp -a "$STAGE_DIR/lib"/. "$RUNTIME_DIR/lib/" 2>/dev/null || true
116
+ cp -a "$STAGE_DIR/share"/. "$RUNTIME_DIR/share/" 2>/dev/null || true
117
+
118
+ # ---------------------------------------------------------------------------
119
+ # Bundle foreign dyn deps — seed from binaries AND every extension module
120
+ # under lib/postgresql/. dlopen-loaded modules (extensions) need the same
121
+ # treatment as direct binary deps; missing them = runtime "ERROR: could not
122
+ # load library" on CREATE EXTENSION.
123
+ # ---------------------------------------------------------------------------
124
+ echo "==> Bundling foreign dynamic dependencies (binaries + extensions)"
125
+
126
+ KEEP_RE='^(linux-vdso|ld-linux|libc\.so|libm\.so|libpthread\.so|libdl\.so|librt\.so|libnsl\.so|libresolv\.so)'
127
+
128
+ collect_deps() {
129
+ ldd "$1" 2>/dev/null \
130
+ | awk '/=>/{print $3}' \
131
+ | grep -E '^/' \
132
+ | while read -r path; do
133
+ bn=$(basename "$path")
134
+ if echo "$bn" | grep -Eq "$KEEP_RE"; then
135
+ continue # system lib — keep on host
136
+ fi
137
+ echo "$path"
138
+ done \
139
+ | sort -u
140
+ }
141
+
142
+ declare -A SEEN
143
+ declare -a QUEUE
144
+ # Seeds: binaries + every .so under lib/postgresql/ (extensions, contrib mods).
145
+ for seed in "$RUNTIME_DIR/bin/postgres" "$RUNTIME_DIR/bin/psql" \
146
+ "$RUNTIME_DIR/bin/pg_ctl"; do
147
+ [[ -f "$seed" ]] && QUEUE+=("$seed")
148
+ done
149
+ while IFS= read -r -d '' ext; do
150
+ QUEUE+=("$ext")
151
+ done < <(find "$RUNTIME_DIR/lib/postgresql" -name '*.so' -print0 2>/dev/null)
152
+
153
+ declare -a FOREIGN
154
+ while [[ ${#QUEUE[@]} -gt 0 ]]; do
155
+ current="${QUEUE[0]}"
156
+ QUEUE=("${QUEUE[@]:1}")
157
+ while IFS= read -r dep; do
158
+ real="$(readlink -f "$dep")"
159
+ [[ -n "${SEEN[$real]+x}" ]] && continue
160
+ SEEN[$real]=1
161
+ FOREIGN+=("$dep")
162
+ QUEUE+=("$dep")
163
+ done < <(collect_deps "$current")
164
+ done
165
+
166
+ # Sanity: glibc / loader must never be bundled — if KEEP_RE regresses this catches it.
167
+ if find "$RUNTIME_DIR/lib" -maxdepth 1 \
168
+ \( -name 'libc.so*' -o -name 'ld-linux*' -o -name 'libpthread.so*' \) \
169
+ | grep -q .; then
170
+ echo "FATAL: bundled glibc/loader detected — KEEP_RE filter did not work"
171
+ exit 1
172
+ fi
173
+
174
+ echo " bundling ${#FOREIGN[@]} foreign libraries"
175
+ for so_path in "${FOREIGN[@]}"; do
176
+ real_path="$(readlink -f "$so_path")"
177
+ real_name="$(basename "$real_path")"
178
+ if [[ ! -f "$RUNTIME_DIR/lib/$real_name" ]]; then
179
+ cp -L "$real_path" "$RUNTIME_DIR/lib/$real_name"
180
+ chmod u+w "$RUNTIME_DIR/lib/$real_name"
181
+ echo " + $real_name"
182
+ fi
183
+ link="$so_path"
184
+ while [[ -L "$link" ]]; do
185
+ link_name="$(basename "$link")"
186
+ link_target="$(readlink "$link")"
187
+ if [[ ! -e "$RUNTIME_DIR/lib/$link_name" ]]; then
188
+ ln -sf "$(basename "$link_target")" "$RUNTIME_DIR/lib/$link_name"
189
+ fi
190
+ link="$(dirname "$link")/$link_target"
191
+ done
192
+ done
193
+
194
+ echo "==> Stripping static archives from lib/"
195
+ find "$RUNTIME_DIR/lib" -name '*.a' -delete
196
+
197
+ echo "==> Patching rpath"
198
+ # Binaries: $ORIGIN/../lib (from bin/ to lib/)
199
+ find "$RUNTIME_DIR/bin" -type f -executable | while read -r bin; do
200
+ if file "$bin" 2>/dev/null | grep -q ELF; then
201
+ patchelf --set-rpath '$ORIGIN/../lib' "$bin" 2>/dev/null || true
202
+ fi
203
+ done
204
+ # Top-level lib/*.so*: $ORIGIN
205
+ find "$RUNTIME_DIR/lib" -maxdepth 1 -type f -name '*.so*' | while read -r so; do
206
+ if file "$so" 2>/dev/null | grep -q ELF; then
207
+ patchelf --set-rpath '$ORIGIN' "$so" 2>/dev/null || true
208
+ fi
209
+ done
210
+ # Every extension module under lib/postgresql/: $ORIGIN/.. (=lib/) and $ORIGIN/../.. (=runtime root)
211
+ find "$RUNTIME_DIR/lib/postgresql" -name '*.so' 2>/dev/null | while read -r ext; do
212
+ if file "$ext" 2>/dev/null | grep -q ELF; then
213
+ patchelf --set-rpath '$ORIGIN/..:$ORIGIN/../..' "$ext" 2>/dev/null || true
214
+ fi
215
+ done
216
+
217
+ # ---------------------------------------------------------------------------
218
+ # Self-contained smoke — full PG lifecycle, not just --version
219
+ # ---------------------------------------------------------------------------
220
+ echo "==> Self-contained smoke test (initdb + CREATE EXTENSION vector + distance query)"
221
+ unset LD_LIBRARY_PATH
222
+ SMOKE_DATA="$BUILD_DIR/smoke-pgdata"
223
+ SMOKE_LOG="$BUILD_DIR/smoke-pg.log"
224
+ SMOKE_PORT=55899
225
+
226
+ # PG initdb refuses to run as root. Inside ubuntu:20.04 container we are root,
227
+ # so create an unprivileged user and run the smoke under it.
228
+ if [[ "$(id -u)" -eq 0 ]]; then
229
+ if ! id pguser >/dev/null 2>&1; then useradd -m -s /bin/bash pguser; fi
230
+ chown -R pguser:pguser "$BUILD_DIR"
231
+ RUN_AS=(runuser -u pguser --)
232
+ else
233
+ RUN_AS=()
234
+ fi
235
+
236
+ "${RUN_AS[@]}" "$RUNTIME_DIR/bin/postgres" --version || { echo "FAIL: postgres --version"; exit 1; }
237
+ MISSING="$(ldd "$RUNTIME_DIR/bin/postgres" 2>&1 | grep 'not found' || true)"
238
+ if [[ -n "$MISSING" ]]; then echo "FAIL: missing deps in postgres:"; echo "$MISSING"; exit 1; fi
239
+
240
+ SMOKE_OK=""
241
+ for attempt in 1 2 3; do
242
+ echo "==> Self-contained smoke (attempt $attempt/3)"
243
+ rm -rf "$SMOKE_DATA"
244
+ set +e
245
+ (
246
+ set -e
247
+ "${RUN_AS[@]}" "$RUNTIME_DIR/bin/initdb" -D "$SMOKE_DATA" --auth-local=trust --no-locale -E UTF8 -U postgres > /dev/null
248
+ "${RUN_AS[@]}" "$RUNTIME_DIR/bin/pg_ctl" -D "$SMOKE_DATA" -o "-p $SMOKE_PORT -h 127.0.0.1" -l "$SMOKE_LOG" -w start
249
+ "${RUN_AS[@]}" "$RUNTIME_DIR/bin/psql" -h 127.0.0.1 -p "$SMOKE_PORT" -U postgres -d postgres -c "CREATE EXTENSION vector;" > /dev/null
250
+ EXTV="$("${RUN_AS[@]}" "$RUNTIME_DIR/bin/psql" -h 127.0.0.1 -p "$SMOKE_PORT" -U postgres -d postgres -tAc "SELECT extversion FROM pg_extension WHERE extname='vector';")"
251
+ DIST="$("${RUN_AS[@]}" "$RUNTIME_DIR/bin/psql" -h 127.0.0.1 -p "$SMOKE_PORT" -U postgres -d postgres -tAc "SELECT '[1,2,3]'::vector <-> '[1,2,4]'::vector;")"
252
+ echo " vector extension version: $EXTV"
253
+ echo " distance query result: $DIST"
254
+ [[ "$EXTV" == "$PGVECTOR_VERSION" ]] || { echo "FAIL: extversion=$EXTV expected=$PGVECTOR_VERSION"; exit 1; }
255
+ "${RUN_AS[@]}" "$RUNTIME_DIR/bin/pg_ctl" -D "$SMOKE_DATA" -m fast stop > /dev/null
256
+ )
257
+ attempt_rc=$?
258
+ set -e
259
+ if [[ $attempt_rc -eq 0 ]]; then
260
+ rm -rf "$SMOKE_DATA"
261
+ echo " PASS smoke (extension load + vector distance)"
262
+ SMOKE_OK=1
263
+ break
264
+ fi
265
+ echo " attempt $attempt failed (rc=$attempt_rc) — cleaning up"
266
+ echo " -- last 20 lines of $SMOKE_LOG --"
267
+ tail -20 "$SMOKE_LOG" 2>/dev/null || true
268
+ "${RUN_AS[@]}" "$RUNTIME_DIR/bin/pg_ctl" -D "$SMOKE_DATA" -m immediate stop > /dev/null 2>&1 || true
269
+ pkill -f "$RUNTIME_DIR/bin/postgres" > /dev/null 2>&1 || true
270
+ rm -rf "$SMOKE_DATA"
271
+ done
272
+ if [[ -z "${SMOKE_OK:-}" ]]; then
273
+ echo "FAIL: smoke failed all 3 attempts"
274
+ exit 1
275
+ fi
276
+
277
+ # Licenses
278
+ curl -fsSL "https://raw.githubusercontent.com/postgres/postgres/REL_16_STABLE/COPYRIGHT" \
279
+ -o "$RUNTIME_DIR/LICENSE.postgresql"
280
+ cp "$BUILD_DIR/pgvector/LICENSE" "$RUNTIME_DIR/LICENSE.pgvector"
281
+
282
+ echo "==> Creating tarball: $OUTPUT_NAME"
283
+ tar czf "$DIST_DIR/$OUTPUT_NAME" -C "$RUNTIME_DIR" .
284
+
285
+ echo "==> Generating sha256 sidecar"
286
+ cd "$DIST_DIR"
287
+ sha256sum "$OUTPUT_NAME" > "${OUTPUT_NAME}.sha256"
288
+
289
+ # ---------------------------------------------------------------------------
290
+ # Phase A: Re-smoke from EXTRACTED tarball with hostile env. Catches false-pass
291
+ # where the build host happens to satisfy a dep that the tarball is missing.
292
+ # Uses `env -i` to clear all variables, minimal PATH, fresh data dir.
293
+ # ---------------------------------------------------------------------------
294
+ echo "==> Re-smoke from extracted tarball (hostile env, verify self-contained)"
295
+ EXTRACT_DIR="$BUILD_DIR/extract-smoke"
296
+ rm -rf "$EXTRACT_DIR"; mkdir -p "$EXTRACT_DIR"
297
+ tar xzf "$DIST_DIR/$OUTPUT_NAME" -C "$EXTRACT_DIR"
298
+ if [[ "$(id -u)" -eq 0 ]]; then chown -R pguser:pguser "$EXTRACT_DIR"; fi
299
+ EXTRACT_DATA="$EXTRACT_DIR/extract-pgdata"
300
+ EXTRACT_LOG="$EXTRACT_DIR/extract-pg.log"
301
+ EXTRACT_PORT=55898
302
+
303
+ "${RUN_AS[@]}" env -i HOME="$EXTRACT_DIR" PATH="/usr/bin:/bin" \
304
+ "$EXTRACT_DIR/bin/postgres" --version
305
+
306
+ EXTRACT_SMOKE_OK=""
307
+ for attempt in 1 2 3; do
308
+ echo "==> Re-smoke from extracted tarball (attempt $attempt/3)"
309
+ rm -rf "$EXTRACT_DATA"
310
+ set +e
311
+ (
312
+ set -e
313
+ "${RUN_AS[@]}" env -i HOME="$EXTRACT_DIR" PATH="/usr/bin:/bin" \
314
+ "$EXTRACT_DIR/bin/initdb" -D "$EXTRACT_DATA" --auth-local=trust --no-locale -E UTF8 -U postgres > /dev/null
315
+ "${RUN_AS[@]}" env -i HOME="$EXTRACT_DIR" PATH="/usr/bin:/bin" \
316
+ "$EXTRACT_DIR/bin/pg_ctl" -D "$EXTRACT_DATA" -o "-p $EXTRACT_PORT -h 127.0.0.1" -l "$EXTRACT_LOG" -w start
317
+ "${RUN_AS[@]}" env -i HOME="$EXTRACT_DIR" PATH="/usr/bin:/bin" \
318
+ "$EXTRACT_DIR/bin/psql" -h 127.0.0.1 -p "$EXTRACT_PORT" -U postgres -d postgres -c "CREATE EXTENSION vector;" > /dev/null
319
+ EXTV2="$("${RUN_AS[@]}" env -i HOME="$EXTRACT_DIR" PATH="/usr/bin:/bin" \
320
+ "$EXTRACT_DIR/bin/psql" -h 127.0.0.1 -p "$EXTRACT_PORT" -U postgres -d postgres -tAc \
321
+ "SELECT extversion FROM pg_extension WHERE extname='vector';")"
322
+ [[ "$EXTV2" == "$PGVECTOR_VERSION" ]] || { echo "FAIL: extracted-smoke extversion=$EXTV2"; exit 1; }
323
+ "${RUN_AS[@]}" env -i HOME="$EXTRACT_DIR" PATH="/usr/bin:/bin" \
324
+ "$EXTRACT_DIR/bin/pg_ctl" -D "$EXTRACT_DATA" -m fast stop > /dev/null
325
+ )
326
+ attempt_rc=$?
327
+ set -e
328
+ if [[ $attempt_rc -eq 0 ]]; then
329
+ rm -rf "$EXTRACT_DIR"
330
+ echo " PASS extracted-tarball smoke"
331
+ EXTRACT_SMOKE_OK=1
332
+ break
333
+ fi
334
+ echo " attempt $attempt failed (rc=$attempt_rc) — cleaning up"
335
+ echo " -- last 20 lines of $EXTRACT_LOG --"
336
+ tail -20 "$EXTRACT_LOG" 2>/dev/null || true
337
+ "${RUN_AS[@]}" env -i HOME="$EXTRACT_DIR" PATH="/usr/bin:/bin" \
338
+ "$EXTRACT_DIR/bin/pg_ctl" -D "$EXTRACT_DATA" -m immediate stop > /dev/null 2>&1 || true
339
+ pkill -f "$EXTRACT_DIR/bin/postgres" > /dev/null 2>&1 || true
340
+ rm -rf "$EXTRACT_DATA"
341
+ done
342
+ if [[ -z "${EXTRACT_SMOKE_OK:-}" ]]; then
343
+ echo "FAIL: extracted-tarball smoke failed all 3 attempts"
344
+ exit 1
345
+ fi
346
+
347
+ echo "==> Done: $DIST_DIR/$OUTPUT_NAME"
348
+ ls -lh "$DIST_DIR/$OUTPUT_NAME"