mixdog 1.4.0 → 1.4.2

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 (493) hide show
  1. package/package.json +3 -2
  2. package/scripts/build-shipped-history.mjs +115 -33
  3. package/scripts/check-voice-runtime-inputs.mjs +25 -0
  4. package/scripts/gate-local.mjs +183 -63
  5. package/scripts/latest-guard.mjs +21 -9
  6. package/scripts/lib/gate-scheduler.mjs +138 -32
  7. package/scripts/lib/gate-timing-reporter.mjs +16 -0
  8. package/scripts/lib/isolated-test-env.mjs +10 -5
  9. package/scripts/lib/run-node-tests.mjs +91 -66
  10. package/scripts/lib/scoped-temp.d.mts +16 -0
  11. package/scripts/lib/scoped-temp.mjs +124 -0
  12. package/scripts/lib/stale-temp.mjs +44 -0
  13. package/scripts/lib/test-failure-records.mjs +14 -1
  14. package/scripts/lib/test-failure-summary.mjs +16 -4
  15. package/scripts/lib/test-spawn-ledger.mjs +12 -3
  16. package/scripts/post-release-check.mjs +1 -6
  17. package/scripts/pptx-kit-index.mjs +17 -5
  18. package/scripts/suite-health.mjs +115 -24
  19. package/scripts/test-data-dir.mjs +10 -7
  20. package/scripts/test.mjs +9 -14
  21. package/src/agents/advisor/AGENT.md +4 -4
  22. package/src/agents/advisor/agent.json +1 -1
  23. package/src/agents/explore/AGENT.md +22 -0
  24. package/src/agents/explore/agent.json +6 -0
  25. package/src/app.mjs +4 -0
  26. package/src/cli.mjs +7 -0
  27. package/src/defaults/agents.json +7 -0
  28. package/src/defaults/shipped-history.json +23 -2
  29. package/src/defaults/skills/browser-use/SKILL.md +10 -5
  30. package/src/defaults/skills/code-tidy/SKILL.md +71 -102
  31. package/src/defaults/skills/code-tidy/references/agent-cleanup.md +66 -114
  32. package/src/defaults/skills/code-tidy/references/cleanup-report.md +19 -35
  33. package/src/defaults/skills/code-tidy/references/dead-code.md +3 -9
  34. package/src/defaults/skills/code-tidy/references/test-suite-slop.md +16 -27
  35. package/src/defaults/skills/computer-use/SKILL.md +16 -4
  36. package/src/defaults/skills/goal-management/SKILL.md +1 -1
  37. package/src/defaults/skills/setup/references/actions.md +12 -6
  38. package/src/defaults/skills/setup/references/surfaces.md +2 -2
  39. package/src/defaults/skills/setup-guide/SKILL.md +5 -2
  40. package/src/defaults/skills/setup-guide/model-recommendations.json +13 -0
  41. package/src/defaults/skills/skill-creator/SKILL.md +85 -190
  42. package/src/defaults/skills/skill-creator/references/design.md +70 -162
  43. package/src/defaults/skills/skill-creator/references/evaluation.md +33 -92
  44. package/src/defaults/skills/test-suite/SKILL.md +194 -0
  45. package/src/defaults/skills/test-suite/references/case-management.md +51 -0
  46. package/src/defaults/skills/test-suite/references/flaky-policy.md +39 -0
  47. package/src/defaults/skills/test-suite/references/reliability.md +49 -0
  48. package/src/defaults/skills/test-suite/references/suite-design.md +78 -0
  49. package/src/defaults/skills/test-suite/scripts/inventory.mjs +335 -0
  50. package/src/defaults/skills/visualize/SKILL.md +6 -2
  51. package/src/lib/rules-builder.cjs +35 -42
  52. package/src/output-styles/common.md +2 -2
  53. package/src/output-styles/simple.md +1 -1
  54. package/src/rules/agent/AGENT.md +5 -4
  55. package/src/rules/lead/LEAD.md +12 -7
  56. package/src/rules/shared/10-tool-workflow.md +3 -1
  57. package/src/rules/shared/20-research.md +3 -6
  58. package/src/rules/shared/40-editing.md +1 -0
  59. package/src/rules/shared/45-destructive.md +0 -2
  60. package/src/rules/shared/50-execution.md +2 -0
  61. package/src/rules/shared/60-verification.md +10 -9
  62. package/src/rules/shared/70-delivery.md +8 -9
  63. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/abort-link.mjs +5 -1
  64. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +10 -1
  65. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +156 -24
  66. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +13 -0
  67. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +4 -1
  68. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +2 -0
  69. package/src/runtime/agent/orchestrator/config.mjs +12 -7
  70. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +8 -8
  71. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +5 -26
  72. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +3 -1
  73. package/src/runtime/agent/orchestrator/internal-agents.mjs +3 -14
  74. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +122 -12
  75. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -3
  76. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +13 -2
  77. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +152 -58
  78. package/src/runtime/agent/orchestrator/mcp/client.mjs +51 -18
  79. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +88 -54
  80. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +42 -26
  81. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +54 -8
  82. package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +1 -1
  83. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +37 -31
  84. package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +8 -2
  85. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +6 -2
  86. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +2 -2
  87. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +31 -21
  89. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +3 -1
  90. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -3
  91. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +6 -0
  92. package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -2
  93. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +3 -1
  94. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -2
  95. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
  96. package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +1 -10
  97. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/providers/cursor.mjs +3 -0
  99. package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +40 -0
  100. package/src/runtime/agent/orchestrator/providers/custom-provider-keys.mjs +34 -0
  101. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +3 -0
  102. package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +14 -7
  103. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +3 -2
  104. package/src/runtime/agent/orchestrator/providers/devin.mjs +3 -0
  105. package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +2 -2
  106. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +4 -1
  107. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +248 -41
  108. package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -11
  109. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +45 -6
  110. package/src/runtime/agent/orchestrator/providers/lib/first-output-window.mjs +58 -0
  111. package/src/runtime/agent/orchestrator/providers/lib/listen-on.mjs +14 -0
  112. package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +0 -18
  113. package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +138 -6
  114. package/src/runtime/agent/orchestrator/providers/mimo-oauth-login.mjs +1 -10
  115. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +1 -1
  116. package/src/runtime/agent/orchestrator/providers/model-cache.mjs +100 -33
  117. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +1 -1
  118. package/src/runtime/agent/orchestrator/providers/oauth-secret-fields.fixtures.mjs +45 -0
  119. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -0
  120. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +9 -2
  121. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +7 -0
  122. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +14 -3
  123. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +4 -2
  124. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +1 -0
  125. package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +5 -3
  126. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +16 -3
  127. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +1 -0
  128. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/output-items.mjs +5 -2
  129. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +13 -3
  130. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +13 -2
  131. package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +81 -42
  132. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +8 -0
  133. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +160 -31
  134. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +22 -12
  135. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +7 -1
  136. package/src/runtime/agent/orchestrator/providers/openai-ws-watchdogs.mjs +49 -22
  137. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +1 -1
  138. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +3 -1
  139. package/src/runtime/agent/orchestrator/providers/registry.mjs +45 -2
  140. package/src/runtime/agent/orchestrator/providers/vertex-ai-auth.mjs +622 -0
  141. package/src/runtime/agent/orchestrator/providers/vertex-ai-maas.mjs +185 -0
  142. package/src/runtime/agent/orchestrator/providers/vertex-ai-models.mjs +445 -0
  143. package/src/runtime/agent/orchestrator/providers/vertex-ai.mjs +567 -0
  144. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +41 -13
  145. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
  146. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +8 -6
  147. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +7 -3
  148. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
  149. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +6 -1
  150. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +14 -5
  151. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -0
  152. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +5 -2
  153. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +30 -5
  154. package/src/runtime/agent/orchestrator/session/agent-room.mjs +171 -37
  155. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +1 -3
  156. package/src/runtime/agent/orchestrator/session/cache-warm.mjs +102 -12
  157. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
  158. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +1 -8
  159. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +23 -0
  160. package/src/runtime/agent/orchestrator/session/compact/observation-supersession.mjs +103 -23
  161. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +160 -36
  162. package/src/runtime/agent/orchestrator/session/context-utils.mjs +13 -1
  163. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +164 -23
  164. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +139 -19
  165. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +38 -7
  166. package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +3 -0
  167. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +100 -22
  168. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -3
  169. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +36 -6
  170. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +33 -3
  171. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +9 -2
  172. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -1
  173. package/src/runtime/agent/orchestrator/session/loop/provider-tool-exclusions.mjs +13 -5
  174. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +14 -0
  175. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +13 -1
  176. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +23 -4
  177. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +2 -0
  178. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +23 -1
  179. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +39 -21
  180. package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +13 -8
  181. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +50 -28
  182. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -3
  183. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +3 -1
  184. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +2 -2
  185. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +1 -0
  186. package/src/runtime/agent/orchestrator/session/manager/route-fallback-chain.mjs +2 -1
  187. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +30 -13
  188. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +63 -6
  189. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +7 -0
  190. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +15 -0
  191. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +35 -13
  192. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +24 -14
  193. package/src/runtime/agent/orchestrator/session/manager/session-scratch-environment.mjs +70 -0
  194. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +0 -3
  195. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +10 -6
  196. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-context.mjs +4 -0
  197. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +3 -0
  198. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +56 -29
  199. package/src/runtime/agent/orchestrator/session/manager/turn-reap.mjs +125 -0
  200. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +11 -5
  201. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +5 -1
  202. package/src/runtime/agent/orchestrator/session/prompt-cache-touch-store.mjs +30 -7
  203. package/src/runtime/agent/orchestrator/session/restart-prefix-parity.fixture.mjs +154 -0
  204. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +7 -0
  205. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +4 -0
  206. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +15 -0
  207. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +37 -8
  208. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +252 -39
  209. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +163 -64
  210. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +164 -4
  211. package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +142 -18
  212. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +61 -2
  213. package/src/runtime/agent/orchestrator/stall-policy.mjs +23 -0
  214. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +6 -2
  215. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +2 -2
  216. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -2
  217. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -1
  218. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -20
  219. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +7 -1
  220. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +67 -31
  221. package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +25 -13
  222. package/src/runtime/agent/orchestrator/tools/builtin/lib/scoped-search-flight.mjs +9 -5
  223. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +58 -20
  224. package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +7 -1
  225. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +6 -3
  226. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +1 -1
  227. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +23 -12
  228. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +6 -1
  229. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +1 -8
  230. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +20 -15
  231. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +96 -20
  232. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +59 -10
  233. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/federation.mjs +3 -1
  234. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
  235. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  236. package/src/runtime/agent/orchestrator/tools/lib/isolated-test-env.mjs +4 -3
  237. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +99 -2
  238. package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +2 -0
  239. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +103 -13
  240. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +3 -1
  241. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +49 -15
  242. package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +45 -27
  243. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +34 -10
  244. package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +28 -4
  245. package/src/runtime/agent/orchestrator/tools/lib/wait-until-fixture.mjs +26 -0
  246. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -2
  247. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
  248. package/src/runtime/channels/lib/webhook/signature.mjs +4 -1
  249. package/src/runtime/channels/lib/whisper-port.mjs +29 -11
  250. package/src/runtime/channels/lib/whisper-server.mjs +10 -1
  251. package/src/runtime/computer-bridge/action-schema.mjs +2 -2
  252. package/src/runtime/computer-bridge/core-actions.mjs +2 -2
  253. package/src/runtime/computer-bridge/error-recovery.mjs +5 -4
  254. package/src/runtime/computer-bridge/linux-action-capabilities.mjs +24 -5
  255. package/src/runtime/computer-bridge/linux-readiness.d.mts +7 -2
  256. package/src/runtime/connectors/connector-accounts-mcp.mjs +236 -103
  257. package/src/runtime/connectors/gmail-app-password.mjs +3 -3
  258. package/src/runtime/explore/explorer.mjs +93 -0
  259. package/src/runtime/explore/tool-defs.mjs +35 -0
  260. package/src/runtime/explore/tool.mjs +123 -0
  261. package/src/runtime/local-provider/server-process.mjs +2 -1
  262. package/src/runtime/media/adapters/gemini-video.mjs +4 -1
  263. package/src/runtime/media/adapters/xai-media.mjs +2 -1
  264. package/src/runtime/memory/index.mjs +4 -0
  265. package/src/runtime/memory/lib/embedding-provider.mjs +4 -1
  266. package/src/runtime/memory/lib/embedding-worker.mjs +10 -3
  267. package/src/runtime/memory/lib/memory-action-handlers/cycle-backfill-action.mjs +1 -7
  268. package/src/runtime/memory/lib/memory-chunk-quality.mjs +1 -6
  269. package/src/runtime/office/authoring/pdf-author-action.mjs +1 -6
  270. package/src/runtime/office/design/design-art-direction.mjs +29 -27
  271. package/src/runtime/office/pdf/pdf-writer.mjs +6 -1
  272. package/src/runtime/office/portable/portable-snapshot-docx.mjs +7 -1
  273. package/src/runtime/office/portable/portable-snapshot-shared.mjs +6 -1
  274. package/src/runtime/office/quality/assurance-rendered.mjs +9 -2
  275. package/src/runtime/office/quality/assurance-structure-docx.mjs +4 -2
  276. package/src/runtime/office/quality/design-review.mjs +1 -5
  277. package/src/runtime/office/quality/document-brief.mjs +1 -5
  278. package/src/runtime/office/quality/forced-break-pages.mjs +9 -6
  279. package/src/runtime/office/quality/quality-pipeline.mjs +1 -1
  280. package/src/runtime/shared/agent-route-config.mjs +17 -2
  281. package/src/runtime/shared/atomic-file.mjs +103 -2
  282. package/src/runtime/shared/background-tasks.mjs +15 -6
  283. package/src/runtime/shared/bot-owned-work.mjs +56 -0
  284. package/src/runtime/shared/connector-accounts.mjs +5 -5
  285. package/src/runtime/shared/connector-catalog.d.mts +11 -4
  286. package/src/runtime/shared/connector-catalog.mjs +60 -52
  287. package/src/runtime/shared/custom-provider-keys.mjs +20 -0
  288. package/src/runtime/shared/defaults-separation.mjs +1 -4
  289. package/src/runtime/shared/fetch-safe-listen.d.mts +16 -0
  290. package/src/runtime/shared/fetch-safe-listen.mjs +50 -0
  291. package/src/runtime/shared/file-has-data.mjs +26 -2
  292. package/src/runtime/shared/file-lock.mjs +13 -15
  293. package/src/runtime/shared/git-launch.d.mts +9 -0
  294. package/src/runtime/shared/git-launch.mjs +213 -0
  295. package/src/runtime/shared/git-runner-worker.mjs +2 -1
  296. package/src/runtime/shared/handoff-artifacts.d.mts +2 -1
  297. package/src/runtime/shared/handoff-artifacts.mjs +18 -8
  298. package/src/runtime/shared/idle-auto-clear-gate.mjs +29 -7
  299. package/src/runtime/shared/llm/cost.mjs +11 -2
  300. package/src/runtime/shared/llm/http-agent.mjs +313 -29
  301. package/src/runtime/shared/llm/model-catalog-projection.mjs +3 -2
  302. package/src/runtime/shared/llm/model-catalog.mjs +46 -6
  303. package/src/runtime/shared/llm/provider-catalog-registry.mjs +3 -0
  304. package/src/runtime/shared/llm/system-proxy.mjs +452 -0
  305. package/src/runtime/shared/loopback-listener.mjs +25 -2
  306. package/src/runtime/shared/model-route-chain.d.mts +7 -2
  307. package/src/runtime/shared/model-route-chain.mjs +22 -4
  308. package/src/runtime/shared/open-url.mjs +10 -0
  309. package/src/runtime/shared/provider-accounts.mjs +52 -2
  310. package/src/runtime/shared/session-activity-signal.mjs +14 -0
  311. package/src/runtime/shared/session-scratch.mjs +37 -0
  312. package/src/runtime/shared/staged-install-worker.mjs +5 -0
  313. package/src/runtime/shared/staged-update.mjs +47 -2
  314. package/src/runtime/shared/tool-card-model/agent-surface.mjs +1 -3
  315. package/src/runtime/shared/tool-card-model/card-status.mjs +15 -2
  316. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  317. package/src/runtime/shared/tool-surface.mjs +113 -36
  318. package/src/runtime/shared/tool-work-units.mjs +6 -0
  319. package/src/runtime/shared/turn-snapshot-store.mjs +13 -6
  320. package/src/runtime/shared/turn-worktree-snapshot.mjs +414 -54
  321. package/src/runtime/shared/worktrees/activity-gate.mjs +215 -318
  322. package/src/runtime/shared/worktrees/activity-ledger-worker.mjs +56 -0
  323. package/src/runtime/shared/worktrees/activity-ledger.mjs +391 -0
  324. package/src/runtime/shared/worktrees/branch-management.mjs +9 -3
  325. package/src/runtime/shared/worktrees/contract.mjs +4 -1
  326. package/src/runtime/shared/worktrees/git.mjs +72 -0
  327. package/src/runtime/shared/worktrees/manager.mjs +285 -168
  328. package/src/runtime/shared/worktrees/test-support.mjs +6 -10
  329. package/src/runtime/tidy/install.mjs +95 -15
  330. package/src/runtime/tidy/languages.mjs +17 -15
  331. package/src/runtime/tidy/process.mjs +7 -4
  332. package/src/runtime/tidy/resolve.mjs +19 -16
  333. package/src/runtime/tidy/runners/biome.mjs +3 -3
  334. package/src/runtime/tidy/tool.mjs +1 -1
  335. package/src/runtime/tidy/worktree.mjs +10 -6
  336. package/src/runtime/visualize-bridge/client.mjs +6 -1
  337. package/src/runtime/visualize-bridge/tool-defs.mjs +7 -2
  338. package/src/runtime/web-search/index.mjs +1 -1
  339. package/src/session-runtime/agent-home-delivery.mjs +41 -0
  340. package/src/session-runtime/agent-task-sessions.mjs +458 -312
  341. package/src/session-runtime/agent-tool-routing.mjs +8 -41
  342. package/src/session-runtime/boot/tools.mjs +7 -1
  343. package/src/session-runtime/bot-status.mjs +107 -101
  344. package/src/session-runtime/bot-target.mjs +38 -0
  345. package/src/session-runtime/channel-config-api.mjs +6 -1
  346. package/src/session-runtime/connector-merge-migration.mjs +18 -15
  347. package/src/session-runtime/conversation-import/codex-titles.mjs +88 -0
  348. package/src/session-runtime/conversation-import/discovery.mjs +401 -36
  349. package/src/session-runtime/conversation-import/service.mjs +35 -8
  350. package/src/session-runtime/handoff-attachments.mjs +82 -31
  351. package/src/session-runtime/held-requests.mjs +3 -1
  352. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +12 -13
  353. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +1 -0
  354. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -0
  355. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +8 -0
  356. package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +4 -1
  357. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +145 -112
  358. package/src/session-runtime/lifecycle/cold-heir.mjs +11 -6
  359. package/src/session-runtime/lifecycle/session-catalog.mjs +9 -1
  360. package/src/session-runtime/lifecycle/teardown/session-close.mjs +6 -1
  361. package/src/session-runtime/lifecycle/teardown.mjs +1 -1
  362. package/src/session-runtime/mcp-glue/server-catalog.mjs +16 -10
  363. package/src/session-runtime/mcp-glue/server-input.mjs +4 -1
  364. package/src/session-runtime/mixy-heartbeat.mjs +1 -1
  365. package/src/session-runtime/model-route/set-route.mjs +11 -2
  366. package/src/session-runtime/provider-auth/login.mjs +2 -2
  367. package/src/session-runtime/provider-catalog-cache.mjs +82 -36
  368. package/src/session-runtime/provider-models/catalog-load.mjs +23 -6
  369. package/src/session-runtime/provider-models.mjs +4 -2
  370. package/src/session-runtime/resource-mcp-api.mjs +98 -35
  371. package/src/session-runtime/resource-mcp-connector-accounts.mjs +44 -31
  372. package/src/session-runtime/resource-mcp-secrets.mjs +28 -11
  373. package/src/session-runtime/resource-mcp-servers.mjs +5 -4
  374. package/src/session-runtime/resource-recall-api.mjs +6 -1
  375. package/src/session-runtime/runtime-feature-gates.mjs +5 -1
  376. package/src/session-runtime/services/agent-tool/execute.mjs +14 -9
  377. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +20 -4
  378. package/src/session-runtime/services/agent-tool/model-select.mjs +48 -7
  379. package/src/session-runtime/services/agent-tool/render.mjs +71 -17
  380. package/src/session-runtime/services/agent-tool/send-flow.mjs +10 -2
  381. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +8 -3
  382. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
  383. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +6 -2
  384. package/src/session-runtime/services/agent-tool/tool-def.mjs +9 -9
  385. package/src/session-runtime/services/agent-watchdog-registry.mjs +32 -4
  386. package/src/session-runtime/services/channel-worker/daemon-spawn.mjs +2 -0
  387. package/src/session-runtime/services/daemon-candidate.mjs +4 -1
  388. package/src/session-runtime/services/memory-runtime-proxy/daemon-spawn.mjs +4 -0
  389. package/src/session-runtime/services/plugin-admin.mjs +10 -6
  390. package/src/session-runtime/services/provider-admin.mjs +283 -11
  391. package/src/session-runtime/session-turn-api.mjs +2 -0
  392. package/src/session-runtime/settings-builtin-tools-api.mjs +4 -1
  393. package/src/session-runtime/settings-system-api.mjs +1 -1
  394. package/src/session-runtime/setup-tool/agent-settings-actions.mjs +13 -3
  395. package/src/session-runtime/setup-tool/connector-widen-approval.mjs +96 -0
  396. package/src/session-runtime/setup-tool/executor.mjs +6 -2
  397. package/src/session-runtime/setup-tool/tool-defs.mjs +2 -1
  398. package/src/session-runtime/tool-policy/policy-stage.mjs +1 -0
  399. package/src/session-runtime/turn/agent-ops.mjs +23 -0
  400. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +49 -11
  401. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +5 -1
  402. package/src/session-runtime/workflow-agents-api/agent-route.mjs +25 -5
  403. package/src/session-runtime/workflow-agents-api/onboarding.mjs +3 -3
  404. package/src/session-runtime/workspace/activity-guards.mjs +176 -0
  405. package/src/session-runtime/workspace/session-view.mjs +149 -0
  406. package/src/session-runtime/workspace/side-checkouts.mjs +203 -0
  407. package/src/session-runtime/workspace/transition-runner.mjs +188 -0
  408. package/src/session-runtime/workspace/transitions.mjs +108 -1041
  409. package/src/session-runtime/workspace/workspace-merge.mjs +132 -0
  410. package/src/session-runtime/workspace/worktree-intents.mjs +242 -0
  411. package/src/session-runtime/workspace/worktree-maintenance.mjs +80 -0
  412. package/src/session-runtime/workspace/worktree-setup.mjs +85 -0
  413. package/src/session-runtime/workspace/worktree-tool.mjs +73 -56
  414. package/src/standalone/daemon-desktop-runtime.mjs +2 -0
  415. package/src/standalone/daemon-session-control.mjs +169 -138
  416. package/src/standalone/daemon.mjs +7 -0
  417. package/src/standalone/desktop-service-registry.mjs +9 -0
  418. package/src/standalone/local-session-runtime.mjs +5 -1
  419. package/src/standalone/session-client/daemon-unavailable.mjs +3 -0
  420. package/src/standalone/session-client.mjs +5 -1
  421. package/src/standalone/session-frame-stream.mjs +83 -11
  422. package/src/standalone/session-protocol.mjs +4 -0
  423. package/src/standalone/session-service/agent-tree/agent-turns.mjs +16 -12
  424. package/src/standalone/session-service/agent-tree.mjs +45 -9
  425. package/src/standalone/session-service/projection/transcript-window.mjs +700 -64
  426. package/src/standalone/session-service/session-calls/session-action.mjs +10 -3
  427. package/src/standalone/session-service/session-calls/session-views.mjs +160 -104
  428. package/src/standalone/session-service/stored-reader.mjs +127 -40
  429. package/src/standalone/session-transport/client-registry/client-grace.mjs +12 -9
  430. package/src/tui/App.jsx +154 -164
  431. package/src/tui/app/app-view/floating-panel.jsx +10 -4
  432. package/src/tui/app/app-view/text-entry-labels.mjs +1 -0
  433. package/src/tui/app/clipboard.mjs +21 -10
  434. package/src/tui/app/model-picker/route-selection/fast-parameters.mjs +1 -3
  435. package/src/tui/app/model-picker/route-selection.mjs +5 -6
  436. package/src/tui/app/prompt-handlers/use-prompt-history-nav.mjs +1 -1
  437. package/src/tui/app/prompt-handlers/use-prompt-interrupt.mjs +3 -0
  438. package/src/tui/app/prompt-submit/provider-prompts.mjs +3 -1
  439. package/src/tui/app/provider-setup/api-key-actions.mjs +31 -1
  440. package/src/tui/app/provider-setup/oauth-actions.mjs +58 -21
  441. package/src/tui/app/provider-setup/provider-items.mjs +8 -2
  442. package/src/tui/app/provider-setup-picker.mjs +29 -18
  443. package/src/tui/app/shell-layout/row-budgets.mjs +12 -10
  444. package/src/tui/app/slash-commands.mjs +1 -0
  445. package/src/tui/app/slash-dispatch/session-commands.mjs +46 -0
  446. package/src/tui/app/slash-dispatch.mjs +1 -1
  447. package/src/tui/app/use-btw-panel.mjs +29 -0
  448. package/src/tui/app/use-context-panel-refresh.mjs +31 -0
  449. package/src/tui/app/use-model-catalog-cache.mjs +13 -10
  450. package/src/tui/app/use-mouse-input.mjs +11 -0
  451. package/src/tui/app/use-onboarding-start.mjs +8 -3
  452. package/src/tui/app/use-panel-transition.mjs +10 -1
  453. package/src/tui/app/use-pasted-buffers.mjs +28 -20
  454. package/src/tui/app/use-prompt-draft-flow.mjs +7 -0
  455. package/src/tui/app/use-prompt-layout-rows.mjs +3 -3
  456. package/src/tui/app/use-prompt-queue-history.mjs +3 -2
  457. package/src/tui/app/use-selection-focus.mjs +11 -2
  458. package/src/tui/app/use-selection-geometry.mjs +18 -9
  459. package/src/tui/app/use-selection-paint.mjs +4 -4
  460. package/src/tui/app/use-selection-stitch.mjs +3 -3
  461. package/src/tui/app/use-slash-palette.mjs +62 -0
  462. package/src/tui/app/use-terminal-chrome.mjs +2 -2
  463. package/src/tui/app/use-transcript-activity.mjs +47 -32
  464. package/src/tui/app/use-transcript-follow.mjs +12 -4
  465. package/src/tui/app/use-transcript-scroll.mjs +46 -17
  466. package/src/tui/app/use-transcript-swap-reset.mjs +27 -0
  467. package/src/tui/app/use-transcript-window/geometry.mjs +19 -5
  468. package/src/tui/app/use-transcript-window/instance-state.mjs +4 -0
  469. package/src/tui/app/use-transcript-window/selection-shift.mjs +5 -1
  470. package/src/tui/app/use-transcript-window.mjs +21 -11
  471. package/src/tui/app/use-tui-ready.mjs +18 -0
  472. package/src/tui/app/use-welcome-prompt-hint.mjs +5 -0
  473. package/src/tui/app/use-workflow-tab-cycle.mjs +5 -3
  474. package/src/tui/components/Picker.jsx +27 -3
  475. package/src/tui/components/QuestionPanel.jsx +13 -4
  476. package/src/tui/components/SignInUrl.jsx +68 -0
  477. package/src/tui/components/TextEntryPanel.jsx +76 -46
  478. package/src/tui/components/sign-in-url-layout.mjs +24 -0
  479. package/src/tui/dist/index.mjs +1963 -1361
  480. package/src/tui/session/goal-turn-state.mjs +6 -2
  481. package/src/tui/session/session-api/controls.mjs +4 -0
  482. package/src/tui/session/session-api/extensions.mjs +8 -0
  483. package/src/tui/session/session-api/intake/abort.mjs +2 -1
  484. package/src/tui/session/session-api/intake.mjs +2 -0
  485. package/src/tui/session/session-api/integrations.mjs +2 -2
  486. package/src/tui/session/session-api/routes-agent-definitions.mjs +1 -0
  487. package/src/tui/session/session-api/settings.mjs +2 -2
  488. package/src/tui/session/session-flow/queue/take-entries.mjs +11 -0
  489. package/src/tui/session/transcript-spill/spill-writer.mjs +15 -0
  490. package/src/tui/session/transcript-spill.mjs +92 -4
  491. package/src/tui/session/transcript-store.mjs +2 -0
  492. package/src/ui/statusline-agents.mjs +1 -3
  493. package/src/defaults/skills/skill-creator/references/source-review.md +0 -94
@@ -1,42 +1,148 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
1
3
  import { availableParallelism } from 'node:os';
2
- import { testConcurrencyArg } from './run-node-tests.mjs';
3
4
 
4
- // A local gate has one budget, not a full machine's worth of workers per lane.
5
- // Six is the interactive-desktop default; an explicit budget still wins.
6
- export function gateTestBudget(env = process.env, parallelism = availableParallelism()) {
7
- const argument = testConcurrencyArg([], env);
8
- return argument ? Number(argument.split('=')[1]) : Math.min(6, parallelism);
5
+ // The local gate runs every lane from one shared pool of workers: each lane's
6
+ // files are cut into shards, and whichever worker is free takes the longest
7
+ // shard left, whatever its lane. Shard sizes come from the previous run's
8
+ // measured per-file times, so the split stays balanced without hand-kept
9
+ // weights, and no worker idles while another lane still has work.
10
+
11
+ // The gate runs its whole process tree at below-normal priority, so a
12
+ // foreground app always wins the CPU. Priority does not bound what the
13
+ // desktop shell shares with every process (handles, desktop heap, work
14
+ // queues; explorer.exe has failed with "no work queue available" under many
15
+ // concurrent test processes), so the default also caps the worker count:
16
+ // half the cores, leaving the other half for what the budget does not count
17
+ // (each shard's runner, the processes tests spawn, the desktop build's own
18
+ // threads), and never more than DEFAULT_MAX_WORKERS. Each worker is three
19
+ // node processes (shard runner, its node --test batch, the test file) plus
20
+ // whatever the test spawns. Measured on a 20-core Windows PC with other work
21
+ // running: at 10 workers, test-file processes began dying within ~20 ms with
22
+ // no output (every remaining file of a shard, in 2 of 2 runs); at 8 and 6
23
+ // they did not.
24
+ // An explicit budget still wins: --test-concurrency (gate argument), then
25
+ // MIXDOG_TEST_CONCURRENCY.
26
+ const DEFAULT_MAX_WORKERS = 8;
27
+ export function gateTestBudget(argv = [], env = process.env, parallelism = availableParallelism()) {
28
+ const index = argv.findIndex((arg) => arg === '--test-concurrency' || arg.startsWith('--test-concurrency='));
29
+ let explicit = env.MIXDOG_TEST_CONCURRENCY || undefined;
30
+ // A flag without a value is an error, not "unspecified".
31
+ if (index >= 0) explicit = argv[index].includes('=') ? argv[index].split('=')[1] : (argv[index + 1] ?? '');
32
+ if (explicit === undefined) return Math.max(1, Math.min(DEFAULT_MAX_WORKERS, Math.floor(parallelism / 2)));
33
+ const budget = Number(explicit);
34
+ if (!Number.isInteger(budget) || budget < 1) throw new Error(`gate test budget must be a positive integer (got "${explicit}")`);
35
+ return budget;
36
+ }
37
+
38
+ // Estimates for work with no recorded time yet: the next run records it.
39
+ export const DEFAULT_ESTIMATE_MS = { fast: 2_000, slow: 25_000, command: 60_000 };
40
+
41
+ // Previous runs' measured times ({ version, ms: { key: ms } }). A missing,
42
+ // unreadable or malformed file counts as no history, and an entry that is
43
+ // not a finite positive number is dropped (its work gets the default).
44
+ export function readTimingHistory(file) {
45
+ let ms;
46
+ try {
47
+ ms = JSON.parse(readFileSync(file, 'utf8'))?.ms;
48
+ } catch {
49
+ return {};
50
+ }
51
+ if (!ms || typeof ms !== 'object' || Array.isArray(ms)) return {};
52
+ return Object.fromEntries(
53
+ Object.entries(ms).filter(([, value]) => typeof value === 'number' && Number.isFinite(value) && value > 0)
54
+ );
9
55
  }
10
56
 
11
- // Splits the budget across legs in proportion to leg.weight (default 1), at least
12
- // one worker each. Spare workers go one at a time to the leg with the highest
13
- // weight per worker, so longer legs get more. Needs budget >= legs.length.
14
- export function allocateWorkers(legs, budget) {
15
- const shares = legs.map(() => 1);
16
- for (let spare = budget - legs.length; spare > 0; spare--) {
17
- let best = 0;
18
- for (let i = 1; i < legs.length; i++) {
19
- if ((legs[i].weight ?? 1) / shares[i] > (legs[best].weight ?? 1) / shares[best]) best = i;
57
+ // Writes the history through a temp file of its own (concurrent gates in
58
+ // other worktrees share the file) and renames it into place. Never throws:
59
+ // returns the error, or null once saved; the timings only plan the next run.
60
+ export function saveTimingHistory(file, ms) {
61
+ const temp = `${file}.${process.pid}.${randomUUID()}.tmp`;
62
+ try {
63
+ writeFileSync(temp, `${JSON.stringify({ version: 1, ms })}\n`);
64
+ renameSync(temp, file);
65
+ return null;
66
+ } catch (error) {
67
+ try {
68
+ rmSync(temp, { force: true });
69
+ } catch {
70
+ // The save error is the one reported.
20
71
  }
21
- shares[best]++;
72
+ return error;
22
73
  }
23
- return shares;
24
74
  }
75
+ // Enough shards per worker that the pool's tail (the shards still running
76
+ // when the queue is empty) stays short; the floor bounds the per-shard runner
77
+ // start-up and process sweep (~1-2 s each).
78
+ const SHARDS_PER_WORKER = 8;
79
+ const MIN_SHARD_MS = 15_000;
25
80
 
26
- // The test runners read their worker count when spawned, so it cannot grow
27
- // mid-run: shares are static. With fewer workers than legs, each worker runs
28
- // legs one after another.
29
- export async function runGateLegs(legs, budget, runLeg) {
30
- if (!Number.isInteger(budget) || budget < 1) throw new Error('gate test budget must be a positive integer');
31
- if (budget < legs.length) {
32
- let next = 0;
33
- await Promise.all(
34
- Array.from({ length: budget }, async () => {
35
- while (next < legs.length) await runLeg(legs[next++], 1);
36
- })
37
- );
38
- return;
81
+ // test.mjs selects every lane file whose path contains a given filter, so a
82
+ // file whose path contains another file's path has to share that file's shard
83
+ // or it would run twice. Returns the indivisible groups.
84
+ export function filterGroups(files) {
85
+ const parent = new Map(files.map((file) => [file, file]));
86
+ const find = (file) => (parent.get(file) === file ? file : find(parent.get(file)));
87
+ for (const inner of files) {
88
+ for (const outer of files) if (outer !== inner && outer.includes(inner)) parent.set(find(outer), find(inner));
39
89
  }
40
- const shares = allocateWorkers(legs, budget);
41
- await Promise.all(legs.map((leg, index) => runLeg(leg, shares[index])));
90
+ const groups = new Map();
91
+ for (const file of files) groups.set(find(file), [...(groups.get(find(file)) ?? []), file]);
92
+ return [...groups.values()];
93
+ }
94
+
95
+ // Cuts each lane ({ name, files, estimateOf }) into shards of about one
96
+ // target size, longest-first into the least-loaded shard, so a file longer
97
+ // than the target runs alone. Returns [{ lane, files, estimate }].
98
+ export function planShards(lanes, budget) {
99
+ const total = lanes.reduce((sum, lane) => sum + lane.files.reduce((s, file) => s + lane.estimateOf(file), 0), 0);
100
+ const target = Math.max(MIN_SHARD_MS, total / (budget * SHARDS_PER_WORKER));
101
+ const shards = [];
102
+ for (const lane of lanes) {
103
+ const items = filterGroups(lane.files)
104
+ .map((files) => ({ files, estimate: files.reduce((s, file) => s + lane.estimateOf(file), 0) }))
105
+ .sort((a, b) => b.estimate - a.estimate);
106
+ const laneTotal = items.reduce((sum, item) => sum + item.estimate, 0);
107
+ const bins = Array.from({ length: Math.min(items.length, Math.max(1, Math.round(laneTotal / target))) }, () => ({
108
+ lane: lane.name,
109
+ files: [],
110
+ estimate: 0,
111
+ }));
112
+ for (const item of items) {
113
+ const bin = bins.reduce((least, candidate) => (candidate.estimate < least.estimate ? candidate : least));
114
+ bin.files.push(...item.files);
115
+ bin.estimate += item.estimate;
116
+ }
117
+ shards.push(...bins);
118
+ }
119
+ return shards;
120
+ }
121
+
122
+ // Per-file time of a finished shard: what its own tests reported plus an
123
+ // equal share of the rest of the shard's wall time (process start-up, module
124
+ // load, the runner's own work). Returns { file: ms }.
125
+ export function measuredFileTimes(files, reportedMs, wallMs) {
126
+ const reported = files.reduce((sum, file) => sum + (reportedMs[file] ?? 0), 0);
127
+ const overhead = Math.max(0, wallMs - reported) / files.length;
128
+ return Object.fromEntries(files.map((file) => [file, (reportedMs[file] ?? 0) + overhead]));
129
+ }
130
+
131
+ // Runs every unit on `budget` workers, longest estimate first. A failed unit
132
+ // marked stopOnFail stops new starts (running units finish). Returns the
133
+ // units never started.
134
+ export async function runPool(units, budget, run) {
135
+ if (!Number.isInteger(budget) || budget < 1) throw new Error('gate test budget must be a positive integer');
136
+ const queue = [...units].sort((a, b) => b.estimate - a.estimate);
137
+ let stopped = false;
138
+ await Promise.all(
139
+ Array.from({ length: Math.min(budget, queue.length) }, async () => {
140
+ while (!stopped && queue.length > 0) {
141
+ const unit = queue.shift();
142
+ unit.ok = await run(unit);
143
+ if (!unit.ok && unit.stopOnFail) stopped = true;
144
+ }
145
+ })
146
+ );
147
+ return queue;
42
148
  }
@@ -0,0 +1,16 @@
1
+ // node:test reporter the local gate adds to each shard it runs: one JSON line
2
+ // per test file, { file, ms }, the file's top-level test time (relative to the
3
+ // package the run started in, like `scripts/test.mjs --list`). The gate turns
4
+ // these into the per-file estimates its next run plans shards from.
5
+ import { relative } from 'node:path';
6
+
7
+ export default async function* gateTimingReporter(source) {
8
+ const byFile = new Map();
9
+ for await (const event of source) {
10
+ if (event.type !== 'test:pass' && event.type !== 'test:fail') continue;
11
+ if (event.data.nesting !== 0 || !event.data.file) continue;
12
+ const file = relative(process.cwd(), String(event.data.file)).replaceAll('\\', '/');
13
+ byFile.set(file, (byFile.get(file) ?? 0) + (Number(event.data.details?.duration_ms) || 0));
14
+ }
15
+ for (const [file, ms] of byFile) yield `${JSON.stringify({ file, ms })}\n`;
16
+ }
@@ -2,15 +2,20 @@
2
2
  // evaluates imports in order) so the data/home dirs resolve to a scratch dir
3
3
  // before any module under test reads them, even under plain `node --test` from a
4
4
  // shell that exports the signed-in profile's MIXDOG_DATA_DIR.
5
- import { mkdtempSync, rmSync } from 'node:fs';
5
+ import { mkdtempSync } from 'node:fs';
6
6
  import { tmpdir } from 'node:os';
7
7
  import { join } from 'node:path';
8
+ import { registerOwnedTempDir } from './scoped-temp.mjs';
9
+ import { pruneStaleTempDirs, removeTempDirSync } from './stale-temp.mjs';
8
10
 
9
- const root = mkdtempSync(join(tmpdir(), 'mixdog-test-profile-'));
11
+ // The exit handler below never runs for a killed process, so each start sweeps
12
+ // what earlier killed processes left.
13
+ const PREFIX = 'mixdog-test-profile-';
14
+ pruneStaleTempDirs([PREFIX]);
15
+ const root = mkdtempSync(join(tmpdir(), PREFIX));
16
+ registerOwnedTempDir(root);
10
17
  process.env.MIXDOG_DATA_DIR = join(root, 'data');
11
18
  process.env.MIXDOG_HOME = join(root, 'home');
12
19
  process.on('exit', () => {
13
- try {
14
- rmSync(root, { recursive: true, force: true });
15
- } catch {}
20
+ removeTempDirSync(root);
16
21
  });
@@ -1,8 +1,9 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { appendFile, mkdtemp, readdir, readFile, rm, stat } from 'node:fs/promises';
2
+ import { appendFile, mkdtemp, readFile, rm } from 'node:fs/promises';
3
3
  import { availableParallelism, tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { setTimeout as delay } from 'node:timers/promises';
6
+ import { pruneStaleTempDirs } from './stale-temp.mjs';
6
7
  import { FAILURE_RECORDS_ENV, spawnLedgerPath } from './test-failure-records.mjs';
7
8
  import { classifyRerunOutcomes, formatFailureSummary } from './test-failure-summary.mjs';
8
9
 
@@ -20,28 +21,14 @@ const HEAP_FLAG = /^--(?:max-old-space-size|max-semi-space-size)=\d+$/;
20
21
  // An argument costs its own characters plus the separator and its quotes.
21
22
  const argCost = (arg) => arg.length + 3;
22
23
 
23
- // Full logs are kept for reading after the run; older ones are stale.
24
+ // A green run removes its log directory; a failed run keeps it (the failure
25
+ // summary points at it) until the start-of-run prune finds it stale. The
26
+ // failure and scratch directories are removed when their run ends, so only a
27
+ // killed runner leaves them for that same prune.
24
28
  const TEST_LOG_PREFIX = 'mixdog-test-output-';
25
- const TEST_LOG_RETENTION_MS = 3 * 24 * 60 * 60 * 1000;
26
-
27
- async function pruneStaleTestLogs(root) {
28
- let names = [];
29
- try {
30
- names = await readdir(root);
31
- } catch {
32
- return;
33
- }
34
- const cutoff = Date.now() - TEST_LOG_RETENTION_MS;
35
- for (const name of names) {
36
- if (!name.startsWith(TEST_LOG_PREFIX)) continue;
37
- const path = join(root, name);
38
- try {
39
- if ((await stat(path)).mtimeMs < cutoff) await rm(path, { recursive: true, force: true });
40
- } catch {
41
- /* another run may own or be removing it */
42
- }
43
- }
44
- }
29
+ const TEST_FAILURES_PREFIX = 'mixdog-test-failures-';
30
+ const TEST_SCRATCH_PREFIX = 'mixdog-test-scratch-';
31
+ export const RUNNER_TEMP_PREFIXES = [TEST_LOG_PREFIX, TEST_FAILURES_PREFIX, TEST_SCRATCH_PREFIX];
45
32
 
46
33
  // Batches keep the file order Node was given; a single path larger than the
47
34
  // budget still gets its own batch rather than being dropped. An empty list
@@ -95,7 +82,8 @@ const concurrencyFlag = (arg) => arg === '--test-concurrency' || arg.startsWith(
95
82
  function concurrencyOf(runArgs, fileArgs) {
96
83
  const args = [...runArgs, ...fileArgs];
97
84
  const index = args.findIndex(concurrencyFlag);
98
- const value = index < 0 ? null : positiveInt(args[index].includes('=') ? args[index].split('=')[1] : args[index + 1]);
85
+ let value = null;
86
+ if (index >= 0) value = positiveInt(args[index].includes('=') ? args[index].split('=')[1] : args[index + 1]);
99
87
  return value ?? Math.max(1, availableParallelism() - 1);
100
88
  }
101
89
 
@@ -202,17 +190,29 @@ async function processSnapshot() {
202
190
  });
203
191
  }
204
192
 
205
- const CLOCK_SLACK_MS = 2000;
193
+ // How far a snapshot's creation time may sit from Date.now() read around the
194
+ // spawn. Windows reports creation to the microsecond from the same system
195
+ // clock, so only a clock tick separates them; POSIX ps reports whole elapsed
196
+ // seconds. No wider: Windows hands a freed pid to the next process within
197
+ // milliseconds, and a 2 s window made a process another run had just started
198
+ // pass for one of this run's exited spawns, so it was killed with them.
199
+ const clockSlackMs = (platform) => (platform === 'win32' ? 50 : 2000);
206
200
 
207
201
  // The processes this run owns, out of a snapshot: an anchor (the batch, or a
208
202
  // child a test process recorded in its spawn ledger) still alive under its
209
- // pid, the orphans an exited anchor left (they still name it as their parent;
210
- // a later process that reuses its pid only has younger children), every
211
- // descendant of those, and anything executing from the run's scratch root
212
- // (e.g. the postgres the memory runtime downloads into MIXDOG_DATA_DIR).
203
+ // pid, the orphans an exited anchor left (they still name it as their parent),
204
+ // every descendant of those, and anything executing from the run's scratch
205
+ // root (e.g. the postgres the memory runtime downloads into MIXDOG_DATA_DIR).
206
+ // An anchor is { pid, before, since, exited? }: the clock just before and just
207
+ // after its spawn, and when its exit was seen. A pid alone never identifies a
208
+ // process: an anchor seen to exit no longer holds its pid, so whatever holds
209
+ // it now is another process; a live holder was created within its spawn; an
210
+ // orphan was created while its anchor lived (a later process that reuses the
211
+ // pid only has younger children); a descendant is younger than its parent.
213
212
  // Command-line text never makes a process the run's: an editor or terminal
214
213
  // opened on the scratch root is not.
215
- export function ownedProcesses(snapshot, anchors, { batchStart, scratchDir, platform = process.platform }) {
214
+ export function ownedProcesses(snapshot, anchors, { scratchDir, platform = process.platform }) {
215
+ const slack = clockSlackMs(platform);
216
216
  const byPid = new Map(snapshot.map((entry) => [entry.pid, entry]));
217
217
  const childrenOf = new Map();
218
218
  for (const entry of snapshot) childrenOf.set(entry.ppid, [...(childrenOf.get(entry.ppid) ?? []), entry]);
@@ -223,14 +223,19 @@ export function ownedProcesses(snapshot, anchors, { batchStart, scratchDir, plat
223
223
  owned.set(entry.pid, entry);
224
224
  queue.push(entry);
225
225
  };
226
- for (const { pid, since } of anchors) {
226
+ for (const { pid, before, since, exited } of anchors) {
227
227
  const holder = byPid.get(pid);
228
- if (holder && holder.created >= batchStart - CLOCK_SLACK_MS && holder.created <= since + CLOCK_SLACK_MS) {
228
+ if (holder && exited === undefined && holder.created >= before - slack && holder.created <= since + slack) {
229
229
  claim(holder);
230
230
  continue;
231
231
  }
232
232
  for (const orphan of childrenOf.get(pid) ?? []) {
233
- if (orphan.created >= since - CLOCK_SLACK_MS && (!holder || orphan.created < holder.created)) claim(orphan);
233
+ if (
234
+ orphan.created >= before - slack &&
235
+ orphan.created <= (exited ?? Number.POSITIVE_INFINITY) + slack &&
236
+ (!holder || orphan.created < holder.created)
237
+ )
238
+ claim(orphan);
234
239
  }
235
240
  }
236
241
  const fold = (path) => (platform === 'win32' ? path.toLowerCase() : path);
@@ -242,26 +247,30 @@ export function ownedProcesses(snapshot, anchors, { batchStart, scratchDir, plat
242
247
  while (queue.length > 0) {
243
248
  const parent = queue.pop();
244
249
  for (const entry of childrenOf.get(parent.pid) ?? []) {
245
- if (entry.created >= parent.created - CLOCK_SLACK_MS) claim(entry);
250
+ if (entry.created >= parent.created - slack) claim(entry);
246
251
  }
247
252
  }
248
253
  return [...owned.values()];
249
254
  }
250
255
 
256
+ // One anchor per recorded spawn (scripts/lib/test-spawn-ledger.mjs): its
257
+ // "<pid> <before> <since>" line, with "<exited>" from the line that repeats
258
+ // it once the exit was seen.
251
259
  async function readSpawnLedger(path) {
252
260
  const text = await readFile(path, 'utf8').catch(() => '');
253
- return text
254
- .split('\n')
255
- .filter(Boolean)
256
- .map((line) => {
257
- const [pid, since] = line.split(' ').map(Number);
258
- return { pid, since };
259
- });
261
+ const spawns = new Map();
262
+ for (const line of text.split('\n').filter(Boolean)) {
263
+ const [pid, before, since, exited] = line.split(' ').map(Number);
264
+ const key = `${pid} ${before} ${since}`;
265
+ spawns.set(key, { ...(spawns.get(key) ?? { pid, before, since }), ...(exited ? { exited } : {}) });
266
+ }
267
+ return [...spawns.values()];
260
268
  }
261
269
 
262
270
  // A process a test leaves running must not outlive its batch: it can hold a
263
271
  // port, a database, or files in the scratch root the run removes at the end.
264
- async function reapLeftovers(child, { batchStart, ledger, scratchDir }) {
272
+ // `batch` is the batch's own anchor, its exit already seen.
273
+ async function reapLeftovers(child, { batch, ledger, scratchDir }) {
265
274
  // The batch itself has exited: only its process group can remain.
266
275
  if (process.platform !== 'win32' && child.pid) {
267
276
  try {
@@ -270,12 +279,12 @@ async function reapLeftovers(child, { batchStart, ledger, scratchDir }) {
270
279
  /* the group is empty */
271
280
  }
272
281
  }
273
- const anchors = [...(child.pid ? [{ pid: child.pid, since: batchStart }] : []), ...(await readSpawnLedger(ledger))];
274
- await killOwned(anchors, { batchStart, scratchDir });
282
+ const anchors = [...(child.pid ? [batch] : []), ...(await readSpawnLedger(ledger))];
283
+ await killOwned(anchors, { scratchDir });
275
284
  }
276
285
 
277
- async function killOwned(anchors, { batchStart, scratchDir }) {
278
- const leftovers = ownedProcesses(await processSnapshot(), anchors, { batchStart, scratchDir });
286
+ async function killOwned(anchors, { scratchDir }) {
287
+ const leftovers = ownedProcesses(await processSnapshot(), anchors, { scratchDir });
279
288
  if (leftovers.length === 0) return;
280
289
  const pids = leftovers.map((entry) => entry.pid);
281
290
  console.error(`Killing ${pids.length} process(es) a test left running: ${pids.join(' ')}`);
@@ -292,12 +301,13 @@ async function killOwned(anchors, { batchStart, scratchDir }) {
292
301
  }
293
302
  }
294
303
 
295
- // Resolves { code, hung } once the batch exited and its leftovers are gone.
304
+ // Resolves { code, hung, pid } once the batch exited and its leftovers are gone.
296
305
  // `scratchDir` '' = leave the scratch-root sweep to the caller (side-by-side batches).
297
306
  function spawnBatch(args, env, { files, ledger, scratchDir, timeoutMs }) {
298
307
  return new Promise((resolve) => {
299
- const batchStart = Date.now();
308
+ const before = Date.now();
300
309
  const child = spawn(process.execPath, args, { stdio: 'inherit', env, detached: process.platform !== 'win32' });
310
+ const since = Date.now();
301
311
  let hung = '';
302
312
  const watchdog = setTimeout(() => {
303
313
  hung = `hung after ${minutes(timeoutMs)} min: ${files.join(' ') || '(discovered files)'}`;
@@ -315,10 +325,11 @@ function spawnBatch(args, env, { files, ledger, scratchDir, timeoutMs }) {
315
325
  const signals = process.platform === 'win32' ? [] : ['SIGINT', 'SIGTERM'];
316
326
  for (const signal of signals) process.on(signal, forward);
317
327
  const settle = async (code) => {
328
+ const batch = { pid: child.pid, before, since, exited: Date.now() };
318
329
  clearTimeout(watchdog);
319
330
  for (const signal of signals) process.off(signal, forward);
320
- await reapLeftovers(child, { batchStart, ledger, scratchDir });
321
- resolve({ code: hung ? 1 : code, hung });
331
+ await reapLeftovers(child, { batch, ledger, scratchDir });
332
+ resolve({ code: hung ? 1 : code, hung, pid: child.pid });
322
333
  };
323
334
  child.on('error', (error) => {
324
335
  console.error(error);
@@ -331,14 +342,14 @@ function spawnBatch(args, env, { files, ledger, scratchDir, timeoutMs }) {
331
342
  // rm's own maxRetries re-walks the tree on every nested retry, so one held
332
343
  // file a few directories down multiplied its delay into hours. Each pass here
333
344
  // is a single bounded walk.
334
- async function removeScratch(dir) {
345
+ async function removeTempDir(dir) {
335
346
  for (let attempt = 1; ; attempt += 1) {
336
347
  try {
337
348
  await rm(dir, { recursive: true, force: true });
338
349
  return;
339
350
  } catch (error) {
340
351
  if (attempt === 5) {
341
- console.error(`Test scratch left behind (${error.code}): ${dir}`);
352
+ console.error(`Test temp dir left behind (${error.code}): ${dir}`);
342
353
  return;
343
354
  }
344
355
  await delay(200 * attempt);
@@ -372,21 +383,37 @@ async function readFailureRecords(path) {
372
383
  // log can still name what failed.
373
384
  //
374
385
  // `testTimeoutMs` is the per-file --test-timeout default (see testTimeoutArg).
375
- export async function runNodeTests(
376
- nodeArgs,
377
- fileArgs,
378
- { argBudget = ARG_BUDGET, coverage = false, rerunFailed = 0, testTimeoutMs = DEFAULT_TEST_TIMEOUT_MS } = {}
379
- ) {
380
- await pruneStaleTestLogs(tmpdir());
381
- const logPath = join(await mkdtemp(join(tmpdir(), TEST_LOG_PREFIX)), 'full.log');
386
+ export async function runNodeTests(nodeArgs, fileArgs, options = {}) {
387
+ pruneStaleTempDirs(RUNNER_TEMP_PREFIXES);
388
+ const outputDir = await mkdtemp(join(tmpdir(), TEST_LOG_PREFIX));
389
+ const logPath = join(outputDir, 'full.log');
382
390
  console.error(`Full test log: ${logPath}`);
383
391
  // Failure records live outside the log directory: the log directory holds
384
392
  // exactly one merged log, and these records are an internal artifact.
385
- const failureDir = await mkdtemp(join(tmpdir(), 'mixdog-test-failures-'));
393
+ const failureDir = await mkdtemp(join(tmpdir(), TEST_FAILURES_PREFIX));
386
394
  // Test processes get a private temp root that is removed after the run, so
387
395
  // every fixture directory a test forgets to delete goes with it instead of
388
396
  // piling up in the system temp directory.
389
- const scratchDir = await mkdtemp(join(tmpdir(), 'mixdog-test-scratch-'));
397
+ const scratchDir = await mkdtemp(join(tmpdir(), TEST_SCRATCH_PREFIX));
398
+ let status = 1;
399
+ try {
400
+ status = await executeRun(nodeArgs, fileArgs, options, { logPath, failureDir, scratchDir });
401
+ } finally {
402
+ await removeTempDir(failureDir);
403
+ // Leftover processes are already killed; what a process outside the run
404
+ // still holds stays behind.
405
+ await removeTempDir(scratchDir);
406
+ // A throw keeps the log too: the run did not pass.
407
+ if (status === 0) await removeTempDir(outputDir);
408
+ }
409
+ }
410
+
411
+ async function executeRun(
412
+ nodeArgs,
413
+ fileArgs,
414
+ { argBudget = ARG_BUDGET, coverage = false, rerunFailed = 0, testTimeoutMs = DEFAULT_TEST_TIMEOUT_MS },
415
+ { logPath, failureDir, scratchDir }
416
+ ) {
390
417
  // The explicit entry also accepts Node options alongside its file paths.
391
418
  const concurrencyArg = testConcurrencyArg([...nodeArgs, ...fileArgs]);
392
419
  const timeoutArg = testTimeoutArg([...nodeArgs, ...fileArgs], process.env, testTimeoutMs);
@@ -458,7 +485,7 @@ export async function runNodeTests(
458
485
  );
459
486
  // Peers were still running while each batch exited, so the scratch root's
460
487
  // own processes (e.g. a postgres a test started) are swept once, here.
461
- if (overlapped) await killOwned([], { batchStart: Date.now(), scratchDir });
488
+ if (overlapped) await killOwned([], { scratchDir });
462
489
  // Logs and records merge in batch order, whatever order the batches finished.
463
490
  for (const [index, outcome] of outcomes.entries()) {
464
491
  const batchLog = batchLogOf(index);
@@ -509,15 +536,13 @@ export async function runNodeTests(
509
536
  status = 0;
510
537
  process.exitCode = status;
511
538
 
512
- await rm(failureDir, { recursive: true, force: true });
513
- // Leftover processes are already killed; what a process outside the run
514
- // still holds stays behind.
515
- await removeScratch(scratchDir);
516
539
  if (coverageDir) {
517
540
  const { reportCoverage } = await import('./coverage.mjs');
518
541
  await reportCoverage(coverageDir);
519
542
  await rm(coverageDir, { recursive: true, force: true });
520
543
  }
521
544
  // Last line of the run, after every reporter and the coverage report.
522
- process.stdout.write(`\n${formatFailureSummary({ failed, flaky, rerunFailed, hung, exitCode: status })}`);
545
+ const pids = outcomes.filter((outcome) => outcome.code !== 0 && outcome.pid).map((outcome) => outcome.pid);
546
+ process.stdout.write(`\n${formatFailureSummary({ failed, flaky, rerunFailed, hung, exitCode: status, pids })}`);
547
+ return status;
523
548
  }
@@ -0,0 +1,16 @@
1
+ export function makeTempDirSync(prefix: string): string;
2
+ export function registerOwnedTempDir(dir: string): string;
3
+ export function redirectSystemTemp(prefix?: string): string;
4
+ export function removeTempDirSync(dir: string): void;
5
+ export function removeTempDir(dir: string): Promise<void>;
6
+ export function tempDirFor(t: { after(fn: () => unknown): void }, prefix: string): string;
7
+ export interface ScopedTempDir {
8
+ path: string;
9
+ join(...parts: string[]): string;
10
+ remove(): Promise<void>;
11
+ removeSync(): void;
12
+ [Symbol.asyncDispose](): Promise<void>;
13
+ [Symbol.dispose](): void;
14
+ }
15
+ export function scopedTempDir(prefix: string): ScopedTempDir;
16
+ export function withTempDir<T>(prefix: string, fn: (dir: string) => T | Promise<T>): Promise<T>;