@jr2/orchestrator 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/console/components/app.ts +55 -15
  2. package/console/components/bottom-drawer.ts +259 -0
  3. package/console/components/drawer.ts +14 -6
  4. package/console/components/fleet.ts +36 -13
  5. package/console/components/machine-pane.ts +61 -5
  6. package/console/components/nav.ts +54 -8
  7. package/console/conversation.ts +161 -0
  8. package/console/login.ts +123 -0
  9. package/console/main.ts +600 -60
  10. package/console/store.ts +611 -64
  11. package/console/style.css +231 -0
  12. package/docs/agents-block.md +2 -2
  13. package/docs/concepts/agents-and-turns.md +142 -29
  14. package/docs/concepts/architecture.md +60 -34
  15. package/docs/concepts/callers-and-hooks.md +205 -0
  16. package/docs/concepts/charts.md +13 -8
  17. package/docs/concepts/composition.md +10 -1
  18. package/docs/concepts/converge.md +7 -2
  19. package/docs/concepts/durable-runs.md +43 -16
  20. package/docs/concepts/effects.md +133 -50
  21. package/docs/concepts/gates.md +95 -25
  22. package/docs/concepts/machines-and-workflows.md +14 -4
  23. package/docs/concepts/pools-and-sources.md +193 -17
  24. package/docs/concepts/secrets-and-trust.md +186 -62
  25. package/docs/concepts/the-menu.md +14 -0
  26. package/docs/concepts/workspaces.md +135 -56
  27. package/docs/get-started/a-model-decides.md +14 -13
  28. package/docs/get-started/an-agent-in-a-sandbox.md +46 -35
  29. package/docs/get-started/connect-a-model.md +1 -1
  30. package/docs/get-started/create-an-instance.md +17 -14
  31. package/docs/get-started/have-your-assistant-write-a-machine.md +111 -40
  32. package/docs/get-started/install.md +4 -0
  33. package/docs/guides/add-tools-to-the-sandbox.md +72 -15
  34. package/docs/guides/answer-a-gate-from-a-forge.md +304 -0
  35. package/docs/guides/answer-a-gate.md +74 -37
  36. package/docs/guides/bind-open-parts.md +29 -12
  37. package/docs/guides/call-an-mcp-tool-from-a-machine.md +572 -0
  38. package/docs/guides/cancel-a-run.md +26 -16
  39. package/docs/guides/cut-a-worktree.md +560 -0
  40. package/docs/guides/fix-a-failed-converge.md +19 -7
  41. package/docs/guides/give-a-caller-a-scope.md +203 -0
  42. package/docs/guides/give-an-agent-mcp-tools.md +422 -0
  43. package/docs/guides/iterate-on-a-machine-on-kind.md +10 -9
  44. package/docs/guides/let-an-agent-write-a-chart.md +14 -7
  45. package/docs/guides/look-inside-a-sandbox.md +115 -42
  46. package/docs/guides/move-from-kind-to-a-shared-cluster.md +16 -0
  47. package/docs/guides/open-the-console.md +63 -14
  48. package/docs/guides/push-and-sign-a-branch.md +67 -21
  49. package/docs/guides/read-a-conversation.md +225 -0
  50. package/docs/guides/run-a-chart-by-name.md +17 -16
  51. package/docs/guides/run-a-command-or-call-a-url.md +84 -41
  52. package/docs/guides/run-one-worker-per-item.md +34 -27
  53. package/docs/guides/set-an-agents-access-and-dials.md +69 -22
  54. package/docs/guides/size-a-workspace.md +19 -6
  55. package/docs/guides/start-a-run-for-each-alert.md +290 -0
  56. package/docs/guides/start-a-run-from-a-run.md +18 -16
  57. package/docs/guides/start-a-run.md +24 -18
  58. package/docs/guides/steer-an-agent.md +332 -0
  59. package/docs/guides/use-a-hosted-provider.md +7 -1
  60. package/docs/guides/watch-a-run.md +146 -117
  61. package/docs/guides/write-a-hook-kind.md +238 -0
  62. package/docs/guides/write-a-tool-as-an-mcp-server.md +351 -0
  63. package/docs/guides/write-a-workflow.md +18 -8
  64. package/docs/llms.txt +110 -23
  65. package/docs/reference/authoring-surface/agent.md +35 -12
  66. package/docs/reference/authoring-surface/answer.md +20 -4
  67. package/docs/reference/authoring-surface/arg.md +28 -6
  68. package/docs/reference/authoring-surface/basic.md +1 -1
  69. package/docs/reference/authoring-surface/bearer.md +1 -1
  70. package/docs/reference/authoring-surface/branchname.md +15 -0
  71. package/docs/reference/authoring-surface/carry.md +23 -0
  72. package/docs/reference/authoring-surface/chartschema.md +1 -1
  73. package/docs/reference/authoring-surface/chartschemafor.md +1 -1
  74. package/docs/reference/authoring-surface/command.md +37 -22
  75. package/docs/reference/authoring-surface/customize.md +4 -1
  76. package/docs/reference/authoring-surface/defineconfig.md +1 -1
  77. package/docs/reference/authoring-surface/defineevent.md +1 -1
  78. package/docs/reference/authoring-surface/dispatch.md +5 -4
  79. package/docs/reference/authoring-surface/fail.md +18 -5
  80. package/docs/reference/authoring-surface/file.md +37 -17
  81. package/docs/reference/authoring-surface/frompick.md +1 -1
  82. package/docs/reference/authoring-surface/gate.md +118 -0
  83. package/docs/reference/authoring-surface/group-agents.md +15 -0
  84. package/docs/reference/authoring-surface/group-charts.md +16 -0
  85. package/docs/reference/authoring-surface/group-composition.md +25 -0
  86. package/docs/reference/authoring-surface/group-config.md +17 -0
  87. package/docs/reference/authoring-surface/group-effects.md +48 -0
  88. package/docs/reference/authoring-surface/group-failure.md +13 -0
  89. package/docs/reference/authoring-surface/group-gates.md +16 -0
  90. package/docs/reference/authoring-surface/group-machine-and-events.md +15 -0
  91. package/docs/reference/authoring-surface/group-steering.md +12 -0
  92. package/docs/reference/authoring-surface/group-types-these-signatures-name.md +137 -0
  93. package/docs/reference/authoring-surface/group-workspaces-and-repos.md +15 -0
  94. package/docs/reference/authoring-surface/http.md +109 -9
  95. package/docs/reference/authoring-surface/isfailure.md +1 -1
  96. package/docs/reference/authoring-surface/isready.md +14 -0
  97. package/docs/reference/authoring-surface/jr2setup.md +14 -4
  98. package/docs/reference/authoring-surface/mcp.md +107 -0
  99. package/docs/reference/authoring-surface/open.md +41 -8
  100. package/docs/reference/authoring-surface/openbody.md +1 -1
  101. package/docs/reference/authoring-surface/paramsof.md +23 -5
  102. package/docs/reference/authoring-surface/pool.md +79 -11
  103. package/docs/reference/authoring-surface/push.md +31 -13
  104. package/docs/reference/authoring-surface/readyof.md +21 -0
  105. package/docs/reference/authoring-surface/reread.md +19 -0
  106. package/docs/reference/authoring-surface/secret.md +26 -5
  107. package/docs/reference/authoring-surface/sentby.md +1 -1
  108. package/docs/reference/authoring-surface/skip.md +22 -0
  109. package/docs/reference/authoring-surface/source.md +59 -4
  110. package/docs/reference/authoring-surface/steer.md +93 -0
  111. package/docs/reference/authoring-surface/text.md +1 -1
  112. package/docs/reference/authoring-surface/type-agentdefinition.md +16 -3
  113. package/docs/reference/authoring-surface/type-agentmcpserver.md +54 -0
  114. package/docs/reference/authoring-surface/type-agentslots.md +4 -1
  115. package/docs/reference/authoring-surface/type-anyframepath.md +14 -0
  116. package/docs/reference/authoring-surface/type-anyworktreepath.md +15 -0
  117. package/docs/reference/authoring-surface/type-bindactors.md +14 -0
  118. package/docs/reference/authoring-surface/type-binding.md +6 -7
  119. package/docs/reference/authoring-surface/type-caller.md +40 -0
  120. package/docs/reference/authoring-surface/type-callerrecord.md +32 -0
  121. package/docs/reference/authoring-surface/type-carry.md +19 -0
  122. package/docs/reference/authoring-surface/type-chainlink.md +3 -3
  123. package/docs/reference/authoring-surface/type-commandinput.md +4 -3
  124. package/docs/reference/authoring-surface/type-commandoptions.md +0 -22
  125. package/docs/reference/authoring-surface/type-commandoutput.md +23 -5
  126. package/docs/reference/authoring-surface/type-customize.md +20 -3
  127. package/docs/reference/authoring-surface/type-delivered.md +17 -0
  128. package/docs/reference/authoring-surface/type-deliveredevent.md +18 -0
  129. package/docs/reference/authoring-surface/type-dispatchdefinition.md +4 -3
  130. package/docs/reference/authoring-surface/type-dispatchoutput.md +9 -2
  131. package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
  132. package/docs/reference/authoring-surface/type-effectbinding.md +12 -2
  133. package/docs/reference/authoring-surface/type-filebody.md +4 -8
  134. package/docs/reference/authoring-surface/type-framepath.md +14 -0
  135. package/docs/reference/authoring-surface/type-gatebinding.md +15 -0
  136. package/docs/reference/authoring-surface/type-gatehooks.md +12 -0
  137. package/docs/reference/authoring-surface/type-gatelook.md +37 -0
  138. package/docs/reference/authoring-surface/type-gateoutcome.md +32 -0
  139. package/docs/reference/authoring-surface/type-gatereply.md +19 -0
  140. package/docs/reference/authoring-surface/type-gateslot.md +21 -0
  141. package/docs/reference/authoring-surface/type-gateslotinput.md +13 -0
  142. package/docs/reference/authoring-surface/type-gateslotparts.md +18 -0
  143. package/docs/reference/authoring-surface/type-gateslots.md +12 -0
  144. package/docs/reference/authoring-surface/type-gatetypesat.md +13 -0
  145. package/docs/reference/authoring-surface/type-hostinjectedinput.md +3 -5
  146. package/docs/reference/authoring-surface/type-httpdefinition.md +6 -3
  147. package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
  148. package/docs/reference/authoring-surface/type-httpoutput.md +8 -2
  149. package/docs/reference/authoring-surface/type-jr2config.md +37 -2
  150. package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
  151. package/docs/reference/authoring-surface/type-lends.md +5 -1
  152. package/docs/reference/authoring-surface/type-lookoutput.md +14 -0
  153. package/docs/reference/authoring-surface/type-mcpcalloutput.md +78 -0
  154. package/docs/reference/authoring-surface/type-mcpnotreturned.md +42 -0
  155. package/docs/reference/authoring-surface/type-mcpoptions.md +47 -0
  156. package/docs/reference/authoring-surface/type-mcpoutput.md +50 -0
  157. package/docs/reference/authoring-surface/type-mcpreadoutput.md +53 -0
  158. package/docs/reference/authoring-surface/type-mcpremoteserver.md +50 -0
  159. package/docs/reference/authoring-surface/type-mcpserver.md +41 -0
  160. package/docs/reference/authoring-surface/type-mcpstdioserver.md +42 -0
  161. package/docs/reference/authoring-surface/type-namedsecret.md +4 -4
  162. package/docs/reference/authoring-surface/type-namestart.md +77 -0
  163. package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +1 -6
  164. package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
  165. package/docs/reference/authoring-surface/type-openbodydeclaration.md +1 -1
  166. package/docs/reference/authoring-surface/type-openbodyoutput.md +1 -1
  167. package/docs/reference/authoring-surface/type-opencommandinput.md +7 -2
  168. package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +1 -6
  169. package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +5 -10
  170. package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
  171. package/docs/reference/authoring-surface/type-poolspec.md +11 -6
  172. package/docs/reference/authoring-surface/type-pushinput.md +22 -3
  173. package/docs/reference/authoring-surface/type-pushoutput.md +33 -8
  174. package/docs/reference/authoring-surface/type-pushsourcespec.md +38 -0
  175. package/docs/reference/authoring-surface/type-readsourcespec.md +47 -0
  176. package/docs/reference/authoring-surface/type-reposlot.md +6 -6
  177. package/docs/reference/authoring-surface/type-reread.md +18 -0
  178. package/docs/reference/authoring-surface/type-runsconfig.md +41 -0
  179. package/docs/reference/authoring-surface/type-sandboxoptions.md +11 -4
  180. package/docs/reference/authoring-surface/type-sender.md +26 -1
  181. package/docs/reference/authoring-surface/type-sendercallerfield.md +14 -0
  182. package/docs/reference/authoring-surface/type-size.md +1 -1
  183. package/docs/reference/authoring-surface/type-skip.md +19 -0
  184. package/docs/reference/authoring-surface/type-slothandles.md +5 -13
  185. package/docs/reference/authoring-surface/type-sourcehooks.md +23 -0
  186. package/docs/reference/authoring-surface/type-sourcein.md +25 -0
  187. package/docs/reference/authoring-surface/type-sourcereply.md +18 -0
  188. package/docs/reference/authoring-surface/type-sourcespec.md +26 -17
  189. package/docs/reference/authoring-surface/type-sourceverdict.md +49 -0
  190. package/docs/reference/authoring-surface/type-standingpoolmachine.md +12 -0
  191. package/docs/reference/authoring-surface/type-standingpoolspec.md +51 -0
  192. package/docs/reference/authoring-surface/type-steerargs.md +20 -0
  193. package/docs/reference/authoring-surface/type-thinkinglevel.md +1 -1
  194. package/docs/reference/authoring-surface/type-usercontainer.md +1 -1
  195. package/docs/reference/authoring-surface/type-workspaced.md +30 -9
  196. package/docs/reference/authoring-surface/type-workspacehandles.md +5 -6
  197. package/docs/reference/authoring-surface/type-workspaceoptions.md +1 -13
  198. package/docs/reference/authoring-surface/type-worktreefound.md +16 -0
  199. package/docs/reference/authoring-surface/type-worktreeinput.md +56 -0
  200. package/docs/reference/authoring-surface/type-worktreekey.md +14 -0
  201. package/docs/reference/authoring-surface/type-worktreeoptions.md +29 -0
  202. package/docs/reference/authoring-surface/type-worktreeoutput.md +45 -0
  203. package/docs/reference/authoring-surface/type-worktreepath.md +16 -0
  204. package/docs/reference/authoring-surface/type-worktreeready.md +37 -0
  205. package/docs/reference/authoring-surface/type-worktreerefused.md +21 -0
  206. package/docs/reference/authoring-surface/workspace.md +66 -38
  207. package/docs/reference/authoring-surface/worktree.md +71 -0
  208. package/docs/reference/authoring-surface.md +88 -147
  209. package/docs/reference/cli.md +413 -74
  210. package/docs/reference/config.md +248 -6
  211. package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +10 -9
  212. package/docs/reference/decisions/0002-duplex-streaming-actor.md +81 -14
  213. package/docs/reference/decisions/0004-worktree-provisioning-model.md +27 -21
  214. package/docs/reference/decisions/0005-sandbox-pod-composition.md +39 -25
  215. package/docs/reference/decisions/0006-agent-control-surface.md +6 -3
  216. package/docs/reference/decisions/0007-durable-machine-state.md +1 -1
  217. package/docs/reference/decisions/0009-cli-and-instance-interface.md +150 -49
  218. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +5 -3
  219. package/docs/reference/decisions/0011-workflow-defined-events.md +256 -12
  220. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +39 -32
  221. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +54 -30
  222. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +53 -27
  223. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +26 -16
  224. package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +9 -7
  225. package/docs/reference/decisions/0017-pool-over-a-source-port.md +226 -10
  226. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +15 -7
  227. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +53 -27
  228. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +16 -2
  229. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +4 -4
  230. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +124 -43
  231. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +36 -38
  232. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +10 -5
  233. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +16 -7
  234. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +36 -19
  235. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +18 -10
  236. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +3 -2
  237. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +23 -15
  238. package/docs/reference/decisions/0032-the-console-unlocks-with-a-callers-credential.md +101 -0
  239. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +22 -22
  240. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +6 -6
  241. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +13 -12
  242. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +23 -23
  243. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +6 -1
  244. package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +57 -46
  245. package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +5 -0
  246. package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +20 -8
  247. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +55 -40
  248. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +14 -13
  249. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +31 -24
  250. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +18 -10
  251. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +137 -86
  252. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +165 -91
  253. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +7 -7
  254. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +2 -2
  255. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md +109 -0
  256. package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +53 -23
  257. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +221 -153
  258. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +161 -96
  259. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +32 -9
  260. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +48 -33
  261. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +56 -30
  262. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +35 -25
  263. package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +1 -1
  264. package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +296 -0
  265. package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +347 -0
  266. package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
  267. package/docs/reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md +285 -0
  268. package/docs/reference/decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md +198 -0
  269. package/docs/reference/decisions.md +9 -4
  270. package/docs/reference/glossary.md +138 -45
  271. package/docs/reference/hooks.md +1077 -0
  272. package/docs/reference/http-api.md +988 -123
  273. package/docs/reference/machines.md +0 -1
  274. package/docs/why.md +7 -5
  275. package/package.json +10 -3
  276. package/src/actor.ts +194 -42
  277. package/src/agent.ts +56 -6
  278. package/src/ambient.ts +3 -5
  279. package/src/assembly-checks.ts +530 -4
  280. package/src/callers.ts +245 -0
  281. package/src/chart.ts +238 -26
  282. package/src/charts.ts +9 -2
  283. package/src/config.ts +566 -7
  284. package/src/conversations.ts +453 -0
  285. package/src/custodian.lua +17 -5
  286. package/src/custodian.ts +257 -77
  287. package/src/customize.ts +154 -7
  288. package/src/dispatch-effect.ts +142 -26
  289. package/src/durability.ts +2 -0
  290. package/src/duration.ts +20 -0
  291. package/src/effect-checks.ts +52 -4
  292. package/src/effect.ts +401 -132
  293. package/src/effector-wire.ts +278 -30
  294. package/src/fail.ts +17 -4
  295. package/src/gate-brand.ts +16 -0
  296. package/src/gate.ts +634 -48
  297. package/src/harness-client.ts +84 -35
  298. package/src/harness-env.ts +33 -0
  299. package/src/held-secrets.ts +197 -39
  300. package/src/holds.ts +5 -2
  301. package/src/hook-kinds/discord.ts +92 -0
  302. package/src/hook-kinds/door.ts +107 -0
  303. package/src/hook-kinds/github.ts +89 -0
  304. package/src/hook-kinds/gitlab.ts +72 -0
  305. package/src/hook-kinds/hook.ts +7 -0
  306. package/src/hook-kinds/index.ts +465 -0
  307. package/src/hook-kinds/slack.ts +151 -0
  308. package/src/hook-kinds/webhook.ts +113 -0
  309. package/src/hooks.ts +506 -0
  310. package/src/http-effect.ts +164 -70
  311. package/src/http.ts +933 -99
  312. package/src/index.ts +30 -5
  313. package/src/instance.ts +196 -26
  314. package/src/internal.ts +23 -3
  315. package/src/kube-client.ts +6 -3
  316. package/src/machine-doc.ts +29 -5
  317. package/src/mcp-effect.ts +424 -0
  318. package/src/model-keys.ts +24 -0
  319. package/src/mount-digest.ts +41 -7
  320. package/src/names.ts +82 -7
  321. package/src/notices.ts +576 -59
  322. package/src/open-body.ts +110 -30
  323. package/src/open-effect.ts +17 -4
  324. package/src/open.ts +40 -7
  325. package/src/parts.ts +125 -35
  326. package/src/party.ts +35 -1
  327. package/src/pick.ts +43 -9
  328. package/src/pool.ts +1172 -172
  329. package/src/registration.ts +249 -17
  330. package/src/repo-identity.ts +7 -3
  331. package/src/repos.ts +20 -2
  332. package/src/run-host.ts +1614 -190
  333. package/src/run-list.ts +191 -0
  334. package/src/sandbox-effect.ts +217 -66
  335. package/src/sandbox-kube.ts +116 -21
  336. package/src/secret-placement.ts +248 -35
  337. package/src/server.ts +53 -4
  338. package/src/setup.ts +158 -38
  339. package/src/snapshot-store.ts +565 -48
  340. package/src/steer.ts +111 -0
  341. package/src/stub-harness.ts +6 -2
  342. package/src/tokens.ts +362 -46
  343. package/src/vocabulary.ts +23 -4
  344. package/src/wire.ts +27 -10
  345. package/src/workspace.ts +314 -259
  346. package/src/worktree.ts +249 -0
  347. package/docs/reference/authoring-surface/type-noslotspec.md +0 -26
  348. package/docs/reference/authoring-surface/type-workspacespec.md +0 -37
  349. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +0 -61
  350. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +0 -65
package/src/effect.ts CHANGED
@@ -39,11 +39,12 @@ import { randomUUID } from "node:crypto";
39
39
  import type { ActorLogic, AnyActorRef, EventObject, Snapshot } from "xstate";
40
40
  import { ambientHandlesFor, type AmbientHandles } from "./ambient.ts";
41
41
  import { boundRun, type AnyActorSystem } from "./registration.ts";
42
- import type { HttpMethod, HttpOutput, TextPart, WireAuth } from "./effector-wire.ts";
42
+ import type { HttpMethod, HttpOutput, McpOutput, TextPart, WireAuth } from "./effector-wire.ts";
43
43
  import type { ExecOutcome, ExecRead, ExecRequest, SyncAnswer } from "./wire.ts";
44
44
  import { bindOpenSecretIn, type EffectorPortFactory, type OpenSecretIn } from "./http-effect.ts";
45
45
  import { commandEnvParts, credentialedCommandWith, sandboxEffectorPort, type CommandEnv } from "./sandbox-effect.ts";
46
46
  import { open } from "./open.ts";
47
+ import type { AnyWorktreePath, WorktreeOutput } from "./worktree.ts";
47
48
  import {
48
49
  effectBindingShape,
49
50
  openEffectLogic,
@@ -76,30 +77,52 @@ export type Arg<N extends string = string> = { readonly arg: N };
76
77
  *
77
78
  * @example
78
79
  * ```ts
79
- * import { arg, command, fail, jr2Setup, workspace, type Workspaced } from "@jr2/orchestrator";
80
+ * import {
81
+ * arg,
82
+ * command,
83
+ * fail,
84
+ * isReady,
85
+ * jr2Setup,
86
+ * workspace,
87
+ * worktree,
88
+ * type Workspaced,
89
+ * } from "@jr2/orchestrator";
80
90
  *
81
91
  * type Input = Workspaced<{ file: string }, "app">;
82
92
  *
93
+ * const lintTree = (context: Input) => `${context.workspace.repos.app}/lint` as const;
94
+ *
83
95
  * const body = jr2Setup({
84
96
  * types: {} as { context: Input; input: Input },
85
- * actors: { lint: command(["npx", "eslint", arg("file")]), fail: fail() },
97
+ * actors: { cut: worktree(), lint: command(["npx", "eslint", arg("file")]), fail: fail() },
86
98
  * }).createMachine({
87
99
  * id: "lint",
88
100
  * context: ({ input }) => input,
89
101
  * on: { "workspace.lost": ".lost" },
90
- * initial: "lint",
102
+ * initial: "cutting",
91
103
  * states: {
104
+ * cutting: {
105
+ * invoke: {
106
+ * src: "cut",
107
+ * input: ({ context }) => ({ path: lintTree(context), branch: "lint" }),
108
+ * onDone: [{ guard: ({ event }) => isReady(event.output), target: "lint" }, { target: "notCut" }],
109
+ * },
110
+ * },
92
111
  * lint: {
93
- * invoke: { src: "lint", input: ({ context }) => ({ file: context.file }), onDone: "done" },
112
+ * invoke: {
113
+ * src: "lint",
114
+ * input: ({ context }) => ({ file: context.file, worktree: lintTree(context) }),
115
+ * onDone: "done",
116
+ * },
94
117
  * },
95
118
  * done: { type: "final" },
119
+ * notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
96
120
  * lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
97
121
  * },
98
122
  * });
99
123
  *
100
124
  * export const machine = workspace(body, {
101
125
  * repos: { app: { url: "https://github.com/you/app.git" } },
102
- * spec: () => ({ branch: "lint" }),
103
126
  * });
104
127
  * ```
105
128
  *
@@ -109,6 +132,13 @@ export type Arg<N extends string = string> = { readonly arg: N };
109
132
  */
110
133
  export const arg = <const N extends string>(name: N): Arg<N> => ({ arg: name });
111
134
 
135
+ /** What an Effect's input takes: `required` keys, `optional` keys, and exactly one of `oneOf`. */
136
+ export type InputShape = {
137
+ readonly required: readonly string[];
138
+ readonly optional: readonly string[];
139
+ readonly oneOf?: readonly string[];
140
+ };
141
+
112
142
  /** A `command` Effect's plain data. */
113
143
  export type CommandParts = {
114
144
  readonly kind: "command";
@@ -124,8 +154,12 @@ export type CommandParts = {
124
154
  /** A credentialed command's `pin`: its input names the commit (`{ sha }`, an approved one) in
125
155
  * place of the Agent's branch tip (ADR-0067). */
126
156
  readonly pin?: true;
127
- /** The Repo Slot whose Worktree is the cwd. Needed only where the Workspace carries several. */
128
- readonly slot?: string;
157
+ /** The kit's cut (`worktree()`): its `expect`, static, so that a Chart cannot change it. A command
158
+ * with this is the kit's program, and its output is a `WorktreeOutput`. */
159
+ readonly cut?: { readonly expect?: "new" | "existing" };
160
+ /** Marked by the kit, on a cut alone: a restore or a Harness restart sends it again, under its
161
+ * id. An author has no such option: a command is at most once (ADR-0067). */
162
+ readonly idempotent?: true;
129
163
  readonly timeoutMs: number;
130
164
  /** Characters of the stream's end kept in the output. The full stream goes to the feed. */
131
165
  readonly tail: number;
@@ -156,16 +190,15 @@ export type HttpParts = {
156
190
  /** Marked by the author: a restore that cannot find the Effect sends it again. */
157
191
  readonly idempotent: boolean;
158
192
  /** The body is a Worktree file (`body: file(...)`, ADR-0067): the Effect runs in the Sandbox
159
- * Effector of the Workspace whose body invokes it. `slot` is the Repo Slot whose Worktree holds
160
- * the file — needed only where the Workspace carries several. Absent: a json, text or no body, in
193
+ * Effector of the Workspace whose body invokes it. The input's `worktree` holds the file. Absent: a json, text or no body, in
161
194
  * the Instance Effector. */
162
- readonly file?: { readonly slot?: string; readonly contentType?: string };
195
+ readonly file?: { readonly contentType?: string };
163
196
  };
164
197
 
165
198
  /**
166
- * A `push` Effect's plain data (ADR-0067, ADR-0051): the Workspace's branch to its Repo Slot's
199
+ * A `push` Effect's plain data (ADR-0067, ADR-0075): a cut Worktree's branch to its Repo Slot's
167
200
  * Binding. `sign`, `squash`, `force` are the author's; `pin` says the Effect's input names the
168
- * commit (an approved one) instead of the Agent's branch tip.
201
+ * commit (an approved one) instead of the branch's tip.
169
202
  */
170
203
  export type PushParts = {
171
204
  readonly kind: "push";
@@ -207,6 +240,51 @@ export type DispatchParts = {
207
240
  readonly openSecret: boolean;
208
241
  };
209
242
 
243
+ /** What an `mcp` Effect asks of its server (ADR-0079): call a tool, read a resource, or get a
244
+ * prompt. */
245
+ export type McpOp = "call" | "read" | "prompt";
246
+
247
+ /**
248
+ * An `mcp` Effect's plain data (ADR-0067, ADR-0079): what `jr2 up` and the Machine doc read — the
249
+ * operation, the server by its name in the `mcp` block of `jr2.config.ts`, the tool (a call), the
250
+ * resource's uri (a read) or the prompt's name (a prompt), the input's keys, the author's timeout,
251
+ * the bound on the output, and whether a restore may send it again. The server's url and secret
252
+ * are the config's, never a part: `jr2 up` reads them from there, and the Instance Effector holds
253
+ * them in its `effector.json` and its secrets.
254
+ */
255
+ export type McpParts = {
256
+ readonly kind: "mcp";
257
+ readonly op: McpOp;
258
+ readonly server: string;
259
+ /** `mcp.call`'s tool. */
260
+ readonly tool?: string;
261
+ /** `mcp.read`'s resource uri. */
262
+ readonly uri?: string;
263
+ /** `mcp.prompt`'s prompt name. */
264
+ readonly prompt?: string;
265
+ /** The keys of the input schema: what a pick param may fill (ADR-0067's load walk reads them). */
266
+ readonly inputKeys: readonly string[];
267
+ /** The author's timeout, in milliseconds. Absent: the server's `timeout` in `jr2.config.ts`. */
268
+ readonly timeoutMs?: number;
269
+ /** The most bytes of `text`, and of `structured` as JSON, in the output. */
270
+ readonly maxBody: number;
271
+ /** Marked by the author: a restore that cannot find the Effect sends it again. The server's
272
+ * `idempotentHint` is not read (ADR-0079). */
273
+ readonly idempotent: boolean;
274
+ /** Always empty: an `mcp` Effect names no secret of its own. Its server's secret is placed in
275
+ * the Instance Effector by `jr2 up`, from the config (ADR-0079). */
276
+ readonly secrets: readonly string[];
277
+ /** Never: the server, not a `secret(open)`, is what a package leaves Open (ADR-0054). */
278
+ readonly openSecret?: never;
279
+ };
280
+
281
+ /** An `mcp` Effect whose server or tool a package left Open (ADR-0054, ADR-0079): the sentinel in
282
+ * that place, until the composer binds it with `customize()`. */
283
+ export type OpenMcpParts = Omit<McpParts, "server" | "tool"> & {
284
+ readonly server: string | typeof open;
285
+ readonly tool?: string | typeof open;
286
+ };
287
+
210
288
  /**
211
289
  * An Effect whose url or argv a package left Open (ADR-0054): the same parts with the sentinel in
212
290
  * that one place. The kind, the placement, the timeout and the secrets the other parts name are
@@ -231,10 +309,12 @@ export type EffectParts =
231
309
  | OpenCommandParts
232
310
  | OpenHttpParts
233
311
  | DispatchParts
234
- | OpenDispatchParts;
312
+ | OpenDispatchParts
313
+ | McpParts
314
+ | OpenMcpParts;
235
315
 
236
316
  /** The kinds, as their parts name them. */
237
- export const EFFECT_KINDS: ReadonlySet<string> = new Set(["command", "http", "push", "dispatch"]);
317
+ export const EFFECT_KINDS: ReadonlySet<string> = new Set(["command", "http", "push", "dispatch", "mcp"]);
238
318
 
239
319
  /**
240
320
  * Where an Effect runs, derived from its kind and what it names, never authored (ADR-0067,
@@ -242,7 +322,8 @@ export const EFFECT_KINDS: ReadonlySet<string> = new Set(["command", "http", "pu
242
322
  * `http` Effect with no Worktree file in the Instance Effector; an `http` whose body is a Worktree
243
323
  * file, a `push` and a `command` that names a secret in the Sandbox Effector of its Workspace; a
244
324
  * `dispatch` with no target in the Orchestrator's own process, and one with a target (an Open one
245
- * included: it has one once bound) in the Instance Effector (ADR-0071).
325
+ * included: it has one once bound) in the Instance Effector (ADR-0071); an `mcp` Effect in the
326
+ * Instance Effector, which calls remote servers only (ADR-0079).
246
327
  */
247
328
  export type Placement = "harness" | "instance" | "sandbox" | "orchestrator";
248
329
  export function placementOf(parts: EffectParts): Placement {
@@ -253,6 +334,8 @@ export function placementOf(parts: EffectParts): Placement {
253
334
  return parts.file ? "sandbox" : "instance";
254
335
  case "push":
255
336
  return "sandbox";
337
+ case "mcp":
338
+ return "instance";
256
339
  case "command":
257
340
  return (parts.secrets?.length ?? 0) > 0 || parts.openSecret ? "sandbox" : "harness";
258
341
  }
@@ -291,10 +374,30 @@ export function targetText(parts: { readonly target: HttpParts["target"] | typeo
291
374
  * `customize()` binds it (ADR-0054); and for a dispatch with no input schema: the callee's door is
292
375
  * the only check, so nothing here says what it reads (ADR-0071). */
293
376
  export function inputKeysOf(parts: EffectParts): string[] | undefined {
294
- if (parts.kind === "dispatch") return parts.inputKeys === undefined ? undefined : [...parts.inputKeys];
295
- if (parts.kind !== "command") return [...parts.inputKeys];
377
+ const shape = inputShapeOf(parts);
378
+ return shape && [...shape.required, ...shape.optional, ...(shape.oneOf ?? [])];
379
+ }
380
+
381
+ /** The shape of a cut's input. */
382
+ const CUT_SHAPE: InputShape = { required: ["path"], optional: ["from"], oneOf: ["branch", "detach"] };
383
+
384
+ /** An Effect's input as a Chart's door and its start hold a Chart to it. Undefined where
385
+ * {@link inputKeysOf} is. `worktree` is optional for a key the Workspace decides: the door makes it
386
+ * required when it knows that the Workspace has Repo Slots, and a start refuses it then. */
387
+ export function inputShapeOf(parts: EffectParts): InputShape | undefined {
388
+ if (parts.kind === "dispatch") {
389
+ return parts.inputKeys === undefined ? undefined : { required: [...parts.inputKeys], optional: [] };
390
+ }
391
+ if (parts.kind === "push" || parts.kind === "mcp") return { required: [...parts.inputKeys], optional: [] };
392
+ if (parts.kind === "http") {
393
+ return { required: [...parts.inputKeys], optional: parts.file ? ["worktree"] : [] };
394
+ }
395
+ if (parts.cut) return CUT_SHAPE;
296
396
  if (parts.argv === open) return undefined;
297
- return [...parts.argv.flatMap((a) => (typeof a === "string" ? [] : [a.arg])), ...(parts.pin ? ["sha"] : [])];
397
+ const keys = [...parts.argv.flatMap((a) => (typeof a === "string" ? [] : [a.arg])), ...(parts.pin ? ["sha"] : [])];
398
+ // A command with an Effect secret runs on a commit of a Worktree, so it names one in each Workspace.
399
+ const credentialed = (parts.secrets?.length ?? 0) > 0 || parts.openSecret === true;
400
+ return credentialed ? { required: [...keys, "worktree"], optional: [] } : { required: keys, optional: ["worktree"] };
298
401
  }
299
402
 
300
403
  // --- Output -------------------------------------------------------------------------------------
@@ -304,6 +407,11 @@ export function inputKeysOf(parts: EffectParts): string[] | undefined {
304
407
  * not run the Effect again. */
305
408
  export type Unknown = { outcome: "unknown"; reason: string };
306
409
 
410
+ /** A value of `outcome` that this kit does not give. It stands in the union so that no author can
411
+ * write an exhaustive check, so that a later outcome is no break: route on what you know, and give
412
+ * the rest a last arm. */
413
+ export type LaterOutcome = { outcome: "(a later outcome)"; reason?: string };
414
+
307
415
  // `dirty` is a credentialed command's alone (ADR-0067): it runs in the Sandbox Effector against
308
416
  // the pinned commit, never the live Worktree.
309
417
  /**
@@ -321,34 +429,48 @@ export type Unknown = { outcome: "unknown"; reason: string };
321
429
  * characters. `dirty` is `true` when a command that names an Effect secret ran against a commit,
322
430
  * and the Worktree held changes that it did not see.
323
431
  *
432
+ * The union is open ({@link LaterOutcome}): give the rest a last arm with no guard.
433
+ *
324
434
  * @example
325
435
  * ```ts
326
436
  * import {
327
437
  * command,
328
438
  * type CommandOutput,
329
439
  * fail,
440
+ * isReady,
330
441
  * jr2Setup,
331
442
  * workspace,
443
+ * worktree,
332
444
  * type Workspaced,
333
445
  * } from "@jr2/orchestrator";
334
446
  *
335
447
  * type Input = Workspaced<{}, "app">;
336
448
  *
449
+ * const testTree = (context: Input) => `${context.workspace.repos.app}/test` as const;
450
+ *
337
451
  * const passed = (output: CommandOutput): boolean =>
338
452
  * output.outcome === "exited" && output.code === 0;
339
453
  *
340
454
  * const body = jr2Setup({
341
455
  * types: {} as { context: Input; input: Input },
342
- * actors: { tests: command(["npm", "test"], { timeout: "15m" }), fail: fail() },
456
+ * actors: { cut: worktree(), tests: command(["npm", "test"], { timeout: "15m" }), fail: fail() },
343
457
  * }).createMachine({
344
458
  * id: "tests",
345
459
  * context: ({ input }) => input,
346
460
  * on: { "workspace.lost": ".lost" },
347
- * initial: "testing",
461
+ * initial: "cutting",
348
462
  * states: {
463
+ * cutting: {
464
+ * invoke: {
465
+ * src: "cut",
466
+ * input: ({ context }) => ({ path: testTree(context), branch: "tests" }),
467
+ * onDone: [{ guard: ({ event }) => isReady(event.output), target: "testing" }, { target: "notCut" }],
468
+ * },
469
+ * },
349
470
  * testing: {
350
471
  * invoke: {
351
472
  * src: "tests",
473
+ * input: ({ context }) => ({ worktree: testTree(context) }),
352
474
  * onDone: [
353
475
  * { guard: ({ event }) => passed(event.output), target: "green" },
354
476
  * { target: "red" },
@@ -358,13 +480,13 @@ export type Unknown = { outcome: "unknown"; reason: string };
358
480
  * green: { type: "final" },
359
481
  * // Not a final state: a final state means `done`, and the run ends `failed` here.
360
482
  * red: { invoke: { src: "fail", input: { reason: "npm test did not pass" } } },
483
+ * notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
361
484
  * lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
362
485
  * },
363
486
  * });
364
487
  *
365
488
  * export const machine = workspace(body, {
366
489
  * repos: { app: { url: "https://github.com/you/app.git" } },
367
- * spec: () => ({ branch: "tests" }),
368
490
  * });
369
491
  * ```
370
492
  *
@@ -372,7 +494,7 @@ export type Unknown = { outcome: "unknown"; reason: string };
372
494
  * @see docs/guides/run-a-command-or-call-a-url.md
373
495
  * @see docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md
374
496
  */
375
- export type CommandOutput = (ExecOutcome & { dirty?: boolean }) | Unknown;
497
+ export type CommandOutput = (ExecOutcome & { dirty?: boolean }) | Unknown | LaterOutcome;
376
498
 
377
499
  // Reach is output, as an http Effect's is (ADR-0067): a body a model wrote can be fixed through
378
500
  // the Chain, so a refusal is output too.
@@ -390,6 +512,8 @@ export type CommandOutput = (ExecOutcome & { dirty?: boolean }) | Unknown;
390
512
  * - `unreachable`: the Orchestrator of the target did not answer. No run started.
391
513
  * - `unknown`: jr2 cannot tell if a run started.
392
514
  *
515
+ * The union is open ({@link LaterOutcome}): give the rest a last arm with no guard.
516
+ *
393
517
  * @see docs/concepts/effects.md
394
518
  */
395
519
  export type DispatchOutput =
@@ -402,7 +526,8 @@ export type DispatchOutput =
402
526
  }
403
527
  | { outcome: "refused"; status: number; report: string[] }
404
528
  | { outcome: "unreachable"; reason: string }
405
- | Unknown;
529
+ | Unknown
530
+ | LaterOutcome;
406
531
 
407
532
  // --- The ledger row (persisted in the run's blob, ADR-0007) --------------------------------------
408
533
 
@@ -417,7 +542,7 @@ export type EffectRow =
417
542
  | { state: "intent"; kind: "command"; slot: string; endpoint: string; placement: string; at: string }
418
543
  /** An http Effect's intent names its placement and no replica (ADR-0068): a restore asks every
419
544
  * replica. `idempotent` is the author's mark, which the restore rule reads. */
420
- | { state: "intent"; kind: "http"; slot: string; placement: "instance"; idempotent: boolean; at: string }
545
+ | { state: "intent"; kind: "http" | "mcp"; slot: string; placement: "instance"; idempotent: boolean; at: string }
421
546
  /** An Effect of the Sandbox Effector (ADR-0067): its Sandbox, and — for a push or a credentialed
422
547
  * command — the commit it runs against, written once the start's answer names it. A restore
423
548
  * re-attaches by id, or sends a push or a credentialed command again with only that commit, and
@@ -445,15 +570,19 @@ export type EffectRow =
445
570
  run?: string;
446
571
  at: string;
447
572
  }
448
- /** `sandbox` and `repo`: the Workspace and the Repo Slot an Effect of the Sandbox Effector ran on
449
- * — every later push of its own Workspace and Repo Slot (one branch, one remote) sends its pair,
450
- * and the newest push's unfinished sync runs again in that slot's Worktree. */
573
+ /** `sandbox`, `repo` and `branch`: the Workspace, the Repo Slot and the branch an Effect of the
574
+ * Sandbox Effector ran on — every later push of its own (slot, branch) sends its pair, and the
575
+ * newest push's unfinished sync runs again in `worktree`. `cut`: a `worktree()` that settled
576
+ * `ready`: its path and branch as fields, because a settled row drops its tail. A push, a
577
+ * credentialed command and a file body read their branch here. */
451
578
  | {
452
579
  state: "settled";
453
580
  kind: EffectParts["kind"];
454
581
  slot: string;
455
582
  sandbox?: string;
456
583
  repo?: string;
584
+ branch?: string;
585
+ cut?: { path: string; branch: string | null };
457
586
  output: unknown;
458
587
  at: string;
459
588
  }
@@ -511,6 +640,9 @@ const TAILS: Record<EffectRow["kind"], readonly string[]> = {
511
640
  // settled marker carries both whole. The row keeps the outcome, the run id, its status and the
512
641
  // door's HTTP status (ADR-0071).
513
642
  dispatch: ["output", "report"],
643
+ // An mcp answer's text and its structured value: the feed's settled marker carries both whole.
644
+ // The row keeps the outcome, `isError` and `truncated` (ADR-0079).
645
+ mcp: ["text", "structured"],
514
646
  };
515
647
 
516
648
  /** A kind's tail for one output. The NEWEST push of its Repo Slot whose sync did not finish (`busy`,
@@ -530,19 +662,19 @@ export type PushedRow = { published: string; source: string; sync?: SyncAnswer["
530
662
  unknown
531
663
  >;
532
664
 
533
- /** The ledger's last push of a Workspace's Repo Slot that published: its row, by `at`, never this
534
- * Effect's (`self`). A Workspace with two Repo Slots has two branches on two remotes, so two pairs.
535
- */
665
+ /** The ledger's last push of a (slot, branch) of a Workspace that published: its row, by `at`, never
666
+ * this Effect's (`self`). Each (slot, branch) has its own pairs: one branch on one remote. */
536
667
  export function lastPushOf(
537
668
  rows: Readonly<Record<string, EffectRow>>,
538
669
  sandbox: string,
539
670
  repo: string,
671
+ branch: string,
540
672
  self?: string,
541
673
  ): { rowId: string; row: Extract<EffectRow, { state: "settled" }>; output: PushedRow } | undefined {
542
674
  let best: { rowId: string; row: Extract<EffectRow, { state: "settled" }>; output: PushedRow } | undefined;
543
675
  for (const [rowId, row] of Object.entries(rows)) {
544
676
  if (rowId === self || row.state !== "settled" || row.kind !== "push") continue;
545
- if (row.sandbox !== sandbox || row.repo !== repo) continue;
677
+ if (row.sandbox !== sandbox || row.repo !== repo || row.branch !== branch) continue;
546
678
  const output = row.output as Partial<PushedRow> & { outcome?: unknown };
547
679
  if (output?.outcome !== "pushed" || typeof output.published !== "string" || typeof output.source !== "string") {
548
680
  continue;
@@ -552,20 +684,21 @@ export function lastPushOf(
552
684
  return best;
553
685
  }
554
686
 
555
- /** Every pair the ledger holds for a Workspace's Repo Slot (ADR-0067): each settled push row that
556
- * published, oldest first by `at`, never this Effect's (`self`) — its published commit, its source,
557
- * and its own `squashed`. What a push start carries, so the Effector sees every push's pair. */
687
+ /** Every pair the ledger holds for a (slot, branch) of a Workspace (ADR-0067): each settled push row
688
+ * that published, oldest first by `at`, never this Effect's (`self`) — its published commit, its
689
+ * source, and its own `squashed`. What a push start carries, so the Effector sees every push's pair. */
558
690
  export function pushPairsOf(
559
691
  rows: Readonly<Record<string, EffectRow>>,
560
692
  sandbox: string,
561
693
  repo: string,
694
+ branch: string,
562
695
  self?: string,
563
696
  ): Array<{ published: string; source: string; squashed?: "signed" | "unsigned" }> {
564
697
  const found: Array<{ at: string; pair: { published: string; source: string; squashed?: "signed" | "unsigned" } }> =
565
698
  [];
566
699
  for (const [rowId, row] of Object.entries(rows)) {
567
700
  if (rowId === self || row.state !== "settled" || row.kind !== "push") continue;
568
- if (row.sandbox !== sandbox || row.repo !== repo) continue;
701
+ if (row.sandbox !== sandbox || row.repo !== repo || row.branch !== branch) continue;
569
702
  const output = row.output as Partial<PushedRow> & { outcome?: unknown; squashed?: unknown };
570
703
  if (output?.outcome !== "pushed" || typeof output.published !== "string" || typeof output.source !== "string") {
571
704
  continue;
@@ -579,21 +712,41 @@ export function pushPairsOf(
579
712
  return found.sort((a, b) => (a.at < b.at ? -1 : a.at > b.at ? 1 : 0)).map((f) => f.pair);
580
713
  }
581
714
 
582
- /** The row id of each Repo Slot's newest push that published (`lastPushOf`), over the ledger. */
715
+ /** The row id of each (slot, branch)'s newest push that published (`lastPushOf`), over the ledger. */
583
716
  function newestPushesOf(effects: Readonly<Record<string, EffectRow>>): Set<string> {
584
717
  const ids = new Set<string>();
585
718
  const seen = new Set<string>();
586
719
  for (const row of Object.values(effects)) {
587
720
  if (row.state !== "settled" || row.kind !== "push" || row.sandbox === undefined || row.repo === undefined) continue;
588
- const key = JSON.stringify([row.sandbox, row.repo]);
721
+ if (row.branch === undefined) continue;
722
+ const key = JSON.stringify([row.sandbox, row.repo, row.branch]);
589
723
  if (seen.has(key)) continue;
590
724
  seen.add(key);
591
- const last = lastPushOf(effects, row.sandbox, row.repo);
725
+ const last = lastPushOf(effects, row.sandbox, row.repo, row.branch);
592
726
  if (last !== undefined) ids.add(last.rowId);
593
727
  }
594
728
  return ids;
595
729
  }
596
730
 
731
+ /** The cut that made `path` in this Sandbox: the newest settled `ready` row, whatever its Repo Slot.
732
+ * What a push, a credentialed command and a file body read. A row of another Sandbox says nothing
733
+ * of this one: a re-provision has an empty `/work`. */
734
+ export function cutRowOf(
735
+ rows: Readonly<Record<string, EffectRow>>,
736
+ sandbox: string,
737
+ path: unknown,
738
+ ): { repo: string; path: string; branch: string | null } | undefined {
739
+ let best: { at: string; repo: string; path: string; branch: string | null } | undefined;
740
+ for (const row of Object.values(rows)) {
741
+ if (row.state !== "settled" || row.cut === undefined || row.sandbox !== sandbox) continue;
742
+ if (row.cut.path !== path || row.repo === undefined) continue;
743
+ if (best === undefined || row.at > best.at) {
744
+ best = { at: row.at, repo: row.repo, path: row.cut.path, branch: row.cut.branch };
745
+ }
746
+ }
747
+ return best;
748
+ }
749
+
597
750
  /** One Effect line on the run's feed (Instance-token band, like the Turn markers). `output` lines
598
751
  * carry the stream; `aborted` and an `unknown` settle say the outside world may have acted. */
599
752
  export type EffectMarker = {
@@ -602,7 +755,7 @@ export type EffectMarker = {
602
755
  effect: string;
603
756
  stage: "started" | "output" | "settled" | "aborted";
604
757
  text?: string;
605
- output?: CommandOutput | HttpOutput | DispatchOutput;
758
+ output?: CommandOutput | WorktreeOutput | HttpOutput | DispatchOutput | McpOutput;
606
759
  };
607
760
 
608
761
  // --- The port -----------------------------------------------------------------------------------
@@ -643,19 +796,33 @@ export type EffectLogic<I, O> = ActorLogic<EffectSnapshot<O, I>, EventObject, I>
643
796
  type ArgNames<A extends readonly unknown[]> = A[number] extends infer E ? (E extends Arg<infer N> ? N : never) : never;
644
797
  /** A command's input: one string per argv slot, and with `pin` the approved commit (`{ sha }`);
645
798
  * none when there is neither. */
646
- export type CommandInput<A extends readonly unknown[], P = false> = [
799
+ export type CommandInput<A extends readonly unknown[], P = false, Opt = unknown> = [
647
800
  ArgNames<A> | ([P] extends [true] ? "sha" : never),
648
801
  ] extends [never]
649
- ? void
650
- : { [K in ArgNames<A> | ([P] extends [true] ? "sha" : never)]: string };
802
+ ? (WorktreeKey<Opt> & NotWorktreeKeys) | ([Opt] extends [{ readonly env: object }] ? never : void)
803
+ : { [K in ArgNames<A> | ([P] extends [true] ? "sha" : never)]: string } & WorktreeKey<Opt> &
804
+ Omit<NotWorktreeKeys, ArgNames<A>>;
805
+
806
+ // A command takes its Worktree in the invoke's input. `jr2Setup` makes the key required under a body with Repo Slots, and drops it under a body with
807
+ // none. A credentialed command runs against one commit of a Worktree, so there it is always required.
808
+ /** @internal */
809
+ export type WorktreeKey<Opt> = [Opt] extends [{ readonly env: object }]
810
+ ? { worktree: AnyWorktreePath }
811
+ : { worktree?: AnyWorktreePath };
812
+
813
+ /** The keys of a Turn and a cut, refused by name on an Effect's input: a mapper's excess key is no
814
+ * compile error, and the Worktree would be lost.
815
+ * @internal */
816
+ export type NotWorktreeKeys = { cwd?: never; path?: never; branch?: never };
651
817
 
652
818
  // An Open argv's slots are the composer's (ADR-0054), so any key is one string — what the
653
819
  // package's invoke hands it, read by the argv the composer binds (`argvOf`).
654
820
  /** The input of a `command(open)`: a string for each `arg()` of the argv that the Instance binds,
655
821
  * and with `pin`, the commit (`{ sha }`). The compiler cannot check the names. When the input has
656
822
  * no string for an `arg()`, the Effect fails when it starts. */
657
- export type OpenCommandInput<P = false> = Readonly<Record<string, string>> &
658
- ([P] extends [true] ? { sha: string } : unknown);
823
+ export type OpenCommandInput<P = false, Opt = unknown> = Readonly<Record<string, string>> &
824
+ ([P] extends [true] ? { sha: string } : unknown) &
825
+ WorktreeKey<Opt>;
659
826
 
660
827
  /** A length of time: a positive number and a unit, `s`, `m` or `h`. For example `"90s"`, `"10m"`
661
828
  * or `"1h"`. */
@@ -664,28 +831,6 @@ export type Duration = `${number}s` | `${number}m` | `${number}h`;
664
831
  /** The options of `command()`: the Repo Slot it runs in, how long it can run, how much of its
665
832
  * output it keeps, its Effect secrets, and the commit it runs against. */
666
833
  export type CommandOptions = {
667
- /**
668
- * The Repo Slot whose Worktree is the working directory of the command. Write it only when the
669
- * Workspace has more than one Repo Slot. In a Workspace with no Repo Slot, the command starts in
670
- * `/work`, the folder that the steps of the Workspace share.
671
- *
672
- * The command starts in the root of that Worktree. To run it in a subfolder, give the path as an
673
- * argument, or write `["sh", "-c", "cd <dir> && <program>"]`.
674
- *
675
- * When it starts, it refuses:
676
- * - no `slot` under a Workspace with more than one Repo Slot: `command "<slot>" runs under a
677
- * Workspace carrying more than one Repo Slot (<slots>) and names none — say which Worktree it
678
- * runs in: command(argv, { slot: "<slot>" }) (ADR-0051)`. When `env` names an Effect secret,
679
- * the text is `command "<slot>" runs under a Workspace carrying more than one Repo Slot
680
- * (<slots>) — say which: { slot: "<slot>" } (ADR-0051)`.
681
- * - with an Effect secret in `env`, a Workspace with no Repo Slot: `command "<slot>" runs under
682
- * a Workspace with no Repo Slot: a command with an Effect secret runs against one commit of a
683
- * Repo Slot's branch, and there is none — declare a Repo Slot, or name no secret in its env, and
684
- * it runs in /work beside the Agent (ADR-0051)`.
685
- * - a `slot` that the Workspace does not have: `command "<slot>" names Repo Slot "<name>", which
686
- * its Workspace does not carry (<slots>)`.
687
- */
688
- slot?: string;
689
834
  /**
690
835
  * How long the command can run: a number and a unit, such as `"90s"`, `"10m"` or `"1h"`.
691
836
  * Default `"10m"`. When the time ends, jr2 kills the process group, and the output is
@@ -735,10 +880,14 @@ export type EnvOpenParts<Opt> = Opt extends { readonly env: infer E }
735
880
 
736
881
  /**
737
882
  * Why an Effect kind cannot run in a Workspace with no Repo Slot (ADR-0051, ADR-0067), or undefined
738
- * when it can: a `push` and a credentialed `command` run on a commit of a slot's branch, and that
739
- * Workspace has none. The invoke and the `jr2 up` walk refuse with the same words.
883
+ * when it can: a `push` and a credentialed `command` run on a commit of a slot's branch, and a
884
+ * `worktree` makes a Worktree of a slot's clone, and that Workspace has none. The invoke and the
885
+ * `jr2 up` walk refuse with the same words.
740
886
  */
741
887
  export function needsASlot(kind: string): string | undefined {
888
+ if (kind === "worktree") {
889
+ return 'a cut makes a Worktree of a Repo Slot, and there is none — declare one: repos: { app: "<url>" }';
890
+ }
742
891
  if (kind === "push")
743
892
  return 'a push publishes a Repo Slot\'s branch, and there is none — declare one: repos: { app: "<url>" }';
744
893
  if (kind === "command") {
@@ -773,7 +922,7 @@ export function commandWith<const Opt extends CommandOptions = CommandOptions>(
773
922
  argv: typeof open,
774
923
  options?: Opt,
775
924
  effectorPorts?: EffectorPortFactory,
776
- ): EffectLogic<OpenCommandInput<Opt["pin"]>, CommandOutput> & OpenEffect<"argv" | EnvOpenParts<Opt>>;
925
+ ): EffectLogic<OpenCommandInput<Opt["pin"], Opt>, CommandOutput> & OpenEffect<"argv" | EnvOpenParts<Opt>>;
777
926
  export function commandWith<
778
927
  const A extends readonly (string | Arg)[],
779
928
  const Opt extends CommandOptions = CommandOptions,
@@ -782,12 +931,15 @@ export function commandWith<
782
931
  argv: A,
783
932
  options?: Opt,
784
933
  effectorPorts?: EffectorPortFactory,
785
- ): EffectLogic<CommandInput<A, Opt["pin"]>, CommandOutput> & OpenOf<EnvOpenParts<Opt>>;
934
+ cut?: CommandParts["cut"],
935
+ ): EffectLogic<CommandInput<A, Opt["pin"], Opt>, CommandOutput> & OpenOf<EnvOpenParts<Opt>>;
786
936
  export function commandWith(
787
937
  portFactory: ExecPortFactory,
788
938
  argv: readonly (string | Arg)[] | typeof open,
789
939
  options: CommandOptions = {},
790
940
  effectorPorts: EffectorPortFactory = sandboxEffectorPort,
941
+ // `worktree()` alone passes this: an author's command is at most once.
942
+ cut?: CommandParts["cut"],
791
943
  // The implementation of the overloads above: each one states the input type.
792
944
  ): EffectLogic<any, CommandOutput> {
793
945
  if (argv === open) {
@@ -814,9 +966,9 @@ export function commandWith(
814
966
  argv: [...argv],
815
967
  timeoutMs: options.timeout === undefined ? DEFAULT_TIMEOUT_MS : parseDuration(options.timeout),
816
968
  tail: options.tail ?? DEFAULT_TAIL,
817
- ...(options.slot !== undefined ? { slot: options.slot } : {}),
818
969
  ...(options.env !== undefined && Object.keys(options.env).length > 0 ? commandEnvParts(options.env) : {}),
819
970
  ...(options.pin === true ? { pin: true as const } : {}),
971
+ ...(cut !== undefined ? { cut, idempotent: true as const } : {}),
820
972
  };
821
973
  if (parts.pin && (parts.secrets?.length ?? 0) === 0 && !parts.openSecret) {
822
974
  // A command that names no secret runs in the Harness container, in the live Worktree: there
@@ -844,7 +996,7 @@ export function commandWith(
844
996
  ) as unknown as EffectLogic<unknown, CommandOutput>;
845
997
  }
846
998
  type I = unknown;
847
- type O = CommandOutput;
999
+ type O = CommandOutput | WorktreeOutput;
848
1000
  const logic: ActorLogic<EffectSnapshot<O, I>, EventObject, I> = {
849
1001
  getInitialSnapshot: (_scope, input) => ({
850
1002
  status: "active",
@@ -878,12 +1030,63 @@ export function commandWith(
878
1030
  },
879
1031
  getPersistedSnapshot: (snapshot) => snapshot,
880
1032
  };
881
- return Object.assign(logic, { effect: parts });
1033
+ return Object.assign(logic, { effect: parts }) as unknown as EffectLogic<any, CommandOutput>;
882
1034
  }
883
1035
 
884
1036
  /** The slot's name as its parent invoked it — what the ledger and the feed call the Effect. */
885
1037
  const slotOf = (self: AnyActorRef): string => (typeof self.src === "string" ? self.src : self.id);
886
1038
 
1039
+ /** How many times a marked Effect goes out again after a Harness that lost it. Bounded, so that a
1040
+ * Harness in a crash loop ends the wait: the Effect settles `unknown`, and the Machine routes. */
1041
+ const RESENDS = 3;
1042
+
1043
+ /** What a cut's program answers: its last line. Anything else is a cut that did not end. */
1044
+ function cutOutputOf(raw: CommandOutput): WorktreeOutput {
1045
+ if (raw.outcome === "unknown") return raw as Unknown;
1046
+ const tail = "tail" in raw && typeof raw.tail === "string" ? raw.tail : "";
1047
+ if (raw.outcome === "exited" && raw.code === 0) {
1048
+ try {
1049
+ const answer = JSON.parse(tail.trim().split("\n").pop() ?? "") as { outcome?: unknown };
1050
+ if (answer.outcome === "ready" || answer.outcome === "refused") return answer as WorktreeOutput;
1051
+ } catch {
1052
+ // Falls through: the program said no answer.
1053
+ }
1054
+ }
1055
+ return { outcome: "unknown", reason: `the cut ended ${raw.outcome} with no answer: ${tail.slice(-400)}` };
1056
+ }
1057
+
1058
+ /** Why a cut's input cannot go to the Harness, or undefined. The door checked a Chart's literals; a
1059
+ * reference, a pick param or a mapper can still say this. No exec, no fetch. */
1060
+ function cutFaultOf(input: unknown, ambient: AmbientHandles): string | undefined {
1061
+ const given = (input ?? {}) as { path?: unknown; branch?: unknown; detach?: unknown; from?: unknown };
1062
+ const fault = worktreePathFault(given.path, ambient, { cut: true });
1063
+ if (fault !== undefined) return fault;
1064
+ if ((given.branch === undefined) === (given.detach === undefined))
1065
+ return "a cut names a branch, or a commit in `detach`";
1066
+ if (given.detach !== undefined && given.from !== undefined) return "a detached cut takes no `from`";
1067
+ for (const key of ["branch", "detach", "from"] as const) {
1068
+ if (given[key] !== undefined && typeof given[key] !== "string") return `a cut's \`${key}\` is a string`;
1069
+ }
1070
+ return undefined;
1071
+ }
1072
+
1073
+ /** The cut's one argument: its input, its id (a re-send answers the first `found`), its static
1074
+ * `expect`, and the base, sent each time, so that no checkout of an Agent's can move it. */
1075
+ function cutRequestOf(id: string, input: unknown, parts: CommandParts, ambient: AmbientHandles): string {
1076
+ const given = (input ?? {}) as { path: string; branch?: string; detach?: string; from?: string };
1077
+ const slot = given.path.split("/")[2]!;
1078
+ const base = ambient.bases?.[slot];
1079
+ return JSON.stringify({
1080
+ id,
1081
+ path: given.path,
1082
+ ...(given.branch !== undefined ? { branch: given.branch } : {}),
1083
+ ...(given.detach !== undefined ? { detach: given.detach } : {}),
1084
+ ...(given.from !== undefined ? { from: given.from } : {}),
1085
+ ...(given.branch !== undefined && parts.cut?.expect ? { expect: parts.cut.expect } : {}),
1086
+ ...(base !== undefined ? { base } : {}),
1087
+ });
1088
+ }
1089
+
887
1090
  /** Run, re-attach to, or deliver one Effect, by its ledger row (see the module header). */
888
1091
  async function drive<I, O>(
889
1092
  self: AnyActorRef,
@@ -900,14 +1103,30 @@ async function drive<I, O>(
900
1103
  const now = (): string => new Date().toISOString();
901
1104
  const mark = (marker: Omit<EffectMarker, "kind" | "slot" | "effect">): void =>
902
1105
  binding?.marker?.({ kind: "effect", slot, effect: id, ...marker });
903
- const settle = (output: CommandOutput): void => {
904
- if (signal.aborted || self.getSnapshot().status !== "active") return;
905
- // The settled row first, saved at the write: a restart before the transition is saved delivers
906
- // this output, and runs nothing.
907
- binding?.recordEffect?.(id, { state: "settled", kind: parts.kind, slot, output, at: now() });
908
- mark({ stage: "settled", output });
909
- self.send({ type: "jr2.effect.settle", output } satisfies Settle);
910
- };
1106
+ /** A settled row of an exec. `sandbox` is the row's own on a restore: the Workspace it ran in. */
1107
+ const settleIn =
1108
+ (sandbox: string | undefined) =>
1109
+ (raw: CommandOutput | WorktreeOutput): void => {
1110
+ if (signal.aborted || self.getSnapshot().status !== "active") return;
1111
+ const output = parts.cut && raw.outcome !== "refused" ? cutOutputOf(raw as CommandOutput) : raw;
1112
+ // The settled row first, saved at the write: a restart before the transition is saved delivers
1113
+ // this output, and runs nothing. A `ready` cut's row keeps its fields: a push reads them.
1114
+ const cutRow =
1115
+ parts.cut && output.outcome === "ready"
1116
+ ? { sandbox, repo: output.slot, cut: { path: output.path, branch: output.branch } }
1117
+ : {};
1118
+ binding?.recordEffect?.(id, {
1119
+ state: "settled",
1120
+ kind: parts.kind,
1121
+ slot,
1122
+ ...(cutRow as object),
1123
+ output,
1124
+ at: now(),
1125
+ });
1126
+ mark({ stage: "settled", output });
1127
+ self.send({ type: "jr2.effect.settle", output } satisfies Settle);
1128
+ };
1129
+ const settle = settleIn(undefined);
911
1130
 
912
1131
  const row = binding?.effectRow?.(id);
913
1132
  if (row?.state === "settled") {
@@ -923,13 +1142,14 @@ async function drive<I, O>(
923
1142
  return;
924
1143
  }
925
1144
  if (row?.state === "intent" && row.kind === "command" && "endpoint" in row) {
926
- // In flight at an Orchestrator restart: re-attach by id, or settle `unknown`. Never run again.
1145
+ // In flight at an Orchestrator restart: re-attach by id, or settle `unknown`. Never run again,
1146
+ // unless the kit marked it: a cut goes on below, under the same id.
927
1147
  const port = portFactory(row.endpoint, binding?.harnessBearer?.(row.placement));
928
1148
  run.port = port;
929
1149
  run.started = true;
930
1150
  const first = await retrying(() => port.read(id, undefined, signal), signal);
931
1151
  if (signal.aborted) return;
932
- if (first === undefined) {
1152
+ if (first === undefined && !parts.idempotent) {
933
1153
  const output: Unknown = {
934
1154
  outcome: "unknown",
935
1155
  reason: "it was running when the Orchestrator restarted, and the Harness no longer knows it; not run again",
@@ -937,8 +1157,11 @@ async function drive<I, O>(
937
1157
  settle(output);
938
1158
  return;
939
1159
  }
940
- await follow(port, id, first, signal, mark, settle);
941
- return;
1160
+ if (first !== undefined) {
1161
+ const lost = await follow(port, id, first, signal, mark, settleIn(row.placement), parts.idempotent === true);
1162
+ if (!lost) return;
1163
+ }
1164
+ mark({ stage: "started", text: "sent again: the Harness lost it, and a cut is idempotent" });
942
1165
  }
943
1166
 
944
1167
  // Fresh: nothing was sent. Where it runs is the enclosing Workspace's Harness — refused outside one.
@@ -949,9 +1172,43 @@ async function drive<I, O>(
949
1172
  `workspace() — invoke it from a Workspace's body (ADR-0067)`,
950
1173
  );
951
1174
  }
1175
+ const settled = settleIn(ambient.sandbox);
1176
+ // A bad Worktree is a word an Agent or a Chart can say, so it is output (ADR-0067), in the form a
1177
+ // command that could not start has: exited 127, with the reason in the tail. A key that no one
1178
+ // wrote is a defect of the Machine: a throw, and `onError`.
1179
+ const worktree = (snapshot.input as { worktree?: unknown } | undefined | void)?.worktree;
1180
+ if (parts.cut) {
1181
+ if (Object.keys(ambient.repos).length === 0) {
1182
+ throw new Error(
1183
+ `worktree "${slot}" runs under a Workspace with no Repo Slot: ${needsASlot("worktree")} (ADR-0075)`,
1184
+ );
1185
+ }
1186
+ const fault = cutFaultOf(snapshot.input, ambient);
1187
+ if (fault !== undefined) {
1188
+ settled({ outcome: "refused", reason: fault });
1189
+ return;
1190
+ }
1191
+ } else if (worktree === undefined) {
1192
+ if (Object.keys(ambient.repos).length > 0) {
1193
+ throw new Error(
1194
+ `command "${slot}" runs under a Workspace with Repo Slots (${Object.keys(ambient.repos).join(", ")}) and its ` +
1195
+ "input names no `worktree`: say which, a Worktree that a cut gave or `<slot>/default`",
1196
+ );
1197
+ }
1198
+ } else {
1199
+ const fault = worktreePathFault(worktree, ambient);
1200
+ if (fault !== undefined) {
1201
+ settled({ outcome: "exited", code: 127, tail: `jr2: ${fault}; nothing ran\n` });
1202
+ return;
1203
+ }
1204
+ }
952
1205
  const req: ExecRequest = {
953
- argv: argvOf(slot, parts, snapshot.input),
954
- ...cwdOf(slot, parts, ambient),
1206
+ argv: parts.cut
1207
+ ? [...(parts.argv as string[]), cutRequestOf(id, snapshot.input, parts, ambient)]
1208
+ : argvOf(slot, parts, snapshot.input),
1209
+ // Named, never left to the Harness: a spawn with no cwd runs in the image's own WORKDIR.
1210
+ cwd: parts.cut ? WORK_ROOT : ((worktree as string | undefined) ?? WORK_ROOT),
1211
+ ...(!parts.cut && worktree !== undefined ? { root: true as const } : {}),
955
1212
  timeoutMs: parts.timeoutMs,
956
1213
  tail: parts.tail,
957
1214
  };
@@ -969,26 +1226,38 @@ async function drive<I, O>(
969
1226
  });
970
1227
  run.started = true;
971
1228
  mark({ stage: "started", text: req.argv.join(" ") });
972
- const sent = await startOnce(port, id, req, signal);
973
- if (signal.aborted) return;
974
- const first = await retrying(() => port.read(id, 0, signal), signal);
975
- if (signal.aborted) return;
976
- if (first === undefined) {
977
- if (sent === "uncertain") {
978
- // The start may still be on its way to the Harness that answers now: a kill keeps it from
979
- // running there (exec.ts keeps an unknown id killed). Best effort — `unknown` either way.
980
- void killUntilKept(port, id, parts.timeoutMs).catch(() => {});
1229
+ // One pass for every Effect but the kit's cut: a Harness that restarted knows no id, and a start
1230
+ // that may have reached it is never sent again. A cut goes out again: it is idempotent.
1231
+ const passes = parts.idempotent ? RESENDS + 1 : 1;
1232
+ for (let pass = 1; pass <= passes; pass++) {
1233
+ const sent = await startOnce(port, id, req, signal);
1234
+ if (signal.aborted) return;
1235
+ const first = await retrying(() => port.read(id, 0, signal), signal);
1236
+ if (signal.aborted) return;
1237
+ const last = pass === passes;
1238
+ if (first === undefined) {
1239
+ if (!last) {
1240
+ mark({ stage: "started", text: "sent again: the Harness restarted, and a cut is idempotent" });
1241
+ continue;
1242
+ }
1243
+ if (sent === "uncertain" && !parts.idempotent) {
1244
+ // The start may still be on its way to the Harness that answers now: a kill keeps it from
1245
+ // running there (exec.ts keeps an unknown id killed). Best effort — `unknown` either way.
1246
+ void killUntilKept(port, id, parts.timeoutMs).catch(() => {});
1247
+ }
1248
+ settled({
1249
+ outcome: "unknown",
1250
+ reason:
1251
+ sent === "uncertain"
1252
+ ? "its start may have reached the Harness, which no longer knows it (it restarted); not run again"
1253
+ : "the Harness lost it after it started (it restarted); not run again",
1254
+ });
1255
+ return;
981
1256
  }
982
- settle({
983
- outcome: "unknown",
984
- reason:
985
- sent === "uncertain"
986
- ? "its start may have reached the Harness, which no longer knows it (it restarted); not run again"
987
- : "the Harness lost it after it started (it restarted); not run again",
988
- });
989
- return;
1257
+ const lost = await follow(port, id, first, signal, mark, settled, !last);
1258
+ if (!lost) return;
1259
+ mark({ stage: "started", text: "sent again: the Harness lost it, and a cut is idempotent" });
990
1260
  }
991
- await follow(port, id, first, signal, mark, settle);
992
1261
  }
993
1262
 
994
1263
  /**
@@ -1018,7 +1287,8 @@ async function startOnce(
1018
1287
  }
1019
1288
  }
1020
1289
 
1021
- /** Read the exec to its end: each chunk of stream to the feed, the output to the Machine. */
1290
+ /** Read the exec to its end: each chunk of stream to the feed, the output to the Machine. With
1291
+ * `again`, a Harness that lost it settles nothing: the answer is true, and the caller sends it. */
1022
1292
  async function follow(
1023
1293
  port: ExecPort,
1024
1294
  id: string,
@@ -1026,22 +1296,24 @@ async function follow(
1026
1296
  signal: AbortSignal,
1027
1297
  mark: (marker: Omit<EffectMarker, "kind" | "slot" | "effect">) => void,
1028
1298
  settle: (output: CommandOutput) => void,
1029
- ): Promise<void> {
1299
+ again = false,
1300
+ ): Promise<boolean> {
1030
1301
  let read: ExecRead | undefined = first;
1031
1302
  for (;;) {
1032
1303
  if (read === undefined) {
1304
+ if (again) return true;
1033
1305
  settle({ outcome: "unknown", reason: "the Harness lost it while it ran (it restarted); not run again" });
1034
- return;
1306
+ return false;
1035
1307
  }
1036
1308
  if (read.stream) mark({ stage: "output", text: read.stream });
1037
1309
  if (read.state === "settled" && read.output) {
1038
1310
  settle(read.output);
1039
1311
  void port.forget(id).catch(() => {});
1040
- return;
1312
+ return false;
1041
1313
  }
1042
1314
  const offset: number = read.next;
1043
1315
  read = await retrying(() => port.read(id, offset, signal), signal);
1044
- if (signal.aborted) return;
1316
+ if (signal.aborted) return false;
1045
1317
  }
1046
1318
  }
1047
1319
 
@@ -1146,30 +1418,27 @@ function argvOf(slot: string, parts: CommandParts, input: unknown): string[] {
1146
1418
  }
1147
1419
 
1148
1420
  /**
1149
- * The cwd: a Repo Slot's Worktree (ADR-0051). Named by `slot`, or the one slot where the Workspace
1150
- * carries one; a Workspace with none runs it in `/work`, which its steps share. Named, never left to
1151
- * the Harness: a spawn with no cwd runs in the Sandbox Image's own `WORKDIR`, which is the image's.
1152
- * Several and none named is refused, as a Turn's Frame is (ADR-0057): the kit gives no slot a
1153
- * meaning.
1421
+ * Why `path` is not a Worktree of this Workspace, or undefined when it is. Exactly
1422
+ * `/work/<slot>/<name>`, of one of its Repo Slots, one segment that starts with neither `.` nor `-`.
1423
+ * `default` is a Worktree (the clone), and with `cut` it is not one a cut makes.
1154
1424
  */
1155
- function cwdOf(slot: string, parts: CommandParts, ambient: AmbientHandles): { cwd?: string } {
1425
+ export function worktreePathFault(
1426
+ path: unknown,
1427
+ ambient: AmbientHandles,
1428
+ opts: { cut?: boolean } = {},
1429
+ ): string | undefined {
1430
+ const match = typeof path === "string" ? path.match(/^\/work\/([^/]+)\/([^/]+)$/) : null;
1431
+ if (match === null) {
1432
+ return `${JSON.stringify(path)} is not a Worktree path: /work/<slot>/<name>, with one segment in <name>`;
1433
+ }
1434
+ const [, slot, name] = match as unknown as [string, string, string];
1435
+ if (name.startsWith(".") || name.startsWith("-")) return `"${path}": a Worktree's name starts with neither . nor -`;
1436
+ if (opts.cut && name === "default") return `"${path}" is the clone: it stays as the attach left it; cut a sibling`;
1156
1437
  const slots = Object.keys(ambient.repos);
1157
- if (parts.slot !== undefined) {
1158
- const cwd = ambient.repos[parts.slot];
1159
- if (cwd === undefined) {
1160
- throw new Error(
1161
- `command "${slot}" names Repo Slot "${parts.slot}", which its Workspace does not carry ` +
1162
- `(${slots.join(", ") || "none"})`,
1163
- );
1164
- }
1165
- return { cwd };
1438
+ if (!slots.includes(slot)) {
1439
+ return `"${path}" names Repo Slot "${slot}", which its Workspace does not carry (${slots.join(", ") || "none"})`;
1166
1440
  }
1167
- if (slots.length === 0) return { cwd: WORK_ROOT };
1168
- if (slots.length === 1) return { cwd: ambient.repos[slots[0]!]! };
1169
- throw new Error(
1170
- `command "${slot}" runs under a Workspace carrying more than one Repo Slot (${slots.join(", ")}) and names ` +
1171
- `none — say which Worktree it runs in: command(argv, { slot: "<slot>" }) (ADR-0051)`,
1172
- );
1441
+ return undefined;
1173
1442
  }
1174
1443
 
1175
1444
  /** Ask again on a transport failure, with backoff, until it answers or the Effect stops. For a