@selesai/code 0.13.15 → 0.13.18

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 (473) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +2 -14
  3. package/dist/bun/cli.d.ts +3 -1
  4. package/dist/bun/cli.js +3 -9
  5. package/dist/bun/{register-bedrock.js → runtime-setup.js} +5 -0
  6. package/dist/bun/sandbox-env-setup.d.ts +1 -0
  7. package/dist/bun/sandbox-env-setup.js +3 -0
  8. package/dist/cli/auth-command.d.ts +23 -0
  9. package/dist/cli/auth-command.js +102 -0
  10. package/dist/cli/config-selector.js +2 -1
  11. package/dist/cli/setup.d.ts +1 -0
  12. package/dist/cli/setup.js +11 -0
  13. package/dist/cli.js +2 -14
  14. package/dist/config.d.ts +3 -2
  15. package/dist/config.js +9 -4
  16. package/dist/core/agent-session-runtime.js +15 -7
  17. package/dist/core/agent-session.d.ts +2 -6
  18. package/dist/core/agent-session.js +15 -4
  19. package/dist/core/compaction/branch-summarization.d.ts +1 -1
  20. package/dist/core/compaction/branch-summarization.js +2 -1
  21. package/dist/core/extensions/loader.js +3 -0
  22. package/dist/core/extensions/types.d.ts +16 -2
  23. package/dist/core/http-dispatcher.js +2 -0
  24. package/dist/core/keybindings.d.ts +5 -0
  25. package/dist/core/keybindings.js +4 -0
  26. package/dist/core/messages.d.ts +1 -1
  27. package/dist/core/model-config.d.ts +15 -0
  28. package/dist/core/model-config.js +3 -0
  29. package/dist/core/model-runtime.d.ts +1 -0
  30. package/dist/core/model-runtime.js +5 -2
  31. package/dist/core/provider-composer.js +8 -2
  32. package/dist/core/session-export.d.ts +3 -0
  33. package/dist/core/session-export.js +31 -0
  34. package/dist/core/session-manager.d.ts +3 -2
  35. package/dist/core/session-manager.js +40 -13
  36. package/dist/core/skills.d.ts +1 -1
  37. package/dist/core/skills.js +4 -2
  38. package/dist/core/system-prompt.js +10 -10
  39. package/dist/core/tools/bash.js +3 -117
  40. package/dist/core/tools/edit.d.ts +1 -12
  41. package/dist/core/tools/edit.js +5 -163
  42. package/dist/core/tools/find.js +4 -55
  43. package/dist/core/tools/grep.js +4 -60
  44. package/dist/core/tools/ls.js +4 -49
  45. package/dist/core/tools/read.d.ts +4 -0
  46. package/dist/core/tools/read.js +10 -122
  47. package/dist/core/tools/renderers/bash.d.ts +11 -0
  48. package/dist/core/tools/renderers/bash.js +129 -0
  49. package/dist/core/tools/renderers/edit.d.ts +22 -0
  50. package/dist/core/tools/renderers/edit.js +166 -0
  51. package/dist/core/tools/renderers/find.d.ts +9 -0
  52. package/dist/core/tools/renderers/find.js +63 -0
  53. package/dist/core/tools/renderers/grep.d.ts +9 -0
  54. package/dist/core/tools/renderers/grep.js +68 -0
  55. package/dist/core/tools/renderers/index.d.ts +27 -0
  56. package/dist/core/tools/renderers/index.js +46 -0
  57. package/dist/core/tools/renderers/ls.d.ts +9 -0
  58. package/dist/core/tools/renderers/ls.js +57 -0
  59. package/dist/core/tools/renderers/read.d.ts +10 -0
  60. package/dist/core/tools/renderers/read.js +128 -0
  61. package/dist/core/tools/renderers/write.d.ts +9 -0
  62. package/dist/core/tools/renderers/write.js +152 -0
  63. package/dist/core/tools/write.js +5 -146
  64. package/dist/extensions/capability-gateway/index.ts +12 -1
  65. package/dist/extensions/capability-gateway/integration.test.ts +23 -0
  66. package/dist/extensions/pi-hermes-memory/CHANGELOG.md +18 -0
  67. package/dist/extensions/pi-hermes-memory/README.md +16 -12
  68. package/dist/extensions/pi-hermes-memory/docs/ROADMAP.md +1 -1
  69. package/dist/extensions/pi-hermes-memory/package-lock.json +587 -62
  70. package/dist/extensions/pi-hermes-memory/package.json +4 -4
  71. package/dist/extensions/pi-hermes-memory/src/config.ts +32 -0
  72. package/dist/extensions/pi-hermes-memory/src/constants.ts +12 -2
  73. package/dist/extensions/pi-hermes-memory/src/handlers/background-review.ts +109 -51
  74. package/dist/extensions/pi-hermes-memory/src/handlers/index-sessions.ts +9 -3
  75. package/dist/extensions/pi-hermes-memory/src/handlers/review-memory-ops.ts +257 -98
  76. package/dist/extensions/pi-hermes-memory/src/handlers/session-backfill.ts +9 -2
  77. package/dist/extensions/pi-hermes-memory/src/handlers/sync-markdown-memories.ts +5 -0
  78. package/dist/extensions/pi-hermes-memory/src/index.ts +22 -2
  79. package/dist/extensions/pi-hermes-memory/src/paths.ts +1 -1
  80. package/dist/extensions/pi-hermes-memory/src/store/db.ts +6 -0
  81. package/dist/extensions/pi-hermes-memory/src/store/fts-query.ts +58 -1
  82. package/dist/extensions/pi-hermes-memory/src/store/memory-store.ts +60 -8
  83. package/dist/extensions/pi-hermes-memory/src/store/session-indexer.ts +169 -7
  84. package/dist/extensions/pi-hermes-memory/src/store/session-search.ts +7 -21
  85. package/dist/extensions/pi-hermes-memory/src/store/sqlite-memory-store.ts +134 -15
  86. package/dist/extensions/pi-hermes-memory/src/tools/memory-search-tool.ts +4 -2
  87. package/dist/extensions/pi-hermes-memory/src/tools/memory-tool.ts +24 -9
  88. package/dist/extensions/pi-hermes-memory/src/types.ts +11 -0
  89. package/dist/extensions/pi-hermes-memory/tests/config.test.ts +42 -0
  90. package/dist/extensions/pi-hermes-memory/tests/handlers/background-review.test.ts +159 -3
  91. package/dist/extensions/pi-hermes-memory/tests/handlers/review-memory-ops.test.ts +452 -0
  92. package/dist/extensions/pi-hermes-memory/tests/handlers/session-backfill.test.ts +39 -0
  93. package/dist/extensions/pi-hermes-memory/tests/integration/corruption-recovery-integration.test.ts +91 -0
  94. package/dist/extensions/pi-hermes-memory/tests/store/db.test.ts +29 -0
  95. package/dist/extensions/pi-hermes-memory/tests/store/fts-query.test.ts +116 -0
  96. package/dist/extensions/pi-hermes-memory/tests/store/memory-store.test.ts +163 -0
  97. package/dist/extensions/pi-hermes-memory/tests/store/session-indexer.test.ts +233 -0
  98. package/dist/extensions/pi-hermes-memory/tests/store/session-search.test.ts +12 -0
  99. package/dist/extensions/pi-hermes-memory/tests/store/sqlite-memory-store.test.ts +175 -0
  100. package/dist/extensions/pi-hermes-memory/tests/store/sqlite-native.test.ts +2 -2
  101. package/dist/extensions/pi-hermes-memory/tests/tools/memory-search-tool.test.ts +22 -0
  102. package/dist/extensions/pi-hermes-memory/tests/tools/memory-tool.test.ts +37 -1
  103. package/dist/extensions/pi-intercom/CHANGELOG.md +13 -0
  104. package/dist/extensions/pi-intercom/README.md +16 -2
  105. package/dist/extensions/pi-intercom/broker/spawn.test.ts +19 -2
  106. package/dist/extensions/pi-intercom/broker/spawn.ts +4 -4
  107. package/dist/extensions/pi-intercom/index.ts +1 -1
  108. package/dist/extensions/pi-intercom/intercom.integration.test.ts +121 -2
  109. package/dist/extensions/pi-intercom/package.json +1 -1
  110. package/dist/extensions/pi-intercom/project-agent.test.ts +1 -1
  111. package/dist/extensions/pi-intercom/project-agent.ts +1 -6
  112. package/dist/extensions/pi-intercom/skills/pi-intercom/SKILL.md +7 -6
  113. package/dist/extensions/pi-rewind-hook/index.test.ts +7 -0
  114. package/dist/extensions/pi-subagents/.github/workflows/test.yml +36 -4
  115. package/dist/extensions/pi-subagents/CHANGELOG.md +120 -0
  116. package/dist/extensions/pi-subagents/README.md +3 -3
  117. package/dist/extensions/pi-subagents/agents/researcher.md +23 -13
  118. package/dist/extensions/pi-subagents/agents/reviewer.md +1 -1
  119. package/dist/extensions/pi-subagents/agents/scout.md +1 -1
  120. package/dist/extensions/pi-subagents/docs/agents.md +32 -16
  121. package/dist/extensions/pi-subagents/docs/configuration.md +41 -15
  122. package/dist/extensions/pi-subagents/docs/extension-api.md +109 -6
  123. package/dist/extensions/pi-subagents/docs/missions.md +2 -0
  124. package/dist/extensions/pi-subagents/docs/models.md +58 -1
  125. package/dist/extensions/pi-subagents/docs/observability.md +50 -12
  126. package/dist/extensions/pi-subagents/docs/tool-reference.md +17 -10
  127. package/dist/extensions/pi-subagents/docs/watchdog.md +1 -1
  128. package/dist/extensions/pi-subagents/docs/workflows.md +26 -9
  129. package/dist/extensions/pi-subagents/package-lock.json +747 -177
  130. package/dist/extensions/pi-subagents/package.json +6 -4
  131. package/dist/extensions/pi-subagents/runner-server-preload.mjs +13 -0
  132. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +2 -1
  133. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  134. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +21 -4
  135. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
  136. package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
  137. package/dist/extensions/pi-subagents/src/agents/advertised-agent-prompt.ts +63 -0
  138. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +61 -16
  139. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
  140. package/dist/extensions/pi-subagents/src/agents/agents.ts +8 -0
  141. package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +0 -1
  142. package/dist/extensions/pi-subagents/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
  143. package/dist/extensions/pi-subagents/src/api/preflight.ts +7 -4
  144. package/dist/extensions/pi-subagents/src/api/shared-types.ts +2 -2
  145. package/dist/extensions/pi-subagents/src/api/workflow-resources.ts +6 -0
  146. package/dist/extensions/pi-subagents/src/extension/config.ts +4 -2
  147. package/dist/extensions/pi-subagents/src/extension/doctor.ts +2 -10
  148. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +9 -11
  149. package/dist/extensions/pi-subagents/src/extension/index.ts +99 -10
  150. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +13 -0
  151. package/dist/extensions/pi-subagents/src/extension/rpc.ts +7 -23
  152. package/dist/extensions/pi-subagents/src/extension/schemas.ts +11 -8
  153. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +25 -7
  154. package/dist/extensions/pi-subagents/src/integrations/pi-web-session-liveness.ts +73 -0
  155. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +228 -136
  156. package/dist/extensions/pi-subagents/src/intercom/supervisor-ui.ts +244 -0
  157. package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +37 -16
  158. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +18 -18
  159. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +51 -25
  160. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +46 -3
  161. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -3
  162. package/dist/extensions/pi-subagents/src/runs/background/async-retention.ts +9 -0
  163. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +10 -12
  164. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +18 -15
  165. package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +40 -29
  166. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +8 -0
  167. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +79 -247
  168. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +88 -12
  169. package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +6 -6
  170. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +24 -24
  171. package/dist/extensions/pi-subagents/src/runs/background/retained-nested-route-tracker.ts +96 -0
  172. package/dist/extensions/pi-subagents/src/runs/background/run-child-session.ts +642 -0
  173. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +75 -6
  174. package/dist/extensions/pi-subagents/src/runs/background/runner-aliases.ts +163 -0
  175. package/dist/extensions/pi-subagents/src/runs/background/runner-child-launch.ts +86 -0
  176. package/dist/extensions/pi-subagents/src/runs/background/runner-child-sessions.ts +31 -0
  177. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +18 -4
  178. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +3 -1
  179. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +597 -1092
  180. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +3 -0
  181. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +4 -0
  182. package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +20 -17
  183. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +296 -394
  184. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +4 -0
  185. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +3 -1
  186. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +9 -5
  187. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +630 -283
  188. package/dist/extensions/pi-subagents/src/runs/foreground/workflow-detach-reconcile.ts +8 -5
  189. package/dist/extensions/pi-subagents/src/runs/foreground/workflow-foreground-steering.ts +78 -98
  190. package/dist/extensions/pi-subagents/src/runs/shared/abort-recovery.ts +3 -3
  191. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +16 -3
  192. package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +1 -1
  193. package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +47 -47
  194. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +1 -2
  195. package/dist/extensions/pi-subagents/src/runs/shared/child-hooks.ts +174 -0
  196. package/dist/extensions/pi-subagents/src/runs/shared/child-identity.ts +14 -3
  197. package/dist/extensions/pi-subagents/src/runs/shared/child-launch.ts +319 -0
  198. package/dist/extensions/pi-subagents/src/runs/shared/child-lifecycle.ts +25 -0
  199. package/dist/extensions/pi-subagents/src/runs/shared/child-runtime-config.ts +126 -0
  200. package/dist/extensions/pi-subagents/src/runs/shared/child-session.ts +373 -0
  201. package/dist/extensions/pi-subagents/src/runs/shared/child-tool-plan.ts +530 -0
  202. package/dist/extensions/pi-subagents/src/runs/shared/completion-evidence.ts +2 -2
  203. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +2 -1
  204. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-preflight.ts +16 -0
  205. package/dist/extensions/pi-subagents/src/runs/shared/host-step-status.ts +11 -11
  206. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +30 -20
  207. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
  208. package/dist/extensions/pi-subagents/src/runs/shared/model-exclusions.ts +84 -15
  209. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +88 -12
  210. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +37 -53
  211. package/dist/extensions/pi-subagents/src/runs/shared/nested-path.ts +1 -1
  212. package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +18 -7
  213. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +57 -12
  214. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +3 -4
  215. package/dist/extensions/pi-subagents/src/runs/shared/permissions.ts +0 -11
  216. package/dist/extensions/pi-subagents/src/runs/shared/process-signal.ts +4 -1
  217. package/dist/extensions/pi-subagents/src/runs/shared/readonly-drain-observation.ts +42 -0
  218. package/dist/extensions/pi-subagents/src/runs/shared/readonly-model-continuation.ts +69 -0
  219. package/dist/extensions/pi-subagents/src/runs/shared/readonly-session-evidence.ts +307 -0
  220. package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +8 -21
  221. package/dist/extensions/pi-subagents/src/runs/shared/runtime-acknowledged-extensions.ts +3 -30
  222. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +17 -4
  223. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +2 -0
  224. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +99 -386
  225. package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +18 -62
  226. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +0 -14
  227. package/dist/extensions/pi-subagents/src/runs/shared/worktree-cleanup-plan.ts +31 -9
  228. package/dist/extensions/pi-subagents/src/runs/shared/worktree-setup-command.ts +190 -0
  229. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +506 -226
  230. package/dist/extensions/pi-subagents/src/shared/child-session-name.ts +1 -1
  231. package/dist/extensions/pi-subagents/src/shared/jsonl-writer.ts +11 -0
  232. package/dist/extensions/pi-subagents/src/shared/model-response-aliases.ts +13 -0
  233. package/dist/extensions/pi-subagents/src/shared/thinking-ceiling.ts +0 -6
  234. package/dist/extensions/pi-subagents/src/shared/types.ts +145 -89
  235. package/dist/extensions/pi-subagents/src/shared/utils.ts +13 -6
  236. package/dist/extensions/pi-subagents/src/shared/watch-strategy.ts +2 -0
  237. package/dist/extensions/pi-subagents/src/shared/workflow-child-permit.ts +18 -13
  238. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +0 -6
  239. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +1 -1
  240. package/dist/extensions/pi-subagents/src/tui/fleet.ts +11 -6
  241. package/dist/extensions/pi-subagents/src/tui/render.ts +277 -46
  242. package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +0 -1
  243. package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +12 -12
  244. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +3 -3
  245. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +116 -15
  246. package/dist/extensions/pi-subagents/src/workflows/workflow-checklist.ts +15 -18
  247. package/dist/extensions/pi-subagents/src/workflows/workflow-child-summary.ts +57 -8
  248. package/dist/extensions/pi-subagents/src/workflows/workflow-preflight.ts +19 -19
  249. package/dist/extensions/pi-subagents/src/workflows/workflow-receipt.ts +3 -3
  250. package/dist/extensions/pi-subagents/src/workflows/workflow-resources.ts +97 -22
  251. package/dist/extensions/pi-subagents/src/workflows/workflow-settlement.ts +3 -0
  252. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +336 -21
  253. package/dist/extensions/pi-subagents/test/integration/async-execution.part-1.test.ts +1181 -0
  254. package/dist/extensions/pi-subagents/test/integration/async-execution.part-2.test.ts +2240 -0
  255. package/dist/extensions/pi-subagents/test/integration/async-execution.part-3.test.ts +2268 -0
  256. package/dist/extensions/pi-subagents/test/integration/async-execution.part-4.test.ts +1512 -0
  257. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +231 -16
  258. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +4 -18
  259. package/dist/extensions/pi-subagents/test/integration/foreign-workflow-steering.test.ts +95 -0
  260. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +9 -15
  261. package/dist/extensions/pi-subagents/test/integration/in-process-child.test.ts +374 -0
  262. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +20 -10
  263. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +18 -6
  264. package/dist/extensions/pi-subagents/test/integration/recovered-assistant-error.test.ts +151 -0
  265. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +42 -1
  266. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +150 -25
  267. package/dist/extensions/pi-subagents/test/integration/result-publication.test.ts +354 -0
  268. package/dist/extensions/pi-subagents/test/integration/shared-cwd-terminal-evidence.test.ts +117 -0
  269. package/dist/extensions/pi-subagents/test/integration/single-execution.part-1.test.ts +3678 -0
  270. package/dist/extensions/pi-subagents/test/integration/{single-execution.test.ts → single-execution.part-2.test.ts} +500 -4207
  271. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1 -2
  272. package/dist/extensions/pi-subagents/test/integration/workflow-result-publication.test.ts +127 -0
  273. package/dist/extensions/pi-subagents/test/integration/workflow-steer-inbox.test.ts +365 -0
  274. package/dist/extensions/pi-subagents/test/smoke/pi085-child.ts +31 -0
  275. package/dist/extensions/pi-subagents/test/smoke/pi085-clean-install.mjs +74 -0
  276. package/dist/extensions/pi-subagents/test/smoke/pi085-extension.ts +16 -0
  277. package/dist/extensions/pi-subagents/test/support/async-execution-fixture.ts +699 -0
  278. package/dist/extensions/pi-subagents/test/support/fake-child-session.ts +418 -0
  279. package/dist/extensions/pi-subagents/test/support/foreign-workflow-steer-host.mjs +13 -0
  280. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +30 -76
  281. package/dist/extensions/pi-subagents/test/support/notify-diagnostics-fixture.ts +84 -0
  282. package/dist/extensions/pi-subagents/test/support/result-publication-capacity-preload.mjs +70 -0
  283. package/dist/extensions/pi-subagents/test/support/runner-child-session-factory.ts +17 -0
  284. package/dist/extensions/pi-subagents/test/support/single-execution-fixture.ts +390 -0
  285. package/dist/extensions/pi-subagents/test/unit/acceptance-compaction.test.ts +120 -0
  286. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +24 -3
  287. package/dist/extensions/pi-subagents/test/unit/advertised-agent-prompt.test.ts +87 -0
  288. package/dist/extensions/pi-subagents/test/unit/advertised-agent-refresh.test.ts +163 -0
  289. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +70 -31
  290. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +78 -3
  291. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +2 -8
  292. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +1 -1
  293. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +73 -66
  294. package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +51 -1
  295. package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +49 -1
  296. package/dist/extensions/pi-subagents/test/unit/async-spawn-preload.test.ts +103 -0
  297. package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +3 -3
  298. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +9 -8
  299. package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +10 -7
  300. package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +48 -6
  301. package/dist/extensions/pi-subagents/test/unit/child-runtime-config.test.ts +90 -0
  302. package/dist/extensions/pi-subagents/test/unit/{pi-args-permission-system.test.ts → child-tool-plan-permission-system.test.ts} +48 -86
  303. package/dist/extensions/pi-subagents/test/unit/child-tool-plan.test.ts +33 -0
  304. package/dist/extensions/pi-subagents/test/unit/compaction-resume.test.ts +18 -18
  305. package/dist/extensions/pi-subagents/test/unit/completion-evidence.test.ts +9 -9
  306. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +8 -7
  307. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +225 -65
  308. package/dist/extensions/pi-subagents/test/unit/extension-bindings.test.ts +17 -22
  309. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +11 -1
  310. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +47 -3
  311. package/dist/extensions/pi-subagents/test/unit/foreign-workflow-steering.test.ts +74 -0
  312. package/dist/extensions/pi-subagents/test/unit/fork-cache-key.test.ts +23 -29
  313. package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +96 -4
  314. package/dist/extensions/pi-subagents/test/unit/host-step-status.test.ts +4 -4
  315. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +116 -55
  316. package/dist/extensions/pi-subagents/test/unit/index-segment.test.ts +58 -1
  317. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +104 -3
  318. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +10 -1
  319. package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +31 -1
  320. package/dist/extensions/pi-subagents/test/unit/model-exclusions.test.ts +102 -51
  321. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +196 -6
  322. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +181 -38
  323. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +62 -70
  324. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +58 -53
  325. package/dist/extensions/pi-subagents/test/unit/notify-diagnostics.test.ts +42 -0
  326. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +84 -0
  327. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +13 -5
  328. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +21 -5
  329. package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +5 -4
  330. package/dist/extensions/pi-subagents/test/unit/permissions.test.ts +2 -6
  331. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +28 -2
  332. package/dist/extensions/pi-subagents/test/unit/pi-web-session-liveness.test.ts +209 -0
  333. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +38 -1
  334. package/dist/extensions/pi-subagents/test/unit/prompt-audit-optional-peers.test.ts +43 -0
  335. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +24 -2
  336. package/dist/extensions/pi-subagents/test/unit/readonly-drain-observation.test.ts +174 -0
  337. package/dist/extensions/pi-subagents/test/unit/readonly-model-continuation.test.ts +94 -0
  338. package/dist/extensions/pi-subagents/test/unit/readonly-session-evidence.test.ts +1329 -0
  339. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +29 -80
  340. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +11 -8
  341. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +2 -2
  342. package/dist/extensions/pi-subagents/test/unit/retained-nested-route-tracker.test.ts +175 -0
  343. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +38 -12
  344. package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +2 -5
  345. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +89 -1
  346. package/dist/extensions/pi-subagents/test/unit/runtime-acknowledged-extensions.test.ts +2 -16
  347. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +27 -3
  348. package/dist/extensions/pi-subagents/test/unit/scheduled-supervisor-demand.test.ts +215 -0
  349. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +15 -11
  350. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +237 -1
  351. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +2 -5
  352. package/dist/extensions/pi-subagents/test/unit/streamed-progress-bounds.test.ts +2 -2
  353. package/dist/extensions/pi-subagents/test/unit/subagent-guide.test.ts +14 -1
  354. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +130 -739
  355. package/dist/extensions/pi-subagents/test/unit/supervisor-ask-registration.test.ts +682 -0
  356. package/dist/extensions/pi-subagents/test/unit/supervisor-ui.test.ts +137 -0
  357. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +1 -11
  358. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +23 -3
  359. package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +8 -1
  360. package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +19 -1
  361. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +72 -4
  362. package/dist/extensions/pi-subagents/test/unit/workflow-checklist.test.ts +17 -3
  363. package/dist/extensions/pi-subagents/test/unit/workflow-child-summary.test.ts +34 -0
  364. package/dist/extensions/pi-subagents/test/unit/workflow-detach-reconcile.test.ts +12 -2
  365. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +46 -6
  366. package/dist/extensions/pi-subagents/test/unit/workflow-receipt.test.ts +3 -3
  367. package/dist/extensions/pi-subagents/test/unit/workflow-resources.test.ts +133 -1
  368. package/dist/extensions/pi-subagents/test/unit/workflow-resume-hint.test.ts +53 -2
  369. package/dist/extensions/pi-subagents/test/unit/worktree-cleanup-plan.test.ts +222 -14
  370. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +526 -85
  371. package/dist/extensions/pi-web-agent/src/backends/config.ts +1 -0
  372. package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +5 -3
  373. package/dist/extensions/pi-web-agent/src/search/tokenin.ts +7 -3
  374. package/dist/extensions/pi-web-agent/src/types.ts +1 -1
  375. package/dist/extensions/pi-zentui/README.md +17 -0
  376. package/dist/extensions/pi-zentui/assets/screenshots/footer-status-hyperlink.png +0 -0
  377. package/dist/extensions/pi-zentui/docs/configuration.md +71 -2
  378. package/dist/extensions/pi-zentui/extensions/zentui/config.ts +11 -0
  379. package/dist/extensions/pi-zentui/extensions/zentui/editor-transfer.ts +14 -0
  380. package/dist/extensions/pi-zentui/extensions/zentui/extension-status.ts +29 -13
  381. package/dist/extensions/pi-zentui/extensions/zentui/footer-layout.ts +5 -4
  382. package/dist/extensions/pi-zentui/extensions/zentui/footer-text.ts +11 -0
  383. package/dist/extensions/pi-zentui/extensions/zentui/footer.ts +11 -10
  384. package/dist/extensions/pi-zentui/extensions/zentui/index.ts +77 -3
  385. package/dist/extensions/pi-zentui/extensions/zentui/presets.ts +77 -0
  386. package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +103 -2
  387. package/dist/extensions/pi-zentui/extensions/zentui/working-line-extension-segments.ts +115 -0
  388. package/dist/extensions/pi-zentui/extensions/zentui/working-line.ts +87 -5
  389. package/dist/extensions/pi-zentui/package-lock.json +277 -517
  390. package/dist/extensions/pi-zentui/package.json +7 -4
  391. package/dist/extensions/pi-zentui/test/config-presets.test.ts +238 -0
  392. package/dist/extensions/pi-zentui/test/editor-transfer.test.ts +24 -1
  393. package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +570 -2
  394. package/dist/extensions/pi-zentui/test/extension-status.test.ts +90 -0
  395. package/dist/extensions/pi-zentui/test/footer-hyperlink.test.ts +243 -0
  396. package/dist/extensions/pi-zentui/test/package-contents.mjs +1 -0
  397. package/dist/extensions/pi-zentui/test/presets.test.ts +121 -0
  398. package/dist/extensions/pi-zentui/test/settings-command.test.ts +205 -1
  399. package/dist/extensions/pi-zentui/test/working-line-extension-segments.test.ts +139 -0
  400. package/dist/extensions/pi-zentui/test/working-line-lifecycle.integration.test.ts +237 -3
  401. package/dist/extensions/pi-zentui/test/working-line.test.ts +94 -2
  402. package/dist/index.d.ts +1 -1
  403. package/dist/main.js +4 -1
  404. package/dist/modes/interactive/chat-viewport.d.ts +19 -0
  405. package/dist/modes/interactive/chat-viewport.js +27 -0
  406. package/dist/modes/interactive/components/assistant-message.d.ts +6 -2
  407. package/dist/modes/interactive/components/assistant-message.js +30 -13
  408. package/dist/modes/interactive/components/custom-editor.d.ts +10 -1
  409. package/dist/modes/interactive/components/custom-editor.js +39 -1
  410. package/dist/modes/interactive/components/first-time-setup.js +1 -1
  411. package/dist/modes/interactive/components/index.d.ts +1 -1
  412. package/dist/modes/interactive/components/markdown-transform.d.ts +2 -0
  413. package/dist/modes/interactive/components/markdown-transform.js +18 -0
  414. package/dist/modes/interactive/components/model-selector.js +19 -17
  415. package/dist/modes/interactive/components/scoped-models-selector.js +21 -23
  416. package/dist/modes/interactive/components/settings-selector.d.ts +7 -1
  417. package/dist/modes/interactive/components/settings-selector.js +128 -20
  418. package/dist/modes/interactive/components/settings-submenu.d.ts +71 -0
  419. package/dist/modes/interactive/components/settings-submenu.js +164 -0
  420. package/dist/modes/interactive/components/status-indicator.d.ts +3 -1
  421. package/dist/modes/interactive/components/status-indicator.js +10 -3
  422. package/dist/modes/interactive/components/thinking-selector.d.ts +15 -3
  423. package/dist/modes/interactive/components/thinking-selector.js +75 -18
  424. package/dist/modes/interactive/components/tool-execution.d.ts +21 -4
  425. package/dist/modes/interactive/components/tool-execution.js +35 -33
  426. package/dist/modes/interactive/components/trust-selector.js +2 -2
  427. package/dist/modes/interactive/interactive-mode.d.ts +13 -15
  428. package/dist/modes/interactive/interactive-mode.js +144 -80
  429. package/dist/modes/interactive/session-share.d.ts +15 -0
  430. package/dist/modes/interactive/session-share.js +183 -0
  431. package/dist/modes/interactive/theme/dark.json +2 -1
  432. package/dist/modes/interactive/theme/light.json +2 -1
  433. package/dist/modes/interactive/theme/theme-controller.d.ts +1 -0
  434. package/dist/modes/interactive/theme/theme-controller.js +5 -0
  435. package/dist/modes/interactive/theme/theme-json.d.ts +83 -0
  436. package/dist/modes/interactive/theme/theme-json.js +129 -0
  437. package/dist/modes/interactive/theme/theme-schema.json +9 -1
  438. package/dist/modes/interactive/theme/theme.d.ts +14 -5
  439. package/dist/modes/interactive/theme/theme.js +18 -119
  440. package/dist/modes/interactive/tui-renderer.d.ts +20 -0
  441. package/dist/modes/interactive/tui-renderer.js +65 -0
  442. package/dist/utils/exif-orientation.js +2 -3
  443. package/dist/utils/management-http.d.ts +24 -0
  444. package/dist/utils/management-http.js +53 -0
  445. package/dist/utils/text.d.ts +7 -0
  446. package/dist/utils/text.js +8 -0
  447. package/dist/utils/tools-manager.d.ts +1 -0
  448. package/dist/utils/tools-manager.js +43 -21
  449. package/docs/docs.json +0 -4
  450. package/package.json +4 -4
  451. package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +0 -415
  452. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +0 -1059
  453. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +0 -116
  454. package/dist/extensions/pi-subagents/src/shared/post-exit-stdio-guard.ts +0 -85
  455. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +0 -224
  456. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +0 -6986
  457. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +0 -447
  458. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +0 -185
  459. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +0 -311
  460. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +0 -84
  461. package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +0 -202
  462. package/dist/extensions/pi-subagents/test/unit/close-grace-timer.test.ts +0 -101
  463. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +0 -2347
  464. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +0 -104
  465. package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +0 -49
  466. package/dist/skills/imagegen-frontend-mobile/SKILL.md +0 -1465
  467. package/dist/skills/imagegen-frontend-web/SKILL.md +0 -987
  468. package/dist/skills/implanger/SKILL.md +0 -70
  469. package/dist/skills/output/SKILL.md +0 -49
  470. package/dist/skills/planger/SKILL.md +0 -167
  471. package/dist/skills/workflow/SKILL.md +0 -95
  472. package/docs/workflows.md +0 -18
  473. /package/dist/bun/{register-bedrock.d.ts → runtime-setup.d.ts} +0 -0
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { spawnSync } from "node:child_process";
3
+ import { runSetupCommand, type SetupCommandOptions, type SetupCommandResult } from "./worktree-setup-command.ts";
3
4
  import * as fs from "node:fs";
4
5
  import * as os from "node:os";
5
6
  import * as path from "node:path";
@@ -9,6 +10,7 @@ import { getAgentDir } from "../../shared/utils.ts";
9
10
  import type { ManagedWorktreeProvider, WorktreeNaming, WorktreeProvider } from "../../shared/types.ts";
10
11
 
11
12
  export const DEFAULT_WORKTREE_PROVIDER: WorktreeProvider = "auto";
13
+ export const DEFAULT_WORKTREE_BASE_REF = "HEAD";
12
14
  export const DEFAULT_WORKTREE_BRANCH_PREFIX = "pi-subagents/";
13
15
  /** Internal marker used to defer Worktrunk-dependent instruction paths to launch time. */
14
16
  export const WORKTREE_AGENT_CWD_PLACEHOLDER = path.join(path.parse(process.cwd()).root, "__pi_subagents_worktree_cwd__");
@@ -16,6 +18,9 @@ const WORKTREE_NAMING_COMPONENT_MAX_BYTES = 96;
16
18
  const WORKTREE_NAMING_LABEL_MAX_BYTES = 256;
17
19
  const WORKTREE_NAMING_BRANCH_MAX_BYTES = 256;
18
20
  const WORKTREE_COMMAND_OUTPUT_MAX_BYTES = 128 * 1024;
21
+ export const MACHINE_DIFF_OPTIONS = ["--no-color", "--no-ext-diff", "--no-textconv", "--default-prefix", "--line-prefix=", "--no-relative"] as const;
22
+ const MACHINE_PATCH_OPTIONS = [...MACHINE_DIFF_OPTIONS, "--binary"] as const;
23
+ const PATCH_VALIDATION_OPTIONS = ["apply", "--check", "--cached", "--reverse", "--binary", "--whitespace=nowarn"] as const;
19
24
 
20
25
  export interface WorktreeNamingInput {
21
26
  runId: string;
@@ -114,6 +119,10 @@ interface WorktreeSetupHookConfig {
114
119
  }
115
120
 
116
121
  export interface CreateWorktreesOptions {
122
+ signal?: AbortSignal;
123
+ /** Existing absolute run deadline only; setup does not start a child budget. */
124
+ deadlineAt?: number;
125
+ onProgress?: (snapshot: WorktreeSetupProgress) => void;
117
126
  agents?: string[];
118
127
  /** Optional stable labels used to make branch identity readable. */
119
128
  labels?: Array<string | undefined>;
@@ -121,12 +130,12 @@ export interface CreateWorktreesOptions {
121
130
  tasks?: Array<string | undefined>;
122
131
  /** Worktree allocator selection; auto prefers Worktrunk when available. */
123
132
  provider?: WorktreeProvider;
133
+ /** Git ref used as the worktree base; defaults to `HEAD`. */
134
+ baseRef?: string;
124
135
  /** Branch namespace; defaults to `pi-subagents/`. */
125
136
  branchPrefix?: string;
126
137
  setupHook?: WorktreeSetupHookConfig;
127
138
  baseDir?: string;
128
- /** Called with deterministic ownership metadata before setup hooks and child launch; native reports planned paths before allocation, while Worktrunk reports its returned paths after allocation. */
129
- beforeCreate?: (setup: WorktreeSetup) => void;
130
139
  }
131
140
 
132
141
  interface ResolvedWorktreeSetupHook {
@@ -164,8 +173,107 @@ interface RepoState {
164
173
 
165
174
  const DEFAULT_WORKTREE_SETUP_HOOK_TIMEOUT_MS = 30000;
166
175
 
167
- function runGit(cwd: string, args: string[]): GitResult {
168
- const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8", windowsHide: true, shell: false });
176
+ /** In-memory evidence only. Callers project this into existing handoff diagnostics. */
177
+ export interface WorktreeSetupProgress {
178
+ setup: WorktreeSetup;
179
+ attempts: Array<{ index: number; branch: string; path?: string; validated: boolean; command?: WorktreeSetupProgress["command"]; hookCommand?: WorktreeSetupProgress["command"] }>;
180
+ phase: string;
181
+ command?: { command: string; args: string[]; pid?: number; processGroupId?: number; result?: Omit<SetupCommandResult, "stdout" | "stderr"> };
182
+ unknown?: string;
183
+ cleanup?: WorktreeCleanupReport;
184
+ }
185
+
186
+ export class WorktreeSetupError extends Error {
187
+ readonly snapshot: WorktreeSetupProgress;
188
+ constructor(error: unknown, snapshot: WorktreeSetupProgress) {
189
+ super(`${error instanceof Error ? error.message : String(error)}${snapshot.unknown ? `; setup settlement unknown: ${snapshot.unknown}; manual reconciliation required` : ""}`, { cause: error });
190
+ this.name = "WorktreeSetupError";
191
+ this.snapshot = snapshot;
192
+ }
193
+ }
194
+
195
+ let worktreeTurn: Promise<void> = Promise.resolve();
196
+ let setupActive = false;
197
+ let setupPoison: string | undefined;
198
+
199
+ function assertWorktreeMutationAllowed(): void {
200
+ if (setupPoison) throw new Error(`Worktree mutation blocked: ${setupPoison}`);
201
+ if (setupActive) throw new Error("Worktree setup is active; finalization must await withWorktreeTransaction");
202
+ }
203
+
204
+ /** Later async finalization owners wrap their synchronous diff/cleanup as ONE turn. */
205
+ export async function withWorktreeTransaction<T>(action: () => T | Promise<T>): Promise<T> {
206
+ const previous = worktreeTurn;
207
+ let release!: () => void;
208
+ worktreeTurn = new Promise<void>((resolve) => { release = resolve; });
209
+ await previous;
210
+ try {
211
+ if (setupPoison) throw new Error(`Worktree mutation blocked: ${setupPoison}`);
212
+ return await action();
213
+ } finally { release(); }
214
+ }
215
+
216
+ class SetupTransaction {
217
+ readonly progress: WorktreeSetupProgress;
218
+ readonly options: CreateWorktreesOptions;
219
+ constructor(cwd: string, options: CreateWorktreesOptions) {
220
+ this.options = options;
221
+ this.progress = { setup: { cwd, baseCommit: "", worktrees: [] }, attempts: [], phase: "preflight" };
222
+ }
223
+ snapshot(): WorktreeSetupProgress {
224
+ return structuredClone(this.progress);
225
+ }
226
+ publish(): void { this.options.onProgress?.(this.snapshot()); }
227
+ check(): void {
228
+ if (setupPoison) throw new Error(setupPoison);
229
+ if (this.options.signal?.aborted) throw new Error("Worktree setup aborted");
230
+ if (this.options.deadlineAt !== undefined && Date.now() >= this.options.deadlineAt) throw new Error("Worktree setup deadline exceeded");
231
+ }
232
+ unknown(error: unknown): void {
233
+ const reason = error instanceof Error ? error.message : String(error);
234
+ this.progress.unknown ??= reason;
235
+ setupPoison ??= reason;
236
+ }
237
+ async command(command: string, args: string[], options: SetupCommandOptions = {}): Promise<SetupCommandResult> {
238
+ this.check();
239
+ this.progress.command = { command, args };
240
+ if (this.progress.phase === "allocation") this.progress.attempts.at(-1)!.command = this.progress.command;
241
+ if (this.progress.phase === "hook") this.progress.attempts.find((attempt) => attempt.path === options.cwd)!.hookCommand = this.progress.command;
242
+ const result = await runSetupCommand(command, args, {
243
+ ...options, signal: this.options.signal, deadlineAt: this.options.deadlineAt,
244
+ onSpawn: (process) => { Object.assign(this.progress.command!, process); this.publish(); },
245
+ });
246
+ const { stdout: _stdout, stderr: _stderr, ...metadata } = result;
247
+ this.progress.command.result = metadata;
248
+ if (result.processTree?.state === "unknown") this.unknown(result.error ?? "Command tree settlement unverified");
249
+ this.publish();
250
+ if (result.error) throw result.error;
251
+ return result;
252
+ }
253
+ async git(cwd: string, args: string[], acceptedExitCodes?: readonly number[]): Promise<SetupCommandResult> {
254
+ return this.command("git", ["-C", cwd, ...args], { acceptedExitCodes });
255
+ }
256
+ async gitChecked(cwd: string, args: string[]): Promise<string> {
257
+ const result = await this.git(cwd, args);
258
+ return result.stdout;
259
+ }
260
+ attempt(index: number, branch: string, worktreePath?: string): void {
261
+ this.check();
262
+ this.progress.phase = "allocation";
263
+ this.progress.command = undefined;
264
+ this.progress.attempts.push({ index, branch, path: worktreePath, validated: false });
265
+ this.publish();
266
+ }
267
+ validated(worktree: WorktreeInfo): void {
268
+ this.progress.phase = "validated";
269
+ Object.assign(this.progress.attempts.at(-1)!, { path: worktree.path, validated: true });
270
+ this.progress.setup.worktrees.push(worktree);
271
+ this.publish();
272
+ }
273
+ }
274
+
275
+ function runGit(cwd: string, args: string[], env?: NodeJS.ProcessEnv): GitResult {
276
+ const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8", windowsHide: true, shell: false, ...(env ? { env: { ...process.env, ...env } } : {}) });
169
277
  return {
170
278
  stdout: result.stdout ?? "",
171
279
  stderr: result.stderr ?? "",
@@ -183,28 +291,78 @@ function runGitChecked(cwd: string, args: string[]): string {
183
291
  return result.stdout;
184
292
  }
185
293
 
186
- function findGitWorktreePath(cwd: string, branch: string): string | undefined {
187
- const targetBranch = `branch refs/heads/${branch}`;
188
- let currentPath: string | undefined;
189
- for (const line of runGitChecked(cwd, ["worktree", "list", "--porcelain"]).split("\n")) {
190
- if (line.startsWith("worktree ")) currentPath = line.slice("worktree ".length).trim();
191
- else if (line.trim() === targetBranch) return currentPath;
294
+ /** Validate a captured patch against the worktree index without changing either. */
295
+ export function validateWorktreePatch(worktreePath: string, patchPath: string): string | undefined {
296
+ const result = runGit(worktreePath, [...PATCH_VALIDATION_OPTIONS, patchPath]);
297
+ if (result.status === 0) return undefined;
298
+ return result.stderr.trim() || result.stdout.trim() || `git -C ${worktreePath} apply --check failed`;
299
+ }
300
+
301
+ function currentWorktreePatch(worktreePath: string, baseCommit: string): { patch: string } | { error: string } {
302
+ assertWorktreeMutationAllowed();
303
+ let tempDir: string;
304
+ try {
305
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-worktree-index-"));
306
+ } catch (error) {
307
+ return { error: error instanceof Error ? error.message : String(error) };
192
308
  }
309
+ const env = { GIT_INDEX_FILE: path.join(tempDir, "index") };
310
+ try {
311
+ const readTree = runGit(worktreePath, ["read-tree", "HEAD"], env);
312
+ if (readTree.status !== 0) return { error: readTree.stderr.trim() || readTree.stdout.trim() || "git read-tree failed" };
313
+ const add = runGit(worktreePath, ["add", "-A"], env);
314
+ if (add.status !== 0) return { error: add.stderr.trim() || add.stdout.trim() || "git add failed" };
315
+ const diff = runGit(worktreePath, ["diff", "--cached", ...MACHINE_PATCH_OPTIONS, baseCommit], env);
316
+ if (diff.status !== 0) return { error: diff.stderr.trim() || diff.stdout.trim() || "git diff failed" };
317
+ return { patch: diff.stdout };
318
+ } finally {
319
+ try {
320
+ fs.rmSync(tempDir, { recursive: true, force: true });
321
+ } catch {
322
+ // Cleanup safety depends on preserving the worktree, not on best-effort temp-index deletion.
323
+ }
324
+ }
325
+ }
326
+
327
+ export function validateWorktreePatchRepresentsCurrentWorktree(worktreePath: string, baseCommit: string, patchPath: string): string | undefined {
328
+ const validationError = validateWorktreePatch(worktreePath, patchPath);
329
+ if (validationError) return validationError;
330
+ let capturedPatch: string;
331
+ try {
332
+ capturedPatch = fs.readFileSync(patchPath, "utf-8");
333
+ } catch (error) {
334
+ return error instanceof Error ? error.message : String(error);
335
+ }
336
+ const current = currentWorktreePatch(worktreePath, baseCommit);
337
+ if ("error" in current) return current.error || "failed to capture current worktree patch";
338
+ if (capturedPatch !== current.patch) return "captured handoff patch does not match current worktree changes";
193
339
  return undefined;
194
340
  }
195
341
 
196
- function resolveRepoState(cwd: string): RepoState {
197
- const cwdRelative = resolveRepoCwdRelative(cwd);
198
- const toplevel = runGitChecked(cwd, ["rev-parse", "--show-toplevel"]).trim();
342
+ async function resolveRepoState(tx: SetupTransaction, cwd: string, requestedBaseRef: string | undefined): Promise<RepoState> {
343
+ const repoCheck = await tx.git(cwd, ["rev-parse", "--is-inside-work-tree"], [0, 128]);
344
+ if (repoCheck.status !== 0 || repoCheck.stdout.trim() !== "true") throw new Error("worktree isolation requires a git repository");
345
+ const rawPrefix = (await tx.gitChecked(cwd, ["rev-parse", "--show-prefix"])).trim();
346
+ const cwdRelative = rawPrefix ? path.normalize(rawPrefix.replace(/[\\/]+$/, "")) : "";
347
+ const toplevel = (await tx.gitChecked(cwd, ["rev-parse", "--show-toplevel"])).trim();
199
348
 
200
349
  // pi-subagents writes durable runtime state under .pi-subagents/ by default;
201
350
  // that state must not make managed isolation unusable for later runs.
202
- const status = runGitChecked(toplevel, ["status", "--porcelain", "--", `:!${PROJECT_SUBAGENTS_RELATIVE_DIR}`]);
351
+ const status = await tx.gitChecked(toplevel, ["status", "--porcelain", "--", `:!${PROJECT_SUBAGENTS_RELATIVE_DIR}`]);
203
352
  if (status.trim().length > 0) {
204
353
  throw new Error("worktree isolation requires a clean git working tree. Commit or stash changes first.");
205
354
  }
206
355
 
207
- const baseCommit = runGitChecked(toplevel, ["rev-parse", "HEAD"]).trim();
356
+ const baseRef = normalizeWorktreeBaseRef(requestedBaseRef) ?? DEFAULT_WORKTREE_BASE_REF;
357
+ let baseCommit: string;
358
+ try {
359
+ const base = await tx.git(toplevel, ["rev-parse", "--verify", "--end-of-options", `${baseRef}^{commit}`], [0, 128]);
360
+ if (base.status !== 0) throw new Error(base.stderr.trim() || "ref not found");
361
+ baseCommit = base.stdout.trim();
362
+ } catch (error) {
363
+ throw new Error(`baseRef '${baseRef}' could not be resolved to a commit: ${error instanceof Error ? error.message : String(error)}`, { cause: error instanceof Error ? error : undefined });
364
+ }
365
+ if (!baseCommit) throw new Error(`baseRef '${baseRef}' could not be resolved to a commit`);
208
366
  return { toplevel, cwdRelative, baseCommit };
209
367
  }
210
368
 
@@ -214,7 +372,13 @@ function normalizeComparableCwd(cwd: string): string {
214
372
  const missingSegments: string[] = [];
215
373
  while (true) {
216
374
  try {
217
- return path.join(fs.realpathSync(existing), ...missingSegments.reverse());
375
+ let realpath: string;
376
+ try {
377
+ realpath = fs.realpathSync.native(existing);
378
+ } catch {
379
+ realpath = fs.realpathSync(existing);
380
+ }
381
+ return path.join(realpath, ...missingSegments.reverse());
218
382
  } catch {
219
383
  const parent = path.dirname(existing);
220
384
  if (parent === existing) return resolved;
@@ -279,11 +443,19 @@ export function sanitizeWorktreePathComponent(value: string, maxBytes = WORKTREE
279
443
  }
280
444
 
281
445
  function validGitRef(ref: string): boolean {
282
- if (!ref || Buffer.byteLength(ref, "utf-8") > 1024 || ref.startsWith("/") || ref.endsWith("/") || ref.includes("//") || ref.includes("..") || ref.includes("@{")) return false;
283
- if (/[[\]\\~^:?*\u0000-\u0020]/u.test(ref) || ref.endsWith(".") || ref.endsWith(".lock")) return false;
446
+ if (!ref || ref === "@" || Buffer.byteLength(ref, "utf-8") > 1024 || ref.startsWith("/") || ref.endsWith("/") || ref.includes("//") || ref.includes("..") || ref.includes("@{")) return false;
447
+ if (/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/iu.test(ref)) return false;
448
+ if (/[[\]\\~^:?*\u0000-\u0020\u007f]/u.test(ref) || ref.endsWith(".") || ref.endsWith(".lock")) return false;
284
449
  return ref.split("/").every((component) => component.length > 0 && component !== "." && component !== ".." && !component.startsWith(".") && !component.endsWith(".") && !component.endsWith(".lock"));
285
450
  }
286
451
 
452
+ /** Normalize and validate a configured worktree base ref without resolving it. */
453
+ export function normalizeWorktreeBaseRef(value: unknown): string | undefined {
454
+ if (value === undefined) return undefined;
455
+ if (typeof value !== "string" || !validGitRef(value)) throw new Error("baseRef must be a valid Git ref: use HEAD or a supported named ref (for example, refs/heads/main). Full 40/64-character commit IDs and revision expressions are unsupported.");
456
+ return value;
457
+ }
458
+
287
459
  /** Normalize and validate the configured Git branch namespace. */
288
460
  export function normalizeWorktreeBranchPrefix(value: string | undefined): string {
289
461
  const raw = value === undefined ? DEFAULT_WORKTREE_BRANCH_PREFIX : value.trim();
@@ -359,11 +531,6 @@ interface WorktrunkCapability {
359
531
  reason?: string;
360
532
  }
361
533
 
362
- interface SourceCheckoutSnapshot {
363
- head: string;
364
- branch?: string;
365
- }
366
-
367
534
  function runWorktrunk(args: string[], cwd?: string): WorktreeCommandResult {
368
535
  try {
369
536
  const result = spawnSync("wt", args, {
@@ -396,18 +563,6 @@ function probeWorktrunk(): WorktrunkCapability {
396
563
  return { available: true };
397
564
  }
398
565
 
399
- function snapshotSourceCheckout(toplevel: string, head: string): SourceCheckoutSnapshot {
400
- const branch = runGit(toplevel, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
401
- const branchName = branch.status === 0 ? branch.stdout.trim() : "";
402
- return branchName ? { head, branch: branchName } : { head };
403
- }
404
-
405
- function restoreSourceCheckoutIfWorktrunkSwitchedIt(toplevel: string, createdBranch: string, sourceCheckout: SourceCheckoutSnapshot): void {
406
- const current = runGit(toplevel, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
407
- if (current.status !== 0 || current.stdout.trim() !== createdBranch) return;
408
- runGitChecked(toplevel, sourceCheckout.branch ? ["checkout", sourceCheckout.branch] : ["checkout", "--detach", sourceCheckout.head]);
409
- }
410
-
411
566
  /** Resolve a requested provider without silently switching after allocation starts. */
412
567
  export function resolveWorktreeProvider(requested: WorktreeProvider | undefined, baseDir?: string): ManagedWorktreeProvider {
413
568
  const selection = requested ?? DEFAULT_WORKTREE_PROVIDER;
@@ -423,36 +578,117 @@ export function resolveWorktreeProvider(requested: WorktreeProvider | undefined,
423
578
  return "native";
424
579
  }
425
580
 
581
+ async function resolveSetupProvider(tx: SetupTransaction, requested: WorktreeProvider | undefined, baseDir?: string): Promise<ManagedWorktreeProvider> {
582
+ const selection = requested ?? DEFAULT_WORKTREE_PROVIDER;
583
+ if (selection !== "auto" && selection !== "native" && selection !== "worktrunk") throw new Error('worktree provider must be "auto", "native", or "worktrunk"');
584
+ if (selection === "native") return "native";
585
+ if (hasConfiguredWorktreeBaseDir(baseDir)) {
586
+ if (selection === "worktrunk") throw new Error("worktreeProvider='worktrunk' cannot be combined with worktreeBaseDir or SELESAI_SUBAGENTS_WORKTREE_DIR");
587
+ return "native";
588
+ }
589
+ let reason: string | undefined;
590
+ try {
591
+ const probeExitCodes = Array.from({ length: 256 }, (_, code) => code);
592
+ const version = await tx.command("wt", ["--version"], { maxBuffer: WORKTREE_COMMAND_OUTPUT_MAX_BYTES, acceptedExitCodes: probeExitCodes });
593
+ if (version.status !== 0 || !/\b(?:wt\s+)?v?(\d+\.\d+(?:\.\d+)?)\b/i.test(version.stdout.trim())) reason = "Worktrunk is unavailable or returned an invalid version";
594
+ else {
595
+ const help = await tx.command("wt", ["switch", "--help"], { maxBuffer: WORKTREE_COMMAND_OUTPUT_MAX_BYTES, acceptedExitCodes: probeExitCodes });
596
+ const missing = ["--create", "--base", "--no-cd", "--no-hooks", "--format"].filter((flag) => !`${help.stdout}\n${help.stderr}`.includes(flag));
597
+ if (help.status !== 0 || missing.length) reason = `Worktrunk switch capability unavailable: ${missing.join(", ")}`;
598
+ }
599
+ } catch (error) {
600
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") throw error;
601
+ reason = error.message;
602
+ }
603
+ if (!reason) return "worktrunk";
604
+ if (selection === "worktrunk") throw new Error(`Worktrunk provider is unavailable: ${reason}`);
605
+ return "native";
606
+ }
607
+
426
608
  /** Whether a launch must bind its worktree-dependent paths after allocation. */
427
609
  export function shouldDeferWorktreeCwd(requested: WorktreeProvider | undefined, baseDir?: string): boolean {
428
610
  return (requested ?? DEFAULT_WORKTREE_PROVIDER) !== "native" && !hasConfiguredWorktreeBaseDir(baseDir);
429
611
  }
430
612
 
431
- function resolveWorktreeBaseDir(configuredBaseDir: string | undefined, repoRoot: string): string {
613
+ /**
614
+ * Resolves the dedicated worktree root: the configured base directory or
615
+ * SELESAI_SUBAGENTS_WORKTREE_DIR when set, otherwise a `worktrees` folder sibling
616
+ * to the repository. Managed leaves always nest one level deeper under the
617
+ * project folder (`basename(repoRoot)`).
618
+ */
619
+ function resolveWorktreeDedicatedRoot(configuredBaseDir: string | undefined, repoRoot: string): string {
432
620
  const rawBaseDir = configuredBaseDir ?? process.env.SELESAI_SUBAGENTS_WORKTREE_DIR;
433
- if (rawBaseDir === undefined || (configuredBaseDir === undefined && !rawBaseDir.trim())) return os.tmpdir();
434
-
435
- const trimmed = rawBaseDir.trim();
436
- if (!trimmed) throw new Error("worktree base directory cannot be empty");
621
+ let expanded: string;
622
+ if (rawBaseDir === undefined || (configuredBaseDir === undefined && !rawBaseDir.trim())) {
623
+ expanded = path.join(path.dirname(repoRoot), "worktrees");
624
+ } else {
625
+ const trimmed = rawBaseDir.trim();
626
+ if (!trimmed) throw new Error("worktree base directory cannot be empty");
437
627
 
438
- const expanded = trimmed.startsWith("~/") ? path.join(os.homedir(), trimmed.slice(2)) : trimmed;
439
- const resolved = path.isAbsolute(expanded) ? expanded : path.resolve(repoRoot, expanded);
628
+ const candidate = trimmed.startsWith("~/") ? path.join(os.homedir(), trimmed.slice(2)) : trimmed;
629
+ expanded = path.isAbsolute(candidate) ? candidate : path.resolve(repoRoot, candidate);
630
+ }
440
631
  const extensionsDir = normalizeComparableCwd(path.join(getAgentDir(), "extensions"));
441
- const relativeToExtensions = path.relative(extensionsDir, normalizeComparableCwd(resolved));
442
- if (!relativeToExtensions || (!relativeToExtensions.startsWith(`..${path.sep}`) && relativeToExtensions !== ".." && !path.isAbsolute(relativeToExtensions))) {
632
+ if (isPathInside(extensionsDir, normalizeComparableCwd(expanded))) {
443
633
  throw new Error(`worktree base directory cannot be inside Pi extensions directory: ${extensionsDir}. Choose a directory outside it.`);
444
634
  }
635
+ return expanded;
636
+ }
637
+
638
+ function buildNativeProjectPath(dedicatedRoot: string, repoRoot: string): string {
639
+ return path.join(dedicatedRoot, path.basename(repoRoot));
640
+ }
641
+
642
+ /**
643
+ * Creates the project folder (parents included) so `git worktree add` can create
644
+ * the leaf inside it. Must only run after `assertSafeWorktreeLocation` accepted
645
+ * the planned leaf — an unsafe base must never be materialized on disk.
646
+ */
647
+ function ensureProjectWorktreeDir(dedicatedRoot: string, repoRoot: string): void {
445
648
  try {
446
- fs.mkdirSync(resolved, { recursive: true });
649
+ fs.mkdirSync(buildNativeProjectPath(dedicatedRoot, repoRoot), { recursive: true });
447
650
  } catch (error) {
448
651
  const message = error instanceof Error ? error.message : String(error);
449
- throw new Error(`failed to create worktree base directory ${resolved}: ${message}`);
652
+ throw new Error(`failed to create worktree base directory ${dedicatedRoot}: ${message}`);
450
653
  }
451
- return resolved;
452
654
  }
453
655
 
454
- function buildNativeWorktreePath(baseDir: string, runId: string, index: number): string {
455
- return path.join(baseDir, `pi-worktree-${sanitizeWorktreePathComponent(runId, 120)}-${index}`);
656
+ function buildNativeWorktreePath(dedicatedRoot: string, repoRoot: string, runId: string, index: number): string {
657
+ return path.join(buildNativeProjectPath(dedicatedRoot, repoRoot), `pi-worktree-${sanitizeWorktreePathComponent(runId, 120)}-${index}`);
658
+ }
659
+
660
+ function isPathInside(parent: string, child: string): boolean {
661
+ const relative = path.relative(parent, child);
662
+ if (!relative || relative === ".") return true;
663
+ return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
664
+ }
665
+
666
+ function isStrictChildPath(parent: string, child: string): boolean {
667
+ const relative = path.relative(parent, child);
668
+ return Boolean(relative) && relative !== "." && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
669
+ }
670
+
671
+ /** Fail closed before `git worktree add`: reject unsafe planned/realpath locations. */
672
+ function assertSafeWorktreeLocation(worktreePath: string, repoRoot: string, dedicatedRoot: string): void {
673
+ const resolvedLeaf = normalizeComparableCwd(worktreePath);
674
+ const resolvedRepoRoot = normalizeComparableCwd(repoRoot);
675
+ const projectDir = normalizeComparableCwd(buildNativeProjectPath(dedicatedRoot, repoRoot));
676
+ const repoParent = normalizeComparableCwd(path.dirname(resolvedRepoRoot));
677
+ const leafParent = normalizeComparableCwd(path.dirname(resolvedLeaf));
678
+ const extensionsDir = normalizeComparableCwd(path.join(getAgentDir(), "extensions"));
679
+
680
+ if (isPathInside(extensionsDir, projectDir) || isPathInside(extensionsDir, resolvedLeaf)) {
681
+ throw new Error(`worktree path cannot be inside Pi extensions directory: ${extensionsDir}. Choose a directory outside it.`);
682
+ }
683
+ if (isPathInside(resolvedRepoRoot, resolvedLeaf)) {
684
+ throw new Error(`worktree path would land inside the repository checkout: ${resolvedLeaf}`);
685
+ }
686
+ if (leafParent === repoParent) {
687
+ throw new Error(`worktree path would be a direct child of the repository parent: ${resolvedLeaf}`);
688
+ }
689
+ if (!isStrictChildPath(projectDir, resolvedLeaf)) {
690
+ throw new Error(`worktree path must be a strict child of the project worktree directory ${projectDir}: ${resolvedLeaf}`);
691
+ }
456
692
  }
457
693
 
458
694
  function resolveRepoCwdRelative(cwd: string): string {
@@ -470,7 +706,9 @@ function resolveRepoCwdRelative(cwd: string): string {
470
706
  export function resolveExpectedWorktreeAgentCwd(cwd: string, runId: string, index: number, baseDir?: string): string {
471
707
  const cwdRelative = resolveRepoCwdRelative(cwd);
472
708
  const repoRoot = runGitChecked(cwd, ["rev-parse", "--show-toplevel"]).trim();
473
- const worktreePath = buildNativeWorktreePath(resolveWorktreeBaseDir(baseDir, repoRoot), runId, index);
709
+ const dedicatedRoot = resolveWorktreeDedicatedRoot(baseDir, repoRoot);
710
+ const worktreePath = buildNativeWorktreePath(dedicatedRoot, repoRoot, runId, index);
711
+ assertSafeWorktreeLocation(worktreePath, repoRoot, dedicatedRoot);
474
712
  return cwdRelative ? path.join(worktreePath, cwdRelative) : worktreePath;
475
713
  }
476
714
 
@@ -544,11 +782,6 @@ function normalizeSyntheticPath(worktreePath: string, rawPath: string): string {
544
782
  return path.normalize(relative);
545
783
  }
546
784
 
547
- function hasTrackedEntries(worktreePath: string, relativePath: string): boolean {
548
- const result = runGit(worktreePath, ["ls-files", "--", relativePath]);
549
- return result.status === 0 && result.stdout.trim().length > 0;
550
- }
551
-
552
785
  function parseWorktreeSetupHookOutput(rawStdout: string): WorktreeSetupHookOutput {
553
786
  const trimmed = rawStdout.trim();
554
787
  if (!trimmed) {
@@ -567,53 +800,49 @@ function parseWorktreeSetupHookOutput(rawStdout: string): WorktreeSetupHookOutpu
567
800
  return parsed as WorktreeSetupHookOutput;
568
801
  }
569
802
 
570
- function runWorktreeSetupHook(
803
+ async function runWorktreeSetupHook(
804
+ tx: SetupTransaction,
571
805
  hook: ResolvedWorktreeSetupHook,
572
806
  input: WorktreeSetupHookInput,
573
- ): string[] {
574
- const result = spawnSync(hook.hookPath, [], {
575
- windowsHide: true,
576
- cwd: input.worktreePath,
577
- encoding: "utf-8",
578
- input: JSON.stringify(input),
579
- timeout: hook.timeoutMs,
580
- shell: false,
581
- });
582
-
583
- if (result.error) {
584
- const code = "code" in result.error ? result.error.code : undefined;
585
- if (code === "ETIMEDOUT") {
586
- throw new Error(`worktree setup hook timed out after ${hook.timeoutMs}ms`);
587
- }
588
- throw new Error(`worktree setup hook failed: ${result.error.message}`);
589
- }
590
-
591
- if (result.status !== 0) {
592
- const details = result.stderr.trim() || result.stdout.trim() || "no output";
593
- throw new Error(`worktree setup hook failed with exit code ${result.status}: ${details}`);
594
- }
595
-
596
- const output = parseWorktreeSetupHookOutput(result.stdout);
597
- if (output.syntheticPaths === undefined) return [];
598
- if (!Array.isArray(output.syntheticPaths)) {
599
- throw new Error("worktree setup hook output field 'syntheticPaths' must be an array of relative paths");
807
+ ): Promise<string[]> {
808
+ tx.progress.phase = "hook";
809
+ let result: SetupCommandResult;
810
+ try {
811
+ result = await tx.command(hook.hookPath, [], {
812
+ cwd: input.worktreePath,
813
+ input: JSON.stringify(input),
814
+ hookTimeoutMs: hook.timeoutMs,
815
+ });
816
+ } catch (error) {
817
+ throw new Error(`worktree setup hook failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
600
818
  }
819
+ tx.progress.phase = "hook-validation";
601
820
 
602
- const uniquePaths = new Set<string>();
603
- for (const candidate of output.syntheticPaths) {
604
- if (typeof candidate !== "string") {
605
- throw new Error("worktree setup hook output field 'syntheticPaths' must contain only strings");
821
+ try {
822
+ const output = parseWorktreeSetupHookOutput(result.stdout);
823
+ if (output.syntheticPaths === undefined) return [];
824
+ if (!Array.isArray(output.syntheticPaths)) {
825
+ throw new Error("worktree setup hook output field 'syntheticPaths' must be an array of relative paths");
606
826
  }
607
- const normalizedPath = normalizeSyntheticPath(input.worktreePath, candidate);
608
- if (hasTrackedEntries(input.worktreePath, normalizedPath)) {
609
- throw new Error(`worktree setup hook cannot mark tracked paths as synthetic: ${normalizedPath}`);
827
+ const uniquePaths = new Set<string>();
828
+ for (const candidate of output.syntheticPaths) {
829
+ if (typeof candidate !== "string") throw new Error("worktree setup hook output field 'syntheticPaths' must contain only strings");
830
+ const normalizedPath = normalizeSyntheticPath(input.worktreePath, candidate);
831
+ if ((await tx.gitChecked(input.worktreePath, ["ls-files", "--", normalizedPath])).trim()) {
832
+ throw new Error(`worktree setup hook cannot mark tracked paths as synthetic: ${normalizedPath}`);
833
+ }
834
+ uniquePaths.add(normalizedPath);
610
835
  }
611
- uniquePaths.add(normalizedPath);
836
+ return [...uniquePaths];
837
+ } catch (error) {
838
+ // Semantic rejection occurs after exit: retain uncertainty, never signal an old PID.
839
+ tx.unknown(error);
840
+ throw error;
612
841
  }
613
- return [...uniquePaths];
614
842
  }
615
843
 
616
- function finalizeCreatedWorktree(
844
+ async function finalizeCreatedWorktree(
845
+ tx: SetupTransaction,
617
846
  toplevel: string,
618
847
  cwdRelative: string,
619
848
  runId: string,
@@ -621,38 +850,23 @@ function finalizeCreatedWorktree(
621
850
  setupHook: ResolvedWorktreeSetupHook | undefined,
622
851
  agent: string | undefined,
623
852
  worktree: WorktreeInfo,
624
- ): WorktreeInfo {
853
+ ): Promise<WorktreeInfo> {
625
854
  const agentCwd = cwdRelative ? path.join(worktree.path, cwdRelative) : worktree.path;
626
- try {
627
- const nodeModulesLinked = linkNodeModulesIfPresent(toplevel, worktree.path);
628
- const syntheticPaths = nodeModulesLinked ? ["node_modules"] : [];
629
- if (setupHook) {
630
- const hookSyntheticPaths = runWorktreeSetupHook(setupHook, {
631
- version: 1,
632
- repoRoot: toplevel,
633
- worktreePath: worktree.path,
634
- agentCwd,
635
- branch: worktree.branch,
636
- index: worktree.index,
637
- runId,
638
- baseCommit,
639
- agent,
640
- });
641
- syntheticPaths.push(...hookSyntheticPaths);
642
- }
643
- return { ...worktree, agentCwd, nodeModulesLinked, syntheticPaths };
644
- } catch (error) {
645
- try { runGitChecked(toplevel, ["worktree", "remove", "--force", worktree.path]); } catch {
646
- // Best-effort rollback; preserve the original setup failure.
647
- }
648
- try { runGitChecked(toplevel, ["branch", "-D", worktree.branch]); } catch {
649
- // Best-effort rollback; preserve the original setup failure.
650
- }
651
- throw error;
855
+ tx.check();
856
+ const nodeModulesLinked = linkNodeModulesIfPresent(toplevel, worktree.path);
857
+ const syntheticPaths = nodeModulesLinked ? ["node_modules"] : [];
858
+ if (setupHook) {
859
+ const hookSyntheticPaths = await runWorktreeSetupHook(tx, setupHook, {
860
+ version: 1, repoRoot: toplevel, worktreePath: worktree.path, agentCwd,
861
+ branch: worktree.branch, index: worktree.index, runId, baseCommit, agent,
862
+ });
863
+ syntheticPaths.push(...hookSyntheticPaths);
652
864
  }
865
+ return { ...worktree, agentCwd, nodeModulesLinked, syntheticPaths };
653
866
  }
654
867
 
655
- function createNativeWorktree(
868
+ async function createNativeWorktree(
869
+ tx: SetupTransaction,
656
870
  toplevel: string,
657
871
  cwdRelative: string,
658
872
  runId: string,
@@ -660,19 +874,24 @@ function createNativeWorktree(
660
874
  baseCommit: string,
661
875
  setupHook: ResolvedWorktreeSetupHook | undefined,
662
876
  agent: string | undefined,
663
- baseDir: string,
877
+ dedicatedRoot: string,
664
878
  labels: Array<string | undefined> | undefined,
665
879
  tasks: Array<string | undefined> | undefined,
666
880
  branchPrefix: string | undefined,
667
- ): WorktreeInfo {
881
+ ): Promise<WorktreeInfo> {
668
882
  const naming = buildWorktreeNaming({ runId, index, agent, label: labels?.[index], task: tasks?.[index], branchPrefix });
669
- const worktreePath = buildNativeWorktreePath(baseDir, runId, index);
670
- const add = runGit(toplevel, ["worktree", "add", worktreePath, "-b", naming.requestedBranch, baseCommit]);
671
- if (add.status !== 0) {
672
- const message = add.stderr.trim() || add.stdout.trim() || `failed to create worktree ${worktreePath}`;
673
- throw new Error(message);
674
- }
675
- return finalizeCreatedWorktree(toplevel, cwdRelative, runId, baseCommit, setupHook, agent, {
883
+ const worktreePath = buildNativeWorktreePath(dedicatedRoot, toplevel, runId, index);
884
+ assertSafeWorktreeLocation(worktreePath, toplevel, dedicatedRoot);
885
+ // Absence is required before claiming any partial artifacts from this attempt.
886
+ const branch = await tx.git(toplevel, ["show-ref", "--verify", "--quiet", `refs/heads/${naming.requestedBranch}`], [0, 1]);
887
+ let pathExists = false;
888
+ try { fs.lstatSync(worktreePath); pathExists = true; }
889
+ catch (error) { if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") throw error; }
890
+ if (branch.status !== 1 || pathExists) throw new Error(`worktree path or branch already exists: ${worktreePath}, ${naming.requestedBranch}`);
891
+ tx.attempt(index, naming.requestedBranch, worktreePath);
892
+ ensureProjectWorktreeDir(dedicatedRoot, toplevel);
893
+ await tx.git(toplevel, ["worktree", "add", worktreePath, "-b", naming.requestedBranch, baseCommit]);
894
+ const worktree: WorktreeInfo = {
676
895
  path: worktreePath,
677
896
  agentCwd: worktreePath,
678
897
  branch: naming.requestedBranch,
@@ -681,7 +900,9 @@ function createNativeWorktree(
681
900
  syntheticPaths: [],
682
901
  provider: "native",
683
902
  naming,
684
- });
903
+ };
904
+ tx.validated(worktree);
905
+ return finalizeCreatedWorktree(tx, toplevel, cwdRelative, runId, baseCommit, setupHook, agent, worktree);
685
906
  }
686
907
 
687
908
  interface WorktrunkSwitchOutput {
@@ -706,7 +927,8 @@ function parseWorktrunkSwitchOutput(rawStdout: string): WorktrunkSwitchOutput {
706
927
  return parsed as WorktrunkSwitchOutput;
707
928
  }
708
929
 
709
- function createWorktrunkWorktree(
930
+ async function createWorktrunkWorktree(
931
+ tx: SetupTransaction,
710
932
  toplevel: string,
711
933
  cwdRelative: string,
712
934
  runId: string,
@@ -716,27 +938,19 @@ function createWorktrunkWorktree(
716
938
  labels: Array<string | undefined> | undefined,
717
939
  tasks: Array<string | undefined> | undefined,
718
940
  branchPrefix: string | undefined,
719
- ): WorktreeInfo {
941
+ ): Promise<WorktreeInfo> {
720
942
  const naming = buildWorktreeNaming({ runId, index, agent: agents?.[index], label: labels?.[index], task: tasks?.[index], branchPrefix });
721
- const sourceCheckout = snapshotSourceCheckout(toplevel, baseCommit);
722
943
  const args = ["-C", toplevel, "switch", "--create", naming.requestedBranch, "--base", baseCommit, "--no-cd", "--no-hooks", "--format", "json"];
723
- const result = runWorktrunk(args, toplevel);
724
- if (result.status !== 0) {
725
- const message = result.error?.message || result.stderr.trim() || result.stdout.trim() || "Worktrunk provisioning failed";
726
- throw new Error(`Worktrunk provisioning failed: ${message}`);
727
- }
728
- let createdAllocation = false;
729
- let returnedPath: string | undefined;
944
+ tx.attempt(index, naming.requestedBranch);
945
+ const result = await tx.command("wt", args, { cwd: toplevel, maxBuffer: WORKTREE_COMMAND_OUTPUT_MAX_BYTES });
946
+ tx.progress.phase = "validation";
730
947
  try {
731
948
  const output = parseWorktrunkSwitchOutput(result.stdout);
732
- createdAllocation = output.action === "created" && output.created_branch === true;
733
- if (typeof output.path === "string" && path.isAbsolute(output.path)) returnedPath = path.resolve(output.path);
734
- if (!createdAllocation || output.branch !== naming.requestedBranch || output.base_branch !== baseCommit) {
949
+ if (output.action !== "created" || output.created_branch !== true || output.branch !== naming.requestedBranch || output.base_branch !== baseCommit) {
735
950
  throw new Error("Worktrunk provisioning returned inconsistent creation metadata");
736
951
  }
737
952
  if (typeof output.path !== "string" || !path.isAbsolute(output.path)) throw new Error("Worktrunk provisioning returned a non-absolute worktree path");
738
- const worktreePathCandidate = returnedPath;
739
- if (!worktreePathCandidate) throw new Error("Worktrunk provisioning returned a non-absolute worktree path");
953
+ const worktreePathCandidate = path.resolve(output.path);
740
954
  let stat: fs.Stats;
741
955
  try {
742
956
  stat = fs.lstatSync(worktreePathCandidate);
@@ -746,16 +960,16 @@ function createWorktrunkWorktree(
746
960
  if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("Worktrunk provisioning returned a path that is not a real directory");
747
961
  const worktreePath = normalizeComparableCwd(worktreePathCandidate);
748
962
  if (worktreePath === normalizeComparableCwd(toplevel)) throw new Error("Worktrunk provisioning returned the source checkout path");
749
- const sourceCommonDirRaw = runGitChecked(toplevel, ["rev-parse", "--git-common-dir"]).trim();
750
- const returnedCommonDirRaw = runGitChecked(worktreePath, ["rev-parse", "--git-common-dir"]).trim();
963
+ const sourceCommonDirRaw = (await tx.gitChecked(toplevel, ["rev-parse", "--git-common-dir"])).trim();
964
+ const returnedCommonDirRaw = (await tx.gitChecked(worktreePath, ["rev-parse", "--git-common-dir"])).trim();
751
965
  const sourceCommonDir = normalizeComparableCwd(path.isAbsolute(sourceCommonDirRaw) ? sourceCommonDirRaw : path.resolve(toplevel, sourceCommonDirRaw));
752
966
  const returnedCommonDir = normalizeComparableCwd(path.isAbsolute(returnedCommonDirRaw) ? returnedCommonDirRaw : path.resolve(worktreePath, returnedCommonDirRaw));
753
967
  if (returnedCommonDir !== sourceCommonDir) throw new Error("Worktrunk provisioning returned a worktree for a different repository");
754
- const returnedBranch = runGitChecked(worktreePath, ["symbolic-ref", "--quiet", "--short", "HEAD"]).trim();
968
+ const returnedBranch = (await tx.gitChecked(worktreePath, ["symbolic-ref", "--quiet", "--short", "HEAD"])).trim();
755
969
  if (returnedBranch !== naming.requestedBranch) throw new Error("Worktrunk provisioning returned a worktree on a different branch");
756
- const returnedHead = runGitChecked(worktreePath, ["rev-parse", "HEAD"]).trim();
970
+ const returnedHead = (await tx.gitChecked(worktreePath, ["rev-parse", "HEAD"])).trim();
757
971
  if (returnedHead !== baseCommit) throw new Error("Worktrunk provisioning returned a worktree at a different base commit");
758
- return {
972
+ const worktree: WorktreeInfo = {
759
973
  path: worktreePath,
760
974
  agentCwd: cwdRelative ? path.join(worktreePath, cwdRelative) : worktreePath,
761
975
  branch: naming.requestedBranch,
@@ -765,35 +979,16 @@ function createWorktrunkWorktree(
765
979
  provider: "worktrunk",
766
980
  naming,
767
981
  };
982
+ tx.validated(worktree);
983
+ return worktree;
768
984
  } catch (error) {
769
- if (createdAllocation) {
770
- try { restoreSourceCheckoutIfWorktrunkSwitchedIt(toplevel, naming.requestedBranch, sourceCheckout); } catch {
771
- // Best-effort rollback; preserve the validation failure.
772
- }
773
- try {
774
- const listedPath = findGitWorktreePath(toplevel, naming.requestedBranch);
775
- const candidates = [listedPath, returnedPath].filter((candidate, candidateIndex, all): candidate is string => Boolean(candidate) && all.indexOf(candidate) === candidateIndex);
776
- for (const candidate of candidates) {
777
- if (normalizeComparableCwd(candidate) === normalizeComparableCwd(toplevel)) continue;
778
- try {
779
- runGitChecked(toplevel, ["worktree", "remove", "--force", candidate]);
780
- break;
781
- } catch {
782
- // Try another provider-reported/listed path before giving up.
783
- }
784
- }
785
- } catch {
786
- // Best-effort rollback; preserve the validation failure.
787
- }
788
- try { runGitChecked(toplevel, ["branch", "-D", naming.requestedBranch]); } catch {
789
- // Best-effort rollback; preserve the validation failure.
790
- }
791
- }
985
+ tx.unknown(error);
792
986
  throw error;
793
987
  }
794
988
  }
795
989
 
796
990
  function removeSyntheticPath(worktree: WorktreeInfo, syntheticPath: string): void {
991
+ assertWorktreeMutationAllowed();
797
992
  const resolved = path.resolve(worktree.path, syntheticPath);
798
993
  const relative = path.relative(worktree.path, resolved);
799
994
  if (!relative || relative === "." || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
@@ -872,15 +1067,18 @@ function captureWorktreeDiff(
872
1067
  ): WorktreeDiff {
873
1068
  removeSyntheticPathsBeforeDiff(worktree);
874
1069
  runGitChecked(worktree.path, ["add", "-A"]);
875
- const diffStat = runGitChecked(worktree.path, ["diff", "--cached", "--stat", setup.baseCommit]).trim();
876
- const patch = runGitChecked(worktree.path, ["diff", "--cached", setup.baseCommit]);
877
- const numstat = runGitChecked(worktree.path, ["diff", "--cached", "--numstat", setup.baseCommit]);
1070
+ const diffStat = runGitChecked(worktree.path, ["diff", "--cached", ...MACHINE_DIFF_OPTIONS, "--stat", setup.baseCommit]).trim();
1071
+ const patch = runGitChecked(worktree.path, ["diff", "--cached", ...MACHINE_PATCH_OPTIONS, setup.baseCommit]);
1072
+ const numstat = runGitChecked(worktree.path, ["diff", "--cached", ...MACHINE_DIFF_OPTIONS, "--numstat", setup.baseCommit]);
878
1073
  fs.writeFileSync(patchPath, patch, "utf-8");
879
1074
 
880
1075
  if (!patch.trim()) {
881
1076
  return emptyDiff(worktree.index, agent, worktree.branch, patchPath);
882
1077
  }
883
1078
 
1079
+ const validationError = validateWorktreePatch(worktree.path, patchPath);
1080
+ if (validationError) throw new Error(`captured worktree patch is not machine-applyable: ${validationError}`);
1081
+
884
1082
  const parsed = parseNumstat(numstat);
885
1083
  return {
886
1084
  index: worktree.index,
@@ -949,7 +1147,7 @@ function cleanupSingleWorktree(
949
1147
  errors.push(`synthetic path cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
950
1148
  }
951
1149
  const status = runGit(worktree.path, ["status", "--porcelain"]);
952
- const baseDiff = runGit(worktree.path, ["diff", "--quiet", setup.baseCommit, "--"]);
1150
+ const baseDiff = runGit(worktree.path, ["diff", "--quiet", ...MACHINE_DIFF_OPTIONS, setup.baseCommit, "--"]);
953
1151
  if (status.status !== 0 || (baseDiff.status !== 0 && baseDiff.status !== 1)) {
954
1152
  const reason = status.status !== 0
955
1153
  ? status.stderr.trim() || status.stdout.trim() || "git status failed"
@@ -970,13 +1168,25 @@ function cleanupSingleWorktree(
970
1168
  const hasWork = status.stdout.trim().length > 0 || baseDiff.status === 1;
971
1169
  if (hasWork && intent.kind === "preserve") {
972
1170
  const captured = (intent.capturedDiffs ?? setup.capturedDiffs)?.find((diff) => diff.index === worktree.index);
973
- const patchCaptured = captured !== undefined
1171
+ let patchValidationError: string | undefined;
1172
+ let patchCaptured = false;
1173
+ if (captured !== undefined
974
1174
  && captured.error === undefined
975
1175
  && fs.existsSync(captured.patchPath)
976
- && fs.statSync(captured.patchPath).size > 0
977
- && handoffRecordsPatch(intent.handoffManifestPath, captured.patchPath);
1176
+ && handoffRecordsPatch(intent.handoffManifestPath, captured.patchPath)) {
1177
+ try {
1178
+ if (fs.statSync(captured.patchPath).size > 0) {
1179
+ patchValidationError = validateWorktreePatchRepresentsCurrentWorktree(worktree.path, setup.baseCommit, captured.patchPath);
1180
+ patchCaptured = patchValidationError === undefined;
1181
+ }
1182
+ } catch (error) {
1183
+ patchValidationError = error instanceof Error ? error.message : String(error);
1184
+ }
1185
+ }
978
1186
  if (!patchCaptured) {
979
- const reason = "worktree contains changes that are not represented by a captured handoff patch";
1187
+ const reason = patchValidationError
1188
+ ? `captured handoff patch failed validation: ${patchValidationError}`
1189
+ : "worktree contains changes that are not represented by a captured handoff patch";
980
1190
  return {
981
1191
  index: worktree.index,
982
1192
  path: worktree.path,
@@ -1043,54 +1253,111 @@ function hasWorktreeChanges(diff: WorktreeDiff): boolean {
1043
1253
  return diff.filesChanged > 0 || diff.insertions > 0 || diff.deletions > 0 || diff.diffStat.trim().length > 0;
1044
1254
  }
1045
1255
 
1046
- export function createWorktrees(cwd: string, runId: string, count: number, options?: CreateWorktreesOptions): WorktreeSetup {
1256
+ async function compensateSetup(tx: SetupTransaction): Promise<WorktreeCleanupReport> {
1257
+ const { setup, attempts } = tx.progress;
1258
+ const report: WorktreeCleanupReport = { state: "partial", tasks: [], pruned: false, errors: [] };
1259
+ tx.progress.cleanup = report;
1260
+ tx.progress.phase = "rollback";
1261
+ // Cancellation stops allocation, not accounting. Compensation has ONLY the
1262
+ // remaining original absolute deadline; it does not reuse the launch signal.
1263
+ const git = async (args: string[], acceptedExitCodes: readonly number[] = [0]) => {
1264
+ if (setupPoison) throw new Error(setupPoison);
1265
+ tx.progress.command = { command: "git", args: ["-C", setup.cwd, ...args] };
1266
+ const result = await runSetupCommand("git", tx.progress.command.args, {
1267
+ deadlineAt: tx.options.deadlineAt, acceptedExitCodes,
1268
+ onSpawn: (process) => { Object.assign(tx.progress.command!, process); tx.publish(); },
1269
+ });
1270
+ const { stdout: _stdout, stderr: _stderr, ...metadata } = result;
1271
+ tx.progress.command.result = metadata;
1272
+ if (result.processTree?.state === "unknown") tx.unknown(result.error ?? "Rollback command settlement unverified");
1273
+ tx.publish();
1274
+ if (result.error || result.status === null || !acceptedExitCodes.includes(result.status)) throw result.error ?? new Error(result.stderr.trim() || "Rollback command failed");
1275
+ return result;
1276
+ };
1277
+ for (const attempt of [...attempts].reverse()) {
1278
+ if (!attempt.path) {
1279
+ tx.unknown(`Allocation path unconfirmed for branch ${attempt.branch}`);
1280
+ report.errors!.push(tx.progress.unknown!);
1281
+ continue;
1282
+ }
1283
+ const known = setup.worktrees.find((worktree) => worktree.index === attempt.index);
1284
+ const task: WorktreeCleanupTask = { index: attempt.index, path: attempt.path, branch: attempt.branch,
1285
+ provider: known?.provider ?? "native", naming: known?.naming, worktreeRemoved: false, branchRemoved: false };
1286
+ report.tasks.push(task);
1287
+ try {
1288
+ if (normalizeComparableCwd(attempt.path) === normalizeComparableCwd(setup.cwd)) throw new Error("Refusing source-checkout removal");
1289
+ if (attempt.validated) {
1290
+ await git(["worktree", "remove", "--force", attempt.path]);
1291
+ task.worktreeRemoved = true;
1292
+ } else {
1293
+ // Native attempts were absent before mutation. Reconcile only exact
1294
+ // registered path/branch identity; never recursively delete a guessed leaf.
1295
+ const listed = (await git(["worktree", "list", "--porcelain"])).stdout;
1296
+ const entry = listed.split("\n\n").find((block) => block.split("\n").includes(`worktree ${attempt.path}`));
1297
+ if (entry) {
1298
+ if (!entry.split("\n").includes(`branch refs/heads/${attempt.branch}`)) {
1299
+ tx.unknown("Partial allocation ownership mismatch");
1300
+ throw new Error(tx.progress.unknown);
1301
+ }
1302
+ await git(["worktree", "remove", "--force", attempt.path]);
1303
+ } else if (fs.existsSync(attempt.path)) {
1304
+ tx.unknown("Unregistered partial allocation retained");
1305
+ throw new Error(tx.progress.unknown);
1306
+ }
1307
+ task.worktreeRemoved = true;
1308
+ }
1309
+ const branch = await git(["show-ref", "--verify", "--quiet", `refs/heads/${attempt.branch}`], [0, 1]);
1310
+ if (branch.status === 0) await git(["branch", "-D", attempt.branch]);
1311
+ task.branchRemoved = true;
1312
+ } catch (error) {
1313
+ task.preserved = true;
1314
+ task.errors = [error instanceof Error ? error.message : String(error)];
1315
+ }
1316
+ }
1317
+ try {
1318
+ if (attempts.length > 0) { await git(["worktree", "prune"]); report.pruned = true; }
1319
+ } catch (error) { report.errors!.push(error instanceof Error ? error.message : String(error)); }
1320
+ if (tx.progress.unknown) report.errors!.push(tx.progress.unknown);
1321
+ report.tasks.sort((a, b) => a.index - b.index);
1322
+ if (!setupPoison && (report.pruned || attempts.length === 0) && report.tasks.every((task) => task.worktreeRemoved && task.branchRemoved) && !report.errors!.length) report.state = "complete";
1323
+ return report;
1324
+ }
1325
+
1326
+ async function allocateWorktrees(tx: SetupTransaction, cwd: string, runId: string, count: number): Promise<WorktreeSetup> {
1327
+ const options = tx.options;
1328
+ tx.check();
1047
1329
  if (!Number.isSafeInteger(count) || count < 0) throw new Error("worktree count must be a non-negative integer");
1048
- const repo = resolveRepoState(cwd);
1330
+ const repo = await resolveRepoState(tx, cwd, options?.baseRef);
1331
+ tx.progress.setup.cwd = repo.toplevel;
1332
+ tx.progress.setup.baseCommit = repo.baseCommit;
1049
1333
  const setupHook = resolveWorktreeSetupHook(repo.toplevel, options?.setupHook);
1050
- let provider = resolveWorktreeProvider(options?.provider, options?.baseDir);
1334
+ const provider = await resolveSetupProvider(tx, options?.provider, options?.baseDir);
1051
1335
  const branchPrefix = normalizeWorktreeBranchPrefix(options?.branchPrefix);
1052
- let baseDir = provider === "native" ? resolveWorktreeBaseDir(options?.baseDir, repo.toplevel) : undefined;
1053
- const worktrees: WorktreeInfo[] = [];
1336
+ const dedicatedRoot = provider === "native" ? resolveWorktreeDedicatedRoot(options?.baseDir, repo.toplevel) : undefined;
1337
+ const worktrees = tx.progress.setup.worktrees;
1054
1338
 
1055
1339
  try {
1056
1340
  if (provider === "native") {
1057
- const plannedSetup: WorktreeSetup = {
1058
- cwd: repo.toplevel,
1059
- baseCommit: repo.baseCommit,
1060
- worktrees: Array.from({ length: count }, (_, index) => {
1061
- const naming = buildWorktreeNaming({ runId, index, agent: options?.agents?.[index], label: options?.labels?.[index], task: options?.tasks?.[index], branchPrefix });
1062
- const worktreePath = buildNativeWorktreePath(baseDir!, runId, index);
1063
- return {
1064
- path: worktreePath,
1065
- agentCwd: repo.cwdRelative ? path.join(worktreePath, repo.cwdRelative) : worktreePath,
1066
- branch: naming.requestedBranch,
1067
- index,
1068
- nodeModulesLinked: false,
1069
- syntheticPaths: [],
1070
- provider: "native",
1071
- naming,
1072
- };
1073
- }),
1074
- };
1075
- options?.beforeCreate?.(plannedSetup);
1076
1341
  for (let index = 0; index < count; index++) {
1077
- worktrees.push(createNativeWorktree(
1078
- repo.toplevel,
1342
+ worktrees[index] = await createNativeWorktree(
1343
+ tx,
1344
+ repo.toplevel,
1079
1345
  repo.cwdRelative,
1080
1346
  runId,
1081
1347
  index,
1082
1348
  repo.baseCommit,
1083
1349
  setupHook,
1084
1350
  options?.agents?.[index],
1085
- baseDir!,
1351
+ dedicatedRoot!,
1086
1352
  options?.labels,
1087
1353
  options?.tasks,
1088
1354
  branchPrefix,
1089
- ));
1355
+ );
1090
1356
  }
1091
1357
  } else {
1092
1358
  for (let index = 0; index < count; index++) {
1093
- worktrees.push(createWorktrunkWorktree(
1359
+ await createWorktrunkWorktree(
1360
+ tx,
1094
1361
  repo.toplevel,
1095
1362
  repo.cwdRelative,
1096
1363
  runId,
@@ -1100,13 +1367,11 @@ export function createWorktrees(cwd: string, runId: string, count: number, optio
1100
1367
  options?.labels,
1101
1368
  options?.tasks,
1102
1369
  branchPrefix,
1103
- ));
1370
+ );
1104
1371
  }
1105
- // Worktrunk determines its path only after creation. Journal the exact
1106
- // returned ownership before Pi runs setup hooks or launches a child.
1107
- options?.beforeCreate?.({ cwd: repo.toplevel, baseCommit: repo.baseCommit, worktrees: [...worktrees] });
1108
1372
  for (let index = 0; index < worktrees.length; index++) {
1109
- worktrees[index] = finalizeCreatedWorktree(
1373
+ worktrees[index] = await finalizeCreatedWorktree(
1374
+ tx,
1110
1375
  repo.toplevel,
1111
1376
  repo.cwdRelative,
1112
1377
  runId,
@@ -1117,23 +1382,37 @@ export function createWorktrees(cwd: string, runId: string, count: number, optio
1117
1382
  );
1118
1383
  }
1119
1384
  }
1385
+ tx.check();
1386
+ tx.progress.phase = "ready";
1387
+ tx.publish();
1388
+ tx.check();
1389
+ return tx.progress.setup;
1120
1390
  } catch (error) {
1121
- cleanupWorktrees({
1122
- cwd: repo.toplevel,
1123
- worktrees,
1124
- baseCommit: repo.baseCommit,
1125
- }, { kind: "setup-rollback" });
1391
+ await compensateSetup(tx);
1126
1392
  throw error;
1127
1393
  }
1394
+ }
1128
1395
 
1129
- return {
1130
- cwd: repo.toplevel,
1131
- worktrees,
1132
- baseCommit: repo.baseCommit,
1133
- };
1396
+ export async function createWorktrees(cwd: string, runId: string, count: number, options: CreateWorktreesOptions = {}): Promise<WorktreeSetup> {
1397
+ const tx = new SetupTransaction(cwd, options);
1398
+ try {
1399
+ return await withWorktreeTransaction(async () => {
1400
+ setupActive = true;
1401
+ try { return await allocateWorktrees(tx, cwd, runId, count); }
1402
+ finally { setupActive = false; }
1403
+ });
1404
+ } catch (error) {
1405
+ if (setupPoison) tx.progress.unknown ??= setupPoison;
1406
+ tx.progress.cleanup ??= { state: tx.progress.unknown ? "partial" : "complete", tasks: [], pruned: false, errors: tx.progress.unknown ? [tx.progress.unknown] : [] };
1407
+ try { tx.publish(); } catch (publicationError) {
1408
+ tx.progress.cleanup?.errors?.push(`Recovery publication failed: ${String(publicationError)}`);
1409
+ }
1410
+ throw new WorktreeSetupError(error, tx.snapshot());
1411
+ }
1134
1412
  }
1135
1413
 
1136
1414
  export function diffWorktrees(setup: WorktreeSetup, agents: string[], diffsDir: string): WorktreeDiff[] {
1415
+ assertWorktreeMutationAllowed();
1137
1416
  try {
1138
1417
  fs.mkdirSync(diffsDir, { recursive: true });
1139
1418
  } catch {
@@ -1163,6 +1442,7 @@ export function cleanupWorktrees(
1163
1442
  setup: WorktreeSetup,
1164
1443
  intent: WorktreeCleanupIntent = { kind: "preserve", ...(setup.capturedDiffs ? { capturedDiffs: setup.capturedDiffs } : {}) },
1165
1444
  ): WorktreeCleanupReport {
1445
+ assertWorktreeMutationAllowed();
1166
1446
  const tasks: WorktreeCleanupTask[] = [];
1167
1447
  for (let index = setup.worktrees.length - 1; index >= 0; index--) {
1168
1448
  tasks.push(cleanupSingleWorktree(setup, setup.worktrees[index]!, intent));