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
@@ -8,8 +8,10 @@
8
8
  * loop each read stretched to seconds. This worker runs exactly that pipeline
9
9
  * (stat → read → stat, digest, readTopLevelLifecycleRecord →
10
10
  * projectStoredTranscript with no turn checkpoint) and hands back the
11
- * digest and the same projection value; only the finished window crosses
12
- * back.
11
+ * digest and one slice of the session's display history: the history itself
12
+ * is built once per content and kept here with its base, and only the window
13
+ * or page a request names (`slice`: `{ limit, skip }`) crosses back, with its
14
+ * serialized size measured here.
13
15
  *
14
16
  * Inside the worker, one projection base (recovery, prepared session and
15
17
  * whole-transcript context gauge) per session content is shared by every
@@ -60,8 +62,14 @@ function clientFor(sessionId) {
60
62
  * the full file stamp the bytes provably came from, or null. Rejects when the
61
63
  * worker cannot answer; callers then run the pipeline on their own thread.
62
64
  */
63
- export function projectStoredTranscriptOffThread({ sessionId, recordPath, itemLimit, known = null }) {
64
- return clientFor(sessionId)({ sessionId, recordPath, itemLimit, known });
65
+ export function projectStoredTranscriptOffThread({
66
+ sessionId,
67
+ recordPath,
68
+ known = null,
69
+ fingerprint = '',
70
+ slice = null,
71
+ }) {
72
+ return clientFor(sessionId)({ sessionId, recordPath, known, fingerprint, slice });
65
73
  }
66
74
 
67
75
  /** Whether the projection worker of `sessionId` is attached (a request now
@@ -321,15 +329,108 @@ async function buildBase(sessionId, text, previousMessages, timing) {
321
329
 
322
330
  const baseBuilds = createBaseBuilds({ build: buildBase });
323
331
 
324
- async function project({ sessionId, recordPath, itemLimit, known }) {
332
+ // Serialized bytes of the display histories this worker keeps for slicing:
333
+ // one larger than the per-session bound is rebuilt per request instead, and
334
+ // the oldest go once all of them exceed the worker's bound. The worker's idle
335
+ // exit releases every one.
336
+ // Measured: a history is about as large as its record (74 MB record: 75 MB
337
+ // history) and a page of a retained one costs ~0.2 s, a rebuilt one ~1 s. 96
338
+ // MB keeps the largest sessions seen (~78 MB records) smooth; 128 MB holds
339
+ // one of those beside a few ordinary ones.
340
+ export const WORKER_HISTORY_SESSION_MAX_BYTES = 96 * 1024 * 1024;
341
+ export const WORKER_HISTORY_MAX_BYTES = 128 * 1024 * 1024;
342
+
343
+ /**
344
+ * The display histories one worker retains (`{ value, rowBytes, bytes }`,
345
+ * keyed by session, Map order LRU) within `sessionMaxBytes` each and
346
+ * `maxBytes` together. A history of other content (`owner`, the record
347
+ * content it was built from, and its sidecar fingerprint) is never served.
348
+ */
349
+ export function createHistoryRetention({
350
+ sessionMaxBytes = WORKER_HISTORY_SESSION_MAX_BYTES,
351
+ maxBytes = WORKER_HISTORY_MAX_BYTES,
352
+ } = {}) {
353
+ const histories = new Map();
354
+ let retainedBytes = 0;
355
+ const drop = (sessionId) => {
356
+ const entry = histories.get(sessionId);
357
+ if (!entry) return;
358
+ histories.delete(sessionId);
359
+ retainedBytes -= entry.history.bytes;
360
+ };
361
+ return {
362
+ get(sessionId, owner, fingerprint) {
363
+ const entry = histories.get(sessionId);
364
+ if (!entry || entry.owner !== owner || entry.fingerprint !== fingerprint) return null;
365
+ histories.delete(sessionId);
366
+ histories.set(sessionId, entry);
367
+ return entry.history;
368
+ },
369
+ /** Keeps `history` when it fits; whether it was kept. */
370
+ retain(sessionId, owner, fingerprint, history) {
371
+ drop(sessionId);
372
+ if (history.bytes > sessionMaxBytes) return false;
373
+ histories.set(sessionId, { owner, fingerprint, history });
374
+ retainedBytes += history.bytes;
375
+ while (retainedBytes > maxBytes && histories.size > 1) drop(histories.keys().next().value);
376
+ return true;
377
+ },
378
+ forget: drop,
379
+ stats: () => ({ histories: histories.size, retainedBytes }),
380
+ };
381
+ }
382
+
383
+ const historyRetention = createHistoryRetention();
384
+ /** sessionId -> { owner, fingerprint, promise }: histories being built. */
385
+ const historyBuilds = new Map();
386
+
387
+ /** The display history of `built` (one base) under sidecar `fingerprint`,
388
+ * with each row's serialized bytes: retained, joined while another request
389
+ * builds it, or built now. */
390
+ async function displayHistory(sessionId, built, content, fingerprint, projection) {
391
+ const retained = historyRetention.get(sessionId, content, fingerprint);
392
+ if (retained) return { history: retained, historyBuilt: false };
393
+ const pending = historyBuilds.get(sessionId);
394
+ if (pending && pending.owner === content && pending.fingerprint === fingerprint) {
395
+ return { history: await pending.promise, historyBuilt: false };
396
+ }
397
+ const promise = (async () => {
398
+ const value = await projection.windowStoredTranscript(sessionId, built.base, {
399
+ history: true,
400
+ includeMessages: false,
401
+ });
402
+ const rowBytes = new Float64Array(value.items.length);
403
+ let bytes = 0;
404
+ for (let index = 0; index < value.items.length; index += 1) {
405
+ rowBytes[index] = Buffer.byteLength(JSON.stringify(value.items[index] ?? null));
406
+ bytes += rowBytes[index];
407
+ }
408
+ return { value, rowBytes, bytes };
409
+ })();
410
+ const record = { owner: content, fingerprint, promise };
411
+ historyBuilds.set(sessionId, record);
412
+ try {
413
+ const history = await promise;
414
+ if (historyBuilds.get(sessionId) === record) historyRetention.retain(sessionId, content, fingerprint, history);
415
+ return { history, historyBuilt: true };
416
+ } finally {
417
+ if (historyBuilds.get(sessionId) === record) historyBuilds.delete(sessionId);
418
+ }
419
+ }
420
+
421
+ async function project({ sessionId, recordPath, known, fingerprint = '', slice = null }) {
325
422
  const importStartedAt = performance.now();
326
- const [{ statSessionStamp, sameSessionStamp }, { readTextFile, PROBE_PRESENT }, { transcriptTextHash }, projection] =
327
- await Promise.all([
328
- import('./store/canonical-reader.mjs'),
329
- import('./store/fs-probe.mjs'),
330
- import('./store-transcript-cache.mjs'),
331
- import('./store-transcript-projection.mjs'),
332
- ]);
423
+ const [
424
+ { statSessionStamp, sameSessionStamp },
425
+ { readTextFile, PROBE_PRESENT },
426
+ { transcriptTextHash, historyStampOf },
427
+ projection,
428
+ ] = await Promise.all([
429
+ import('./store/canonical-reader.mjs'),
430
+ import('./store/fs-probe.mjs'),
431
+ import('./store-transcript-cache.mjs'),
432
+ import('./store-transcript-projection.mjs'),
433
+ ]);
333
434
  const timing = { importMs: performance.now() - importStartedAt };
334
435
  const observe = () => {
335
436
  try {
@@ -374,12 +475,32 @@ async function project({ sessionId, recordPath, itemLimit, known }) {
374
475
  const built = await pending;
375
476
  if (built === RESTART_READ) continue;
376
477
  if (built.unreadable) return { ...digest, unreadable: true };
377
- const value = await projection.windowStoredTranscript(sessionId, built.base, {
378
- itemLimit,
379
- includeMessages: false,
380
- });
478
+ // The whole display history stays here (within its budgets); only the
479
+ // requested slice crosses back.
480
+ const { history, historyBuilt } = await displayHistory(
481
+ sessionId,
482
+ built,
483
+ `${digest.textHash}:${digest.textChars}`,
484
+ fingerprint,
485
+ projection
486
+ );
487
+ const request = slice || {};
488
+ const historyStamp = historyStampOf(sessionId, digest.textHash, digest.textChars, fingerprint, request.epoch);
489
+ const proven = Boolean(request.held) && request.baseHistory === historyStamp;
490
+ const value = projection.storedHistorySlice(
491
+ history.value,
492
+ { ...request, proven },
493
+ {
494
+ digestOf: request.held && !proven ? await projection.heldRowsDigester() : null,
495
+ rowBytes: (index) => history.rowBytes[index],
496
+ }
497
+ );
498
+ const { windowStamp } = value;
499
+ delete value.windowStamp;
500
+ value.projectionStamp = `${historyStamp}/${windowStamp}`;
501
+ const bytes = Buffer.byteLength(JSON.stringify(value));
381
502
  timing.projectMs = performance.now() - projectStartedAt;
382
- return { ...digest, value, lifecycle: built.lifecycle };
503
+ return { ...digest, value, bytes, historyBuilt, lifecycle: built.lifecycle };
383
504
  } finally {
384
505
  if (!submitted) baseBuilds.release(ticket);
385
506
  }
@@ -388,6 +509,9 @@ async function project({ sessionId, recordPath, itemLimit, known }) {
388
509
 
389
510
  if (!isMainThread && workerData?.kind === WORKER_KIND) {
390
511
  serveWorkerRequests((payload) => {
391
- return payload?.forget ? baseBuilds.forget(payload.forget) : project(payload);
512
+ if (!payload?.forget) return project(payload);
513
+ historyRetention.forget(payload.forget);
514
+ historyBuilds.delete(payload.forget);
515
+ return baseBuilds.forget(payload.forget);
392
516
  });
393
517
  }
@@ -25,6 +25,9 @@ import {
25
25
  * when the last observed survivor exits, `cancel` terminates them. */
26
26
  function trackSurvivingDescendants(handle, { command, cwd, options, startedAtMs } = {}) {
27
27
  if (!handle?.taskId) return null;
28
+ // Once cancelled, every observation pass terminates survivors that ignored
29
+ // the first kill, so a retry is not needed to finish the job.
30
+ let cancelled = false;
28
31
  try {
29
32
  const task = startBackgroundTask({
30
33
  taskId: handle.taskId,
@@ -36,10 +39,10 @@ function trackSurvivingDescendants(handle, { command, cwd, options, startedAtMs
36
39
  meta: { task_id: handle.taskId, stdout: null, stderr: null, cwd, timeoutMs: 0 },
37
40
  resultType: 'shell_task_result',
38
41
  run: async () => {
39
- await waitForShellDescendants(handle);
42
+ await waitForShellDescendants(handle, { sweep: () => cancelled });
40
43
  // The folder reservation the finished command handed to its
41
44
  // survivors ends with the last of them.
42
- releaseShellJobWorkspace(handle.taskId);
45
+ await releaseShellJobWorkspace(handle.taskId);
43
46
  return {
44
47
  task_id: handle.taskId,
45
48
  status: 'completed',
@@ -48,6 +51,7 @@ function trackSurvivingDescendants(handle, { command, cwd, options, startedAtMs
48
51
  },
49
52
  // The kill's own re-observation is what `task cancel` reports.
50
53
  cancel: () => {
54
+ cancelled = true;
51
55
  task.cancelOutcome = killShellDescendants(handle);
52
56
  },
53
57
  });
@@ -9,11 +9,12 @@ import { withBuiltinPathLocks } from './path-locks.mjs';
9
9
  import { withAdvisoryLocks } from './advisory-lock.mjs';
10
10
  import { gitRepoLockKey, withGitWorktreeWriteLock } from './git-repo-rw-lock.mjs';
11
11
  import { invalidateBuiltinResultCache } from './cache-layers.mjs';
12
- import { tryAdmitWorkspaceActivity } from '../../../../shared/worktrees/activity-gate.mjs';
12
+ import { tryAdmitWorkspaceActivityAsync } from '../../../../shared/worktrees/activity-gate.mjs';
13
13
  import { WORKTREE_SNAPSHOT_KIND } from '../../../../shared/worktrees/contract.mjs';
14
14
  import { isWithin } from '../../../../shared/worktrees/paths.mjs';
15
15
  import { readRegistrySync } from '../../../../shared/worktrees/registry.mjs';
16
16
  import { runAbortable } from '../../../../shared/abort-race.mjs';
17
+ import { startGit } from '../../../../shared/git-launch.mjs';
17
18
  import { sleep } from '../../../../shared/sleep.mjs';
18
19
  import { capMarker } from '../../../../shared/tool-output-marker.mjs';
19
20
  import { drainCodeGraphCache } from '../code-graph-state.mjs';
@@ -437,7 +438,24 @@ function parseCommand(rawCommand, workDir) {
437
438
  return { command: String(command), cwd, globalArgs, operation, args };
438
439
  }
439
440
 
440
- async function runProcess(program, argv, { cwd, signal, maxBytes = MAX_CAPTURE_BYTES, optionalLocks = true } = {}) {
441
+ function gitToolEnvironment(optionalLocks) {
442
+ return {
443
+ ...process.env,
444
+ GIT_TERMINAL_PROMPT: '0',
445
+ GIT_PAGER: 'cat',
446
+ GIT_EDITOR: 'true',
447
+ GIT_SEQUENCE_EDITOR: 'true',
448
+ LC_ALL: 'C',
449
+ // Reads run beside writes without a lock (git-repo-rw-lock.mjs).
450
+ // With optional locks off `git status` never takes index.lock to
451
+ // write back a refreshed index, so it cannot fail a concurrent
452
+ // write on that lock. (`git diff` ignores the setting and may still
453
+ // refresh the index briefly; the write retry absorbs that.)
454
+ ...(optionalLocks ? {} : { GIT_OPTIONAL_LOCKS: '0' }),
455
+ };
456
+ }
457
+
458
+ async function runProcess(program, argv, { cwd, signal, maxBytes = MAX_CAPTURE_BYTES, env } = {}) {
441
459
  let child;
442
460
  try {
443
461
  await ensureNativeSpawnServer();
@@ -446,20 +464,7 @@ async function runProcess(program, argv, { cwd, signal, maxBytes = MAX_CAPTURE_B
446
464
  argv,
447
465
  spawnOptions: {
448
466
  cwd,
449
- env: {
450
- ...process.env,
451
- GIT_TERMINAL_PROMPT: '0',
452
- GIT_PAGER: 'cat',
453
- GIT_EDITOR: 'true',
454
- GIT_SEQUENCE_EDITOR: 'true',
455
- LC_ALL: 'C',
456
- // Reads run beside writes without a lock (git-repo-rw-lock.mjs).
457
- // With optional locks off `git status` never takes index.lock to
458
- // write back a refreshed index, so it cannot fail a concurrent
459
- // write on that lock. (`git diff` ignores the setting and may still
460
- // refresh the index briefly; the write retry absorbs that.)
461
- ...(optionalLocks ? {} : { GIT_OPTIONAL_LOCKS: '0' }),
462
- },
467
+ env,
463
468
  outputLimit: maxBytes,
464
469
  // Bytes as git wrote them, decoded once by the caller: a multibyte
465
470
  // character split across two pipe reads must not become U+FFFD.
@@ -573,11 +578,11 @@ function commandFailure(plan, result, limit = GIT_OUTPUT_LINES_DEFAULT) {
573
578
  }
574
579
 
575
580
  function runGit(plan, argv, options = {}) {
576
- return runProcess('git', [...plan.globalArgs, ...argv], {
577
- cwd: plan.cwd,
578
- signal: options.signal,
579
- optionalLocks: !options.readOnly,
580
- });
581
+ return startGit(
582
+ gitToolEnvironment(!options.readOnly),
583
+ (command, env) => runProcess(command, [...plan.globalArgs, ...argv], { cwd: plan.cwd, signal: options.signal, env }),
584
+ (result) => result.error
585
+ );
581
586
  }
582
587
 
583
588
  // Another process (an editor, a terminal, an IDE) holding one of git's lock
@@ -585,6 +590,10 @@ function runGit(plan, argv, options = {}) {
585
590
  // config.lock. Commands take that lock before changing anything, so a brief
586
591
  // retry with backoff turns the collision into a short wait; a command that had
587
592
  // already done part of its work reports its own state on the retry.
593
+ // Git reports lock contention only as text: lockfile.c's "Unable to create
594
+ // '<path>.lock': File exists" (and config.c's "could not lock config file"),
595
+ // stable English because runProcess pins LC_ALL=C. The delays are a bounded
596
+ // backoff (3.1 s in all), a policy choice rather than anything git reports.
588
597
  const GIT_LOCK_CONTENTION_RE =
589
598
  /Unable to create '[^'\n]*\.lock': File exists|could not lock config file [^\n]*: File exists/;
590
599
  const GIT_LOCK_RETRY_DELAYS_MS = [100, 200, 400, 800, 1600];
@@ -884,12 +893,13 @@ function snapshotContaining(repo) {
884
893
  // lease it holds (activity-gate, the same as a background shell's) keeps the
885
894
  // snapshot from being removed in any process until it ends, and a snapshot
886
895
  // whose removal is under way refuses it. Returns the release.
887
- function admitSnapshotCommand(repo, sessionId) {
896
+ async function admitSnapshotCommand(repo, sessionId) {
888
897
  const snapshot = snapshotContaining(repo);
889
898
  if (!snapshot) return () => {};
890
- const leave = tryAdmitWorkspaceActivity(sessionId || 'git-tool', repo, { kind: 'command' });
899
+ const leave = await tryAdmitWorkspaceActivityAsync(sessionId || 'git-tool', repo, { kind: 'command' });
891
900
  if (!leave) throw new Error(`the snapshot worktree ${snapshot.path} is being removed; nothing ran`);
892
- return leave;
901
+ // Fails only when workspace admission stopped (reported there), which keeps the snapshot held.
902
+ return () => leave().catch(() => {});
893
903
  }
894
904
 
895
905
  function invalidateAfterGitMutation() {
@@ -1057,14 +1067,14 @@ async function executeSingleGitTool(input, workDir, options = {}) {
1057
1067
  // under it, and a snapshot being removed refuses it.
1058
1068
  let leaveSnapshot;
1059
1069
  try {
1060
- leaveSnapshot = admitSnapshotCommand(repo, options.sessionId);
1070
+ leaveSnapshot = await admitSnapshotCommand(repo, options.sessionId);
1061
1071
  } catch (error) {
1062
1072
  return rejected(error.message);
1063
1073
  }
1064
1074
  try {
1065
1075
  return await runInRepository(input, plan, { repo, lockKey, limit, signal, workDir });
1066
1076
  } finally {
1067
- leaveSnapshot();
1077
+ await leaveSnapshot();
1068
1078
  }
1069
1079
  }
1070
1080
 
@@ -334,11 +334,9 @@ async function rawAdd(plan, input, options) {
334
334
  const txId = txIdOf(options, plan.command);
335
335
  const mapped = { mapped: plan.command, requestedPath: resolve(plan.cwd, path) };
336
336
  // A detached checkout, or a commit-ish that names no local branch: a snapshot.
337
- const snapshotCommit = detach
338
- ? (commitish ?? 'HEAD')
339
- : !branch && commitish && !(await localBranchExists(plan.cwd, commitish))
340
- ? commitish
341
- : null;
337
+ let snapshotCommit = null;
338
+ if (detach) snapshotCommit = commitish ?? 'HEAD';
339
+ else if (!branch && commitish && !(await localBranchExists(plan.cwd, commitish))) snapshotCommit = commitish;
342
340
  if (snapshotCommit) {
343
341
  const commit = await commitIn(plan.cwd, snapshotCommit);
344
342
  if (!commit) throw new Error(`git worktree add: ${snapshotCommit} is not a commit`);
@@ -27,24 +27,40 @@ import { join } from 'node:path';
27
27
  import { holdsNoData, writeJsonAtomicAsync } from '../../../../../shared/atomic-file.mjs';
28
28
  import { isPidAlive } from '../../../../../shared/pid-liveness.mjs';
29
29
  import { displayShellCommand } from '../../../../../shared/shell-display.mjs';
30
- import { tryAdmitWorkspaceActivity } from '../../../../../shared/worktrees/activity-gate.mjs';
30
+ import { tryAdmitWorkspaceActivityAsync } from '../../../../../shared/worktrees/activity-gate.mjs';
31
31
  import { worktreeError } from '../../../../../shared/worktrees/contract.mjs';
32
32
  import { workspaceSees } from '../../../../../shared/worktrees/workspace-visibility.mjs';
33
33
 
34
34
  const workspaceLeases = new Map();
35
35
 
36
+ /**
37
+ * Calls both releases in order; settles once both did. Never goes unhandled
38
+ * (an asynchronous release rejects only when workspace admission stopped).
39
+ */
40
+ export function bothReleased(first, second) {
41
+ const both = Promise.all([first(), second()]);
42
+ both.catch(() => {});
43
+ return both;
44
+ }
45
+
46
+ /**
47
+ * A workspace grant requested now, for a release handle needed before it is
48
+ * answered: the release, whenever called, follows the grant (and releases
49
+ * nothing if it was refused or failed).
50
+ */
51
+ export function pendingWorkspaceAdmission(granting) {
52
+ granting.catch(() => {});
53
+ return () =>
54
+ granting.then(
55
+ (leave) => leave?.(),
56
+ () => {}
57
+ );
58
+ }
59
+
36
60
  /** Transfer a foreground reader to a surviving-descendant task without a lease gap. */
37
61
  export function adoptShellWorkspaceAdmission(jobId, leave) {
38
62
  const prior = workspaceLeases.get(jobId);
39
- workspaceLeases.set(
40
- jobId,
41
- prior
42
- ? () => {
43
- prior();
44
- leave();
45
- }
46
- : leave
47
- );
63
+ workspaceLeases.set(jobId, prior ? () => bothReleased(prior, leave) : leave);
48
64
  }
49
65
 
50
66
  // A record whose owner process is gone is garbage. Age alone never deletes a
@@ -88,8 +104,13 @@ function validJobId(jobId) {
88
104
 
89
105
  function terminalRecord(base, detail) {
90
106
  const finishedAt = detail?.finishedAt || new Date().toISOString();
107
+ // A completed record no longer claims an unconfirmed termination: it is
108
+ // written once the processes are confirmed gone, or explicitly released.
109
+ // A completed record no longer claims an unconfirmed termination: it is
110
+ // written once the processes are confirmed gone, or explicitly released.
111
+ const { terminationUnconfirmed: _held, ...open } = base || {};
91
112
  return {
92
- ...(base || {}),
113
+ ...open,
93
114
  jobId: String(detail?.jobId || base?.jobId || ''),
94
115
  kind: 'bash',
95
116
  status: String(detail?.status || 'failed'),
@@ -172,7 +193,14 @@ async function sweepOrphanRecords(dir) {
172
193
  }
173
194
  }
174
195
 
175
- /** Publish the record for a job that has just started or been promoted. */
196
+ /**
197
+ * Publish the record for a job that has just started or been promoted. Its
198
+ * workspace lease (under the job's pid) is requested at the call and adopted
199
+ * under the job id at once, so a later release of the job — or of a
200
+ * predecessor's lease — is ordered after it. Resolves once the lease is in
201
+ * both ledger copies; rejects (and writes no record) when it is refused or
202
+ * cannot be written. The returned promise never goes unhandled.
203
+ */
176
204
  export function publishShellJobRecord(
177
205
  task,
178
206
  {
@@ -184,16 +212,20 @@ export function publishShellJobRecord(
184
212
  } = {}
185
213
  ) {
186
214
  const jobId = String(task?.jobId || '').trim();
187
- if (!jobId) return;
215
+ if (!jobId) return Promise.resolve();
216
+ let admitted = Promise.resolve();
188
217
  if (task?.cwd) {
189
- const leave = tryAdmitWorkspaceActivity(ownerSessionId || jobId, task.cwd, {
218
+ const granting = tryAdmitWorkspaceActivityAsync(ownerSessionId || jobId, task.cwd, {
190
219
  pid: Number(task.pid) || process.pid,
191
220
  kind: 'command',
192
221
  });
193
- if (!leave) throw worktreeError('WORKTREE_BUSY_JOBS', 'The command folder is being managed.');
222
+ admitted = granting.then((leave) => {
223
+ if (!leave) throw worktreeError('WORKTREE_BUSY_JOBS', 'The command folder is being managed.');
224
+ });
225
+ admitted.catch(() => {});
194
226
  // A descendant handoff can reuse the job id with a different live pid.
195
- // Acquire its reader before releasing any predecessor's admission.
196
- adoptShellWorkspaceAdmission(jobId, leave);
227
+ // Its reader is requested before any predecessor's admission is released.
228
+ adoptShellWorkspaceAdmission(jobId, pendingWorkspaceAdmission(granting));
197
229
  }
198
230
  const ownerPid = Number(clientHostPid ?? task?.clientHostPid) || process.pid;
199
231
  const dir = jobsDir();
@@ -202,6 +234,8 @@ export function publishShellJobRecord(
202
234
  paths.set(jobId, [jsonPath, ownerPath]);
203
235
  void enqueue(jobId, async () => {
204
236
  try {
237
+ // No record for a job whose workspace lease was not granted.
238
+ await admitted;
205
239
  await mkdir(dir, { recursive: true });
206
240
  if (!sweepStarted) {
207
241
  sweepStarted = true;
@@ -244,6 +278,7 @@ export function publishShellJobRecord(
244
278
  /* the record is presentation only; never fail a job for it */
245
279
  }
246
280
  });
281
+ return admitted;
247
282
  }
248
283
 
249
284
  /** Preserve a bounded terminal snapshot for manual recovery after restart. */
@@ -335,8 +370,9 @@ export function releaseShellJobWorkspace(jobId) {
335
370
  const key = String(jobId || '').trim();
336
371
  const leave = workspaceLeases.get(key);
337
372
  if (!leave) return;
338
- leave();
373
+ const released = leave();
339
374
  if (workspaceLeases.get(key) === leave) workspaceLeases.delete(key);
375
+ return released;
340
376
  }
341
377
 
342
378
  /** Flag a still-open job record as "termination unconfirmed" for the readouts. */
@@ -390,8 +426,10 @@ export function completeShellJobRecord(jobId, detail) {
390
426
  // Callers complete a record only once its processes are confirmed gone
391
427
  // (or explicitly released): the workspace admission follows that, not
392
428
  // the record write, so a failed write cannot strand it with no handle.
393
- leaveWorkspace?.();
429
+ const released = leaveWorkspace?.();
394
430
  if (workspaceLeases.get(key) === leaveWorkspace) workspaceLeases.delete(key);
431
+ // Fails only when workspace admission stopped (reported there), which keeps the folder held.
432
+ await Promise.resolve(released).catch(() => {});
395
433
  }
396
434
  await removePaths(ownerPaths);
397
435
  paths.set(key, [jsonPath, donePath]);
@@ -486,6 +524,6 @@ export function retireShellJobRecord(jobId) {
486
524
  records.delete(key);
487
525
  incarnations.delete(key);
488
526
  await removePaths(list);
489
- leaveWorkspace?.();
527
+ await leaveWorkspace?.();
490
528
  });
491
529
  }
@@ -1,6 +1,7 @@
1
1
  import { readdirSync, readFileSync, statSync } from 'node:fs';
2
2
  import { spawnSync } from 'node:child_process';
3
3
  import { readdir as readdirAsync } from 'node:fs/promises';
4
+ import { startGitSync } from '../../../../shared/git-launch.mjs';
4
5
  import { runGitOffThread } from '../../../../shared/git-runner.mjs';
5
6
  import { findPathExecutable } from '../../../../shared/path-executable.mjs';
6
7
  import { dirname as pathDirname, join as pathJoin, resolve as pathResolveAbsolute } from 'node:path';
@@ -161,13 +162,15 @@ function _changeStateArgs(root) {
161
162
 
162
163
  function _probeRepositoryChangeStateSync(root) {
163
164
  try {
164
- const result = spawnSync('git', _changeStateArgs(root), {
165
- encoding: 'utf8',
166
- env: { ...process.env, GIT_OPTIONAL_LOCKS: '0' },
167
- maxBuffer: 64 * 1024,
168
- timeout: 1500,
169
- windowsHide: true,
170
- });
165
+ const result = startGitSync({ ...process.env, GIT_OPTIONAL_LOCKS: '0' }, (command, env) =>
166
+ spawnSync(command, _changeStateArgs(root), {
167
+ encoding: 'utf8',
168
+ env,
169
+ maxBuffer: 64 * 1024,
170
+ timeout: 1500,
171
+ windowsHide: true,
172
+ })
173
+ );
171
174
  const output = String(result.stdout || '').trim();
172
175
  if (result.status === 0) return output ? 'changes present' : 'clean';
173
176
  if (result.error?.code === 'ENOBUFS' && output) return 'changes present';
@@ -271,14 +274,16 @@ function _checkIgnoreArgs(directory) {
271
274
 
272
275
  function _probeGitIgnoredEntriesSync(directory, names) {
273
276
  try {
274
- const result = spawnSync('git', _checkIgnoreArgs(directory), {
275
- encoding: 'utf8',
276
- input: `${names.join('\0')}\0`,
277
- env: { ...process.env, GIT_OPTIONAL_LOCKS: '0' },
278
- maxBuffer: 1024 * 1024,
279
- timeout: 1500,
280
- windowsHide: true,
281
- });
277
+ const result = startGitSync({ ...process.env, GIT_OPTIONAL_LOCKS: '0' }, (command, env) =>
278
+ spawnSync(command, _checkIgnoreArgs(directory), {
279
+ encoding: 'utf8',
280
+ input: `${names.join('\0')}\0`,
281
+ env,
282
+ maxBuffer: 1024 * 1024,
283
+ timeout: 1500,
284
+ windowsHide: true,
285
+ })
286
+ );
282
287
  // Exit 1 is "nothing ignored", not a failure.
283
288
  if (result.status !== 0 && result.status !== 1) return new Set();
284
289
  return new Set(