@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
@@ -53,7 +53,7 @@ import type {
53
53
  PushParts,
54
54
  Unknown,
55
55
  } from "./effect.ts";
56
- import { lastPushOf, pushPairsOf } from "./effect.ts";
56
+ import { cutRowOf, lastPushOf, needsASlot, pushPairsOf, worktreePathFault, type LaterOutcome } from "./effect.ts";
57
57
  import {
58
58
  createEffectorClient,
59
59
  EffectorForged,
@@ -89,6 +89,7 @@ import {
89
89
  import { open } from "./open.ts";
90
90
  import type { OpenOf } from "./open-effect.ts";
91
91
  import { boundRun, type AnyActorSystem } from "./registration.ts";
92
+ import type { AnyWorktreePath } from "./worktree.ts";
92
93
  import { repoKey } from "./repo-identity.ts";
93
94
  import { harnessSyncPort, type SyncPortFactory } from "./sync-client.ts";
94
95
  import type { SyncAnswer } from "./wire.ts";
@@ -126,23 +127,51 @@ function slotOf(kind: string, slot: string, named: string | undefined, ambient:
126
127
  return named;
127
128
  }
128
129
  if (slots.length === 1) return slots[0]!;
130
+ const why = slots.length === 0 ? needsASlot(kind) : undefined;
131
+ if (why !== undefined)
132
+ throw new Error(`${kind} "${slot}" runs under a Workspace with no Repo Slot: ${why} (ADR-0051)`);
129
133
  throw new Error(
130
134
  `${kind} "${slot}" runs under a Workspace carrying ${slots.length === 0 ? "no" : "more than one"} Repo Slot ` +
131
135
  `(${slots.join(", ") || "none"}) — say which: { slot: "<slot>" } (ADR-0051)`,
132
136
  );
133
137
  }
134
138
 
139
+ /**
140
+ * The cut behind an Effect's `worktree`, or why there is none. A push, a credentialed command and a
141
+ * file body act with a credential, so a path alone does not name their target: the path must be a
142
+ * Worktree a cut made in this Sandbox, and the branch is the cut's record. Nothing reads HEAD, and
143
+ * no input names a branch, so a Chart cannot fill one, and `git switch` in the Worktree moves nothing.
144
+ */
145
+ function cutTargetOf(
146
+ worktree: unknown,
147
+ ctx: Pick<KindContext, "ambient" | "binding">,
148
+ opts: { repo?: string },
149
+ ): { target: { repo: string; path: string; branch: string | null } } | { fault: string } {
150
+ const bad = worktreePathFault(worktree, ctx.ambient);
151
+ if (bad !== undefined) return { fault: bad };
152
+ const row = cutRowOf(ctx.binding?.effectRows?.() ?? {}, ctx.ambient.sandbox, worktree);
153
+ if (row === undefined) return { fault: `no cut made ${String(worktree)} in this Sandbox: cut it with worktree()` };
154
+ if (opts.repo !== undefined && row.repo !== opts.repo) {
155
+ return { fault: `${String(worktree)} is a Worktree of Repo Slot "${row.repo}", not of "${opts.repo}"` };
156
+ }
157
+ return { target: row };
158
+ }
159
+
135
160
  /** The slot's repositories, relative to the work root: the Agent's common git dir is the pod-local
136
161
  * clone's, beside the branch Worktree (ADR-0004); the cache is the Binding's key (ADR-0051). */
137
- export function repoRefOf(ambient: AmbientHandles, repoSlot: string): RepoRef & { url: string } {
138
- const worktree = ambient.repos[repoSlot]!;
162
+ export function repoRefOf(
163
+ ambient: AmbientHandles,
164
+ repoSlot: string,
165
+ worktree: string = ambient.repos[repoSlot]!,
166
+ ): RepoRef & { url: string } {
139
167
  const url = ambient.bindings?.[repoSlot];
140
168
  if (url === undefined) {
141
169
  throw new Error(`Repo Slot "${repoSlot}" has no Binding url in its Workspace's handles — a jr2 defect`);
142
170
  }
143
171
  const rel = posix.relative(WORK_ROOT, worktree);
144
172
  return {
145
- gitDir: posix.join(posix.dirname(rel), "default", ".git"),
173
+ // The clone of the slot, read from the slot and not from the Worktree's folder.
174
+ gitDir: posix.join(repoSlot, "default", ".git"),
146
175
  worktree: rel,
147
176
  cache: repoKey(url),
148
177
  url,
@@ -151,42 +180,197 @@ export function repoRefOf(ambient: AmbientHandles, repoSlot: string): RepoRef &
151
180
 
152
181
  // --- The definitions -------------------------------------------------------------------------------
153
182
 
154
- /** `push()`'s options (ADR-0067). */
183
+ /** The options of `push()`. */
155
184
  export type PushOptions = {
156
- /** The Effect's input names the commit (`{ sha }`, one a Gate approved); absent, the Agent's
157
- * branch tip when the Effect starts. A pinned push the Agent pruned is refused by the remote,
158
- * never substituted. */
185
+ // A pinned commit the Agent pruned is refused by the remote, never substituted (ADR-0067).
186
+ /**
187
+ * `true`: the input of the Effect names the commit to push (`{ sha }`, a full commit id, for
188
+ * example one that a Gate approved). Default: the tip of the Agent's branch when the Effect
189
+ * starts. When the Agent removed that commit from its branch, the remote refuses the push. jr2
190
+ * never pushes another commit in its place.
191
+ *
192
+ * When it starts, it refuses a `sha` that is not a full commit id: `push({ pin: true }): its
193
+ * input's sha is not a full commit id (got <sha>)`.
194
+ */
159
195
  pin?: boolean;
160
- /** Push signed copies; the matching `git.credentials` entry's `sign` names the key. */
196
+ /**
197
+ * `true`: push signed copies of the commits. The `sign` field of the `git.credentials` entry that
198
+ * the Binding matches names the key. Default `false`.
199
+ *
200
+ * At `jr2 up`, it refuses an entry with no `sign` field: `<push> signed, and git.credentials
201
+ * entry "<match>" holds no sign field — add sign: { sshKey: "<a Secret with identity>", name:
202
+ * "<committer name>", email: "<committer email>" } to it; the email must belong to the forge
203
+ * account that holds the key (ADR-0051, ADR-0067)`. `<push>` names the push: `workflow
204
+ * "<workflow>": push "<slot>" pushes Repo Slot "<name>" (<url>)`.
205
+ */
161
206
  sign?: boolean;
162
- /** Squash into one commit; the Effect's input carries the message (`{ message }`). Signed, its
163
- * committer is the signer's; unsigned, the matching `git.credentials` entry's `committer`, which
164
- * `jr2 up` requires when the entry has no `sign` (ADR-0067). */
207
+ /**
208
+ * `true`: push one commit that holds all the changes. The input of the Effect gives its message
209
+ * (`{ message }`). Signed, its committer is the signer. Unsigned, it is the `committer` of the
210
+ * `git.credentials` entry that the Binding matches. Default `false`.
211
+ *
212
+ * When it starts, it refuses an input with no message: `push({ squash: true }): its input
213
+ * carries no message`.
214
+ *
215
+ * At `jr2 up`, it refuses an unsigned squash whose entry has no `sign` and no `committer`:
216
+ * `<push> squashed without sign, and git.credentials entry "<match>" holds neither sign nor
217
+ * committer — the squashed commit is new and needs a committer; add committer: { name:
218
+ * "<committer name>", email: "<committer email>" } to it, or sign the push — push({ squash:
219
+ * true, sign: true }) — and add sign: { sshKey: "<a Secret with identity>", name: "<committer
220
+ * name>", email: "<committer email>" } to it (ADR-0051, ADR-0067)`.
221
+ */
165
222
  squash?: boolean;
166
- /** Replace what the remote holds. */
223
+ /** `true`: replace what the remote branch holds. Default `false`. */
167
224
  force?: boolean;
168
- /** The Repo Slot pushed — needed only where the Workspace carries several. */
225
+ /**
226
+ * The Repo Slot to push. Write it only when the Workspace has more than one.
227
+ *
228
+ * When it starts, it refuses:
229
+ * - no `slot` under a Workspace with more than one Repo Slot: `push "<slot>" runs under a
230
+ * Workspace carrying more than one Repo Slot (<slots>) — say which: { slot: "<slot>" }
231
+ * (ADR-0051)`
232
+ * - a `slot` that the Workspace does not have: `push "<slot>" names Repo Slot "<name>", which
233
+ * its Workspace does not carry (<slots>)`
234
+ * - a Workspace with no Repo Slot: `push "<slot>" runs under a Workspace with no Repo Slot: a
235
+ * push publishes a Repo Slot's branch, and there is none — declare one: repos: { app: "<url>" }
236
+ * (ADR-0051)`. `jr2 up` refuses it first, with the same reason.
237
+ */
169
238
  slot?: string;
170
- /** Default 5m. Past it with no answer: output `unknown`. */
239
+ // Static, so that `jr2 up` places the credential per slot.
240
+ /**
241
+ * How long jr2 waits for the push: a number and a unit, such as `"90s"` or `"5m"`. Default
242
+ * `"5m"`. With no answer in that time, the output is `unknown`. It refuses a value that is not a
243
+ * positive duration: `timeout "<timeout>" is not a positive duration (like "30s", "5m")`.
244
+ */
171
245
  timeout?: Duration;
172
246
  };
173
247
 
174
- /** A push's input: what `pin` and `squash` add. */
175
- export type PushInput<O extends PushOptions> = (O["pin"] extends true ? { sha: string } : unknown) &
248
+ /** The input of a `push` Effect: the Worktree that a cut made, `{ sha }` with `pin`, and
249
+ * `{ message }` with `squash`. It names no branch: jr2 reads it from the cut's record, so no word of
250
+ * a Chart reaches a remote branch. The other keys are refused by name, because each is a key of a
251
+ * cut or a Turn, and a mapper's excess key is no compile error otherwise. */
252
+ export type PushInput<O extends PushOptions> = {
253
+ /** The Worktree to push: `event.output.path` of its cut, of the push's Repo Slot. */
254
+ worktree: O extends { slot: infer S extends string } ? `/work/${S}/${string}` : AnyWorktreePath;
255
+ branch?: never;
256
+ to?: never;
257
+ cwd?: never;
258
+ path?: never;
259
+ } & (O["pin"] extends true ? { sha: string } : unknown) &
176
260
  (O["squash"] extends true ? { message: string } : unknown);
177
261
 
178
- /** The sync's answer as a push's output carries it: a field beside the outcome, never the outcome. */
262
+ /**
263
+ * The sync's answer as a push's output carries it: a field beside the outcome, never the outcome.
264
+ *
265
+ * @internal
266
+ */
179
267
  export type SyncState = SyncAnswer["sync"];
180
268
 
181
- /** A push's output, as the Machine routes on it: `pushed | rejected | diverged | unknown`, and beside
182
- * `pushed` the sync's answer — `synced | already | skipped | busy | not-synced`, with its reason
183
- * when it did not move the branch. The sync's bundle never reaches the Machine. */
269
+ // The sync's bundle never reaches the Machine: `deliver` drops it from the output (ADR-0067).
270
+ /**
271
+ * The output of a `push` Effect: what the `onDone` of its state gets as `event.output`. Every
272
+ * outcome is output, so route on `outcome`:
273
+ *
274
+ * - `pushed`: the remote has the commit. `published` is the commit on the remote, and `source` is
275
+ * the Agent's commit that it comes from. They are the same commit, unless the push was signed or
276
+ * squashed. `previous` is what the remote had before, and `unchanged` is `true` when the remote
277
+ * had the commit already. `squashed` is `signed` or `unsigned` when the commit on the remote is
278
+ * one squashed commit. `via` tells how jr2 found the new commits after a signed push:
279
+ * `ancestry`, `pair`, `cherry` or `force`.
280
+ * - `rejected`: git or the remote refused the push. `stage` tells where, `reason` tells why, and
281
+ * `detail` has the words of git.
282
+ * - `diverged`: the commit on the remote is not in the history of the Agent's branch, and jr2
283
+ * could not pair it with a commit there. Nothing was pushed. `reason` tells why, and `published`
284
+ * and `source` name the two commits.
285
+ * - `unknown`: jr2 cannot tell what the push did. `reason` tells why.
286
+ *
287
+ * Beside `pushed`, `sync` tells what jr2 did to the Agent's branch after the push:
288
+ *
289
+ * - `synced`: the commit on the remote is not the Agent's own, and jr2 moved the branch to the
290
+ * published commits.
291
+ * - `already`: the branch has the published commit already.
292
+ * - `skipped`: the remote has the Agent's own commit, so the branch does not move.
293
+ * - `busy`: a rebase, merge, cherry-pick or revert of the branch is in progress. Nothing changed.
294
+ * - `not-synced`: the branch did not move.
295
+ *
296
+ * With `busy` or `not-synced`, `syncReason` tells why, and jr2 tries again before the next push.
297
+ *
298
+ * The union is open (`LaterOutcome`): give the rest a last arm with no guard.
299
+ *
300
+ * @example
301
+ * ```ts
302
+ * import {
303
+ * fail,
304
+ * isReady,
305
+ * jr2Setup,
306
+ * push,
307
+ * type PushOutput,
308
+ * workspace,
309
+ * worktree,
310
+ * type Workspaced,
311
+ * } from "@jr2/orchestrator";
312
+ *
313
+ * type Input = Workspaced<{ branch: string; sha: string }, "app">;
314
+ *
315
+ * const releaseTree = (context: Input) => `${context.workspace.repos.app}/release` as const;
316
+ *
317
+ * // A push that did not happen (`rejected` or `diverged`) is a different problem from one jr2
318
+ * // cannot see (`unknown`).
319
+ * const notPushed = (output: PushOutput): boolean =>
320
+ * output.outcome === "rejected" || output.outcome === "diverged";
321
+ *
322
+ * const body = jr2Setup({
323
+ * types: {} as { context: Input; input: Input },
324
+ * actors: { cut: worktree({ expect: "existing" }), publish: push({ pin: true }), fail: fail() },
325
+ * }).createMachine({
326
+ * id: "release",
327
+ * context: ({ input }) => input,
328
+ * on: { "workspace.lost": ".lost" },
329
+ * initial: "cutting",
330
+ * states: {
331
+ * cutting: {
332
+ * invoke: {
333
+ * src: "cut",
334
+ * input: ({ context }) => ({ path: releaseTree(context), branch: context.branch }),
335
+ * onDone: [{ guard: ({ event }) => isReady(event.output), target: "publishing" }, { target: "notCut" }],
336
+ * },
337
+ * },
338
+ * publishing: {
339
+ * invoke: {
340
+ * src: "publish",
341
+ * input: ({ context }) => ({ worktree: releaseTree(context), sha: context.sha }),
342
+ * onDone: [
343
+ * { guard: ({ event }) => event.output.outcome === "pushed", target: "published" },
344
+ * { guard: ({ event }) => notPushed(event.output), target: "notPushed" },
345
+ * { target: "unknown" },
346
+ * ],
347
+ * },
348
+ * },
349
+ * published: { type: "final" },
350
+ * // Not final states: each one fails the run, with its own reason.
351
+ * notPushed: { invoke: { src: "fail", input: { reason: "the remote refused the push" } } },
352
+ * unknown: { invoke: { src: "fail", input: { reason: "jr2 cannot tell if the push happened" } } },
353
+ * notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
354
+ * lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
355
+ * },
356
+ * });
357
+ *
358
+ * export const machine = workspace(body, {
359
+ * repos: { app: { url: "https://github.com/you/app.git" } },
360
+ * });
361
+ * ```
362
+ *
363
+ * @see docs/guides/push-and-sign-a-branch.md
364
+ * @see docs/concepts/effects.md
365
+ * @see docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md
366
+ */
184
367
  export type PushOutput =
185
368
  | Exclude<PushWireOutput, { outcome: "pushed" }>
186
- | (Omit<Extract<PushWireOutput, { outcome: "pushed" }>, "bundle"> & { sync: SyncState; syncReason?: string });
369
+ | (Omit<Extract<PushWireOutput, { outcome: "pushed" }>, "bundle"> & { sync: SyncState; syncReason?: string })
370
+ | LaterOutcome;
187
371
 
188
372
  export function pushParts(options: PushOptions = {}): PushParts {
189
- const inputKeys = [...(options.pin ? ["sha"] : []), ...(options.squash ? ["message"] : [])];
373
+ const inputKeys = ["worktree", ...(options.pin ? ["sha"] : []), ...(options.squash ? ["message"] : [])];
190
374
  return {
191
375
  kind: "push",
192
376
  pin: options.pin === true,
@@ -232,8 +416,8 @@ export function pushStartOf(
232
416
  };
233
417
  }
234
418
 
235
- /** A file-body http's start for one run (ADR-0067): the definition's parts, the url's segments, and the
236
- * Worktree file as the body. */
419
+ /** A file-body http's start for one run (ADR-0067): the definition's parts, the url's segments, and
420
+ * the Worktree file as the body. */
237
421
  export function fileStartOf(
238
422
  parts: HttpParts,
239
423
  worktree: string,
@@ -248,7 +432,8 @@ export function fileStartOf(
248
432
  return { ...http, body: { file: { worktree, path: run.path, ...(contentType ? { contentType } : {}) } } };
249
433
  }
250
434
 
251
- /** A credentialed command's env as its definition names it. */
435
+ /** The `env` of a `command`: for each variable, `secret("name")` for the value of an Effect
436
+ * secret, or `secret("name").path` for the path of a file that holds it. */
252
437
  export type CommandEnv = Readonly<Record<string, { readonly secret: string; readonly path: unknown }>>;
253
438
 
254
439
  export function commandEnvParts(env: CommandEnv): Pick<CommandParts, "env" | "secrets" | "openSecret"> {
@@ -309,6 +494,7 @@ type KindContext = {
309
494
  slot: string;
310
495
  signal: AbortSignal;
311
496
  mark: (marker: Omit<EffectMarker, "kind" | "slot" | "effect">) => void;
497
+ input: unknown;
312
498
  };
313
499
 
314
500
  /** How one Sandbox kind builds its start and what a restore may send again. */
@@ -321,13 +507,25 @@ type KindSpec = {
321
507
  /** The Repo Slot the Effect runs on, written to its settled row. */
322
508
  repoOf: (ambient: AmbientHandles, slot: string) => string;
323
509
  /** The start for this run; `pinned` is the commit the intent row recorded. */
324
- build: (input: unknown, ambient: AmbientHandles, slot: string, pinned?: string, extra?: StartExtra) => StartRequest;
510
+ build: (
511
+ input: unknown,
512
+ ambient: AmbientHandles,
513
+ slot: string,
514
+ pinned?: string,
515
+ extra?: StartExtra,
516
+ binding?: ReturnType<typeof boundRun>,
517
+ ) => StartRequest;
325
518
  /** Run before a start is sent (a push: the last sync again, and the ledger's pair). */
326
519
  before?: (ctx: KindContext) => Promise<StartExtra>;
327
520
  /** Run on the Effector's settled output before it is ledgered (a push: the sync). */
328
521
  finish?: (output: unknown, ctx: KindContext) => Promise<unknown>;
329
522
  /** What a settled output becomes for the Machine. */
330
523
  deliver?: (output: unknown) => unknown;
524
+ /** Why this start cannot be sent, as the kind's own output: a word an Agent or a Chart can say.
525
+ * It settles before any intent row: nothing went out. */
526
+ refuse?: (ctx: KindContext) => unknown;
527
+ /** The fields of the settled row that a later Effect reads. */
528
+ rowFields?: (ctx: KindContext) => { branch?: string; worktree?: string; repo?: string };
331
529
  };
332
530
 
333
531
  type StartExtra = { pairs?: readonly Pair[] };
@@ -406,6 +604,9 @@ async function drive<I, O>(
406
604
  kind: spec.kind,
407
605
  slot,
408
606
  ...(ambient ? { sandbox: ambient.sandbox, ...repoField(spec, ambient, slot) } : {}),
607
+ ...(ambient && spec.rowFields
608
+ ? spec.rowFields({ ambient, binding, id, slot, signal, mark, input: snapshot.input })
609
+ : {}),
409
610
  output,
410
611
  at: now(),
411
612
  });
@@ -447,7 +648,7 @@ async function drive<I, O>(
447
648
  // A credentialed command's stop goes through this port when its state exits (ADR-0067).
448
649
  const run = running.get(self);
449
650
  if (spec.kind === "command" && run) run.port = port;
450
- const ctx: KindContext = { ambient, binding, id, slot, signal, mark };
651
+ const ctx: KindContext = { ambient, binding, id, slot, signal, mark, input: snapshot.input };
451
652
 
452
653
  const find = async (): Promise<{ found: EffectState | undefined } | undefined> => {
453
654
  try {
@@ -477,12 +678,12 @@ async function drive<I, O>(
477
678
  });
478
679
  const extra = spec.before ? await spec.before(ctx) : {};
479
680
  if (signal.aborted) return;
480
- await send(spec.build(snapshot.input, ambient, slot, pinned, extra), pinned);
681
+ await send(spec.build(snapshot.input, ambient, slot, pinned, extra, binding), pinned);
481
682
  return;
482
683
  }
483
684
  if (spec.kind === "http" && spec.idempotent) {
484
685
  mark({ stage: "started", text: `${spec.label(ambient)} (sent again: it is idempotent)` });
485
- await send(spec.build(snapshot.input, ambient, slot), undefined);
686
+ await send(spec.build(snapshot.input, ambient, slot, undefined, undefined, binding), undefined);
486
687
  return;
487
688
  }
488
689
  settle(unknown(`${why}; not run again`));
@@ -503,9 +704,14 @@ async function drive<I, O>(
503
704
  return;
504
705
  }
505
706
 
707
+ const refusal = spec.refuse?.(ctx);
708
+ if (refusal !== undefined) {
709
+ settle(refusal);
710
+ return;
711
+ }
506
712
  const extra = spec.before ? await spec.before(ctx) : {};
507
713
  if (signal.aborted) return;
508
- const req = spec.build(snapshot.input, ambient, slot, undefined, extra);
714
+ const req = spec.build(snapshot.input, ambient, slot, undefined, extra, binding);
509
715
  const big = tooLarge(req);
510
716
  if (big) {
511
717
  settle(big);
@@ -579,7 +785,8 @@ async function drive<I, O>(
579
785
  }
580
786
  }
581
787
 
582
- /** The settled row's Repo Slot, or none when the Workspace does not name one the kind can run on. */
788
+ /** The settled row's Repo Slot, or none when the Workspace does not name one the kind can run on.
789
+ */
583
790
  function repoField(spec: KindSpec, ambient: AmbientHandles, slot: string): { repo?: string } {
584
791
  try {
585
792
  return { repo: spec.repoOf(ambient, slot) };
@@ -660,9 +867,10 @@ function stoppedText(state: EffectState | undefined): string {
660
867
  }
661
868
 
662
869
  /** A start over the bytes an Effector reads is sent nowhere, and settles as its kind's own outcome
663
- * (ADR-0067): an http `refused`; a push `rejected` at stage `start` — nothing pushed; a credentialed
664
- * command `exited` 127 with the reason in its tail, the Effector's own form for a command that
665
- * could not start. An Agent's pick parameter — a squash message, an argv part — can reach the bound. */
870
+ * (ADR-0067): an http `refused`; a push `rejected` at stage `start` — nothing pushed; a
871
+ * credentialed command `exited` 127 with the reason in its tail, the Effector's own form for a
872
+ * command that could not start. An Agent's pick parameter — a squash message, an argv part — can
873
+ * reach the bound. */
666
874
  function tooLarge(req: StartRequest): HttpOutput | PushWireOutput | CommandWireOutput | undefined {
667
875
  const bytes = Buffer.byteLength(JSON.stringify(req), "utf8");
668
876
  if (bytes <= MAX_START_BYTES) return undefined;
@@ -713,40 +921,65 @@ export function pushWith<const O extends PushOptions>(
713
921
  portFactory: EffectorPortFactory,
714
922
  options?: O,
715
923
  syncPorts: SyncPortFactory = harnessSyncPort,
716
- ): EffectLogic<PushInput<O> | void, PushOutput> {
924
+ ): EffectLogic<PushInput<O>, PushOutput> {
717
925
  const parts = pushParts(options);
718
926
  const repoOf = (ambient: AmbientHandles, slot: string) => slotOf("push", slot, parts.slot, ambient);
719
- const worktreeOf = (ctx: KindContext) => ctx.ambient.repos[repoOf(ctx.ambient, ctx.slot)]!;
720
927
  const syncOf = (ctx: KindContext) =>
721
928
  syncPorts(ctx.ambient.endpoint, ctx.binding?.harnessBearer?.(ctx.ambient.sandbox));
929
+ const worktreeOf = (input: unknown) => (input as { worktree?: unknown } | undefined)?.worktree;
930
+ /** The target of this push: the cut's row. `refuse` has run, so a build finds one; a restored push
931
+ * finds it too, because a row outlives the restart. */
932
+ const targetOf = (ctx: Pick<KindContext, "ambient" | "binding" | "input" | "slot">) => {
933
+ const found = cutTargetOf(worktreeOf(ctx.input), ctx, { repo: repoOf(ctx.ambient, ctx.slot) });
934
+ if ("fault" in found) throw new Error(`push "${ctx.slot}": ${found.fault}`);
935
+ return found.target as { repo: string; path: string; branch: string };
936
+ };
722
937
  return sandboxLogic(portFactory, parts, {
723
938
  kind: "push",
724
939
  idempotent: false,
725
940
  repeatsPinned: true,
726
- label: (ambient) => `push ${ambient.branch}${parts.sign ? " (signed)" : ""}`,
941
+ label: () => `push${parts.sign ? " (signed)" : ""}`,
727
942
  repoOf,
728
- build: (input, ambient, slot, pinned, extra) => {
729
- const repoSlot = slotOf("push", slot, parts.slot, ambient);
730
- const ref = repoRefOf(ambient, repoSlot);
731
- const req = pushStartOf(parts, ref, ambient.branch, input as { sha?: string; message?: string }, extra?.pairs);
943
+ // Anything a Chart or a pick can say, so it is output (ADR-0067): `rejected` at stage start.
944
+ refuse: (ctx) => {
945
+ const found = cutTargetOf(worktreeOf(ctx.input), ctx, { repo: repoOf(ctx.ambient, ctx.slot) });
946
+ const fault =
947
+ "fault" in found
948
+ ? found.fault
949
+ : found.target.branch === null
950
+ ? `${found.target.path} is detached: a push publishes the branch of a Worktree, and it holds none`
951
+ : undefined;
952
+ if (fault === undefined) return undefined;
953
+ return { outcome: "rejected", stage: "start", reason: `${fault}; nothing was pushed` };
954
+ },
955
+ rowFields: (ctx) => {
956
+ const found = cutTargetOf(worktreeOf(ctx.input), ctx, { repo: repoOf(ctx.ambient, ctx.slot) });
957
+ if ("fault" in found || found.target.branch === null) return {};
958
+ return { branch: found.target.branch };
959
+ },
960
+ build: (input, ambient, slot, pinned, extra, binding) => {
961
+ const target = targetOf({ ambient, binding, input, slot });
962
+ const ref = repoRefOf(ambient, target.repo, target.path);
963
+ const req = pushStartOf(parts, ref, target.branch, input as { sha?: string; message?: string }, extra?.pairs);
732
964
  if (pinned) req.push.pin = pinned;
733
965
  return req;
734
966
  },
735
- // Every push of this Workspace's Repo Slot — one branch, one remote — that published gives the
736
- // Effector a pair, oldest first (ADR-0067). The newest one's sync, when it did not finish, runs
737
- // again first, in that slot's Worktree and from the bundle the row keeps while it is the newest
967
+ // Every push of this Repo Slot's branch that published gives the Effector a pair, oldest first
968
+ // (ADR-0067). The newest one's sync, when it did not finish, runs again first, in this push's
969
+ // Worktree and from the bundle the row keeps while it is the newest
738
970
  // push and its sync did not finish (`dropSavedTails`); the push does not depend on it.
739
971
  before: async (ctx) => {
740
972
  const rows = ctx.binding?.effectRows?.() ?? {};
741
973
  const repo = repoOf(ctx.ambient, ctx.slot);
742
- const last = lastPushOf(rows, ctx.ambient.sandbox, repo, ctx.id);
974
+ const target = targetOf(ctx);
975
+ const last = lastPushOf(rows, ctx.ambient.sandbox, repo, target.branch, ctx.id);
743
976
  if (last === undefined) return {};
744
977
  const { row, rowId, output } = last;
745
978
  if (output.sync === "busy" || output.sync === "not-synced") {
746
979
  const answer = await syncOf(ctx).sync(
747
980
  {
748
- worktree: worktreeOf(ctx),
749
- branch: ctx.ambient.branch,
981
+ worktree: target.path,
982
+ branch: target.branch,
750
983
  published: output.published,
751
984
  source: output.source,
752
985
  ...(typeof output.bundle === "string" ? { bundle: output.bundle } : {}),
@@ -758,7 +991,7 @@ export function pushWith<const O extends PushOptions>(
758
991
  ctx.binding?.recordEffect?.(rowId, { ...row, output: { ...output, ...syncFields(answer) } });
759
992
  }
760
993
  // Each row's `squashed` rides beside its pair: a push that maps the same commit names what it is.
761
- return { pairs: pushPairsOf(rows, ctx.ambient.sandbox, repo, ctx.id) };
994
+ return { pairs: pushPairsOf(rows, ctx.ambient.sandbox, repo, target.branch, ctx.id) };
762
995
  },
763
996
  // After every pushed outcome whose published commit is not the Agent's own — signed copies, or
764
997
  // one squashed commit, signed or not (ADR-0067): the sync, in the Workspace's Harness, from the
@@ -769,10 +1002,11 @@ export function pushWith<const O extends PushOptions>(
769
1002
  const o = output as PushWireOutput;
770
1003
  if (o.outcome !== "pushed") return o;
771
1004
  if (o.published === o.source) return { ...o, sync: "skipped" satisfies SyncState };
1005
+ const target = targetOf(ctx);
772
1006
  const answer = await syncOf(ctx).sync(
773
1007
  {
774
- worktree: worktreeOf(ctx),
775
- branch: ctx.ambient.branch,
1008
+ worktree: target.path,
1009
+ branch: target.branch,
776
1010
  published: o.published,
777
1011
  source: o.source,
778
1012
  ...(o.bundle !== undefined ? { bundle: o.bundle } : {}),
@@ -787,7 +1021,7 @@ export function pushWith<const O extends PushOptions>(
787
1021
  const { bundle: _bundle, ...rest } = o;
788
1022
  return rest;
789
1023
  },
790
- }) as EffectLogic<PushInput<O> | void, PushOutput>;
1024
+ }) as EffectLogic<PushInput<O>, PushOutput>;
791
1025
  }
792
1026
 
793
1027
  /** The sync's answer as the output's fields: its state, and its reason when it moved nothing. */
@@ -800,8 +1034,9 @@ function syncFields(answer: SyncAnswer): { sync: SyncState; syncReason?: string
800
1034
 
801
1035
  export { lastPushOf };
802
1036
 
803
- /** An http whose body is a Worktree file (ADR-0067), over an injected port factory — the seam `http()`
804
- * and the unit tests share. It runs in the Sandbox Effector of the Workspace whose body invokes it. */
1037
+ /** An http whose body is a Worktree file (ADR-0067), over an injected port factory — the seam
1038
+ * `http()` and the unit tests share. It runs in the Sandbox Effector of the Workspace whose body
1039
+ * invokes it. */
805
1040
  export function httpFileWith<
806
1041
  S extends z.ZodType | undefined = undefined,
807
1042
  const U extends HttpDefinition<S>["url"] = string,
@@ -827,17 +1062,38 @@ export function httpFileWith<S extends z.ZodType | undefined = undefined>(
827
1062
  kind: "http",
828
1063
  idempotent: parts.idempotent,
829
1064
  repeatsPinned: false,
830
- label: () => `http ${parts.method} ${parts.target.scheme}://${parts.target.host} (a Worktree file)`,
831
- repoOf: (ambient, slot) => slotOf("http", slot, body.slot, ambient),
1065
+ // With no Repo Slot the file is in `/work`, which is no Worktree (ADR-0051).
1066
+ label: (ambient) =>
1067
+ `http ${parts.method} ${parts.target.scheme}://${parts.target.host} ` +
1068
+ (Object.keys(ambient.repos).length === 0 ? "(a file of /work)" : "(a Worktree file)"),
1069
+ repoOf: (ambient, slot) => slotOf("http", slot, undefined, ambient),
1070
+ // The same row check as a push: a word of a Chart must not choose what leaves the Sandbox with a
1071
+ // credential. `refused`, the kind's own form.
1072
+ refuse: (ctx) => {
1073
+ const named = (ctx.input as { worktree?: unknown } | undefined)?.worktree;
1074
+ if (Object.keys(ctx.ambient.repos).length === 0) {
1075
+ return named === undefined
1076
+ ? undefined
1077
+ : { outcome: "refused", reason: `no Worktree in a Workspace with no Repo Slot; nothing was sent` };
1078
+ }
1079
+ if (named === undefined) {
1080
+ throw new Error(
1081
+ `http "${ctx.slot}" reads a Worktree file, under a Workspace with Repo Slots, and its input names no \`worktree\``,
1082
+ );
1083
+ }
1084
+ const found = cutTargetOf(named, ctx, {});
1085
+ return "fault" in found ? { outcome: "refused", reason: `${found.fault}; nothing was sent` } : undefined;
1086
+ },
832
1087
  build: (input, ambient, slot) => {
833
- let value: unknown = input;
1088
+ const { worktree: named, ...rest } = (input ?? {}) as { worktree?: string };
1089
+ let value: unknown = rest;
834
1090
  if (def.input !== undefined) {
835
- const parsed = def.input.safeParse(input);
1091
+ const parsed = def.input.safeParse(rest);
836
1092
  if (!parsed.success) throw new Error(`http "${slot}": its input does not match the definition's input schema`);
837
1093
  value = parsed.data;
838
1094
  }
839
- const repoSlot = slotOf("http", slot, body.slot, ambient);
840
- const worktree = posix.relative(WORK_ROOT, ambient.repos[repoSlot]!);
1095
+ // With no Repo Slot the file is in `/work`, which its steps share (ADR-0051).
1096
+ const worktree = named === undefined ? "." : posix.relative(WORK_ROOT, named);
841
1097
  const segments = fromInput<readonly string[]>(body.segments, value);
842
1098
  return fileStartOf(parts, worktree, {
843
1099
  path: fromInput<string>(body.path, value)!,
@@ -847,7 +1103,8 @@ export function httpFileWith<S extends z.ZodType | undefined = undefined>(
847
1103
  });
848
1104
  }
849
1105
 
850
- /** A command whose env names a secret: it runs in the Sandbox Effector against the pinned commit. */
1106
+ /** A command whose env names a secret: it runs in the Sandbox Effector against the pinned commit.
1107
+ */
851
1108
  export function credentialedCommandWith(
852
1109
  portFactory: EffectorPortFactory,
853
1110
  parts: CommandParts,
@@ -859,10 +1116,30 @@ export function credentialedCommandWith(
859
1116
  idempotent: false,
860
1117
  repeatsPinned: true,
861
1118
  label: () => parts.argv.map((a) => (typeof a === "string" ? a : `<${(a as Arg).arg}>`)).join(" "),
862
- repoOf: (ambient, slot) => slotOf("command", slot, parts.slot, ambient),
863
- build: (input, ambient, slot, pinned) => {
864
- const repoSlot = slotOf("command", slot, parts.slot, ambient);
865
- const ref = repoRefOf(ambient, repoSlot);
1119
+ repoOf: (ambient, slot) => slotOf("command", slot, undefined, ambient),
1120
+ // The same row check as a push, in the form a command that could not start has (ADR-0067). No
1121
+ // Repo Slot at all is a defect of the Machine, as for a push: a throw, and `onError` (ADR-0051).
1122
+ refuse: (ctx) => {
1123
+ if (Object.keys(ctx.ambient.repos).length === 0) {
1124
+ throw new Error(
1125
+ `command "${ctx.slot}" runs under a Workspace with no Repo Slot: ${needsASlot("command")} (ADR-0051)`,
1126
+ );
1127
+ }
1128
+ const found = cutTargetOf((ctx.input as { worktree?: unknown } | undefined)?.worktree, ctx, {});
1129
+ const fault =
1130
+ "fault" in found
1131
+ ? found.fault
1132
+ : found.target.branch === null
1133
+ ? `${found.target.path} is detached, and a credentialed command runs on a commit of a branch`
1134
+ : undefined;
1135
+ return fault === undefined
1136
+ ? undefined
1137
+ : { outcome: "exited", code: 127, tail: `jr2: ${fault}; nothing was run\n` };
1138
+ },
1139
+ build: (input, ambient, slot, pinned, _extra, binding) => {
1140
+ const found = cutTargetOf((input as { worktree?: unknown } | undefined)?.worktree, { ambient, binding }, {});
1141
+ if ("fault" in found) throw new Error(`command "${slot}": ${found.fault}`);
1142
+ const ref = repoRefOf(ambient, found.target.repo, found.target.path);
866
1143
  const approved = pin ? (input as { sha?: unknown } | undefined)?.sha : undefined;
867
1144
  if (pin && (typeof approved !== "string" || !/^[0-9a-f]{40}$/.test(approved))) {
868
1145
  throw new Error(`command "${slot}" (pin): its input's sha is not a full commit id`);
@@ -871,7 +1148,7 @@ export function credentialedCommandWith(
871
1148
  parts,
872
1149
  argvOf(input, slot),
873
1150
  ref,
874
- ambient.branch,
1151
+ found.target.branch!,
875
1152
  pinned ?? (approved as string | undefined),
876
1153
  );
877
1154
  },
@@ -879,12 +1156,121 @@ export function credentialedCommandWith(
879
1156
  }
880
1157
 
881
1158
  /**
882
- * THE authoring surface for a push (ADR-0067): `actors: { publish: push({ sign: true }) }`, invoked
883
- * from a Workspace's body. It pushes the Workspace's branch to its Repo Slot's Binding from the
884
- * Sandbox Effector, with the push credential of the `git.credentials` entry the Binding matches.
885
- * Every outcome is output: `pushed | rejected | diverged | unknown`.
1159
+ * Make a `push` Effect: it pushes the branch of a Worktree to the Binding of its Repo Slot.
1160
+ * Put it in `actors`, and invoke it from the body of a `workspace()`: `actors: { publish: push({
1161
+ * sign: true }) }`.
1162
+ *
1163
+ * Its input names the Worktree: `{ worktree }`. It has no branch. jr2 reads the branch from the
1164
+ * `worktree()` cut of that path, so an Agent that moves `HEAD` does not change what jr2 pushes.
1165
+ *
1166
+ * It runs in the Sandbox Effector of the Workspace, with the push credential of the
1167
+ * `git.credentials` entry that the Binding matches. The Agent never reads that credential. It
1168
+ * pushes one commit: the tip of the Agent's branch when the Effect starts, or, with `pin`, the
1169
+ * commit that the input names. After an Orchestrator restart, jr2 pushes only that same commit
1170
+ * again. With `sign` or `squash`, the commit on the remote is not the Agent's own. Then jr2 moves
1171
+ * the Agent's branch to it.
1172
+ *
1173
+ * Every outcome is output (`PushOutput`): `pushed`, `rejected`, `diverged` or `unknown`. A
1174
+ * `worktree` that no cut made in this Sandbox, a Worktree of another Repo Slot, and a detached
1175
+ * Worktree are `rejected` at `stage` `start`, and nothing is pushed. Each field of
1176
+ * {@link PushOptions} tells what it refuses. When it starts, it also refuses a push
1177
+ * outside a Workspace: `push "<slot>" runs in the Sandbox Effector of a Workspace, and its state is
1178
+ * not inside a workspace() — invoke it from a Workspace's body (ADR-0067)`.
1179
+ *
1180
+ * @param options - Optional. Each field tells what it does.
1181
+ * @returns An Effect, for `actors`.
1182
+ *
1183
+ * @example
1184
+ * ```ts
1185
+ * import { z } from "zod";
1186
+ * import {
1187
+ * agent,
1188
+ * defineEvent,
1189
+ * fail,
1190
+ * isReady,
1191
+ * jr2Setup,
1192
+ * push,
1193
+ * workspace,
1194
+ * worktree,
1195
+ * type Workspaced,
1196
+ * } from "@jr2/orchestrator";
1197
+ *
1198
+ * const committed = defineEvent({
1199
+ * name: "committed",
1200
+ * description: "Say that the fix is committed.",
1201
+ * input: z.object({}),
1202
+ * });
1203
+ *
1204
+ * type Input = Workspaced<{}, "app">;
1205
+ *
1206
+ * const fixTree = (context: Input) => `${context.workspace.repos.app}/fix` as const;
1207
+ *
1208
+ * const body = jr2Setup({
1209
+ * types: {} as { context: Input; input: Input },
1210
+ * events: [committed],
1211
+ * actors: {
1212
+ * coder: agent({
1213
+ * model: "anthropic/claude-haiku-5-5",
1214
+ * instructions: "You are a careful engineer.",
1215
+ * }),
1216
+ * cut: worktree(),
1217
+ * publish: push({ sign: true }),
1218
+ * fail: fail(),
1219
+ * },
1220
+ * }).createMachine({
1221
+ * id: "fix-test",
1222
+ * context: ({ input }) => input,
1223
+ * on: { "workspace.lost": ".failed", "agent.fault": ".failed" },
1224
+ * initial: "cutting",
1225
+ * states: {
1226
+ * cutting: {
1227
+ * invoke: {
1228
+ * src: "cut",
1229
+ * input: ({ context }) => ({ path: fixTree(context), branch: "fix-test" }),
1230
+ * onDone: [{ guard: ({ event }) => isReady(event.output), target: "coding" }, { target: "notCut" }],
1231
+ * },
1232
+ * },
1233
+ * coding: {
1234
+ * invoke: {
1235
+ * src: "coder",
1236
+ * input: ({ context }) => ({ prompt: "Fix the failing test, and commit the fix.", cwd: fixTree(context) }),
1237
+ * },
1238
+ * // The Agent moves the Machine on with a pick. An Agent's actor never ends, so `onDone`
1239
+ * // never fires.
1240
+ * on: { committed: "publishing" },
1241
+ * },
1242
+ * publishing: {
1243
+ * invoke: {
1244
+ * src: "publish",
1245
+ * // jr2 reads the branch from the cut of this Worktree.
1246
+ * input: ({ context }) => ({ worktree: fixTree(context) }),
1247
+ * onDone: [
1248
+ * { guard: ({ event }) => event.output.outcome === "pushed", target: "done" },
1249
+ * { target: "failed" },
1250
+ * ],
1251
+ * },
1252
+ * },
1253
+ * done: { type: "final" },
1254
+ * notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
1255
+ * // Not a final state: the run ends `failed`, and the Workspace deletes the Sandbox.
1256
+ * failed: { invoke: { src: "fail", input: { reason: "the fix was not pushed" } } },
1257
+ * },
1258
+ * });
1259
+ *
1260
+ * export const machine = workspace(body, {
1261
+ * repos: { app: { url: "https://github.com/you/app.git" } },
1262
+ * });
1263
+ * ```
1264
+ *
1265
+ * @see docs/guides/push-and-sign-a-branch.md
1266
+ * @see docs/concepts/effects.md
1267
+ * @see docs/guides/give-the-cluster-git-credentials.md
1268
+ * @see docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md
1269
+ * @see docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md
886
1270
  */
887
- export function push<const O extends PushOptions>(options?: O): EffectLogic<PushInput<O> | void, PushOutput> {
1271
+ export function push<const O extends PushOptions>(options?: O): EffectLogic<PushInput<O>, PushOutput> {
1272
+ // After an Orchestrator restart, a push is sent again with only the commit its intent row holds
1273
+ // (ADR-0067): a run is a function of the commit, never of the Agent's tip now.
888
1274
  return pushWith(sandboxEffectorPort, options);
889
1275
  }
890
1276