mixdog 1.4.1 → 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 (326) hide show
  1. package/package.json +3 -2
  2. package/scripts/build-shipped-history.mjs +115 -33
  3. package/scripts/gate-local.mjs +172 -61
  4. package/scripts/lib/gate-scheduler.mjs +138 -32
  5. package/scripts/lib/gate-timing-reporter.mjs +16 -0
  6. package/scripts/lib/run-node-tests.mjs +54 -29
  7. package/scripts/lib/scoped-temp.mjs +1 -0
  8. package/scripts/lib/test-failure-records.mjs +14 -1
  9. package/scripts/lib/test-failure-summary.mjs +16 -4
  10. package/scripts/lib/test-spawn-ledger.mjs +12 -3
  11. package/scripts/pptx-kit-index.mjs +5 -6
  12. package/scripts/suite-health.mjs +110 -28
  13. package/scripts/test.mjs +9 -14
  14. package/src/agents/advisor/AGENT.md +4 -4
  15. package/src/agents/advisor/agent.json +1 -1
  16. package/src/agents/explore/AGENT.md +22 -0
  17. package/src/agents/explore/agent.json +6 -0
  18. package/src/cli.mjs +7 -0
  19. package/src/defaults/agents.json +7 -0
  20. package/src/defaults/shipped-history.json +18 -2
  21. package/src/defaults/skills/browser-use/SKILL.md +4 -1
  22. package/src/defaults/skills/code-tidy/SKILL.md +71 -102
  23. package/src/defaults/skills/code-tidy/references/agent-cleanup.md +66 -114
  24. package/src/defaults/skills/code-tidy/references/cleanup-report.md +19 -35
  25. package/src/defaults/skills/code-tidy/references/dead-code.md +3 -9
  26. package/src/defaults/skills/code-tidy/references/test-suite-slop.md +16 -27
  27. package/src/defaults/skills/computer-use/SKILL.md +12 -2
  28. package/src/defaults/skills/goal-management/SKILL.md +1 -1
  29. package/src/defaults/skills/setup/references/actions.md +12 -6
  30. package/src/defaults/skills/setup/references/surfaces.md +2 -2
  31. package/src/defaults/skills/setup-guide/SKILL.md +5 -2
  32. package/src/defaults/skills/setup-guide/model-recommendations.json +13 -0
  33. package/src/defaults/skills/skill-creator/SKILL.md +85 -190
  34. package/src/defaults/skills/skill-creator/references/design.md +70 -162
  35. package/src/defaults/skills/skill-creator/references/evaluation.md +33 -92
  36. package/src/defaults/skills/test-suite/SKILL.md +194 -0
  37. package/src/defaults/skills/test-suite/references/case-management.md +51 -0
  38. package/src/defaults/skills/test-suite/references/flaky-policy.md +39 -0
  39. package/src/defaults/skills/test-suite/references/reliability.md +49 -0
  40. package/src/defaults/skills/test-suite/references/suite-design.md +78 -0
  41. package/src/defaults/skills/test-suite/scripts/inventory.mjs +335 -0
  42. package/src/defaults/skills/visualize/SKILL.md +6 -2
  43. package/src/lib/rules-builder.cjs +6 -30
  44. package/src/rules/lead/LEAD.md +3 -3
  45. package/src/rules/shared/20-research.md +3 -6
  46. package/src/rules/shared/40-editing.md +1 -0
  47. package/src/rules/shared/60-verification.md +10 -9
  48. package/src/rules/shared/70-delivery.md +4 -3
  49. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/abort-link.mjs +5 -1
  50. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +10 -1
  51. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +130 -21
  52. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +13 -0
  53. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +4 -1
  54. package/src/runtime/agent/orchestrator/config.mjs +12 -7
  55. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +7 -7
  56. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +5 -26
  57. package/src/runtime/agent/orchestrator/internal-agents.mjs +3 -14
  58. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +122 -12
  59. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -3
  60. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +13 -2
  61. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +152 -58
  62. package/src/runtime/agent/orchestrator/mcp/client.mjs +51 -18
  63. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +83 -52
  64. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +42 -26
  65. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +43 -8
  66. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +30 -28
  67. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +31 -21
  69. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +3 -1
  70. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -2
  71. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
  72. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +1 -1
  73. package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +40 -0
  74. package/src/runtime/agent/orchestrator/providers/custom-provider-keys.mjs +34 -0
  75. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +3 -0
  76. package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +14 -7
  77. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +3 -2
  78. package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +2 -2
  79. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +4 -1
  80. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +48 -31
  81. package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +0 -18
  82. package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +138 -6
  83. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +1 -1
  84. package/src/runtime/agent/orchestrator/providers/model-cache.mjs +100 -33
  85. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +1 -1
  86. package/src/runtime/agent/orchestrator/providers/oauth-secret-fields.fixtures.mjs +45 -0
  87. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +26 -28
  88. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +3 -1
  89. package/src/runtime/agent/orchestrator/providers/openai-ws-watchdogs.mjs +1 -0
  90. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +3 -1
  92. package/src/runtime/agent/orchestrator/providers/registry.mjs +33 -2
  93. package/src/runtime/agent/orchestrator/providers/vertex-ai-auth.mjs +622 -0
  94. package/src/runtime/agent/orchestrator/providers/vertex-ai-maas.mjs +185 -0
  95. package/src/runtime/agent/orchestrator/providers/vertex-ai-models.mjs +445 -0
  96. package/src/runtime/agent/orchestrator/providers/vertex-ai.mjs +567 -0
  97. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
  98. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +2 -1
  99. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
  100. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +3 -0
  101. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +14 -5
  102. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -0
  103. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +30 -5
  104. package/src/runtime/agent/orchestrator/session/agent-room.mjs +171 -37
  105. package/src/runtime/agent/orchestrator/session/cache-warm.mjs +94 -10
  106. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +3 -3
  107. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +10 -5
  108. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +5 -5
  109. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +11 -1
  110. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -3
  111. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +36 -6
  112. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +5 -3
  113. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +21 -2
  114. package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +2 -0
  115. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +23 -1
  116. package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +13 -8
  117. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +50 -28
  118. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +1 -0
  119. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +3 -4
  120. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +2 -0
  121. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +7 -0
  122. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +15 -0
  123. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +25 -14
  124. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +17 -14
  125. package/src/runtime/agent/orchestrator/session/manager/session-scratch-environment.mjs +70 -0
  126. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +0 -3
  127. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -3
  128. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +47 -29
  129. package/src/runtime/agent/orchestrator/session/manager/turn-reap.mjs +125 -0
  130. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +11 -5
  131. package/src/runtime/agent/orchestrator/session/restart-prefix-parity.fixture.mjs +1 -1
  132. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +243 -38
  133. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +163 -64
  134. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +164 -4
  135. package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +142 -18
  136. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +6 -2
  137. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +36 -26
  138. package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +3 -5
  139. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +58 -20
  140. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +20 -15
  141. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +96 -20
  142. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +59 -10
  143. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  144. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +99 -2
  145. package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +2 -0
  146. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +103 -13
  147. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +3 -1
  148. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +49 -15
  149. package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +45 -27
  150. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +34 -10
  151. package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +2 -0
  152. package/src/runtime/agent/orchestrator/tools/lib/wait-until-fixture.mjs +26 -0
  153. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -2
  154. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
  155. package/src/runtime/channels/lib/whisper-port.mjs +19 -10
  156. package/src/runtime/channels/lib/whisper-server.mjs +10 -1
  157. package/src/runtime/connectors/connector-accounts-mcp.mjs +236 -103
  158. package/src/runtime/connectors/gmail-app-password.mjs +3 -3
  159. package/src/runtime/explore/explorer.mjs +93 -0
  160. package/src/runtime/explore/tool-defs.mjs +35 -0
  161. package/src/runtime/explore/tool.mjs +123 -0
  162. package/src/runtime/memory/index.mjs +4 -0
  163. package/src/runtime/memory/lib/embedding-provider.mjs +4 -1
  164. package/src/runtime/memory/lib/embedding-worker.mjs +4 -0
  165. package/src/runtime/office/quality/assurance-structure-docx.mjs +1 -1
  166. package/src/runtime/shared/agent-route-config.mjs +17 -2
  167. package/src/runtime/shared/atomic-file.mjs +103 -2
  168. package/src/runtime/shared/background-tasks.mjs +15 -6
  169. package/src/runtime/shared/connector-accounts.mjs +5 -5
  170. package/src/runtime/shared/connector-catalog.d.mts +11 -4
  171. package/src/runtime/shared/connector-catalog.mjs +60 -52
  172. package/src/runtime/shared/custom-provider-keys.mjs +20 -0
  173. package/src/runtime/shared/file-has-data.mjs +26 -2
  174. package/src/runtime/shared/file-lock.mjs +13 -15
  175. package/src/runtime/shared/git-launch.d.mts +9 -0
  176. package/src/runtime/shared/git-launch.mjs +213 -0
  177. package/src/runtime/shared/git-runner-worker.mjs +2 -1
  178. package/src/runtime/shared/llm/cost.mjs +6 -1
  179. package/src/runtime/shared/llm/http-agent.mjs +313 -29
  180. package/src/runtime/shared/llm/model-catalog-projection.mjs +3 -2
  181. package/src/runtime/shared/llm/model-catalog.mjs +46 -6
  182. package/src/runtime/shared/llm/provider-catalog-registry.mjs +3 -0
  183. package/src/runtime/shared/llm/system-proxy.mjs +452 -0
  184. package/src/runtime/shared/provider-accounts.mjs +52 -2
  185. package/src/runtime/shared/staged-install-worker.mjs +5 -0
  186. package/src/runtime/shared/staged-update.mjs +47 -2
  187. package/src/runtime/shared/tool-surface.mjs +110 -36
  188. package/src/runtime/shared/tool-work-units.mjs +4 -0
  189. package/src/runtime/shared/turn-worktree-snapshot.mjs +414 -54
  190. package/src/runtime/shared/worktrees/activity-gate.mjs +215 -318
  191. package/src/runtime/shared/worktrees/activity-ledger-worker.mjs +56 -0
  192. package/src/runtime/shared/worktrees/activity-ledger.mjs +391 -0
  193. package/src/runtime/shared/worktrees/branch-management.mjs +9 -3
  194. package/src/runtime/shared/worktrees/git.mjs +57 -0
  195. package/src/runtime/shared/worktrees/manager.mjs +205 -137
  196. package/src/runtime/tidy/install.mjs +95 -15
  197. package/src/runtime/tidy/languages.mjs +17 -15
  198. package/src/runtime/tidy/process.mjs +7 -4
  199. package/src/runtime/tidy/resolve.mjs +19 -16
  200. package/src/runtime/tidy/runners/biome.mjs +3 -3
  201. package/src/runtime/tidy/tool.mjs +1 -1
  202. package/src/runtime/tidy/worktree.mjs +10 -6
  203. package/src/runtime/visualize-bridge/client.mjs +4 -1
  204. package/src/runtime/visualize-bridge/tool-defs.mjs +7 -2
  205. package/src/runtime/web-search/index.mjs +1 -1
  206. package/src/session-runtime/agent-task-sessions.mjs +410 -353
  207. package/src/session-runtime/boot/tools.mjs +2 -0
  208. package/src/session-runtime/bot-status.mjs +103 -103
  209. package/src/session-runtime/connector-merge-migration.mjs +18 -15
  210. package/src/session-runtime/conversation-import/discovery.mjs +159 -40
  211. package/src/session-runtime/conversation-import/service.mjs +9 -3
  212. package/src/session-runtime/handoff-attachments.mjs +1 -1
  213. package/src/session-runtime/held-requests.mjs +3 -1
  214. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -0
  215. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +8 -0
  216. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +143 -116
  217. package/src/session-runtime/lifecycle/teardown/session-close.mjs +6 -1
  218. package/src/session-runtime/lifecycle/teardown.mjs +1 -1
  219. package/src/session-runtime/mcp-glue/server-catalog.mjs +16 -10
  220. package/src/session-runtime/mcp-glue/server-input.mjs +4 -1
  221. package/src/session-runtime/mixy-heartbeat.mjs +1 -1
  222. package/src/session-runtime/provider-auth/login.mjs +2 -2
  223. package/src/session-runtime/resource-mcp-api.mjs +98 -35
  224. package/src/session-runtime/resource-mcp-connector-accounts.mjs +44 -31
  225. package/src/session-runtime/resource-mcp-secrets.mjs +28 -11
  226. package/src/session-runtime/resource-mcp-servers.mjs +5 -4
  227. package/src/session-runtime/services/agent-tool/send-flow.mjs +10 -2
  228. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +8 -3
  229. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
  230. package/src/session-runtime/services/agent-watchdog-registry.mjs +32 -4
  231. package/src/session-runtime/services/channel-worker/daemon-spawn.mjs +2 -0
  232. package/src/session-runtime/services/daemon-candidate.mjs +4 -1
  233. package/src/session-runtime/services/memory-runtime-proxy/daemon-spawn.mjs +4 -0
  234. package/src/session-runtime/services/plugin-admin.mjs +10 -6
  235. package/src/session-runtime/services/provider-admin.mjs +281 -10
  236. package/src/session-runtime/session-turn-api.mjs +2 -0
  237. package/src/session-runtime/settings-builtin-tools-api.mjs +4 -1
  238. package/src/session-runtime/settings-system-api.mjs +1 -1
  239. package/src/session-runtime/setup-tool/agent-settings-actions.mjs +13 -3
  240. package/src/session-runtime/setup-tool/connector-widen-approval.mjs +96 -0
  241. package/src/session-runtime/setup-tool/executor.mjs +6 -2
  242. package/src/session-runtime/setup-tool/tool-defs.mjs +2 -1
  243. package/src/session-runtime/turn/agent-ops.mjs +23 -0
  244. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +49 -11
  245. package/src/session-runtime/workflow-agents-api/agent-route.mjs +10 -1
  246. package/src/session-runtime/workspace/activity-guards.mjs +176 -0
  247. package/src/session-runtime/workspace/session-view.mjs +149 -0
  248. package/src/session-runtime/workspace/side-checkouts.mjs +203 -0
  249. package/src/session-runtime/workspace/transition-runner.mjs +188 -0
  250. package/src/session-runtime/workspace/transitions.mjs +108 -1066
  251. package/src/session-runtime/workspace/workspace-merge.mjs +132 -0
  252. package/src/session-runtime/workspace/worktree-intents.mjs +242 -0
  253. package/src/session-runtime/workspace/worktree-maintenance.mjs +80 -0
  254. package/src/session-runtime/workspace/worktree-setup.mjs +85 -0
  255. package/src/session-runtime/workspace/worktree-tool.mjs +54 -54
  256. package/src/standalone/daemon-desktop-runtime.mjs +2 -0
  257. package/src/standalone/daemon-session-control.mjs +151 -136
  258. package/src/standalone/daemon.mjs +7 -0
  259. package/src/standalone/desktop-service-registry.mjs +9 -0
  260. package/src/standalone/local-session-runtime.mjs +5 -1
  261. package/src/standalone/session-client/daemon-unavailable.mjs +3 -0
  262. package/src/standalone/session-client.mjs +5 -1
  263. package/src/standalone/session-frame-stream.mjs +83 -11
  264. package/src/standalone/session-protocol.mjs +4 -0
  265. package/src/standalone/session-service/agent-tree.mjs +45 -9
  266. package/src/standalone/session-service/projection/transcript-window.mjs +700 -64
  267. package/src/standalone/session-service/session-calls/session-views.mjs +160 -104
  268. package/src/standalone/session-service/stored-reader.mjs +127 -40
  269. package/src/tui/App.jsx +154 -164
  270. package/src/tui/app/app-view/floating-panel.jsx +3 -0
  271. package/src/tui/app/app-view/text-entry-labels.mjs +1 -0
  272. package/src/tui/app/clipboard.mjs +21 -10
  273. package/src/tui/app/prompt-handlers/use-prompt-history-nav.mjs +1 -1
  274. package/src/tui/app/prompt-handlers/use-prompt-interrupt.mjs +3 -0
  275. package/src/tui/app/prompt-submit/provider-prompts.mjs +3 -1
  276. package/src/tui/app/provider-setup/api-key-actions.mjs +31 -1
  277. package/src/tui/app/provider-setup/oauth-actions.mjs +25 -6
  278. package/src/tui/app/provider-setup-picker.mjs +20 -13
  279. package/src/tui/app/shell-layout/row-budgets.mjs +12 -10
  280. package/src/tui/app/slash-commands.mjs +1 -0
  281. package/src/tui/app/slash-dispatch/session-commands.mjs +46 -0
  282. package/src/tui/app/slash-dispatch.mjs +1 -1
  283. package/src/tui/app/use-btw-panel.mjs +29 -0
  284. package/src/tui/app/use-context-panel-refresh.mjs +31 -0
  285. package/src/tui/app/use-model-catalog-cache.mjs +13 -10
  286. package/src/tui/app/use-mouse-input.mjs +11 -0
  287. package/src/tui/app/use-onboarding-start.mjs +8 -3
  288. package/src/tui/app/use-panel-transition.mjs +10 -1
  289. package/src/tui/app/use-pasted-buffers.mjs +28 -20
  290. package/src/tui/app/use-prompt-draft-flow.mjs +7 -0
  291. package/src/tui/app/use-prompt-layout-rows.mjs +3 -3
  292. package/src/tui/app/use-prompt-queue-history.mjs +3 -2
  293. package/src/tui/app/use-selection-focus.mjs +11 -2
  294. package/src/tui/app/use-selection-geometry.mjs +18 -9
  295. package/src/tui/app/use-selection-paint.mjs +4 -4
  296. package/src/tui/app/use-selection-stitch.mjs +3 -3
  297. package/src/tui/app/use-slash-palette.mjs +62 -0
  298. package/src/tui/app/use-terminal-chrome.mjs +2 -2
  299. package/src/tui/app/use-transcript-activity.mjs +47 -32
  300. package/src/tui/app/use-transcript-follow.mjs +12 -4
  301. package/src/tui/app/use-transcript-scroll.mjs +46 -17
  302. package/src/tui/app/use-transcript-swap-reset.mjs +27 -0
  303. package/src/tui/app/use-transcript-window/geometry.mjs +19 -5
  304. package/src/tui/app/use-transcript-window/instance-state.mjs +4 -0
  305. package/src/tui/app/use-transcript-window/selection-shift.mjs +5 -1
  306. package/src/tui/app/use-transcript-window.mjs +21 -11
  307. package/src/tui/app/use-tui-ready.mjs +18 -0
  308. package/src/tui/app/use-welcome-prompt-hint.mjs +5 -0
  309. package/src/tui/app/use-workflow-tab-cycle.mjs +5 -3
  310. package/src/tui/components/Picker.jsx +27 -3
  311. package/src/tui/components/SignInUrl.jsx +68 -0
  312. package/src/tui/components/TextEntryPanel.jsx +76 -46
  313. package/src/tui/components/sign-in-url-layout.mjs +24 -0
  314. package/src/tui/dist/index.mjs +1896 -1341
  315. package/src/tui/session/goal-turn-state.mjs +6 -2
  316. package/src/tui/session/session-api/controls.mjs +4 -0
  317. package/src/tui/session/session-api/extensions.mjs +8 -0
  318. package/src/tui/session/session-api/intake/abort.mjs +2 -1
  319. package/src/tui/session/session-api/intake.mjs +2 -0
  320. package/src/tui/session/session-api/integrations.mjs +2 -2
  321. package/src/tui/session/session-api/routes-agent-definitions.mjs +1 -0
  322. package/src/tui/session/session-api/settings.mjs +2 -2
  323. package/src/tui/session/transcript-spill/spill-writer.mjs +15 -0
  324. package/src/tui/session/transcript-spill.mjs +92 -4
  325. package/src/tui/session/transcript-store.mjs +2 -0
  326. 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
+ }
@@ -82,7 +82,8 @@ const concurrencyFlag = (arg) => arg === '--test-concurrency' || arg.startsWith(
82
82
  function concurrencyOf(runArgs, fileArgs) {
83
83
  const args = [...runArgs, ...fileArgs];
84
84
  const index = args.findIndex(concurrencyFlag);
85
- 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]);
86
87
  return value ?? Math.max(1, availableParallelism() - 1);
87
88
  }
88
89
 
@@ -189,17 +190,29 @@ async function processSnapshot() {
189
190
  });
190
191
  }
191
192
 
192
- 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);
193
200
 
194
201
  // The processes this run owns, out of a snapshot: an anchor (the batch, or a
195
202
  // child a test process recorded in its spawn ledger) still alive under its
196
- // pid, the orphans an exited anchor left (they still name it as their parent;
197
- // a later process that reuses its pid only has younger children), every
198
- // descendant of those, and anything executing from the run's scratch root
199
- // (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.
200
212
  // Command-line text never makes a process the run's: an editor or terminal
201
213
  // opened on the scratch root is not.
202
- 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);
203
216
  const byPid = new Map(snapshot.map((entry) => [entry.pid, entry]));
204
217
  const childrenOf = new Map();
205
218
  for (const entry of snapshot) childrenOf.set(entry.ppid, [...(childrenOf.get(entry.ppid) ?? []), entry]);
@@ -210,14 +223,19 @@ export function ownedProcesses(snapshot, anchors, { batchStart, scratchDir, plat
210
223
  owned.set(entry.pid, entry);
211
224
  queue.push(entry);
212
225
  };
213
- for (const { pid, since } of anchors) {
226
+ for (const { pid, before, since, exited } of anchors) {
214
227
  const holder = byPid.get(pid);
215
- 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) {
216
229
  claim(holder);
217
230
  continue;
218
231
  }
219
232
  for (const orphan of childrenOf.get(pid) ?? []) {
220
- 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);
221
239
  }
222
240
  }
223
241
  const fold = (path) => (platform === 'win32' ? path.toLowerCase() : path);
@@ -229,26 +247,30 @@ export function ownedProcesses(snapshot, anchors, { batchStart, scratchDir, plat
229
247
  while (queue.length > 0) {
230
248
  const parent = queue.pop();
231
249
  for (const entry of childrenOf.get(parent.pid) ?? []) {
232
- if (entry.created >= parent.created - CLOCK_SLACK_MS) claim(entry);
250
+ if (entry.created >= parent.created - slack) claim(entry);
233
251
  }
234
252
  }
235
253
  return [...owned.values()];
236
254
  }
237
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.
238
259
  async function readSpawnLedger(path) {
239
260
  const text = await readFile(path, 'utf8').catch(() => '');
240
- return text
241
- .split('\n')
242
- .filter(Boolean)
243
- .map((line) => {
244
- const [pid, since] = line.split(' ').map(Number);
245
- return { pid, since };
246
- });
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()];
247
268
  }
248
269
 
249
270
  // A process a test leaves running must not outlive its batch: it can hold a
250
271
  // port, a database, or files in the scratch root the run removes at the end.
251
- 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 }) {
252
274
  // The batch itself has exited: only its process group can remain.
253
275
  if (process.platform !== 'win32' && child.pid) {
254
276
  try {
@@ -257,12 +279,12 @@ async function reapLeftovers(child, { batchStart, ledger, scratchDir }) {
257
279
  /* the group is empty */
258
280
  }
259
281
  }
260
- const anchors = [...(child.pid ? [{ pid: child.pid, since: batchStart }] : []), ...(await readSpawnLedger(ledger))];
261
- await killOwned(anchors, { batchStart, scratchDir });
282
+ const anchors = [...(child.pid ? [batch] : []), ...(await readSpawnLedger(ledger))];
283
+ await killOwned(anchors, { scratchDir });
262
284
  }
263
285
 
264
- async function killOwned(anchors, { batchStart, scratchDir }) {
265
- const leftovers = ownedProcesses(await processSnapshot(), anchors, { batchStart, scratchDir });
286
+ async function killOwned(anchors, { scratchDir }) {
287
+ const leftovers = ownedProcesses(await processSnapshot(), anchors, { scratchDir });
266
288
  if (leftovers.length === 0) return;
267
289
  const pids = leftovers.map((entry) => entry.pid);
268
290
  console.error(`Killing ${pids.length} process(es) a test left running: ${pids.join(' ')}`);
@@ -279,12 +301,13 @@ async function killOwned(anchors, { batchStart, scratchDir }) {
279
301
  }
280
302
  }
281
303
 
282
- // 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.
283
305
  // `scratchDir` '' = leave the scratch-root sweep to the caller (side-by-side batches).
284
306
  function spawnBatch(args, env, { files, ledger, scratchDir, timeoutMs }) {
285
307
  return new Promise((resolve) => {
286
- const batchStart = Date.now();
308
+ const before = Date.now();
287
309
  const child = spawn(process.execPath, args, { stdio: 'inherit', env, detached: process.platform !== 'win32' });
310
+ const since = Date.now();
288
311
  let hung = '';
289
312
  const watchdog = setTimeout(() => {
290
313
  hung = `hung after ${minutes(timeoutMs)} min: ${files.join(' ') || '(discovered files)'}`;
@@ -302,10 +325,11 @@ function spawnBatch(args, env, { files, ledger, scratchDir, timeoutMs }) {
302
325
  const signals = process.platform === 'win32' ? [] : ['SIGINT', 'SIGTERM'];
303
326
  for (const signal of signals) process.on(signal, forward);
304
327
  const settle = async (code) => {
328
+ const batch = { pid: child.pid, before, since, exited: Date.now() };
305
329
  clearTimeout(watchdog);
306
330
  for (const signal of signals) process.off(signal, forward);
307
- await reapLeftovers(child, { batchStart, ledger, scratchDir });
308
- resolve({ code: hung ? 1 : code, hung });
331
+ await reapLeftovers(child, { batch, ledger, scratchDir });
332
+ resolve({ code: hung ? 1 : code, hung, pid: child.pid });
309
333
  };
310
334
  child.on('error', (error) => {
311
335
  console.error(error);
@@ -461,7 +485,7 @@ async function executeRun(
461
485
  );
462
486
  // Peers were still running while each batch exited, so the scratch root's
463
487
  // own processes (e.g. a postgres a test started) are swept once, here.
464
- if (overlapped) await killOwned([], { batchStart: Date.now(), scratchDir });
488
+ if (overlapped) await killOwned([], { scratchDir });
465
489
  // Logs and records merge in batch order, whatever order the batches finished.
466
490
  for (const [index, outcome] of outcomes.entries()) {
467
491
  const batchLog = batchLogOf(index);
@@ -518,6 +542,7 @@ async function executeRun(
518
542
  await rm(coverageDir, { recursive: true, force: true });
519
543
  }
520
544
  // Last line of the run, after every reporter and the coverage report.
521
- 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 })}`);
522
547
  return status;
523
548
  }
@@ -71,6 +71,7 @@ export function registerOwnedTempDir(dir) {
71
71
  installExitHook();
72
72
  return resolved;
73
73
  }
74
+
74
75
  /**
75
76
  * For code under test that makes its own mixdog-* directories in the system
76
77
  * temp dir: points TEMP/TMP/TMPDIR at one scoped directory for the rest of the
@@ -36,7 +36,20 @@ export function failureRecord(event, stacks) {
36
36
  if (event.data.todo) return null;
37
37
  if (event.data.details?.error?.failureType === 'subtestsFailed') return null;
38
38
  const ancestors = (stacks.get(file) ?? []).slice(0, event.data.nesting);
39
- return { file, name: [...ancestors, event.data.name].join(' > ') };
39
+ const exit = processExit(event.data.details?.error);
40
+ return { file, name: [...ancestors, event.data.name].join(' > '), ...(exit ? { exit } : {}) };
41
+ }
42
+
43
+ // A test file whose process ended badly fails with only "test failed"; node
44
+ // keeps how it ended on the error. A Windows NTSTATUS code (e.g. 0xC0000142,
45
+ // the process could not initialise) reads better in hex.
46
+ export function processExit(error) {
47
+ if (error?.failureType !== 'testCodeFailure') return '';
48
+ if (error.signal) return `test process killed by ${error.signal}`;
49
+ const code = error.exitCode;
50
+ if (!Number.isInteger(code) || code === 0) return '';
51
+ const hex = code > 255 || code < 0 ? ` (0x${(code >>> 0).toString(16).toUpperCase().padStart(8, '0')})` : '';
52
+ return `test process exited with code ${code}${hex}`;
40
53
  }
41
54
 
42
55
  // The runner sets FAILURE_RECORDS_ENV to a per-spawn JSONL path. Without it
@@ -46,19 +46,31 @@ export function policyLine(rerunFailed) {
46
46
  // `hung` holds one "hung after N min: <files>" message per batch the runner's
47
47
  // watchdog killed; such a batch fails the run whatever its tests reported.
48
48
  // A non-zero `exitCode` with no failed test, flaky test or hung batch behind it
49
- // (a bad node flag, a crashed runner) is its own failure, never "0 failed".
50
- export function formatFailureSummary({ failed = [], flaky = [], rerunFailed = 0, hung = [], exitCode = 0 }) {
49
+ // (a bad node flag, a crashed runner, a kill from outside) is its own failure,
50
+ // never "0 failed". `pids` names the node --test processes that exited
51
+ // non-zero, so a kill can be matched to the "Killing ... <pid>" line of
52
+ // whatever killed them.
53
+ export function formatFailureSummary({
54
+ failed = [],
55
+ flaky = [],
56
+ rerunFailed = 0,
57
+ hung = [],
58
+ exitCode = 0,
59
+ pids = [],
60
+ }) {
51
61
  const hungCount = hung.length > 0 ? `, ${hung.length} hung batch(es)` : '';
52
62
  const unexplained = exitCode !== 0 && failed.length === 0 && hung.length === 0;
63
+ const runner = pids.length > 0 ? `node --test pid ${pids.join(' ')}: ` : '';
53
64
  const lines = [
54
65
  unexplained
55
- ? `${SUMMARY_TAG} RUN FAILED: exit code ${exitCode} with no failed test recorded (bad argument or runner error; see output above)`
66
+ ? `${SUMMARY_TAG} RUN FAILED: exit code ${exitCode} with no failed test recorded (${runner}bad argument, runner error or killed from outside; see output above)`
56
67
  : `${SUMMARY_TAG} ${failed.length} failed, ${flaky.length} flaky${hungCount} (every failed test is named below)`,
57
68
  ];
58
69
  for (const message of hung) lines.push(`${SUMMARY_TAG} HUNG ${message}`);
59
70
  for (const record of failed) {
60
71
  const origin = record.firstSeenOnRerun ? ` (first seen on re-run ${record.firstSeenOnRerun})` : '';
61
- lines.push(`${SUMMARY_TAG} FAILED ${record.file} > ${record.name}${origin}`);
72
+ const exit = record.exit ? ` (${record.exit})` : '';
73
+ lines.push(`${SUMMARY_TAG} FAILED ${record.file} > ${record.name}${exit}${origin}`);
62
74
  }
63
75
  for (const record of flaky) {
64
76
  lines.push(
@@ -1,5 +1,5 @@
1
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
2
+ // each child process's pid, spawn time and exit, so a process a test leaves
3
3
  // running can be traced back to the run after its parent exited
4
4
  // (scripts/lib/run-node-tests.mjs). Without the runner's environment it does
5
5
  // nothing.
@@ -54,11 +54,20 @@ if (
54
54
 
55
55
  const ledger = spawnLedgerPath(process.env[FAILURE_RECORDS_ENV]);
56
56
  if (ledger) {
57
- // spawn, exec, execFile and fork all start their process here.
57
+ // spawn, exec, execFile and fork all start their process here. A spawn is
58
+ // "<pid> <before> <since>", the clock just before and just after it; the
59
+ // same line plus "<exited>" follows once its exit is seen. The 'exit' event
60
+ // comes before the child's handle is closed, so the pid is still this
61
+ // child's when the exit is written: from then on another process may hold it.
58
62
  const spawn = ChildProcess.prototype.spawn;
59
63
  ChildProcess.prototype.spawn = function recordedSpawn(...args) {
64
+ const before = Date.now();
60
65
  const result = spawn.apply(this, args);
61
- if (this.pid) appendFileSync(ledger, `${this.pid} ${Date.now()}\n`);
66
+ if (this.pid) {
67
+ const spawned = `${this.pid} ${before} ${Date.now()}`;
68
+ appendFileSync(ledger, `${spawned}\n`);
69
+ this.once('exit', () => appendFileSync(ledger, `${spawned} ${Date.now()}\n`));
70
+ }
62
71
  return result;
63
72
  };
64
73
  }
@@ -21,12 +21,11 @@ const SOURCES = [
21
21
  // A statement this short says its whole meaning (a constant, a one-line arrow); a longer one gives its first line.
22
22
  const WHOLE_STATEMENT = 240;
23
23
 
24
- const propertyName = (property) =>
25
- property.type === 'Property'
26
- ? String(property.key.name ?? property.key.value ?? '')
27
- : property.type === 'SpreadElement'
28
- ? '…'
29
- : '';
24
+ function propertyName(property) {
25
+ if (property.type === 'Property') return String(property.key.name ?? property.key.value ?? '');
26
+ if (property.type === 'SpreadElement') return '…';
27
+ return '';
28
+ }
30
29
 
31
30
  // The keys an object literal offers a script, one level into object-valued keys ("body: { x, y, w, h }"). A table of
32
31
  // like entries (STYLES: an id per style, each with the same fields) reads as its ids and the fields each carries.