@jr2/orchestrator 0.4.1 → 0.6.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 (361) hide show
  1. package/README.md +20 -2
  2. package/console/components/app.ts +2 -0
  3. package/console/components/fleet.ts +14 -1
  4. package/console/components/machine-pane.ts +41 -2
  5. package/console/main.ts +13 -1
  6. package/console/store.ts +102 -6
  7. package/console/style.css +21 -0
  8. package/docs/agents-block.md +5 -0
  9. package/docs/concepts/agents-and-turns.md +478 -0
  10. package/docs/concepts/architecture.md +243 -0
  11. package/docs/concepts/charts.md +146 -0
  12. package/docs/concepts/composition.md +357 -0
  13. package/docs/concepts/converge.md +257 -0
  14. package/docs/concepts/durable-runs.md +189 -0
  15. package/docs/concepts/effects.md +658 -0
  16. package/docs/concepts/gates.md +208 -0
  17. package/docs/concepts/machines-and-workflows.md +298 -0
  18. package/docs/concepts/pools-and-sources.md +230 -0
  19. package/docs/concepts/secrets-and-trust.md +306 -0
  20. package/docs/concepts/the-menu.md +381 -0
  21. package/docs/concepts/workspaces.md +756 -0
  22. package/docs/get-started/a-model-decides.md +563 -0
  23. package/docs/get-started/an-agent-in-a-sandbox.md +383 -0
  24. package/docs/get-started/connect-a-model.md +248 -0
  25. package/docs/get-started/create-an-instance.md +194 -0
  26. package/docs/get-started/have-your-assistant-write-a-machine.md +993 -0
  27. package/docs/get-started/install.md +130 -0
  28. package/docs/guides/add-tools-to-the-sandbox.md +308 -0
  29. package/docs/guides/answer-a-gate.md +304 -0
  30. package/docs/guides/bind-open-parts.md +224 -0
  31. package/docs/guides/cancel-a-run.md +146 -0
  32. package/docs/guides/clean-up.md +206 -0
  33. package/docs/guides/cut-a-worktree.md +560 -0
  34. package/docs/guides/fix-a-failed-converge.md +361 -0
  35. package/docs/guides/give-the-cluster-git-credentials.md +222 -0
  36. package/docs/guides/iterate-on-a-machine-on-kind.md +190 -0
  37. package/docs/guides/let-an-agent-write-a-chart.md +230 -0
  38. package/docs/guides/look-inside-a-sandbox.md +177 -0
  39. package/docs/guides/mirror-the-kit-images.md +176 -0
  40. package/docs/guides/move-from-kind-to-a-shared-cluster.md +318 -0
  41. package/docs/guides/open-the-console.md +91 -0
  42. package/docs/guides/publish-a-machine-as-an-npm-package.md +350 -0
  43. package/docs/guides/push-and-sign-a-branch.md +532 -0
  44. package/docs/guides/run-a-chart-by-name.md +270 -0
  45. package/docs/guides/run-a-command-or-call-a-url.md +587 -0
  46. package/docs/guides/run-one-worker-per-item.md +322 -0
  47. package/docs/guides/set-an-agents-access-and-dials.md +420 -0
  48. package/docs/guides/size-a-workspace.md +217 -0
  49. package/docs/guides/start-a-run-from-a-run.md +237 -0
  50. package/docs/guides/start-a-run.md +207 -0
  51. package/docs/guides/upgrade-the-kit-version.md +240 -0
  52. package/docs/guides/use-a-hosted-provider.md +214 -0
  53. package/docs/guides/use-a-local-model.md +313 -0
  54. package/docs/guides/watch-a-run.md +349 -0
  55. package/docs/guides/write-a-workflow.md +318 -0
  56. package/docs/index.md +44 -0
  57. package/docs/llms.txt +237 -0
  58. package/docs/reference/authoring-surface/agent.md +151 -0
  59. package/docs/reference/authoring-surface/answer.md +156 -0
  60. package/docs/reference/authoring-surface/arg.md +82 -0
  61. package/docs/reference/authoring-surface/basic.md +62 -0
  62. package/docs/reference/authoring-surface/bearer.md +53 -0
  63. package/docs/reference/authoring-surface/branchname.md +15 -0
  64. package/docs/reference/authoring-surface/chartschema.md +23 -0
  65. package/docs/reference/authoring-surface/chartschemafor.md +48 -0
  66. package/docs/reference/authoring-surface/command.md +206 -0
  67. package/docs/reference/authoring-surface/customize.md +64 -0
  68. package/docs/reference/authoring-surface/defineconfig.md +41 -0
  69. package/docs/reference/authoring-surface/defineevent.md +114 -0
  70. package/docs/reference/authoring-surface/dispatch.md +126 -0
  71. package/docs/reference/authoring-surface/fail.md +96 -0
  72. package/docs/reference/authoring-surface/file.md +142 -0
  73. package/docs/reference/authoring-surface/frompick.md +277 -0
  74. package/docs/reference/authoring-surface/http.md +259 -0
  75. package/docs/reference/authoring-surface/isfailure.md +58 -0
  76. package/docs/reference/authoring-surface/isready.md +14 -0
  77. package/docs/reference/authoring-surface/jr2setup.md +237 -0
  78. package/docs/reference/authoring-surface/open.md +105 -0
  79. package/docs/reference/authoring-surface/openbody.md +86 -0
  80. package/docs/reference/authoring-surface/paramsof.md +143 -0
  81. package/docs/reference/authoring-surface/pool.md +186 -0
  82. package/docs/reference/authoring-surface/push.md +130 -0
  83. package/docs/reference/authoring-surface/readyof.md +21 -0
  84. package/docs/reference/authoring-surface/secret.md +139 -0
  85. package/docs/reference/authoring-surface/sentby.md +83 -0
  86. package/docs/reference/authoring-surface/source.md +51 -0
  87. package/docs/reference/authoring-surface/text.md +62 -0
  88. package/docs/reference/authoring-surface/type-agentdeclaration.md +27 -0
  89. package/docs/reference/authoring-surface/type-agentdefinition.md +80 -0
  90. package/docs/reference/authoring-surface/type-agentlogic.md +15 -0
  91. package/docs/reference/authoring-surface/type-agentslots.md +15 -0
  92. package/docs/reference/authoring-surface/type-answeraction.md +12 -0
  93. package/docs/reference/authoring-surface/type-anyframepath.md +14 -0
  94. package/docs/reference/authoring-surface/type-anyworktreepath.md +15 -0
  95. package/docs/reference/authoring-surface/type-arg.md +19 -0
  96. package/docs/reference/authoring-surface/type-argnames.md +12 -0
  97. package/docs/reference/authoring-surface/type-bindactors.md +14 -0
  98. package/docs/reference/authoring-surface/type-binding.md +61 -0
  99. package/docs/reference/authoring-surface/type-bindingof.md +12 -0
  100. package/docs/reference/authoring-surface/type-bodyacceptsdoor.md +15 -0
  101. package/docs/reference/authoring-surface/type-bodyacceptsslots.md +17 -0
  102. package/docs/reference/authoring-surface/type-ceilings.md +41 -0
  103. package/docs/reference/authoring-surface/type-chainlink.md +19 -0
  104. package/docs/reference/authoring-surface/type-chartaction.md +46 -0
  105. package/docs/reference/authoring-surface/type-chartagent.md +28 -0
  106. package/docs/reference/authoring-surface/type-chartevent.md +20 -0
  107. package/docs/reference/authoring-surface/type-chartguard.md +20 -0
  108. package/docs/reference/authoring-surface/type-chartinvoke.md +28 -0
  109. package/docs/reference/authoring-surface/type-chartjson.md +14 -0
  110. package/docs/reference/authoring-surface/type-chartstate.md +46 -0
  111. package/docs/reference/authoring-surface/type-charttext.md +40 -0
  112. package/docs/reference/authoring-surface/type-charttransition.md +22 -0
  113. package/docs/reference/authoring-surface/type-charttransitions.md +14 -0
  114. package/docs/reference/authoring-surface/type-chartvalue.md +15 -0
  115. package/docs/reference/authoring-surface/type-childat.md +12 -0
  116. package/docs/reference/authoring-surface/type-childslots.md +12 -0
  117. package/docs/reference/authoring-surface/type-commandenv.md +15 -0
  118. package/docs/reference/authoring-surface/type-commandinput.md +20 -0
  119. package/docs/reference/authoring-surface/type-commandoptions.md +56 -0
  120. package/docs/reference/authoring-surface/type-commandoutput.md +122 -0
  121. package/docs/reference/authoring-surface/type-customize.md +111 -0
  122. package/docs/reference/authoring-surface/type-dispatchdefinition.md +39 -0
  123. package/docs/reference/authoring-surface/type-dispatchinput.md +14 -0
  124. package/docs/reference/authoring-surface/type-dispatchopenparts.md +14 -0
  125. package/docs/reference/authoring-surface/type-dispatchoutput.md +72 -0
  126. package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
  127. package/docs/reference/authoring-surface/type-duration.md +15 -0
  128. package/docs/reference/authoring-surface/type-effectbinding.md +93 -0
  129. package/docs/reference/authoring-surface/type-effectlogic.md +12 -0
  130. package/docs/reference/authoring-surface/type-envopenparts.md +14 -0
  131. package/docs/reference/authoring-surface/type-eventaudience.md +27 -0
  132. package/docs/reference/authoring-surface/type-eventdef.md +45 -0
  133. package/docs/reference/authoring-surface/type-eventfrom.md +16 -0
  134. package/docs/reference/authoring-surface/type-eventsemantics.md +26 -0
  135. package/docs/reference/authoring-surface/type-execoutcome.md +34 -0
  136. package/docs/reference/authoring-surface/type-failslot.md +14 -0
  137. package/docs/reference/authoring-surface/type-failure.md +19 -0
  138. package/docs/reference/authoring-surface/type-failureinput.md +20 -0
  139. package/docs/reference/authoring-surface/type-filebody.md +37 -0
  140. package/docs/reference/authoring-surface/type-framepath.md +14 -0
  141. package/docs/reference/authoring-surface/type-frominput.md +14 -0
  142. package/docs/reference/authoring-surface/type-hostinjectedinput.md +56 -0
  143. package/docs/reference/authoring-surface/type-httpdefinition.md +104 -0
  144. package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
  145. package/docs/reference/authoring-surface/type-httpinput.md +12 -0
  146. package/docs/reference/authoring-surface/type-httpmethod.md +14 -0
  147. package/docs/reference/authoring-surface/type-httpopenparts.md +12 -0
  148. package/docs/reference/authoring-surface/type-httpoutput.md +95 -0
  149. package/docs/reference/authoring-surface/type-httprequestparts.md +33 -0
  150. package/docs/reference/authoring-surface/type-jr2config.md +126 -0
  151. package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
  152. package/docs/reference/authoring-surface/type-lends.md +38 -0
  153. package/docs/reference/authoring-surface/type-logicat.md +12 -0
  154. package/docs/reference/authoring-surface/type-markedif.md +12 -0
  155. package/docs/reference/authoring-surface/type-mergedactors.md +12 -0
  156. package/docs/reference/authoring-surface/type-namedsecret.md +25 -0
  157. package/docs/reference/authoring-surface/type-namestart.md +77 -0
  158. package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +27 -0
  159. package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
  160. package/docs/reference/authoring-surface/type-openbodydeclaration.md +40 -0
  161. package/docs/reference/authoring-surface/type-openbodylogic.md +14 -0
  162. package/docs/reference/authoring-surface/type-openbodyoutput.md +42 -0
  163. package/docs/reference/authoring-surface/type-openbodyslot.md +18 -0
  164. package/docs/reference/authoring-surface/type-opencommandinput.md +22 -0
  165. package/docs/reference/authoring-surface/type-openeffect.md +12 -0
  166. package/docs/reference/authoring-surface/type-openeffectslots.md +12 -0
  167. package/docs/reference/authoring-surface/type-openof.md +12 -0
  168. package/docs/reference/authoring-surface/type-opensecret.md +23 -0
  169. package/docs/reference/authoring-surface/type-opensecretmark.md +12 -0
  170. package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +26 -0
  171. package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +27 -0
  172. package/docs/reference/authoring-surface/type-poolmachine.md +28 -0
  173. package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
  174. package/docs/reference/authoring-surface/type-poolspec.md +60 -0
  175. package/docs/reference/authoring-surface/type-pushinput.md +34 -0
  176. package/docs/reference/authoring-surface/type-pushoptions.md +79 -0
  177. package/docs/reference/authoring-surface/type-pushoutput.md +149 -0
  178. package/docs/reference/authoring-surface/type-pushwireoutput.md +12 -0
  179. package/docs/reference/authoring-surface/type-quantity.md +14 -0
  180. package/docs/reference/authoring-surface/type-reached.md +12 -0
  181. package/docs/reference/authoring-surface/type-reposlot.md +70 -0
  182. package/docs/reference/authoring-surface/type-reposlotsof.md +16 -0
  183. package/docs/reference/authoring-surface/type-sandboxoptions.md +84 -0
  184. package/docs/reference/authoring-surface/type-secret.md +25 -0
  185. package/docs/reference/authoring-surface/type-sender.md +19 -0
  186. package/docs/reference/authoring-surface/type-sentbyguards.md +12 -0
  187. package/docs/reference/authoring-surface/type-size.md +85 -0
  188. package/docs/reference/authoring-surface/type-slothandles.md +24 -0
  189. package/docs/reference/authoring-surface/type-sourcenextresult.md +17 -0
  190. package/docs/reference/authoring-surface/type-sourcespec.md +44 -0
  191. package/docs/reference/authoring-surface/type-syncstate.md +12 -0
  192. package/docs/reference/authoring-surface/type-text.md +19 -0
  193. package/docs/reference/authoring-surface/type-textpart.md +24 -0
  194. package/docs/reference/authoring-surface/type-thinkinglevel.md +39 -0
  195. package/docs/reference/authoring-surface/type-toparameterizedobject.md +14 -0
  196. package/docs/reference/authoring-surface/type-toprovidedactor.md +14 -0
  197. package/docs/reference/authoring-surface/type-unknown.md +21 -0
  198. package/docs/reference/authoring-surface/type-usercontainer.md +77 -0
  199. package/docs/reference/authoring-surface/type-withoutemptyindex.md +20 -0
  200. package/docs/reference/authoring-surface/type-workflowevent.md +46 -0
  201. package/docs/reference/authoring-surface/type-workspaceaccess.md +16 -0
  202. package/docs/reference/authoring-surface/type-workspaced.md +115 -0
  203. package/docs/reference/authoring-surface/type-workspacehandles.md +22 -0
  204. package/docs/reference/authoring-surface/type-workspacemachine.md +29 -0
  205. package/docs/reference/authoring-surface/type-workspaceof.md +13 -0
  206. package/docs/reference/authoring-surface/type-workspaceoptions.md +27 -0
  207. package/docs/reference/authoring-surface/type-worktreefound.md +16 -0
  208. package/docs/reference/authoring-surface/type-worktreeinput.md +56 -0
  209. package/docs/reference/authoring-surface/type-worktreekey.md +14 -0
  210. package/docs/reference/authoring-surface/type-worktreeoptions.md +29 -0
  211. package/docs/reference/authoring-surface/type-worktreeoutput.md +45 -0
  212. package/docs/reference/authoring-surface/type-worktreepath.md +16 -0
  213. package/docs/reference/authoring-surface/type-worktreeready.md +37 -0
  214. package/docs/reference/authoring-surface/type-worktreerefused.md +21 -0
  215. package/docs/reference/authoring-surface/workspace.md +287 -0
  216. package/docs/reference/authoring-surface/worktree.md +71 -0
  217. package/docs/reference/authoring-surface.md +217 -0
  218. package/docs/reference/cli.md +433 -0
  219. package/docs/reference/config.md +765 -0
  220. package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +114 -0
  221. package/docs/reference/decisions/0002-duplex-streaming-actor.md +117 -0
  222. package/docs/reference/decisions/0004-worktree-provisioning-model.md +130 -0
  223. package/docs/reference/decisions/0005-sandbox-pod-composition.md +240 -0
  224. package/docs/reference/decisions/0006-agent-control-surface.md +50 -0
  225. package/docs/reference/decisions/0007-durable-machine-state.md +109 -0
  226. package/docs/reference/decisions/0008-orchestrator-as-deployed-app.md +60 -0
  227. package/docs/reference/decisions/0009-cli-and-instance-interface.md +275 -0
  228. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +160 -0
  229. package/docs/reference/decisions/0011-workflow-defined-events.md +141 -0
  230. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +103 -0
  231. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +175 -0
  232. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +95 -0
  233. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +271 -0
  234. package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +126 -0
  235. package/docs/reference/decisions/0017-pool-over-a-source-port.md +71 -0
  236. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +111 -0
  237. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +287 -0
  238. package/docs/reference/decisions/0020-private-ca-trust-is-a-named-concept.md +60 -0
  239. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +101 -0
  240. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +103 -0
  241. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +175 -0
  242. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +436 -0
  243. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +65 -0
  244. package/docs/reference/decisions/0026-a-turn-that-is-over-is-never-prompted.md +62 -0
  245. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +245 -0
  246. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +90 -0
  247. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +154 -0
  248. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +100 -0
  249. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +193 -0
  250. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +61 -0
  251. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +126 -0
  252. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +57 -0
  253. package/docs/reference/decisions/0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md +92 -0
  254. package/docs/reference/decisions/0036-a-turn-compacts-its-own-context-mid-flight.md +138 -0
  255. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +196 -0
  256. package/docs/reference/decisions/0038-jr2-up-builds-every-image-it-deploys.md +170 -0
  257. package/docs/reference/decisions/0039-image-garbage-collects-by-reachability.md +115 -0
  258. package/docs/reference/decisions/0041-a-build-the-host-already-holds-is-not-spent-again.md +64 -0
  259. package/docs/reference/decisions/0042-ready-is-not-routable-an-admission-retries-its-connection.md +142 -0
  260. package/docs/reference/decisions/0043-the-kit-is-tested-as-installed-a-local-registry-stands-in-for-npm.md +170 -0
  261. package/docs/reference/decisions/0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md +85 -0
  262. package/docs/reference/decisions/0045-the-platform-joins-the-image-address-and-the-cluster-chooses-it.md +82 -0
  263. package/docs/reference/decisions/0046-a-converge-failure-carries-its-diagnosis.md +34 -0
  264. package/docs/reference/decisions/0047-the-git-ssh-key-source-is-the-users-choice.md +47 -0
  265. package/docs/reference/decisions/0048-the-orchestrator-boots-without-its-repos.md +36 -0
  266. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +126 -0
  267. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +73 -0
  268. package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +238 -0
  269. package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +86 -0
  270. package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +123 -0
  271. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +126 -0
  272. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +107 -0
  273. package/docs/reference/decisions/0056-a-global-jr2-hands-off-to-the-instances-own-and-an-instance-has-one-kit-version.md +62 -0
  274. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +152 -0
  275. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +108 -0
  276. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +427 -0
  277. package/docs/reference/decisions/0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md +111 -0
  278. package/docs/reference/decisions/0061-a-memory-kill-ends-the-agents-processes-not-the-harness.md +83 -0
  279. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +239 -0
  280. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +82 -0
  281. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +79 -0
  282. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +65 -0
  283. package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +552 -0
  284. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +458 -0
  285. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +475 -0
  286. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +180 -0
  287. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +334 -0
  288. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +183 -0
  289. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +144 -0
  290. package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +122 -0
  291. package/docs/reference/decisions/0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md +76 -0
  292. package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +295 -0
  293. package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +320 -0
  294. package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
  295. package/docs/reference/decisions.md +86 -0
  296. package/docs/reference/glossary.md +687 -0
  297. package/docs/reference/http-api.md +1386 -0
  298. package/docs/reference/machines.md +189 -0
  299. package/docs/reference/mechanism-events.md +72 -0
  300. package/docs/why.md +207 -0
  301. package/package.json +5 -3
  302. package/src/actor.ts +694 -106
  303. package/src/agent.ts +114 -38
  304. package/src/ambient.ts +3 -4
  305. package/src/assembly-checks.ts +189 -7
  306. package/src/cause.ts +234 -0
  307. package/src/chart.ts +345 -46
  308. package/src/charts.ts +43 -8
  309. package/src/config.ts +440 -178
  310. package/src/custodian.lua +17 -5
  311. package/src/custodian.ts +325 -99
  312. package/src/customize.ts +199 -49
  313. package/src/dispatch-effect.ts +945 -0
  314. package/src/door-compat.ts +217 -0
  315. package/src/durability.ts +11 -4
  316. package/src/effect-checks.ts +50 -3
  317. package/src/effect.ts +641 -128
  318. package/src/effector-wire.ts +165 -13
  319. package/src/fail.ts +222 -0
  320. package/src/harness-client.ts +587 -50
  321. package/src/harness-env.ts +33 -0
  322. package/src/held-secrets.ts +125 -16
  323. package/src/holds.ts +3 -0
  324. package/src/http-effect.ts +556 -58
  325. package/src/http.ts +480 -30
  326. package/src/index.ts +74 -82
  327. package/src/instance.ts +68 -8
  328. package/src/internal.ts +85 -0
  329. package/src/kube-client.ts +6 -3
  330. package/src/machine-doc.ts +24 -7
  331. package/src/mechanism-routes.ts +168 -0
  332. package/src/model-keys.generated.ts +53 -0
  333. package/src/model-keys.ts +42 -58
  334. package/src/names.ts +19 -6
  335. package/src/notices.ts +483 -41
  336. package/src/open-body.ts +242 -40
  337. package/src/open-effect.ts +81 -8
  338. package/src/open.ts +90 -6
  339. package/src/parts.ts +213 -30
  340. package/src/party.ts +101 -2
  341. package/src/pick.ts +497 -11
  342. package/src/pool.ts +282 -50
  343. package/src/registration.ts +133 -12
  344. package/src/repo-identity.ts +7 -3
  345. package/src/repos.ts +22 -3
  346. package/src/run-host.ts +665 -45
  347. package/src/run-output.ts +38 -0
  348. package/src/sandbox-effect.ts +457 -71
  349. package/src/sandbox-kube.ts +66 -17
  350. package/src/secret-placement.ts +59 -17
  351. package/src/server.ts +3 -0
  352. package/src/setup.ts +325 -62
  353. package/src/size.ts +121 -9
  354. package/src/snapshot-store.ts +54 -10
  355. package/src/stub-harness.ts +44 -16
  356. package/src/timers.ts +14 -1
  357. package/src/tokens.ts +107 -32
  358. package/src/vocabulary.ts +88 -14
  359. package/src/wire.ts +100 -26
  360. package/src/workspace.ts +771 -238
  361. package/src/worktree.ts +249 -0
package/src/effect.ts CHANGED
@@ -4,9 +4,11 @@
4
4
  // route on, the input kept in the persisted snapshot, a `src` typo as a compile error, and
5
5
  // `.provide()` as the unit-test seam.
6
6
  //
7
- // Two kinds exist: `command`, run in the Harness container of the enclosing Workspace's Sandbox
8
- // (the Harness's `/exec/:id`, ADR-0067), here; and `http`, run in the Instance Effector
9
- // (http-effect.ts, ADR-0068). What this module adds to xstate is what makes an Effect safe to
7
+ // The kinds: `command`, run in the Harness container of the enclosing Workspace's Sandbox (the
8
+ // Harness's `/exec/:id`, ADR-0067), here; `http`, run in the Instance Effector (http-effect.ts,
9
+ // ADR-0068); `push`, in the Sandbox Effector (sandbox-effect.ts); and `dispatch`, which starts a run
10
+ // of a registered Workflow, in the Orchestrator's own process or, toward another Orchestrator, in
11
+ // the Instance Effector (dispatch-effect.ts, ADR-0071). What this module adds to xstate is what makes an Effect safe to
10
12
  // restart:
11
13
  //
12
14
  // - THE LEDGER. The run host keeps one row per Effect, in the run's blob beside the snapshot
@@ -42,6 +44,7 @@ import type { ExecOutcome, ExecRead, ExecRequest, SyncAnswer } from "./wire.ts";
42
44
  import { bindOpenSecretIn, type EffectorPortFactory, type OpenSecretIn } from "./http-effect.ts";
43
45
  import { commandEnvParts, credentialedCommandWith, sandboxEffectorPort, type CommandEnv } from "./sandbox-effect.ts";
44
46
  import { open } from "./open.ts";
47
+ import type { AnyWorktreePath, WorktreeOutput } from "./worktree.ts";
45
48
  import {
46
49
  effectBindingShape,
47
50
  openEffectLogic,
@@ -53,10 +56,89 @@ import {
53
56
 
54
57
  // --- Parts: what `jr2 up` and the Machine doc read off a slot, without running anything ---------
55
58
 
56
- /** One argv entry an Effect's input fills: ONE argument, whatever its bytes (never shell text). */
59
+ // ONE argument, whatever its bytes: never shell text.
60
+ /** One entry of a command's argv that the input of the Effect fills, as `arg("name")` makes it. It
61
+ * is one argument, whatever its text. No shell reads it. */
57
62
  export type Arg<N extends string = string> = { readonly arg: N };
63
+
64
+ /**
65
+ * Mark one argv entry of a `command` that the input of the Effect fills. `arg("file")` is one
66
+ * argument: the value of the `file` key of the input, whatever its characters. No shell reads it.
67
+ *
68
+ * The input of the Effect then has a string key for each `arg`. Give it in the `input` of the
69
+ * `invoke`, or use `fromPick` to give it from a pick.
70
+ *
71
+ * When it starts, with no string in the input for an entry, the Effect fails (its `onError`
72
+ * route): `command "<slot>": argv slot <<name>> has no string value in its input (got <type>) —
73
+ * map it in the invoke's input`. The angle brackets around the name are in the text.
74
+ *
75
+ * @param name - The key of the input that fills this entry.
76
+ * @returns The argv entry, for the argv list of `command`.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * import {
81
+ * arg,
82
+ * command,
83
+ * fail,
84
+ * isReady,
85
+ * jr2Setup,
86
+ * workspace,
87
+ * worktree,
88
+ * type Workspaced,
89
+ * } from "@jr2/orchestrator";
90
+ *
91
+ * type Input = Workspaced<{ file: string }, "app">;
92
+ *
93
+ * const lintTree = (context: Input) => `${context.workspace.repos.app}/lint` as const;
94
+ *
95
+ * const body = jr2Setup({
96
+ * types: {} as { context: Input; input: Input },
97
+ * actors: { cut: worktree(), lint: command(["npx", "eslint", arg("file")]), fail: fail() },
98
+ * }).createMachine({
99
+ * id: "lint",
100
+ * context: ({ input }) => input,
101
+ * on: { "workspace.lost": ".lost" },
102
+ * initial: "cutting",
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
+ * },
111
+ * lint: {
112
+ * invoke: {
113
+ * src: "lint",
114
+ * input: ({ context }) => ({ file: context.file, worktree: lintTree(context) }),
115
+ * onDone: "done",
116
+ * },
117
+ * },
118
+ * done: { type: "final" },
119
+ * notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
120
+ * lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
121
+ * },
122
+ * });
123
+ *
124
+ * export const machine = workspace(body, {
125
+ * repos: { app: { url: "https://github.com/you/app.git" } },
126
+ * });
127
+ * ```
128
+ *
129
+ * @see docs/concepts/effects.md
130
+ * @see docs/guides/run-a-command-or-call-a-url.md
131
+ * @see docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md
132
+ */
58
133
  export const arg = <const N extends string>(name: N): Arg<N> => ({ arg: name });
59
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
+
60
142
  /** A `command` Effect's plain data. */
61
143
  export type CommandParts = {
62
144
  readonly kind: "command";
@@ -72,8 +154,12 @@ export type CommandParts = {
72
154
  /** A credentialed command's `pin`: its input names the commit (`{ sha }`, an approved one) in
73
155
  * place of the Agent's branch tip (ADR-0067). */
74
156
  readonly pin?: true;
75
- /** The Repo Slot whose Worktree is the cwd. Needed only where the Workspace carries several. */
76
- 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;
77
163
  readonly timeoutMs: number;
78
164
  /** Characters of the stream's end kept in the output. The full stream goes to the feed. */
79
165
  readonly tail: number;
@@ -103,17 +189,16 @@ export type HttpParts = {
103
189
  readonly maxBody: number;
104
190
  /** Marked by the author: a restore that cannot find the Effect sends it again. */
105
191
  readonly idempotent: boolean;
106
- /** The body is a Worktree file (`body: file(...)`, ADR-0067): the Effect runs in the Sandbox Effector
107
- * of the Workspace whose body invokes it. `slot` is the Repo Slot whose Worktree holds the file —
108
- * needed only where the Workspace carries several. Absent: a json, text or no body, in the
109
- * Instance Effector. */
110
- readonly file?: { readonly slot?: string; readonly contentType?: string };
192
+ /** The body is a Worktree file (`body: file(...)`, ADR-0067): the Effect runs in the Sandbox
193
+ * Effector of the Workspace whose body invokes it. The input's `worktree` holds the file. Absent: a json, text or no body, in
194
+ * the Instance Effector. */
195
+ readonly file?: { readonly contentType?: string };
111
196
  };
112
197
 
113
198
  /**
114
- * 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
115
200
  * Binding. `sign`, `squash`, `force` are the author's; `pin` says the Effect's input names the
116
- * commit (an approved one) instead of the Agent's branch tip.
201
+ * commit (an approved one) instead of the branch's tip.
117
202
  */
118
203
  export type PushParts = {
119
204
  readonly kind: "push";
@@ -128,6 +213,33 @@ export type PushParts = {
128
213
  readonly inputKeys: readonly string[];
129
214
  };
130
215
 
216
+ /** A `dispatch` Effect's target: another Orchestrator's origin and the secret that holds its
217
+ * Instance token, by name (ADR-0071). */
218
+ export type DispatchTarget = { readonly url: string; readonly token: string };
219
+
220
+ /**
221
+ * A `dispatch` Effect's plain data (ADR-0071): the Workflow it starts a run of, as `jr2 run` names
222
+ * it (`review`, or `<wrapper>/<name>` for a named Chart), whether it waits for that run to end, the
223
+ * bound on the start (or the wait), and the target. No target: it runs in the Orchestrator's own
224
+ * process, through the run host, with no wire and no token. A target: it runs in the Instance
225
+ * Effector, which holds the token as an Effect secret.
226
+ */
227
+ export type DispatchParts = {
228
+ readonly kind: "dispatch";
229
+ readonly workflow: string;
230
+ readonly wait: boolean;
231
+ readonly timeoutMs: number;
232
+ readonly target?: DispatchTarget;
233
+ /** The keys of the slot's input schema: what a pick param may fill. Undefined with no schema:
234
+ * the callee's door alone checks the body, so the load walk reads everything (ADR-0071). */
235
+ readonly inputKeys: readonly string[] | undefined;
236
+ /** `[target.token]`, or none in-process: what places the token (ADR-0068). */
237
+ readonly secrets: readonly string[];
238
+ /** Always false: a target's token is never `secret(open)` — the whole target is the Open part
239
+ * (ADR-0071). */
240
+ readonly openSecret: boolean;
241
+ };
242
+
131
243
  /**
132
244
  * An Effect whose url or argv a package left Open (ADR-0054): the same parts with the sentinel in
133
245
  * that one place. The kind, the placement, the timeout and the secrets the other parts name are
@@ -140,21 +252,36 @@ export type OpenHttpParts = Omit<HttpParts, "url" | "target"> & {
140
252
  readonly target: typeof open;
141
253
  };
142
254
 
255
+ /** A `dispatch` whose target a package left Open (ADR-0071): the composer binds the url and the
256
+ * token together, once. */
257
+ export type OpenDispatchParts = Omit<DispatchParts, "target"> & { readonly target: typeof open };
258
+
143
259
  /** Every kind's parts, bound or with an Open part. */
144
- export type EffectParts = CommandParts | HttpParts | PushParts | OpenCommandParts | OpenHttpParts;
260
+ export type EffectParts =
261
+ | CommandParts
262
+ | HttpParts
263
+ | PushParts
264
+ | OpenCommandParts
265
+ | OpenHttpParts
266
+ | DispatchParts
267
+ | OpenDispatchParts;
145
268
 
146
269
  /** The kinds, as their parts name them. */
147
- export const EFFECT_KINDS: ReadonlySet<string> = new Set(["command", "http", "push"]);
270
+ export const EFFECT_KINDS: ReadonlySet<string> = new Set(["command", "http", "push", "dispatch"]);
148
271
 
149
272
  /**
150
273
  * Where an Effect runs, derived from its kind and what it names, never authored (ADR-0067,
151
274
  * ADR-0068): a `command` that names no secret in the Harness container of its Workspace; an
152
275
  * `http` Effect with no Worktree file in the Instance Effector; an `http` whose body is a Worktree
153
- * file, a `push` and a `command` that names a secret in the Sandbox Effector of its Workspace.
276
+ * file, a `push` and a `command` that names a secret in the Sandbox Effector of its Workspace; a
277
+ * `dispatch` with no target in the Orchestrator's own process, and one with a target (an Open one
278
+ * included: it has one once bound) in the Instance Effector (ADR-0071).
154
279
  */
155
- export type Placement = "harness" | "instance" | "sandbox";
280
+ export type Placement = "harness" | "instance" | "sandbox" | "orchestrator";
156
281
  export function placementOf(parts: EffectParts): Placement {
157
282
  switch (parts.kind) {
283
+ case "dispatch":
284
+ return parts.target === undefined ? "orchestrator" : "instance";
158
285
  case "http":
159
286
  return parts.file ? "sandbox" : "instance";
160
287
  case "push":
@@ -192,23 +319,165 @@ export function targetText(parts: { readonly target: HttpParts["target"] | typeo
192
319
  }
193
320
 
194
321
  /** The keys an Effect's input has, read off its parts: a command's argv slot names (and `sha` with
195
- * `pin`); an http Effect's input schema keys; a push's `sha` and `message`. Undefined for an Open
196
- * argv: its slots are the composer's, unknown until `customize()` binds it (ADR-0054). */
322
+ * `pin`); an http Effect's input schema keys; a push's `sha` and `message`; a dispatch's input
323
+ * schema keys. Undefined for an Open argv: its slots are the composer's, unknown until
324
+ * `customize()` binds it (ADR-0054); and for a dispatch with no input schema: the callee's door is
325
+ * the only check, so nothing here says what it reads (ADR-0071). */
197
326
  export function inputKeysOf(parts: EffectParts): string[] | undefined {
198
- if (parts.kind !== "command") return [...parts.inputKeys];
327
+ const shape = inputShapeOf(parts);
328
+ return shape && [...shape.required, ...shape.optional, ...(shape.oneOf ?? [])];
329
+ }
330
+
331
+ /** The shape of a cut's input. */
332
+ const CUT_SHAPE: InputShape = { required: ["path"], optional: ["from"], oneOf: ["branch", "detach"] };
333
+
334
+ /** An Effect's input as a Chart's door and its start hold a Chart to it. Undefined where
335
+ * {@link inputKeysOf} is. `worktree` is optional for a key the Workspace decides: the door makes it
336
+ * required when it knows that the Workspace has Repo Slots, and a start refuses it then. */
337
+ export function inputShapeOf(parts: EffectParts): InputShape | undefined {
338
+ if (parts.kind === "dispatch") {
339
+ return parts.inputKeys === undefined ? undefined : { required: [...parts.inputKeys], optional: [] };
340
+ }
341
+ if (parts.kind === "push") return { required: [...parts.inputKeys], optional: [] };
342
+ if (parts.kind === "http") {
343
+ return { required: [...parts.inputKeys], optional: parts.file ? ["worktree"] : [] };
344
+ }
345
+ if (parts.cut) return CUT_SHAPE;
199
346
  if (parts.argv === open) return undefined;
200
- return [...parts.argv.flatMap((a) => (typeof a === "string" ? [] : [a.arg])), ...(parts.pin ? ["sha"] : [])];
347
+ const keys = [...parts.argv.flatMap((a) => (typeof a === "string" ? [] : [a.arg])), ...(parts.pin ? ["sha"] : [])];
348
+ // A command with an Effect secret runs on a commit of a Worktree, so it names one in each Workspace.
349
+ const credentialed = (parts.secrets?.length ?? 0) > 0 || parts.openSecret === true;
350
+ return credentialed ? { required: [...keys, "worktree"], optional: [] } : { required: keys, optional: ["worktree"] };
201
351
  }
202
352
 
203
353
  // --- Output -------------------------------------------------------------------------------------
204
354
 
205
- /** The outside world may have acted, and jr2 cannot tell: the Harness lost the exec, or its state
206
- * exited while it ran. Output the Machine routes on; never repeated by jr2. */
355
+ /** The output `{ outcome: "unknown", reason }`: the Effect may have acted, and jr2 cannot tell.
356
+ * For example, the Harness lost the exec, or its state exited while it ran. Route on it. jr2 does
357
+ * not run the Effect again. */
207
358
  export type Unknown = { outcome: "unknown"; reason: string };
208
359
 
209
- /** A `command` Effect's output: it exited, its timeout killed it, a signal killed it, or unknown. A
210
- * credentialed command's says `dirty` when the Worktree held edits it did not see (ADR-0067). */
211
- export type CommandOutput = (ExecOutcome & { dirty?: boolean }) | Unknown;
360
+ /** A value of `outcome` that this kit does not give. It stands in the union so that no author can
361
+ * write an exhaustive check, so that a later outcome is no break: route on what you know, and give
362
+ * the rest a last arm. */
363
+ export type LaterOutcome = { outcome: "(a later outcome)"; reason?: string };
364
+
365
+ // `dirty` is a credentialed command's alone (ADR-0067): it runs in the Sandbox Effector against
366
+ // the pinned commit, never the live Worktree.
367
+ /**
368
+ * The output of a `command` Effect: what the `onDone` of its state gets as `event.output`. Every
369
+ * outcome is output. A failed program is output too. Route on `outcome`:
370
+ *
371
+ * - `exited`: the program ended. `code` is its exit code. A program that the container does not
372
+ * have is `exited` with code 127.
373
+ * - `timeout`: the command ran past its `timeout`, and jr2 killed it.
374
+ * - `killed`: a signal ended it. `signal` names the signal.
375
+ * - `unknown`: jr2 cannot tell what the command did, for example because the Harness restarted
376
+ * while it ran. `reason` tells why. jr2 does not run it again.
377
+ *
378
+ * `tail` is the end of what the command wrote, at most the `tail` option of `command` in
379
+ * characters. `dirty` is `true` when a command that names an Effect secret ran against a commit,
380
+ * and the Worktree held changes that it did not see.
381
+ *
382
+ * The union is open ({@link LaterOutcome}): give the rest a last arm with no guard.
383
+ *
384
+ * @example
385
+ * ```ts
386
+ * import {
387
+ * command,
388
+ * type CommandOutput,
389
+ * fail,
390
+ * isReady,
391
+ * jr2Setup,
392
+ * workspace,
393
+ * worktree,
394
+ * type Workspaced,
395
+ * } from "@jr2/orchestrator";
396
+ *
397
+ * type Input = Workspaced<{}, "app">;
398
+ *
399
+ * const testTree = (context: Input) => `${context.workspace.repos.app}/test` as const;
400
+ *
401
+ * const passed = (output: CommandOutput): boolean =>
402
+ * output.outcome === "exited" && output.code === 0;
403
+ *
404
+ * const body = jr2Setup({
405
+ * types: {} as { context: Input; input: Input },
406
+ * actors: { cut: worktree(), tests: command(["npm", "test"], { timeout: "15m" }), fail: fail() },
407
+ * }).createMachine({
408
+ * id: "tests",
409
+ * context: ({ input }) => input,
410
+ * on: { "workspace.lost": ".lost" },
411
+ * initial: "cutting",
412
+ * states: {
413
+ * cutting: {
414
+ * invoke: {
415
+ * src: "cut",
416
+ * input: ({ context }) => ({ path: testTree(context), branch: "tests" }),
417
+ * onDone: [{ guard: ({ event }) => isReady(event.output), target: "testing" }, { target: "notCut" }],
418
+ * },
419
+ * },
420
+ * testing: {
421
+ * invoke: {
422
+ * src: "tests",
423
+ * input: ({ context }) => ({ worktree: testTree(context) }),
424
+ * onDone: [
425
+ * { guard: ({ event }) => passed(event.output), target: "green" },
426
+ * { target: "red" },
427
+ * ],
428
+ * },
429
+ * },
430
+ * green: { type: "final" },
431
+ * // Not a final state: a final state means `done`, and the run ends `failed` here.
432
+ * red: { invoke: { src: "fail", input: { reason: "npm test did not pass" } } },
433
+ * notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
434
+ * lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
435
+ * },
436
+ * });
437
+ *
438
+ * export const machine = workspace(body, {
439
+ * repos: { app: { url: "https://github.com/you/app.git" } },
440
+ * });
441
+ * ```
442
+ *
443
+ * @see docs/concepts/effects.md
444
+ * @see docs/guides/run-a-command-or-call-a-url.md
445
+ * @see docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md
446
+ */
447
+ export type CommandOutput = (ExecOutcome & { dirty?: boolean }) | Unknown | LaterOutcome;
448
+
449
+ // Reach is output, as an http Effect's is (ADR-0067): a body a model wrote can be fixed through
450
+ // the Chain, so a refusal is output too.
451
+ /**
452
+ * The output of a `dispatch` Effect. Route on `outcome`:
453
+ *
454
+ * - `started`: the Orchestrator accepted the new run. `run` is its id.
455
+ * - `settled`: with `wait`, the new run ended. `status` is `done`, `failed`, `error`, `cancelled`,
456
+ * `lost` or `drifted`, and `output` is the output of its Machine. For `failed`, `output` is the
457
+ * Failure, `{ reason, detail? }`. `status` is `live` when the timeout came first, and the run
458
+ * continues.
459
+ * - `refused`: the Orchestrator started no run. `status` is the HTTP status: 400 for an input that
460
+ * the Workflow does not take, 404 for a Workflow that it does not have, 409 for a named Chart that
461
+ * cannot run now, 422 for a Chart that it refuses. `report` holds the lines of the refusal.
462
+ * - `unreachable`: the Orchestrator of the target did not answer. No run started.
463
+ * - `unknown`: jr2 cannot tell if a run started.
464
+ *
465
+ * The union is open ({@link LaterOutcome}): give the rest a last arm with no guard.
466
+ *
467
+ * @see docs/concepts/effects.md
468
+ */
469
+ export type DispatchOutput =
470
+ | { outcome: "started"; run: string }
471
+ | {
472
+ outcome: "settled";
473
+ run: string;
474
+ status: "done" | "failed" | "error" | "cancelled" | "lost" | "drifted" | "live";
475
+ output?: unknown;
476
+ }
477
+ | { outcome: "refused"; status: number; report: string[] }
478
+ | { outcome: "unreachable"; reason: string }
479
+ | Unknown
480
+ | LaterOutcome;
212
481
 
213
482
  // --- The ledger row (persisted in the run's blob, ADR-0007) --------------------------------------
214
483
 
@@ -238,19 +507,38 @@ export type EffectRow =
238
507
  pinned?: string;
239
508
  at: string;
240
509
  }
241
- /** `sandbox` and `repo`: the Workspace and the Repo Slot an Effect of the Sandbox Effector ran on
242
- * — every later push of its own Workspace and Repo Slot (one branch, one remote) sends its pair,
243
- * and the newest push's unfinished sync runs again in that slot's Worktree. */
510
+ /** A dispatch's intent (ADR-0071): where it runs, and — once the start's answer names it — the
511
+ * run it started. A dispatch is idempotent by construction: its ledger id rides every start as
512
+ * the door's key, so a restore sends it again and gets the same run. */
513
+ | {
514
+ state: "intent";
515
+ kind: "dispatch";
516
+ slot: string;
517
+ placement: "orchestrator" | "instance";
518
+ idempotent: true;
519
+ wait: boolean;
520
+ run?: string;
521
+ at: string;
522
+ }
523
+ /** `sandbox`, `repo` and `branch`: the Workspace, the Repo Slot and the branch an Effect of the
524
+ * Sandbox Effector ran on — every later push of its own (slot, branch) sends its pair, and the
525
+ * newest push's unfinished sync runs again in `worktree`. `cut`: a `worktree()` that settled
526
+ * `ready`: its path and branch as fields, because a settled row drops its tail. A push, a
527
+ * credentialed command and a file body read their branch here. */
244
528
  | {
245
529
  state: "settled";
246
530
  kind: EffectParts["kind"];
247
531
  slot: string;
248
532
  sandbox?: string;
249
533
  repo?: string;
534
+ branch?: string;
535
+ cut?: { path: string; branch: string | null };
250
536
  output: unknown;
251
537
  at: string;
252
538
  }
253
- | { state: "aborted"; kind: EffectParts["kind"]; slot: string; at: string };
539
+ /** `run`: for a dispatch, the run it started, kept past the abort — the callee runs on, and the
540
+ * caller's row still names it (ADR-0071). */
541
+ | { state: "aborted"; kind: EffectParts["kind"]; slot: string; run?: string; at: string };
254
542
 
255
543
  /**
256
544
  * Retention (ADR-0067): drop, in place, the output tail of every settled row once a save on disk
@@ -297,36 +585,43 @@ const TAILS: Record<EffectRow["kind"], readonly string[]> = {
297
585
  // every later push of its Repo Slot sends the pair (`pushPairsOf`), and the newest push's sync
298
586
  // runs again when it did not finish. The sync's bundle and git's words are its tail.
299
587
  push: ["bundle", "detail"],
588
+ // A dispatch's tail is the callee's output, which `GET /runs/<id>` serves for as long as the run
589
+ // is kept, and a refusal's report, which can carry the callee door's whole schema: the feed's
590
+ // settled marker carries both whole. The row keeps the outcome, the run id, its status and the
591
+ // door's HTTP status (ADR-0071).
592
+ dispatch: ["output", "report"],
300
593
  };
301
594
 
302
- /** A kind's tail for one output. The NEWEST push of its Repo Slot whose sync did not finish
303
- * (`busy`, `not-synced`) keeps its bundle: the sync that runs again before the next push of that
304
- * slot moves the branch from it, and the Agent's store may not hold the published commits without it.
305
- * Once a sync finished, or once a newer push of the slot published, the bundle goes: only the
306
- * newest push's sync runs again (`lastPushOf`), so an older bundle has no reader. */
595
+ /** A kind's tail for one output. The NEWEST push of its Repo Slot whose sync did not finish (`busy`,
596
+ * `not-synced`) keeps its bundle: the sync that runs again before the next push of that slot moves
597
+ * the branch from it, and the Agent's store may not hold the published commits without it. Once a
598
+ * sync finished, or once a newer push of the slot published, the bundle goes: only the newest
599
+ * push's sync runs again (`lastPushOf`), so an older bundle has no reader. */
307
600
  function tailsOf(kind: EffectRow["kind"], output: Record<string, unknown>, newest: boolean): readonly string[] {
308
601
  if (kind === "push" && newest && (output.sync === "busy" || output.sync === "not-synced")) return ["detail"];
309
602
  return TAILS[kind];
310
603
  }
311
604
 
312
- /** A settled push row that published: its pair, and its sync's answer and bundle when it has them. */
605
+ /** A settled push row that published: its pair, and its sync's answer and bundle when it has them.
606
+ */
313
607
  export type PushedRow = { published: string; source: string; sync?: SyncAnswer["sync"]; bundle?: string } & Record<
314
608
  string,
315
609
  unknown
316
610
  >;
317
611
 
318
- /** The ledger's last push of a Workspace's Repo Slot that published: its row, by `at`, never this
319
- * Effect's (`self`). A Workspace with two Repo Slots has two branches on two remotes, so two pairs. */
612
+ /** The ledger's last push of a (slot, branch) of a Workspace that published: its row, by `at`, never
613
+ * this Effect's (`self`). Each (slot, branch) has its own pairs: one branch on one remote. */
320
614
  export function lastPushOf(
321
615
  rows: Readonly<Record<string, EffectRow>>,
322
616
  sandbox: string,
323
617
  repo: string,
618
+ branch: string,
324
619
  self?: string,
325
620
  ): { rowId: string; row: Extract<EffectRow, { state: "settled" }>; output: PushedRow } | undefined {
326
621
  let best: { rowId: string; row: Extract<EffectRow, { state: "settled" }>; output: PushedRow } | undefined;
327
622
  for (const [rowId, row] of Object.entries(rows)) {
328
623
  if (rowId === self || row.state !== "settled" || row.kind !== "push") continue;
329
- if (row.sandbox !== sandbox || row.repo !== repo) continue;
624
+ if (row.sandbox !== sandbox || row.repo !== repo || row.branch !== branch) continue;
330
625
  const output = row.output as Partial<PushedRow> & { outcome?: unknown };
331
626
  if (output?.outcome !== "pushed" || typeof output.published !== "string" || typeof output.source !== "string") {
332
627
  continue;
@@ -336,20 +631,21 @@ export function lastPushOf(
336
631
  return best;
337
632
  }
338
633
 
339
- /** Every pair the ledger holds for a Workspace's Repo Slot (ADR-0067): each settled push row that
340
- * published, oldest first by `at`, never this Effect's (`self`) — its published commit, its source,
341
- * and its own `squashed`. What a push start carries, so the Effector sees every push's pair. */
634
+ /** Every pair the ledger holds for a (slot, branch) of a Workspace (ADR-0067): each settled push row
635
+ * that published, oldest first by `at`, never this Effect's (`self`) — its published commit, its
636
+ * source, and its own `squashed`. What a push start carries, so the Effector sees every push's pair. */
342
637
  export function pushPairsOf(
343
638
  rows: Readonly<Record<string, EffectRow>>,
344
639
  sandbox: string,
345
640
  repo: string,
641
+ branch: string,
346
642
  self?: string,
347
643
  ): Array<{ published: string; source: string; squashed?: "signed" | "unsigned" }> {
348
644
  const found: Array<{ at: string; pair: { published: string; source: string; squashed?: "signed" | "unsigned" } }> =
349
645
  [];
350
646
  for (const [rowId, row] of Object.entries(rows)) {
351
647
  if (rowId === self || row.state !== "settled" || row.kind !== "push") continue;
352
- if (row.sandbox !== sandbox || row.repo !== repo) continue;
648
+ if (row.sandbox !== sandbox || row.repo !== repo || row.branch !== branch) continue;
353
649
  const output = row.output as Partial<PushedRow> & { outcome?: unknown; squashed?: unknown };
354
650
  if (output?.outcome !== "pushed" || typeof output.published !== "string" || typeof output.source !== "string") {
355
651
  continue;
@@ -363,21 +659,41 @@ export function pushPairsOf(
363
659
  return found.sort((a, b) => (a.at < b.at ? -1 : a.at > b.at ? 1 : 0)).map((f) => f.pair);
364
660
  }
365
661
 
366
- /** The row id of each Repo Slot's newest push that published (`lastPushOf`), over the ledger. */
662
+ /** The row id of each (slot, branch)'s newest push that published (`lastPushOf`), over the ledger. */
367
663
  function newestPushesOf(effects: Readonly<Record<string, EffectRow>>): Set<string> {
368
664
  const ids = new Set<string>();
369
665
  const seen = new Set<string>();
370
666
  for (const row of Object.values(effects)) {
371
667
  if (row.state !== "settled" || row.kind !== "push" || row.sandbox === undefined || row.repo === undefined) continue;
372
- const key = JSON.stringify([row.sandbox, row.repo]);
668
+ if (row.branch === undefined) continue;
669
+ const key = JSON.stringify([row.sandbox, row.repo, row.branch]);
373
670
  if (seen.has(key)) continue;
374
671
  seen.add(key);
375
- const last = lastPushOf(effects, row.sandbox, row.repo);
672
+ const last = lastPushOf(effects, row.sandbox, row.repo, row.branch);
376
673
  if (last !== undefined) ids.add(last.rowId);
377
674
  }
378
675
  return ids;
379
676
  }
380
677
 
678
+ /** The cut that made `path` in this Sandbox: the newest settled `ready` row, whatever its Repo Slot.
679
+ * What a push, a credentialed command and a file body read. A row of another Sandbox says nothing
680
+ * of this one: a re-provision has an empty `/work`. */
681
+ export function cutRowOf(
682
+ rows: Readonly<Record<string, EffectRow>>,
683
+ sandbox: string,
684
+ path: unknown,
685
+ ): { repo: string; path: string; branch: string | null } | undefined {
686
+ let best: { at: string; repo: string; path: string; branch: string | null } | undefined;
687
+ for (const row of Object.values(rows)) {
688
+ if (row.state !== "settled" || row.cut === undefined || row.sandbox !== sandbox) continue;
689
+ if (row.cut.path !== path || row.repo === undefined) continue;
690
+ if (best === undefined || row.at > best.at) {
691
+ best = { at: row.at, repo: row.repo, path: row.cut.path, branch: row.cut.branch };
692
+ }
693
+ }
694
+ return best;
695
+ }
696
+
381
697
  /** One Effect line on the run's feed (Instance-token band, like the Turn markers). `output` lines
382
698
  * carry the stream; `aborted` and an `unknown` settle say the outside world may have acted. */
383
699
  export type EffectMarker = {
@@ -386,14 +702,15 @@ export type EffectMarker = {
386
702
  effect: string;
387
703
  stage: "started" | "output" | "settled" | "aborted";
388
704
  text?: string;
389
- output?: CommandOutput | HttpOutput;
705
+ output?: CommandOutput | WorktreeOutput | HttpOutput | DispatchOutput;
390
706
  };
391
707
 
392
708
  // --- The port -----------------------------------------------------------------------------------
393
709
 
394
710
  /** The Harness's exec, as the Effect drives it. `read` answers undefined for an exec the Harness
395
711
  * does not know (404): it restarted, and jr2 settles `unknown`. `start` rejects with `ExecNotSent`
396
- * only when the request certainly never reached the Harness — the one failure it is sent again on. */
712
+ * only when the request certainly never reached the Harness — the one failure it is sent again on.
713
+ */
397
714
  export interface ExecPort {
398
715
  start(id: string, req: ExecRequest, signal: AbortSignal): Promise<void>;
399
716
  read(id: string, offset: number | undefined, signal: AbortSignal): Promise<ExecRead | undefined>;
@@ -416,48 +733,122 @@ type Fault = { type: "jr2.effect.error"; error: unknown };
416
733
 
417
734
  /** An Effect slot's logic: xstate `ActorLogic`, typed by its input and output, branded with its
418
735
  * parts. The brand is a runtime property (`isEffect`) and part of the type, as `definition` is
419
- * not on an Agent's: nothing substitutes a fake for an Effect's parts. */
736
+ * not on an Agent's: nothing substitutes a fake for an Effect's parts.
737
+ *
738
+ * @internal
739
+ */
420
740
  export type EffectLogic<I, O> = ActorLogic<EffectSnapshot<O, I>, EventObject, I> & EffectSlot;
421
741
 
742
+ /** @internal */
422
743
  type ArgNames<A extends readonly unknown[]> = A[number] extends infer E ? (E extends Arg<infer N> ? N : never) : never;
423
744
  /** A command's input: one string per argv slot, and with `pin` the approved commit (`{ sha }`);
424
745
  * none when there is neither. */
425
- export type CommandInput<A extends readonly unknown[], P = false> = [
746
+ export type CommandInput<A extends readonly unknown[], P = false, Opt = unknown> = [
426
747
  ArgNames<A> | ([P] extends [true] ? "sha" : never),
427
748
  ] extends [never]
428
- ? void
429
- : { [K in ArgNames<A> | ([P] extends [true] ? "sha" : never)]: string };
749
+ ? (WorktreeKey<Opt> & NotWorktreeKeys) | ([Opt] extends [{ readonly env: object }] ? never : void)
750
+ : { [K in ArgNames<A> | ([P] extends [true] ? "sha" : never)]: string } & WorktreeKey<Opt> &
751
+ Omit<NotWorktreeKeys, ArgNames<A>>;
752
+
753
+ // 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
754
+ // none. A credentialed command runs against one commit of a Worktree, so there it is always required.
755
+ /** @internal */
756
+ export type WorktreeKey<Opt> = [Opt] extends [{ readonly env: object }]
757
+ ? { worktree: AnyWorktreePath }
758
+ : { worktree?: AnyWorktreePath };
759
+
760
+ /** The keys of a Turn and a cut, refused by name on an Effect's input: a mapper's excess key is no
761
+ * compile error, and the Worktree would be lost.
762
+ * @internal */
763
+ export type NotWorktreeKeys = { cwd?: never; path?: never; branch?: never };
430
764
 
431
- /** An Open argv's input (ADR-0054): its slots are the composer's, so any key is one string — what
432
- * the package's invoke hands it, read by the argv the composer binds. The compiler checks no slot
433
- * against it: one the invoke does not supply faults when the Effect starts (`argvOf`). */
434
- export type OpenCommandInput<P = false> = Readonly<Record<string, string>> &
435
- ([P] extends [true] ? { sha: string } : unknown);
765
+ // An Open argv's slots are the composer's (ADR-0054), so any key is one string — what the
766
+ // package's invoke hands it, read by the argv the composer binds (`argvOf`).
767
+ /** The input of a `command(open)`: a string for each `arg()` of the argv that the Instance binds,
768
+ * and with `pin`, the commit (`{ sha }`). The compiler cannot check the names. When the input has
769
+ * no string for an `arg()`, the Effect fails when it starts. */
770
+ export type OpenCommandInput<P = false, Opt = unknown> = Readonly<Record<string, string>> &
771
+ ([P] extends [true] ? { sha: string } : unknown) &
772
+ WorktreeKey<Opt>;
436
773
 
774
+ /** A length of time: a positive number and a unit, `s`, `m` or `h`. For example `"90s"`, `"10m"`
775
+ * or `"1h"`. */
437
776
  export type Duration = `${number}s` | `${number}m` | `${number}h`;
438
777
 
778
+ /** The options of `command()`: the Repo Slot it runs in, how long it can run, how much of its
779
+ * output it keeps, its Effect secrets, and the commit it runs against. */
439
780
  export type CommandOptions = {
440
- /** The Repo Slot whose Worktree is the cwd — needed only where the Workspace carries several. */
441
- slot?: string;
442
- /** Default 10m. On expiry the Harness kills the process group: output `timeout`. */
781
+ /**
782
+ * How long the command can run: a number and a unit, such as `"90s"`, `"10m"` or `"1h"`.
783
+ * Default `"10m"`. When the time ends, jr2 kills the process group, and the output is
784
+ * `timeout`. It refuses a value that is not a positive duration: `command: timeout "<timeout>"
785
+ * is not a positive duration (like "90s", "10m", "1h")`.
786
+ */
443
787
  timeout?: Duration;
444
- /** Characters of the stream's end kept in the output. Default 4096. */
788
+ /** The number of characters that `tail` keeps from the end of what the command wrote. Default
789
+ * 4096. */
445
790
  tail?: number;
446
- /** The env, each a secret's value — `secret("x")` — or its file's path — `secret("x").path`
447
- * (ADR-0067). Naming a secret places the command in the Sandbox Effector, against the pinned
448
- * commit; with none it runs in the Harness container. */
791
+ // Naming a secret is the placement (ADR-0067, ADR-0068): there is no placement key.
792
+ /**
793
+ * The environment variables of the command, each an Effect secret. `secret("x")` gives the
794
+ * value, and `secret("x").path` gives the path of a file that holds the value. When `env` names
795
+ * one, the command runs in the Sandbox Effector against one commit, not in the Worktree that the
796
+ * Agent changes. Without `env`, it runs in the Harness container, beside the Agent.
797
+ *
798
+ * It refuses a name that is not a variable name, and `PATH`, `HOME` and `TMPDIR`: ``command(argv,
799
+ * { env }): "<name>" is not a variable the author sets — PATH, HOME and TMPDIR are the
800
+ * Effector's (ADR-0067)``.
801
+ */
449
802
  env?: CommandEnv;
450
- /** A credentialed command's commit: the Effect's input names it (`{ sha }`, an approved one);
451
- * absent, the Agent's branch tip when it starts. */
803
+ /**
804
+ * Only for a command whose `env` names an Effect secret. `true`: the input of the Effect names
805
+ * the commit to run against (`{ sha }`, a full commit id, for example one that a Gate approved).
806
+ * Default: the tip of the Agent's branch when the Effect starts.
807
+ *
808
+ * It refuses `pin: true` on a command that names no Effect secret: ``command(argv, { pin: true
809
+ * }) names no secret in its env: `pin` chooses the commit a credentialed command runs against in
810
+ * the Sandbox Effector, and a command that names no secret runs in the live Worktree (ADR-0067)
811
+ * — name its secret, command(argv, { env: { NAME: secret("x") }, pin: true }), or drop `pin` ``.
812
+ *
813
+ * When it starts, it refuses an input `sha` that is not a full commit id: `command "<slot>"
814
+ * (pin): its input's sha is not a full commit id`.
815
+ */
452
816
  pin?: boolean;
453
817
  };
454
818
 
455
819
  /** The Open part a command's options state (ADR-0054): its secret, when its env holds
456
- * `secret(open)` or its `.path`. */
820
+ * `secret(open)` or its `.path`.
821
+ *
822
+ * @internal
823
+ */
457
824
  export type EnvOpenParts<Opt> = Opt extends { readonly env: infer E }
458
825
  ? OpenSecretIn<E extends Readonly<Record<string, infer V>> ? V : never>
459
826
  : never;
460
827
 
828
+ /**
829
+ * Why an Effect kind cannot run in a Workspace with no Repo Slot (ADR-0051, ADR-0067), or undefined
830
+ * when it can: a `push` and a credentialed `command` run on a commit of a slot's branch, and a
831
+ * `worktree` makes a Worktree of a slot's clone, and that Workspace has none. The invoke and the
832
+ * `jr2 up` walk refuse with the same words.
833
+ */
834
+ export function needsASlot(kind: string): string | undefined {
835
+ if (kind === "worktree") {
836
+ return 'a cut makes a Worktree of a Repo Slot, and there is none — declare one: repos: { app: "<url>" }';
837
+ }
838
+ if (kind === "push")
839
+ return 'a push publishes a Repo Slot\'s branch, and there is none — declare one: repos: { app: "<url>" }';
840
+ if (kind === "command") {
841
+ return (
842
+ "a command with an Effect secret runs against one commit of a Repo Slot's branch, and there is none — " +
843
+ "declare a Repo Slot, or name no secret in its env, and it runs in /work beside the Agent"
844
+ );
845
+ }
846
+ return undefined;
847
+ }
848
+
849
+ /** The work root of a Sandbox pod (ADR-0004): where a command runs in a Workspace with no Repo Slot. */
850
+ const WORK_ROOT = "/work";
851
+
461
852
  const DEFAULT_TIMEOUT_MS = 10 * 60_000;
462
853
  const DEFAULT_TAIL = 4096;
463
854
  /** How long a re-asked request waits, at most, after a transport failure. */
@@ -478,7 +869,7 @@ export function commandWith<const Opt extends CommandOptions = CommandOptions>(
478
869
  argv: typeof open,
479
870
  options?: Opt,
480
871
  effectorPorts?: EffectorPortFactory,
481
- ): EffectLogic<OpenCommandInput<Opt["pin"]>, CommandOutput> & OpenEffect<"argv" | EnvOpenParts<Opt>>;
872
+ ): EffectLogic<OpenCommandInput<Opt["pin"], Opt>, CommandOutput> & OpenEffect<"argv" | EnvOpenParts<Opt>>;
482
873
  export function commandWith<
483
874
  const A extends readonly (string | Arg)[],
484
875
  const Opt extends CommandOptions = CommandOptions,
@@ -487,12 +878,15 @@ export function commandWith<
487
878
  argv: A,
488
879
  options?: Opt,
489
880
  effectorPorts?: EffectorPortFactory,
490
- ): EffectLogic<CommandInput<A, Opt["pin"]>, CommandOutput> & OpenOf<EnvOpenParts<Opt>>;
881
+ cut?: CommandParts["cut"],
882
+ ): EffectLogic<CommandInput<A, Opt["pin"], Opt>, CommandOutput> & OpenOf<EnvOpenParts<Opt>>;
491
883
  export function commandWith(
492
884
  portFactory: ExecPortFactory,
493
885
  argv: readonly (string | Arg)[] | typeof open,
494
886
  options: CommandOptions = {},
495
887
  effectorPorts: EffectorPortFactory = sandboxEffectorPort,
888
+ // `worktree()` alone passes this: an author's command is at most once.
889
+ cut?: CommandParts["cut"],
496
890
  // The implementation of the overloads above: each one states the input type.
497
891
  ): EffectLogic<any, CommandOutput> {
498
892
  if (argv === open) {
@@ -519,9 +913,9 @@ export function commandWith(
519
913
  argv: [...argv],
520
914
  timeoutMs: options.timeout === undefined ? DEFAULT_TIMEOUT_MS : parseDuration(options.timeout),
521
915
  tail: options.tail ?? DEFAULT_TAIL,
522
- ...(options.slot !== undefined ? { slot: options.slot } : {}),
523
916
  ...(options.env !== undefined && Object.keys(options.env).length > 0 ? commandEnvParts(options.env) : {}),
524
917
  ...(options.pin === true ? { pin: true as const } : {}),
918
+ ...(cut !== undefined ? { cut, idempotent: true as const } : {}),
525
919
  };
526
920
  if (parts.pin && (parts.secrets?.length ?? 0) === 0 && !parts.openSecret) {
527
921
  // A command that names no secret runs in the Harness container, in the live Worktree: there
@@ -549,7 +943,7 @@ export function commandWith(
549
943
  ) as unknown as EffectLogic<unknown, CommandOutput>;
550
944
  }
551
945
  type I = unknown;
552
- type O = CommandOutput;
946
+ type O = CommandOutput | WorktreeOutput;
553
947
  const logic: ActorLogic<EffectSnapshot<O, I>, EventObject, I> = {
554
948
  getInitialSnapshot: (_scope, input) => ({
555
949
  status: "active",
@@ -583,12 +977,63 @@ export function commandWith(
583
977
  },
584
978
  getPersistedSnapshot: (snapshot) => snapshot,
585
979
  };
586
- return Object.assign(logic, { effect: parts });
980
+ return Object.assign(logic, { effect: parts }) as unknown as EffectLogic<any, CommandOutput>;
587
981
  }
588
982
 
589
983
  /** The slot's name as its parent invoked it — what the ledger and the feed call the Effect. */
590
984
  const slotOf = (self: AnyActorRef): string => (typeof self.src === "string" ? self.src : self.id);
591
985
 
986
+ /** How many times a marked Effect goes out again after a Harness that lost it. Bounded, so that a
987
+ * Harness in a crash loop ends the wait: the Effect settles `unknown`, and the Machine routes. */
988
+ const RESENDS = 3;
989
+
990
+ /** What a cut's program answers: its last line. Anything else is a cut that did not end. */
991
+ function cutOutputOf(raw: CommandOutput): WorktreeOutput {
992
+ if (raw.outcome === "unknown") return raw as Unknown;
993
+ const tail = "tail" in raw && typeof raw.tail === "string" ? raw.tail : "";
994
+ if (raw.outcome === "exited" && raw.code === 0) {
995
+ try {
996
+ const answer = JSON.parse(tail.trim().split("\n").pop() ?? "") as { outcome?: unknown };
997
+ if (answer.outcome === "ready" || answer.outcome === "refused") return answer as WorktreeOutput;
998
+ } catch {
999
+ // Falls through: the program said no answer.
1000
+ }
1001
+ }
1002
+ return { outcome: "unknown", reason: `the cut ended ${raw.outcome} with no answer: ${tail.slice(-400)}` };
1003
+ }
1004
+
1005
+ /** Why a cut's input cannot go to the Harness, or undefined. The door checked a Chart's literals; a
1006
+ * reference, a pick param or a mapper can still say this. No exec, no fetch. */
1007
+ function cutFaultOf(input: unknown, ambient: AmbientHandles): string | undefined {
1008
+ const given = (input ?? {}) as { path?: unknown; branch?: unknown; detach?: unknown; from?: unknown };
1009
+ const fault = worktreePathFault(given.path, ambient, { cut: true });
1010
+ if (fault !== undefined) return fault;
1011
+ if ((given.branch === undefined) === (given.detach === undefined))
1012
+ return "a cut names a branch, or a commit in `detach`";
1013
+ if (given.detach !== undefined && given.from !== undefined) return "a detached cut takes no `from`";
1014
+ for (const key of ["branch", "detach", "from"] as const) {
1015
+ if (given[key] !== undefined && typeof given[key] !== "string") return `a cut's \`${key}\` is a string`;
1016
+ }
1017
+ return undefined;
1018
+ }
1019
+
1020
+ /** The cut's one argument: its input, its id (a re-send answers the first `found`), its static
1021
+ * `expect`, and the base, sent each time, so that no checkout of an Agent's can move it. */
1022
+ function cutRequestOf(id: string, input: unknown, parts: CommandParts, ambient: AmbientHandles): string {
1023
+ const given = (input ?? {}) as { path: string; branch?: string; detach?: string; from?: string };
1024
+ const slot = given.path.split("/")[2]!;
1025
+ const base = ambient.bases?.[slot];
1026
+ return JSON.stringify({
1027
+ id,
1028
+ path: given.path,
1029
+ ...(given.branch !== undefined ? { branch: given.branch } : {}),
1030
+ ...(given.detach !== undefined ? { detach: given.detach } : {}),
1031
+ ...(given.from !== undefined ? { from: given.from } : {}),
1032
+ ...(given.branch !== undefined && parts.cut?.expect ? { expect: parts.cut.expect } : {}),
1033
+ ...(base !== undefined ? { base } : {}),
1034
+ });
1035
+ }
1036
+
592
1037
  /** Run, re-attach to, or deliver one Effect, by its ledger row (see the module header). */
593
1038
  async function drive<I, O>(
594
1039
  self: AnyActorRef,
@@ -605,14 +1050,30 @@ async function drive<I, O>(
605
1050
  const now = (): string => new Date().toISOString();
606
1051
  const mark = (marker: Omit<EffectMarker, "kind" | "slot" | "effect">): void =>
607
1052
  binding?.marker?.({ kind: "effect", slot, effect: id, ...marker });
608
- const settle = (output: CommandOutput): void => {
609
- if (signal.aborted || self.getSnapshot().status !== "active") return;
610
- // The settled row first, saved at the write: a restart before the transition is saved delivers
611
- // this output, and runs nothing.
612
- binding?.recordEffect?.(id, { state: "settled", kind: parts.kind, slot, output, at: now() });
613
- mark({ stage: "settled", output });
614
- self.send({ type: "jr2.effect.settle", output } satisfies Settle);
615
- };
1053
+ /** A settled row of an exec. `sandbox` is the row's own on a restore: the Workspace it ran in. */
1054
+ const settleIn =
1055
+ (sandbox: string | undefined) =>
1056
+ (raw: CommandOutput | WorktreeOutput): void => {
1057
+ if (signal.aborted || self.getSnapshot().status !== "active") return;
1058
+ const output = parts.cut && raw.outcome !== "refused" ? cutOutputOf(raw as CommandOutput) : raw;
1059
+ // The settled row first, saved at the write: a restart before the transition is saved delivers
1060
+ // this output, and runs nothing. A `ready` cut's row keeps its fields: a push reads them.
1061
+ const cutRow =
1062
+ parts.cut && output.outcome === "ready"
1063
+ ? { sandbox, repo: output.slot, cut: { path: output.path, branch: output.branch } }
1064
+ : {};
1065
+ binding?.recordEffect?.(id, {
1066
+ state: "settled",
1067
+ kind: parts.kind,
1068
+ slot,
1069
+ ...(cutRow as object),
1070
+ output,
1071
+ at: now(),
1072
+ });
1073
+ mark({ stage: "settled", output });
1074
+ self.send({ type: "jr2.effect.settle", output } satisfies Settle);
1075
+ };
1076
+ const settle = settleIn(undefined);
616
1077
 
617
1078
  const row = binding?.effectRow?.(id);
618
1079
  if (row?.state === "settled") {
@@ -628,13 +1089,14 @@ async function drive<I, O>(
628
1089
  return;
629
1090
  }
630
1091
  if (row?.state === "intent" && row.kind === "command" && "endpoint" in row) {
631
- // In flight at an Orchestrator restart: re-attach by id, or settle `unknown`. Never run again.
1092
+ // In flight at an Orchestrator restart: re-attach by id, or settle `unknown`. Never run again,
1093
+ // unless the kit marked it: a cut goes on below, under the same id.
632
1094
  const port = portFactory(row.endpoint, binding?.harnessBearer?.(row.placement));
633
1095
  run.port = port;
634
1096
  run.started = true;
635
1097
  const first = await retrying(() => port.read(id, undefined, signal), signal);
636
1098
  if (signal.aborted) return;
637
- if (first === undefined) {
1099
+ if (first === undefined && !parts.idempotent) {
638
1100
  const output: Unknown = {
639
1101
  outcome: "unknown",
640
1102
  reason: "it was running when the Orchestrator restarted, and the Harness no longer knows it; not run again",
@@ -642,8 +1104,11 @@ async function drive<I, O>(
642
1104
  settle(output);
643
1105
  return;
644
1106
  }
645
- await follow(port, id, first, signal, mark, settle);
646
- return;
1107
+ if (first !== undefined) {
1108
+ const lost = await follow(port, id, first, signal, mark, settleIn(row.placement), parts.idempotent === true);
1109
+ if (!lost) return;
1110
+ }
1111
+ mark({ stage: "started", text: "sent again: the Harness lost it, and a cut is idempotent" });
647
1112
  }
648
1113
 
649
1114
  // Fresh: nothing was sent. Where it runs is the enclosing Workspace's Harness — refused outside one.
@@ -654,9 +1119,43 @@ async function drive<I, O>(
654
1119
  `workspace() — invoke it from a Workspace's body (ADR-0067)`,
655
1120
  );
656
1121
  }
1122
+ const settled = settleIn(ambient.sandbox);
1123
+ // A bad Worktree is a word an Agent or a Chart can say, so it is output (ADR-0067), in the form a
1124
+ // command that could not start has: exited 127, with the reason in the tail. A key that no one
1125
+ // wrote is a defect of the Machine: a throw, and `onError`.
1126
+ const worktree = (snapshot.input as { worktree?: unknown } | undefined | void)?.worktree;
1127
+ if (parts.cut) {
1128
+ if (Object.keys(ambient.repos).length === 0) {
1129
+ throw new Error(
1130
+ `worktree "${slot}" runs under a Workspace with no Repo Slot: ${needsASlot("worktree")} (ADR-0075)`,
1131
+ );
1132
+ }
1133
+ const fault = cutFaultOf(snapshot.input, ambient);
1134
+ if (fault !== undefined) {
1135
+ settled({ outcome: "refused", reason: fault });
1136
+ return;
1137
+ }
1138
+ } else if (worktree === undefined) {
1139
+ if (Object.keys(ambient.repos).length > 0) {
1140
+ throw new Error(
1141
+ `command "${slot}" runs under a Workspace with Repo Slots (${Object.keys(ambient.repos).join(", ")}) and its ` +
1142
+ "input names no `worktree`: say which, a Worktree that a cut gave or `<slot>/default`",
1143
+ );
1144
+ }
1145
+ } else {
1146
+ const fault = worktreePathFault(worktree, ambient);
1147
+ if (fault !== undefined) {
1148
+ settled({ outcome: "exited", code: 127, tail: `jr2: ${fault}; nothing ran\n` });
1149
+ return;
1150
+ }
1151
+ }
657
1152
  const req: ExecRequest = {
658
- argv: argvOf(slot, parts, snapshot.input),
659
- ...cwdOf(slot, parts, ambient),
1153
+ argv: parts.cut
1154
+ ? [...(parts.argv as string[]), cutRequestOf(id, snapshot.input, parts, ambient)]
1155
+ : argvOf(slot, parts, snapshot.input),
1156
+ // Named, never left to the Harness: a spawn with no cwd runs in the image's own WORKDIR.
1157
+ cwd: parts.cut ? WORK_ROOT : ((worktree as string | undefined) ?? WORK_ROOT),
1158
+ ...(!parts.cut && worktree !== undefined ? { root: true as const } : {}),
660
1159
  timeoutMs: parts.timeoutMs,
661
1160
  tail: parts.tail,
662
1161
  };
@@ -674,26 +1173,38 @@ async function drive<I, O>(
674
1173
  });
675
1174
  run.started = true;
676
1175
  mark({ stage: "started", text: req.argv.join(" ") });
677
- const sent = await startOnce(port, id, req, signal);
678
- if (signal.aborted) return;
679
- const first = await retrying(() => port.read(id, 0, signal), signal);
680
- if (signal.aborted) return;
681
- if (first === undefined) {
682
- if (sent === "uncertain") {
683
- // The start may still be on its way to the Harness that answers now: a kill keeps it from
684
- // running there (exec.ts keeps an unknown id killed). Best effort — `unknown` either way.
685
- void killUntilKept(port, id, parts.timeoutMs).catch(() => {});
1176
+ // One pass for every Effect but the kit's cut: a Harness that restarted knows no id, and a start
1177
+ // that may have reached it is never sent again. A cut goes out again: it is idempotent.
1178
+ const passes = parts.idempotent ? RESENDS + 1 : 1;
1179
+ for (let pass = 1; pass <= passes; pass++) {
1180
+ const sent = await startOnce(port, id, req, signal);
1181
+ if (signal.aborted) return;
1182
+ const first = await retrying(() => port.read(id, 0, signal), signal);
1183
+ if (signal.aborted) return;
1184
+ const last = pass === passes;
1185
+ if (first === undefined) {
1186
+ if (!last) {
1187
+ mark({ stage: "started", text: "sent again: the Harness restarted, and a cut is idempotent" });
1188
+ continue;
1189
+ }
1190
+ if (sent === "uncertain" && !parts.idempotent) {
1191
+ // The start may still be on its way to the Harness that answers now: a kill keeps it from
1192
+ // running there (exec.ts keeps an unknown id killed). Best effort — `unknown` either way.
1193
+ void killUntilKept(port, id, parts.timeoutMs).catch(() => {});
1194
+ }
1195
+ settled({
1196
+ outcome: "unknown",
1197
+ reason:
1198
+ sent === "uncertain"
1199
+ ? "its start may have reached the Harness, which no longer knows it (it restarted); not run again"
1200
+ : "the Harness lost it after it started (it restarted); not run again",
1201
+ });
1202
+ return;
686
1203
  }
687
- settle({
688
- outcome: "unknown",
689
- reason:
690
- sent === "uncertain"
691
- ? "its start may have reached the Harness, which no longer knows it (it restarted); not run again"
692
- : "the Harness lost it after it started (it restarted); not run again",
693
- });
694
- return;
1204
+ const lost = await follow(port, id, first, signal, mark, settled, !last);
1205
+ if (!lost) return;
1206
+ mark({ stage: "started", text: "sent again: the Harness lost it, and a cut is idempotent" });
695
1207
  }
696
- await follow(port, id, first, signal, mark, settle);
697
1208
  }
698
1209
 
699
1210
  /**
@@ -723,7 +1234,8 @@ async function startOnce(
723
1234
  }
724
1235
  }
725
1236
 
726
- /** Read the exec to its end: each chunk of stream to the feed, the output to the Machine. */
1237
+ /** Read the exec to its end: each chunk of stream to the feed, the output to the Machine. With
1238
+ * `again`, a Harness that lost it settles nothing: the answer is true, and the caller sends it. */
727
1239
  async function follow(
728
1240
  port: ExecPort,
729
1241
  id: string,
@@ -731,22 +1243,24 @@ async function follow(
731
1243
  signal: AbortSignal,
732
1244
  mark: (marker: Omit<EffectMarker, "kind" | "slot" | "effect">) => void,
733
1245
  settle: (output: CommandOutput) => void,
734
- ): Promise<void> {
1246
+ again = false,
1247
+ ): Promise<boolean> {
735
1248
  let read: ExecRead | undefined = first;
736
1249
  for (;;) {
737
1250
  if (read === undefined) {
1251
+ if (again) return true;
738
1252
  settle({ outcome: "unknown", reason: "the Harness lost it while it ran (it restarted); not run again" });
739
- return;
1253
+ return false;
740
1254
  }
741
1255
  if (read.stream) mark({ stage: "output", text: read.stream });
742
1256
  if (read.state === "settled" && read.output) {
743
1257
  settle(read.output);
744
1258
  void port.forget(id).catch(() => {});
745
- return;
1259
+ return false;
746
1260
  }
747
1261
  const offset: number = read.next;
748
1262
  read = await retrying(() => port.read(id, offset, signal), signal);
749
- if (signal.aborted) return;
1263
+ if (signal.aborted) return false;
750
1264
  }
751
1265
  }
752
1266
 
@@ -851,28 +1365,27 @@ function argvOf(slot: string, parts: CommandParts, input: unknown): string[] {
851
1365
  }
852
1366
 
853
1367
  /**
854
- * The cwd: a Repo Slot's Worktree (ADR-0051). Named by `slot`, or the one slot where the Workspace
855
- * carries one; a Workspace with none keeps the Harness's own (`/work`). Several and none named is
856
- * refused, as a Turn's Frame is (ADR-0057): the kit gives no slot a meaning.
1368
+ * Why `path` is not a Worktree of this Workspace, or undefined when it is. Exactly
1369
+ * `/work/<slot>/<name>`, of one of its Repo Slots, one segment that starts with neither `.` nor `-`.
1370
+ * `default` is a Worktree (the clone), and with `cut` it is not one a cut makes.
857
1371
  */
858
- function cwdOf(slot: string, parts: CommandParts, ambient: AmbientHandles): { cwd?: string } {
1372
+ export function worktreePathFault(
1373
+ path: unknown,
1374
+ ambient: AmbientHandles,
1375
+ opts: { cut?: boolean } = {},
1376
+ ): string | undefined {
1377
+ const match = typeof path === "string" ? path.match(/^\/work\/([^/]+)\/([^/]+)$/) : null;
1378
+ if (match === null) {
1379
+ return `${JSON.stringify(path)} is not a Worktree path: /work/<slot>/<name>, with one segment in <name>`;
1380
+ }
1381
+ const [, slot, name] = match as unknown as [string, string, string];
1382
+ if (name.startsWith(".") || name.startsWith("-")) return `"${path}": a Worktree's name starts with neither . nor -`;
1383
+ if (opts.cut && name === "default") return `"${path}" is the clone: it stays as the attach left it; cut a sibling`;
859
1384
  const slots = Object.keys(ambient.repos);
860
- if (parts.slot !== undefined) {
861
- const cwd = ambient.repos[parts.slot];
862
- if (cwd === undefined) {
863
- throw new Error(
864
- `command "${slot}" names Repo Slot "${parts.slot}", which its Workspace does not carry ` +
865
- `(${slots.join(", ") || "none"})`,
866
- );
867
- }
868
- return { cwd };
1385
+ if (!slots.includes(slot)) {
1386
+ return `"${path}" names Repo Slot "${slot}", which its Workspace does not carry (${slots.join(", ") || "none"})`;
869
1387
  }
870
- if (slots.length === 0) return {};
871
- if (slots.length === 1) return { cwd: ambient.repos[slots[0]!]! };
872
- throw new Error(
873
- `command "${slot}" runs under a Workspace carrying more than one Repo Slot (${slots.join(", ")}) and names ` +
874
- `none — say which Worktree it runs in: command(argv, { slot: "<slot>" }) (ADR-0051)`,
875
- );
1388
+ return undefined;
876
1389
  }
877
1390
 
878
1391
  /** Ask again on a transport failure, with backoff, until it answers or the Effect stops. For a