mixdog 1.0.12 → 1.1.0

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 (256) hide show
  1. package/README.md +75 -181
  2. package/package.json +4 -2
  3. package/scripts/build-shipped-history.mjs +131 -0
  4. package/scripts/lib/run-node-tests.mjs +235 -13
  5. package/scripts/lib/test-failure-records.mjs +5 -0
  6. package/scripts/lib/test-failure-summary.mjs +9 -3
  7. package/scripts/lib/test-spawn-ledger.mjs +19 -0
  8. package/scripts/prune-embedding-runtime.mjs +9 -5
  9. package/scripts/test-direct.mjs +16 -1
  10. package/src/agents/advisor/AGENT.md +38 -0
  11. package/src/agents/advisor/agent.json +6 -0
  12. package/src/agents/reviewer/AGENT.md +5 -2
  13. package/src/agents/reviewer/agent.json +1 -1
  14. package/src/defaults/shipped-history.json +381 -0
  15. package/src/defaults/skills/goal-management/SKILL.md +11 -5
  16. package/src/defaults/skills/setup/SKILL.md +5 -8
  17. package/src/defaults/skills/setup/references/actions.md +61 -31
  18. package/src/defaults/skills/setup/references/surfaces.md +12 -10
  19. package/src/lib/rules-builder.cjs +1 -1
  20. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +3 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +4 -0
  22. package/src/runtime/agent/orchestrator/config-presets.d.mts +1 -0
  23. package/src/runtime/agent/orchestrator/config-presets.mjs +44 -0
  24. package/src/runtime/agent/orchestrator/config-providers-merge.mjs +9 -1
  25. package/src/runtime/agent/orchestrator/config.mjs +102 -21
  26. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +77 -26
  27. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +44 -14
  28. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +2 -1
  29. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +11 -2
  30. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +2 -0
  31. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +3 -3
  32. package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +388 -0
  33. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +92 -109
  34. package/src/runtime/agent/orchestrator/providers/custom-reasoning.mjs +107 -0
  35. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +51 -9
  36. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +2 -1
  37. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +14 -9
  38. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +51 -11
  39. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +6 -1
  40. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +7 -5
  41. package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +12 -0
  42. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +27 -7
  43. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +12 -2
  44. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +6 -1
  45. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +2 -1
  46. package/src/runtime/agent/orchestrator/providers/quota-reading.mjs +70 -0
  47. package/src/runtime/agent/orchestrator/providers/registry.mjs +38 -3
  48. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +19 -35
  49. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +5 -0
  50. package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +0 -17
  51. package/src/runtime/agent/orchestrator/runtime-core/goal-state.mjs +0 -3
  52. package/src/runtime/agent/orchestrator/runtime-core/goal-store.mjs +0 -1
  53. package/src/runtime/agent/orchestrator/runtime-core/goal-text.mjs +1 -1
  54. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +1 -1
  55. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -2
  56. package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +18 -4
  57. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +7 -1
  58. package/src/runtime/agent/orchestrator/runtime-core/session-route-policy.mjs +16 -0
  59. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +2 -0
  60. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +2 -2
  61. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +7 -7
  62. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +6 -0
  63. package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +28 -3
  64. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +24 -11
  65. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +2 -6
  66. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +63 -3
  67. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +4 -4
  68. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +1 -0
  69. package/src/runtime/agent/orchestrator/stall-policy.mjs +0 -1
  70. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +18 -8
  71. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +19 -20
  72. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +6 -11
  73. package/src/runtime/agent/orchestrator/tools/builtin/file-version-test-helpers.mjs +11 -0
  74. package/src/runtime/agent/orchestrator/tools/builtin/file-version.mjs +13 -0
  75. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-single-file-rescue.mjs +8 -5
  76. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-task-output.mjs +25 -12
  77. package/src/runtime/agent/orchestrator/tools/builtin/raw-content-publication.mjs +17 -0
  78. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +136 -76
  79. package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +15 -17
  80. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -2
  81. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +11 -10
  82. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +5 -3
  83. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +30 -16
  84. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +4 -11
  85. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +7 -3
  86. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-validation.mjs +16 -19
  87. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  88. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background.mjs +4 -1
  89. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +14 -4
  90. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +58 -8
  91. package/src/runtime/agent/orchestrator/tools/patch/content-guard.mjs +47 -0
  92. package/src/runtime/agent/orchestrator/tools/patch/delete-quarantine.mjs +155 -0
  93. package/src/runtime/agent/orchestrator/tools/patch/dispatch/native-proofs.mjs +6 -2
  94. package/src/runtime/agent/orchestrator/tools/patch/dispatch/native-snapshots.mjs +6 -2
  95. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +19 -16
  96. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +13 -44
  97. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
  98. package/src/runtime/agent/orchestrator/tools/patch/post-patch-excerpts.mjs +5 -2
  99. package/src/runtime/agent/orchestrator/tools/patch/replay-capture.mjs +47 -7
  100. package/src/runtime/agent/orchestrator/tools/patch/rollback-state.mjs +72 -8
  101. package/src/runtime/agent/orchestrator/tools/patch/section-coalesce.mjs +3 -2
  102. package/src/runtime/agent/orchestrator/tools/patch/sequence/run.mjs +9 -1
  103. package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +9 -1
  104. package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +6 -6
  105. package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +16 -4
  106. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +35 -14
  107. package/src/runtime/agent/orchestrator/tools/patch/wave.mjs +42 -4
  108. package/src/runtime/agent/orchestrator/tools/shell-codepage.mjs +296 -0
  109. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +75 -24
  110. package/src/runtime/channels/lib/config.mjs +3 -8
  111. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +7 -3
  112. package/src/{tui → runtime/channels}/lib/voice-setup.mjs +38 -22
  113. package/src/runtime/channels/lib/webhook/request-handler.mjs +42 -8
  114. package/src/runtime/computer-bridge/actions.mjs +2 -0
  115. package/src/runtime/effort-judge/judge-client.mjs +12 -3
  116. package/src/runtime/effort-judge/model-manifest.json +7 -7
  117. package/src/runtime/effort-judge/previous-human-request.mjs +162 -0
  118. package/src/runtime/effort-judge/verified-context-input.mjs +10 -0
  119. package/src/runtime/memory/lib/embedding-provider.mjs +12 -0
  120. package/src/runtime/memory/lib/http-router/session-routes.mjs +2 -1
  121. package/src/runtime/memory/lib/memory-config-flags.mjs +3 -2
  122. package/src/runtime/memory/lib/memory-embed.mjs +5 -0
  123. package/src/runtime/shared/agent-route-config.mjs +50 -2
  124. package/src/runtime/shared/bounded-file-read.mjs +166 -0
  125. package/src/runtime/shared/builtin-grandfathering.mjs +37 -0
  126. package/src/runtime/shared/config.mjs +24 -4
  127. package/src/runtime/shared/defaults-separation.mjs +263 -0
  128. package/src/runtime/shared/llm/anthropic-effort.mjs +4 -0
  129. package/src/runtime/shared/llm/model-catalog.mjs +23 -0
  130. package/src/runtime/shared/llm/usage-ledger-rollup.mjs +105 -1
  131. package/src/runtime/shared/llm/usage-ledger-worker.mjs +2 -1
  132. package/src/runtime/shared/llm/usage-ledger.mjs +72 -5
  133. package/src/runtime/shared/onnx-runtime-support.mjs +6 -0
  134. package/src/runtime/shared/orchestration.mjs +14 -6
  135. package/src/runtime/shared/projects.mjs +65 -48
  136. package/src/runtime/shared/provider-accounts.mjs +47 -6
  137. package/src/runtime/shared/quota-resume-store.mjs +255 -0
  138. package/src/runtime/shared/shipped-definitions.mjs +275 -0
  139. package/src/runtime/shared/tool-card-model.golden.json +3 -3
  140. package/src/runtime/shared/tool-result-summary.mjs +86 -15
  141. package/src/runtime/shared/transcript-metadata.mjs +2 -0
  142. package/src/runtime/shared/turn-review-reply.mjs +2 -0
  143. package/src/runtime/shared/turn-snapshot.mjs +65 -6
  144. package/src/runtime/shared/turn-worktree-snapshot.mjs +202 -22
  145. package/src/session-runtime/boot/apis.mjs +19 -0
  146. package/src/session-runtime/boot/config.mjs +4 -0
  147. package/src/session-runtime/boot/facade.mjs +4 -1
  148. package/src/session-runtime/boot/infrastructure.mjs +21 -2
  149. package/src/session-runtime/boot/session.mjs +3 -0
  150. package/src/session-runtime/channel-config-api.mjs +5 -0
  151. package/src/session-runtime/config-lifecycle/config-writers.mjs +3 -0
  152. package/src/session-runtime/config-lifecycle.mjs +31 -6
  153. package/src/session-runtime/goal-facade-api.mjs +4 -0
  154. package/src/session-runtime/lifecycle/inheritance.mjs +28 -6
  155. package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +3 -0
  156. package/src/session-runtime/lifecycle/session-switch/resume.mjs +7 -1
  157. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +6 -0
  158. package/src/session-runtime/lifecycle/teardown/session-close.mjs +14 -2
  159. package/src/session-runtime/media-api.mjs +32 -22
  160. package/src/session-runtime/model-recency.mjs +14 -0
  161. package/src/session-runtime/model-route/route-tuning.mjs +3 -5
  162. package/src/session-runtime/model-route/set-route.mjs +49 -21
  163. package/src/session-runtime/model-route-api.mjs +2 -1
  164. package/src/session-runtime/notification-bus.mjs +31 -6
  165. package/src/session-runtime/provider-auth/login.mjs +6 -0
  166. package/src/session-runtime/provider-models/catalog-load.mjs +3 -1
  167. package/src/session-runtime/provider-models/row-hydration.mjs +15 -14
  168. package/src/session-runtime/quick-model-rows/model-meta.mjs +26 -0
  169. package/src/session-runtime/quick-model-rows/provider-rows.mjs +40 -14
  170. package/src/session-runtime/resource-api.mjs +8 -1
  171. package/src/session-runtime/runtime-core.mjs +4 -0
  172. package/src/session-runtime/runtime-facade.mjs +20 -1
  173. package/src/session-runtime/runtime-review-api.mjs +12 -1
  174. package/src/session-runtime/services/agent-tool/helpers.mjs +4 -0
  175. package/src/session-runtime/services/agent-tool/index-write-queue.mjs +10 -15
  176. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +11 -3
  177. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +2 -0
  178. package/src/session-runtime/services/channel-admin.mjs +87 -13
  179. package/src/session-runtime/services/provider-admin.mjs +256 -63
  180. package/src/session-runtime/services/seeds.mjs +73 -142
  181. package/src/session-runtime/services/usage-dashboard-model.mjs +29 -0
  182. package/src/session-runtime/services/usage-dashboard.mjs +7 -0
  183. package/src/session-runtime/session-address.mjs +125 -0
  184. package/src/session-runtime/session-create.mjs +57 -2
  185. package/src/session-runtime/settings-builtin-tools-api.mjs +8 -0
  186. package/src/session-runtime/settings-compaction-api.mjs +13 -0
  187. package/src/session-runtime/setup-tool/executor.mjs +92 -40
  188. package/src/session-runtime/setup-tool/extended-actions.mjs +64 -5
  189. package/src/session-runtime/setup-tool/settings-contract.mjs +44 -1
  190. package/src/session-runtime/setup-tool/tool-defs.mjs +12 -7
  191. package/src/session-runtime/tool-surface.mjs +16 -9
  192. package/src/session-runtime/turn/session-ops.mjs +3 -0
  193. package/src/session-runtime/turn/share-ops.mjs +4 -1
  194. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +6 -0
  195. package/src/session-runtime/usage-stats-api.mjs +15 -0
  196. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +31 -5
  197. package/src/session-runtime/workflow-agents-api/agent-route.mjs +4 -0
  198. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +27 -7
  199. package/src/standalone/daemon-boot-coordinator.mjs +10 -0
  200. package/src/standalone/daemon-drain-policy.mjs +9 -0
  201. package/src/standalone/daemon-stored-session-views.mjs +7 -8
  202. package/src/standalone/daemon.mjs +21 -5
  203. package/src/standalone/local-session-runtime.mjs +11 -0
  204. package/src/standalone/session-client.mjs +52 -7
  205. package/src/standalone/session-protocol.mjs +8 -0
  206. package/src/standalone/session-service/agent-tree/agent-child.mjs +2 -0
  207. package/src/standalone/session-service/entries/lifecycle.mjs +2 -0
  208. package/src/standalone/session-service/entries/session-loads.mjs +9 -0
  209. package/src/standalone/session-service/projection/revisions.mjs +28 -4
  210. package/src/standalone/session-service/projection.mjs +1 -0
  211. package/src/standalone/session-service/quota-resume.mjs +140 -0
  212. package/src/standalone/session-service/session-calls/session-action.mjs +2 -2
  213. package/src/standalone/session-service/session-calls.mjs +10 -2
  214. package/src/standalone/session-service/stored-reader.mjs +26 -1
  215. package/src/standalone/session-service.mjs +21 -0
  216. package/src/standalone/session-transport/routes.mjs +2 -0
  217. package/src/tui/app/doctor.mjs +7 -11
  218. package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +3 -5
  219. package/src/tui/app/settings-picker/settings-rows.mjs +4 -6
  220. package/src/tui/app/settings-picker/settings-toggles/store-toggles.mjs +1 -1
  221. package/src/tui/app/slash-dispatch/route-commands.mjs +3 -2
  222. package/src/tui/components/ContextPanel.jsx +5 -1
  223. package/src/tui/components/Spinner.jsx +34 -319
  224. package/src/tui/components/SpinnerByline.jsx +256 -0
  225. package/src/tui/components/use-spinner-progress.mjs +113 -0
  226. package/src/tui/dist/index.mjs +15540 -14452
  227. package/src/tui/session/agent-job-feed/execution-delivery.mjs +13 -1
  228. package/src/tui/session/agent-job-feed/notification-router.mjs +3 -0
  229. package/src/tui/session/agent-job-feed/pending-resume.mjs +14 -1
  230. package/src/tui/session/agent-job-feed.mjs +4 -0
  231. package/src/tui/session/context-state.mjs +13 -1
  232. package/src/tui/session/initial-state.mjs +6 -0
  233. package/src/tui/session/quota-resume.mjs +263 -0
  234. package/src/tui/session/session-api/intake/abort.mjs +39 -35
  235. package/src/tui/session/session-api/intake/submission.mjs +13 -21
  236. package/src/tui/session/session-api/intake.mjs +3 -3
  237. package/src/tui/session/session-api/integrations.mjs +5 -1
  238. package/src/tui/session/session-api/lifecycle/dispose.mjs +1 -0
  239. package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -2
  240. package/src/tui/session/session-api/lifecycle/session-reset.mjs +4 -0
  241. package/src/tui/session/session-api/lifecycle.mjs +71 -2
  242. package/src/tui/session/session-api/media.mjs +2 -2
  243. package/src/tui/session/session-api/quota-resume.mjs +21 -0
  244. package/src/tui/session/session-api/routes.mjs +2 -0
  245. package/src/tui/session/session-api/settings.mjs +1 -0
  246. package/src/tui/session/session-api.mjs +2 -0
  247. package/src/tui/session/session-flow/auto-clear.mjs +14 -6
  248. package/src/tui/session/session-flow/drain.mjs +52 -5
  249. package/src/tui/session/session-flow/queue/take-entries.mjs +2 -3
  250. package/src/tui/session/session-flow/queue.mjs +3 -0
  251. package/src/tui/session/session-flow/steering.mjs +11 -0
  252. package/src/tui/session/session-flow.mjs +1 -0
  253. package/src/tui/session/tool-approval.mjs +7 -2
  254. package/src/tui/session/transcript-intake.mjs +1 -0
  255. package/src/tui/session/turn.mjs +40 -3
  256. package/src/vendor/statusline/bin/statusline-route.mjs +3 -2
@@ -2,10 +2,12 @@ import { spawn } from 'node:child_process';
2
2
  import { appendFile, mkdtemp, readdir, readFile, rm, stat } from 'node:fs/promises';
3
3
  import { availableParallelism, tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
- import { FAILURE_RECORDS_ENV } from './test-failure-records.mjs';
5
+ import { setTimeout as delay } from 'node:timers/promises';
6
+ import { FAILURE_RECORDS_ENV, spawnLedgerPath } from './test-failure-records.mjs';
6
7
  import { classifyRerunOutcomes, formatFailureSummary } from './test-failure-summary.mjs';
7
8
 
8
9
  const SUMMARY_REPORTER = new URL('./test-summary-reporter.mjs', import.meta.url).href;
10
+ const SPAWN_LEDGER = new URL('./test-spawn-ledger.mjs', import.meta.url).href;
9
11
 
10
12
  // A whole-suite run passes ~700 paths to one node process. Windows caps a
11
13
  // command line at 32767 characters (CreateProcess) and POSIX caps argv+env at
@@ -81,17 +83,212 @@ export function testConcurrencyArg(nodeArgs, env = process.env, parallelism = av
81
83
  return `--test-concurrency=${Math.max(2, Math.floor(parallelism / cap))}`;
82
84
  }
83
85
 
84
- function spawnBatch(args, env) {
86
+ // A batch that has not exited by then is killed with its process tree and
87
+ // fails the run; MIXDOG_TEST_BATCH_TIMEOUT_MS overrides the default.
88
+ const DEFAULT_BATCH_TIMEOUT_MS = 60 * 60 * 1000;
89
+ const minutes = (ms) => Number((ms / 60_000).toFixed(2));
90
+
91
+ // Runs a helper command to completion and returns its stdout.
92
+ function capture(command, args) {
85
93
  return new Promise((resolve) => {
86
- const child = spawn(process.execPath, args, { stdio: 'inherit', env });
94
+ const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
95
+ let out = '';
96
+ child.stdout.setEncoding('utf8');
97
+ child.stdout.on('data', (chunk) => {
98
+ out += chunk;
99
+ });
100
+ child.on('error', (error) => {
101
+ console.error(`${command} failed: ${error.message}`);
102
+ resolve('');
103
+ });
104
+ child.on('close', () => resolve(out));
105
+ });
106
+ }
107
+
108
+ // Kills a process and every descendant still linked to it. On POSIX a batch
109
+ // leads its own process group (spawned detached), so the group goes with it.
110
+ async function killTree(pid) {
111
+ if (process.platform === 'win32') {
112
+ await capture('taskkill', ['/T', '/F', '/PID', String(pid)]);
113
+ return;
114
+ }
115
+ for (const target of [-pid, pid]) {
116
+ try {
117
+ process.kill(target, 'SIGKILL');
118
+ } catch {
119
+ /* already gone */
120
+ }
121
+ }
122
+ }
123
+
124
+ // Every live process: { pid, ppid, created (epoch ms), exe ('' when unknown) }.
125
+ // POSIX ps only reports elapsed seconds, so its creation times are ±1 s.
126
+ async function processSnapshot() {
127
+ if (process.platform === 'win32') {
128
+ const out = await capture('powershell', [
129
+ '-NoProfile',
130
+ '-NonInteractive',
131
+ '-Command',
132
+ "Get-CimInstance Win32_Process | Where-Object CreationDate | ForEach-Object { '{0} {1} {2} {3}' -f $_.ProcessId, $_.ParentProcessId, ([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds(), $_.ExecutablePath }",
133
+ ]);
134
+ return out
135
+ .split(/\r?\n/)
136
+ .filter(Boolean)
137
+ .map((line) => {
138
+ const [pid, ppid, created, ...exe] = line.split(' ');
139
+ return { pid: Number(pid), ppid: Number(ppid), created: Number(created), exe: exe.join(' ') };
140
+ });
141
+ }
142
+ const now = Date.now();
143
+ const out = await capture('ps', ['-A', '-o', 'pid=,ppid=,etime=']);
144
+ return out
145
+ .split('\n')
146
+ .map((line) => line.trim().split(/\s+/))
147
+ .filter((fields) => fields.length === 3)
148
+ .map(([pid, ppid, etime]) => {
149
+ // [[dd-]hh:]mm:ss
150
+ const [days, clock] = etime.includes('-') ? etime.split('-') : ['0', etime];
151
+ const seconds = clock.split(':').reduce((total, part) => total * 60 + Number(part), Number(days) * 86_400);
152
+ return { pid: Number(pid), ppid: Number(ppid), created: now - seconds * 1000, exe: '' };
153
+ });
154
+ }
155
+
156
+ const CLOCK_SLACK_MS = 2000;
157
+
158
+ // The processes this run owns, out of a snapshot: an anchor (the batch, or a
159
+ // child a test process recorded in its spawn ledger) still alive under its
160
+ // pid, the orphans an exited anchor left (they still name it as their parent;
161
+ // a later process that reuses its pid only has younger children), every
162
+ // descendant of those, and anything executing from the run's scratch root
163
+ // (e.g. the postgres the memory runtime downloads into MIXDOG_DATA_DIR).
164
+ // Command-line text never makes a process the run's: an editor or terminal
165
+ // opened on the scratch root is not.
166
+ export function ownedProcesses(snapshot, anchors, { batchStart, scratchDir, platform = process.platform }) {
167
+ const byPid = new Map(snapshot.map((entry) => [entry.pid, entry]));
168
+ const childrenOf = new Map();
169
+ for (const entry of snapshot) childrenOf.set(entry.ppid, [...(childrenOf.get(entry.ppid) ?? []), entry]);
170
+ const owned = new Map();
171
+ const queue = [];
172
+ const claim = (entry) => {
173
+ if (entry.pid === process.pid || owned.has(entry.pid)) return;
174
+ owned.set(entry.pid, entry);
175
+ queue.push(entry);
176
+ };
177
+ for (const { pid, since } of anchors) {
178
+ const holder = byPid.get(pid);
179
+ if (holder && holder.created >= batchStart - CLOCK_SLACK_MS && holder.created <= since + CLOCK_SLACK_MS) {
180
+ claim(holder);
181
+ continue;
182
+ }
183
+ for (const orphan of childrenOf.get(pid) ?? []) {
184
+ if (orphan.created >= since - CLOCK_SLACK_MS && (!holder || orphan.created < holder.created)) claim(orphan);
185
+ }
186
+ }
187
+ const fold = (path) => (platform === 'win32' ? path.toLowerCase() : path);
188
+ const scratchPrefix = fold(join(scratchDir, '/'));
189
+ for (const entry of snapshot) if (entry.exe && fold(entry.exe).startsWith(scratchPrefix)) claim(entry);
190
+ while (queue.length > 0) {
191
+ const parent = queue.pop();
192
+ for (const entry of childrenOf.get(parent.pid) ?? []) {
193
+ if (entry.created >= parent.created - CLOCK_SLACK_MS) claim(entry);
194
+ }
195
+ }
196
+ return [...owned.values()];
197
+ }
198
+
199
+ async function readSpawnLedger(path) {
200
+ const text = await readFile(path, 'utf8').catch(() => '');
201
+ return text
202
+ .split('\n')
203
+ .filter(Boolean)
204
+ .map((line) => {
205
+ const [pid, since] = line.split(' ').map(Number);
206
+ return { pid, since };
207
+ });
208
+ }
209
+
210
+ // A process a test leaves running must not outlive its batch: it can hold a
211
+ // port, a database, or files in the scratch root the run removes at the end.
212
+ async function reapLeftovers(child, { batchStart, ledger, scratchDir }) {
213
+ // The batch itself has exited: only its process group can remain.
214
+ if (process.platform !== 'win32' && child.pid) {
215
+ try {
216
+ process.kill(-child.pid, 'SIGKILL');
217
+ } catch {
218
+ /* the group is empty */
219
+ }
220
+ }
221
+ const anchors = [...(child.pid ? [{ pid: child.pid, since: batchStart }] : []), ...(await readSpawnLedger(ledger))];
222
+ const leftovers = ownedProcesses(await processSnapshot(), anchors, { batchStart, scratchDir });
223
+ if (leftovers.length === 0) return;
224
+ const pids = leftovers.map((entry) => entry.pid);
225
+ console.error(`Killing ${pids.length} process(es) a test left running: ${pids.join(' ')}`);
226
+ if (process.platform === 'win32') {
227
+ await capture('taskkill', ['/T', '/F', ...pids.flatMap((pid) => ['/PID', String(pid)])]);
228
+ return;
229
+ }
230
+ for (const pid of pids) {
231
+ try {
232
+ process.kill(pid, 'SIGKILL');
233
+ } catch {
234
+ /* already gone */
235
+ }
236
+ }
237
+ }
238
+
239
+ // Resolves { code, hung } once the batch exited and its leftovers are gone.
240
+ function spawnBatch(args, env, { files, ledger, scratchDir, timeoutMs }) {
241
+ return new Promise((resolve) => {
242
+ const batchStart = Date.now();
243
+ const child = spawn(process.execPath, args, { stdio: 'inherit', env, detached: process.platform !== 'win32' });
244
+ let hung = '';
245
+ const watchdog = setTimeout(() => {
246
+ hung = `hung after ${minutes(timeoutMs)} min: ${files.join(' ') || '(discovered files)'}`;
247
+ console.error(`Test batch ${hung}; killing its process tree`);
248
+ void killTree(child.pid);
249
+ }, timeoutMs);
250
+ // A detached POSIX batch no longer gets the terminal's signals itself.
251
+ const forward = (signal) => {
252
+ try {
253
+ process.kill(-child.pid, signal);
254
+ } catch {
255
+ /* already gone */
256
+ }
257
+ };
258
+ const signals = process.platform === 'win32' ? [] : ['SIGINT', 'SIGTERM'];
259
+ for (const signal of signals) process.on(signal, forward);
260
+ const settle = async (code) => {
261
+ clearTimeout(watchdog);
262
+ for (const signal of signals) process.off(signal, forward);
263
+ await reapLeftovers(child, { batchStart, ledger, scratchDir });
264
+ resolve({ code: hung ? 1 : code, hung });
265
+ };
87
266
  child.on('error', (error) => {
88
267
  console.error(error);
89
- resolve(1);
268
+ void settle(1);
90
269
  });
91
- child.on('exit', (code, signal) => resolve(code ?? (signal ? 1 : 0)));
270
+ child.on('exit', (code, signal) => void settle(code ?? (signal ? 1 : 0)));
92
271
  });
93
272
  }
94
273
 
274
+ // rm's own maxRetries re-walks the tree on every nested retry, so one held
275
+ // file a few directories down multiplied its delay into hours. Each pass here
276
+ // is a single bounded walk.
277
+ async function removeScratch(dir) {
278
+ for (let attempt = 1; ; attempt += 1) {
279
+ try {
280
+ await rm(dir, { recursive: true, force: true });
281
+ return;
282
+ } catch (error) {
283
+ if (attempt === 5) {
284
+ console.error(`Test scratch left behind (${error.code}): ${dir}`);
285
+ return;
286
+ }
287
+ await delay(200 * attempt);
288
+ }
289
+ }
290
+ }
291
+
95
292
  // The failure reporter writes one JSON line per failed test; a spawn that
96
293
  // died before Node opened its destination leaves no file at all.
97
294
  async function readFailureRecords(path) {
@@ -136,6 +333,8 @@ export async function runNodeTests(
136
333
  const runArgs = [
137
334
  ...nodeArgs,
138
335
  ...(concurrencyArg ? [concurrencyArg] : []),
336
+ // Node's test runner passes --import on to every test file process.
337
+ `--import=${SPAWN_LEDGER}`,
139
338
  `--test-reporter=${SUMMARY_REPORTER}`,
140
339
  '--test-reporter-destination=stdout',
141
340
  '--test-reporter=spec',
@@ -171,15 +370,25 @@ export async function runNodeTests(
171
370
  ...(coverageDir ? { NODE_V8_COVERAGE: coverageDir } : {}),
172
371
  ...(nodeOptions ? { NODE_OPTIONS: nodeOptions } : {}),
173
372
  });
373
+ const batchOptions = (files, recordsPath) => ({
374
+ files,
375
+ ledger: spawnLedgerPath(recordsPath),
376
+ scratchDir,
377
+ timeoutMs: positiveInt(process.env.MIXDOG_TEST_BATCH_TIMEOUT_MS) ?? DEFAULT_BATCH_TIMEOUT_MS,
378
+ });
174
379
  let status = 0;
175
380
  const failures = [];
381
+ const hung = [];
176
382
  for (const [index, batch] of batches.entries()) {
177
383
  const batchLog = batches.length === 1 ? logPath : `${logPath}.${index + 1}`;
178
384
  const batchFailures = join(failureDir, `batch-${index + 1}.jsonl`);
179
- const code = await spawnBatch(
385
+ const outcome = await spawnBatch(
180
386
  [...runArgs, `--test-reporter-destination=${batchLog}`, ...batch],
181
- childEnv(batchFailures)
387
+ childEnv(batchFailures),
388
+ batchOptions(batch, batchFailures)
182
389
  );
390
+ const { code } = outcome;
391
+ if (outcome.hung) hung.push(outcome.hung);
183
392
  if (batchLog !== logPath) {
184
393
  await appendFile(logPath, await readFile(batchLog));
185
394
  await rm(batchLog, { force: true });
@@ -196,10 +405,13 @@ export async function runNodeTests(
196
405
  console.error(`Re-running ${files.length} failed file(s): attempt ${attempt}/${rerunFailed}`);
197
406
  const attemptLog = `${logPath}.rerun-${attempt}`;
198
407
  const attemptFailures = join(failureDir, `rerun-${attempt}.jsonl`);
199
- lastAttemptStatus = await spawnBatch(
408
+ const outcome = await spawnBatch(
200
409
  [...runArgs, `--test-reporter-destination=${attemptLog}`, ...files],
201
- childEnv(attemptFailures)
410
+ childEnv(attemptFailures),
411
+ batchOptions(files, attemptFailures)
202
412
  );
413
+ lastAttemptStatus = outcome.code;
414
+ if (outcome.hung) hung.push(outcome.hung);
203
415
  await appendFile(logPath, await readFile(attemptLog));
204
416
  await rm(attemptLog, { force: true });
205
417
  attempts.push({ files, failures: await readFailureRecords(attemptFailures) });
@@ -209,17 +421,27 @@ export async function runNodeTests(
209
421
  // Flaky tolerance applies only to an ordinary test-failure exit that the
210
422
  // re-runs fully explained: nothing failed twice and the last re-run itself
211
423
  // came back clean.
212
- if (rerunFailed > 0 && status === 1 && failed.length === 0 && flaky.length > 0 && lastAttemptStatus === 0) status = 0;
424
+ // A hung batch is never explained by re-runs.
425
+ if (
426
+ rerunFailed > 0 &&
427
+ status === 1 &&
428
+ hung.length === 0 &&
429
+ failed.length === 0 &&
430
+ flaky.length > 0 &&
431
+ lastAttemptStatus === 0
432
+ )
433
+ status = 0;
213
434
  process.exitCode = status;
214
435
 
215
436
  await rm(failureDir, { recursive: true, force: true });
216
- // A process a test left running may still hold a file; what it holds stays.
217
- await rm(scratchDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }).catch(() => {});
437
+ // Leftover processes are already killed; what a process outside the run
438
+ // still holds stays behind.
439
+ await removeScratch(scratchDir);
218
440
  if (coverageDir) {
219
441
  const { reportCoverage } = await import('./coverage.mjs');
220
442
  await reportCoverage(coverageDir);
221
443
  await rm(coverageDir, { recursive: true, force: true });
222
444
  }
223
445
  // Last line of the run, after every reporter and the coverage report.
224
- process.stdout.write(`\n${formatFailureSummary({ failed, flaky, rerunFailed })}`);
446
+ process.stdout.write(`\n${formatFailureSummary({ failed, flaky, rerunFailed, hung })}`);
225
447
  }
@@ -13,6 +13,11 @@ import { relative } from 'node:path';
13
13
 
14
14
  export const FAILURE_RECORDS_ENV = 'MIXDOG_TEST_FAILURE_RECORDS';
15
15
 
16
+ // Every child process a test process starts is logged beside its spawn's
17
+ // failure records (scripts/lib/test-spawn-ledger.mjs), so the runner can tell
18
+ // a process the run started from one that merely names its directories.
19
+ export const spawnLedgerPath = (recordsPath) => (recordsPath ? `${recordsPath}.spawns` : '');
20
+
16
21
  // Ancestor names are tracked per file: node:test runs files concurrently, so
17
22
  // one flat stack would mix their nestings.
18
23
  export function failureRecord(event, stacks) {
@@ -43,8 +43,14 @@ export function policyLine(rerunFailed) {
43
43
  : 'policy: --rerun-failed is off, so every failure fails the run (pass --rerun-failed <n> to classify flaky)';
44
44
  }
45
45
 
46
- export function formatFailureSummary({ failed = [], flaky = [], rerunFailed = 0 }) {
47
- const lines = [`${SUMMARY_TAG} ${failed.length} failed, ${flaky.length} flaky (every failed test is named below)`];
46
+ // `hung` holds one "hung after N min: <files>" message per batch the runner's
47
+ // watchdog killed; such a batch fails the run whatever its tests reported.
48
+ export function formatFailureSummary({ failed = [], flaky = [], rerunFailed = 0, hung = [] }) {
49
+ const hungCount = hung.length > 0 ? `, ${hung.length} hung batch(es)` : '';
50
+ const lines = [
51
+ `${SUMMARY_TAG} ${failed.length} failed, ${flaky.length} flaky${hungCount} (every failed test is named below)`,
52
+ ];
53
+ for (const message of hung) lines.push(`${SUMMARY_TAG} HUNG ${message}`);
48
54
  for (const record of failed) {
49
55
  const origin = record.firstSeenOnRerun ? ` (first seen on re-run ${record.firstSeenOnRerun})` : '';
50
56
  lines.push(`${SUMMARY_TAG} FAILED ${record.file} > ${record.name}${origin}`);
@@ -54,6 +60,6 @@ export function formatFailureSummary({ failed = [], flaky = [], rerunFailed = 0
54
60
  `${SUMMARY_TAG} FLAKY ${record.file} > ${record.name} (failed, then passed on re-run ${record.passedOnAttempt}/${rerunFailed})`
55
61
  );
56
62
  }
57
- if (failed.length > 0 || flaky.length > 0) lines.push(`${SUMMARY_TAG} ${policyLine(rerunFailed)}`);
63
+ if (failed.length > 0 || flaky.length > 0 || hung.length > 0) lines.push(`${SUMMARY_TAG} ${policyLine(rerunFailed)}`);
58
64
  return `${lines.join('\n')}\n`;
59
65
  }
@@ -0,0 +1,19 @@
1
+ // Preloaded (--import) into every test process the runner starts: records
2
+ // "<pid> <spawn time ms>" for each child process, so a process a test leaves
3
+ // running can be traced back to the run after its parent exited
4
+ // (scripts/lib/run-node-tests.mjs). Without the runner's environment it does
5
+ // nothing.
6
+ import { ChildProcess } from 'node:child_process';
7
+ import { appendFileSync } from 'node:fs';
8
+ import { FAILURE_RECORDS_ENV, spawnLedgerPath } from './test-failure-records.mjs';
9
+
10
+ const ledger = spawnLedgerPath(process.env[FAILURE_RECORDS_ENV]);
11
+ if (ledger) {
12
+ // spawn, exec, execFile and fork all start their process here.
13
+ const spawn = ChildProcess.prototype.spawn;
14
+ ChildProcess.prototype.spawn = function recordedSpawn(...args) {
15
+ const result = spawn.apply(this, args);
16
+ if (this.pid) appendFileSync(ledger, `${this.pid} ${Date.now()}\n`);
17
+ return result;
18
+ };
19
+ }
@@ -3,6 +3,7 @@
3
3
  import { access, readdir, rm } from 'node:fs/promises';
4
4
  import { dirname, join, resolve } from 'node:path';
5
5
  import { fileURLToPath, pathToFileURL } from 'node:url';
6
+ import { onnxRuntimeSupported } from '../src/runtime/shared/onnx-runtime-support.mjs';
6
7
 
7
8
  const SUPPORTED_TARGETS = new Map([
8
9
  ['win32', new Set(['x64', 'arm64'])],
@@ -83,8 +84,9 @@ export async function pruneEmbeddingRuntime(packageRoot, options = {}) {
83
84
  ).find(Boolean);
84
85
  if (!ortRoot) throw new Error('Embedding runtime is incomplete: onnxruntime-node is unavailable');
85
86
 
86
- const targetBinaryDir = join(ortRoot, 'bin', 'napi-v6', target.platform, target.arch);
87
- if (!(await exists(join(targetBinaryDir, 'onnxruntime_binding.node')))) {
87
+ const onnx = onnxRuntimeSupported(target.platform, target.arch);
88
+ const targetBinaryDir = onnx ? join(ortRoot, 'bin', 'napi-v6', target.platform, target.arch) : null;
89
+ if (onnx && !(await exists(join(targetBinaryDir, 'onnxruntime_binding.node')))) {
88
90
  throw new Error(`Embedding runtime is incomplete: missing ${target.key} ONNX binding`);
89
91
  }
90
92
 
@@ -100,10 +102,11 @@ export async function pruneEmbeddingRuntime(packageRoot, options = {}) {
100
102
  // bin/napi-v6/${process.platform}/${process.arch}/onnxruntime_binding.node.
101
103
  // Keep one native payload and remove every foreign OS/architecture.
102
104
  const napiRoot = join(ortRoot, 'bin', 'napi-v6');
103
- await removeChildrenExcept(napiRoot, new Set([target.platform]));
104
- await removeChildrenExcept(join(napiRoot, target.platform), new Set([target.arch]));
105
+ // Targets without an ONNX binding keep no native payload at all.
106
+ await removeChildrenExcept(napiRoot, new Set(onnx ? [target.platform] : []));
107
+ if (onnx) await removeChildrenExcept(join(napiRoot, target.platform), new Set([target.arch]));
105
108
  const removedTargetFiles = [];
106
- if (target.platform === 'win32') {
109
+ if (onnx && target.platform === 'win32') {
107
110
  // The fixed production embedding profile uses CPU inference. Keep
108
111
  // DirectML.dll for explicit device overrides, but remove the optional DXC
109
112
  // shader compiler payload verified unnecessary by packaged-runtime warmup.
@@ -132,6 +135,7 @@ export async function pruneEmbeddingRuntime(packageRoot, options = {}) {
132
135
  ...target,
133
136
  transformerRoot,
134
137
  ortRoot,
138
+ onnx,
135
139
  targetBinaryDir,
136
140
  removedTargetFiles,
137
141
  };
@@ -2,5 +2,20 @@
2
2
  // Explicit Node test options and file paths, without discovery or lane filters.
3
3
  import { runNodeTests } from './lib/run-node-tests.mjs';
4
4
 
5
+ const argv = process.argv.slice(2);
6
+ // The same defaults as scripts/test.mjs: `mock.module` is still flag-gated on
7
+ // 22/24, and a suite that leaves a handle open must not hang the whole run.
8
+ // A flag the caller already passed (to node or to this script) is not repeated.
9
+ const given = new Set([...process.execArgv, ...argv]);
10
+ const unlessGiven = (flag) => (given.has(flag) ? [] : [flag]);
11
+
5
12
  // Preserve parent Node flags such as heap limits and preload modules.
6
- await runNodeTests([...process.execArgv, '--test'], process.argv.slice(2));
13
+ await runNodeTests(
14
+ [
15
+ ...process.execArgv,
16
+ ...unlessGiven('--experimental-test-module-mocks'),
17
+ '--test',
18
+ ...unlessGiven('--test-force-exit'),
19
+ ],
20
+ argv
21
+ );
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: Advisor
3
+ description: Use on non-trivial work after orientation and before committing to an approach, when stuck or results do not converge, before changing approach, and for requested second opinions. Skip for low-difficulty work. Advisory analysis only; the final check belongs to Reviewer.
4
+ ---
5
+
6
+ Independent technical advisor.
7
+
8
+ You are consulted at decision points: once orientation is done and before an
9
+ approach is committed, when errors recur or results stop converging, before a
10
+ change of approach, and when the user asks for a second opinion or
11
+ cross-validation. You do not see the Lead's conversation; work from the brief
12
+ and verify its evidence against the source yourself.
13
+
14
+ For design advice, compare the smallest viable alternatives against the
15
+ requested outcome, established project patterns, correctness, compatibility,
16
+ security, performance, and maintenance cost. Recommend one course of action and
17
+ explain the material trade-off. Do not broaden the task into a general audit or
18
+ redesign.
19
+
20
+ For a stuck or non-converging task, identify the most likely cause from the
21
+ actual execution paths and observed behavior, and name the decisive check that
22
+ separates competing explanations. Reproduce counterexamples with bounded tests
23
+ when needed. A passing test is evidence only for the path it exercises.
24
+
25
+ When the brief reports evidence that conflicts with earlier advice, resolve the
26
+ conflict explicitly: state which constraint decides it and why.
27
+
28
+ Work from source and observed behavior, not from the Lead's conclusion. State
29
+ what is confirmed, inferred, or unverified; name missing evidence and concrete
30
+ blockers rather than inventing certainty.
31
+
32
+ Do not implement fixes, modify project files or settings, stage or commit
33
+ changes, deploy, or delegate work. Keep checks non-destructive and isolate any
34
+ test fixtures from existing user data. The final check belongs to Reviewer.
35
+
36
+ Lead with the recommendation, then actionable findings ordered by severity, with
37
+ evidence located by project-relative path and function or symbol. If no issue
38
+ is found, say so and list only material residual risks and verification limits.
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "advisor",
3
+ "name": "Advisor",
4
+ "description": "Use on non-trivial work after orientation and before committing to an approach, when stuck or results do not converge, before changing approach, and for requested second opinions. Skip for low-difficulty work. Advisory analysis only; the final check belongs to Reviewer.",
5
+ "entry": "AGENT.md"
6
+ }
@@ -1,9 +1,12 @@
1
1
  ---
2
2
  name: Reviewer
3
- description: Use after implementation to independently verify correctness, regressions, risks, and stated acceptance criteria before final reporting.
3
+ description: Use once at the final check of non-trivial work: after the implementation is written and verified, before reporting completion, to independently verify correctness, regressions, risks, and acceptance criteria. Skip for low-difficulty work.
4
4
  ---
5
5
 
6
- Independent regression/risk review agent.
6
+ Independent final-check review agent.
7
+
8
+ You are called once, after the deliverable is written and the Lead's own
9
+ verification has run, before completion is reported.
7
10
 
8
11
  Inspect the diff, affected boundaries, existing tests, and stated acceptance
9
12
  criteria with independent judgment. Run the necessary builds, tests, lint, or
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "reviewer",
3
3
  "name": "Reviewer",
4
- "description": "Use after implementation to independently verify correctness, regressions, risks, and stated acceptance criteria before final reporting.",
4
+ "description": "Use once at the final check of non-trivial work: after the implementation is written and verified, before reporting completion, to independently verify correctness, regressions, risks, and acceptance criteria. Skip for low-difficulty work.",
5
5
  "entry": "AGENT.md"
6
6
  }