@jr2/orchestrator 0.4.1 → 0.5.1

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 (327) 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 +442 -0
  10. package/docs/concepts/architecture.md +230 -0
  11. package/docs/concepts/charts.md +143 -0
  12. package/docs/concepts/composition.md +357 -0
  13. package/docs/concepts/converge.md +254 -0
  14. package/docs/concepts/durable-runs.md +189 -0
  15. package/docs/concepts/effects.md +603 -0
  16. package/docs/concepts/gates.md +208 -0
  17. package/docs/concepts/machines-and-workflows.md +289 -0
  18. package/docs/concepts/pools-and-sources.md +230 -0
  19. package/docs/concepts/secrets-and-trust.md +222 -0
  20. package/docs/concepts/the-menu.md +378 -0
  21. package/docs/concepts/workspaces.md +678 -0
  22. package/docs/get-started/a-model-decides.md +563 -0
  23. package/docs/get-started/an-agent-in-a-sandbox.md +381 -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 +922 -0
  27. package/docs/get-started/install.md +126 -0
  28. package/docs/guides/add-tools-to-the-sandbox.md +295 -0
  29. package/docs/guides/answer-a-gate.md +304 -0
  30. package/docs/guides/bind-open-parts.md +223 -0
  31. package/docs/guides/cancel-a-run.md +146 -0
  32. package/docs/guides/clean-up.md +206 -0
  33. package/docs/guides/fix-a-failed-converge.md +361 -0
  34. package/docs/guides/give-the-cluster-git-credentials.md +222 -0
  35. package/docs/guides/iterate-on-a-machine-on-kind.md +190 -0
  36. package/docs/guides/let-an-agent-write-a-chart.md +226 -0
  37. package/docs/guides/look-inside-a-sandbox.md +174 -0
  38. package/docs/guides/mirror-the-kit-images.md +176 -0
  39. package/docs/guides/move-from-kind-to-a-shared-cluster.md +302 -0
  40. package/docs/guides/open-the-console.md +91 -0
  41. package/docs/guides/publish-a-machine-as-an-npm-package.md +350 -0
  42. package/docs/guides/push-and-sign-a-branch.md +486 -0
  43. package/docs/guides/run-a-chart-by-name.md +270 -0
  44. package/docs/guides/run-a-command-or-call-a-url.md +544 -0
  45. package/docs/guides/run-one-worker-per-item.md +321 -0
  46. package/docs/guides/set-an-agents-access-and-dials.md +375 -0
  47. package/docs/guides/size-a-workspace.md +204 -0
  48. package/docs/guides/start-a-run-from-a-run.md +237 -0
  49. package/docs/guides/start-a-run.md +207 -0
  50. package/docs/guides/upgrade-the-kit-version.md +240 -0
  51. package/docs/guides/use-a-hosted-provider.md +208 -0
  52. package/docs/guides/use-a-local-model.md +313 -0
  53. package/docs/guides/watch-a-run.md +326 -0
  54. package/docs/guides/write-a-workflow.md +318 -0
  55. package/docs/index.md +44 -0
  56. package/docs/llms.txt +216 -0
  57. package/docs/reference/authoring-surface/agent.md +128 -0
  58. package/docs/reference/authoring-surface/answer.md +140 -0
  59. package/docs/reference/authoring-surface/arg.md +60 -0
  60. package/docs/reference/authoring-surface/basic.md +62 -0
  61. package/docs/reference/authoring-surface/bearer.md +53 -0
  62. package/docs/reference/authoring-surface/chartschema.md +23 -0
  63. package/docs/reference/authoring-surface/chartschemafor.md +48 -0
  64. package/docs/reference/authoring-surface/command.md +191 -0
  65. package/docs/reference/authoring-surface/customize.md +64 -0
  66. package/docs/reference/authoring-surface/defineconfig.md +41 -0
  67. package/docs/reference/authoring-surface/defineevent.md +114 -0
  68. package/docs/reference/authoring-surface/dispatch.md +126 -0
  69. package/docs/reference/authoring-surface/fail.md +83 -0
  70. package/docs/reference/authoring-surface/file.md +122 -0
  71. package/docs/reference/authoring-surface/frompick.md +277 -0
  72. package/docs/reference/authoring-surface/http.md +159 -0
  73. package/docs/reference/authoring-surface/isfailure.md +58 -0
  74. package/docs/reference/authoring-surface/jr2setup.md +237 -0
  75. package/docs/reference/authoring-surface/open.md +74 -0
  76. package/docs/reference/authoring-surface/openbody.md +86 -0
  77. package/docs/reference/authoring-surface/paramsof.md +125 -0
  78. package/docs/reference/authoring-surface/pool.md +186 -0
  79. package/docs/reference/authoring-surface/push.md +112 -0
  80. package/docs/reference/authoring-surface/secret.md +118 -0
  81. package/docs/reference/authoring-surface/sentby.md +83 -0
  82. package/docs/reference/authoring-surface/source.md +51 -0
  83. package/docs/reference/authoring-surface/text.md +62 -0
  84. package/docs/reference/authoring-surface/type-agentdeclaration.md +27 -0
  85. package/docs/reference/authoring-surface/type-agentdefinition.md +80 -0
  86. package/docs/reference/authoring-surface/type-agentlogic.md +15 -0
  87. package/docs/reference/authoring-surface/type-agentslots.md +12 -0
  88. package/docs/reference/authoring-surface/type-answeraction.md +12 -0
  89. package/docs/reference/authoring-surface/type-arg.md +19 -0
  90. package/docs/reference/authoring-surface/type-argnames.md +12 -0
  91. package/docs/reference/authoring-surface/type-binding.md +62 -0
  92. package/docs/reference/authoring-surface/type-bindingof.md +12 -0
  93. package/docs/reference/authoring-surface/type-bodyacceptsdoor.md +15 -0
  94. package/docs/reference/authoring-surface/type-bodyacceptsslots.md +17 -0
  95. package/docs/reference/authoring-surface/type-ceilings.md +41 -0
  96. package/docs/reference/authoring-surface/type-chainlink.md +19 -0
  97. package/docs/reference/authoring-surface/type-chartaction.md +46 -0
  98. package/docs/reference/authoring-surface/type-chartagent.md +28 -0
  99. package/docs/reference/authoring-surface/type-chartevent.md +20 -0
  100. package/docs/reference/authoring-surface/type-chartguard.md +20 -0
  101. package/docs/reference/authoring-surface/type-chartinvoke.md +28 -0
  102. package/docs/reference/authoring-surface/type-chartjson.md +14 -0
  103. package/docs/reference/authoring-surface/type-chartstate.md +46 -0
  104. package/docs/reference/authoring-surface/type-charttext.md +40 -0
  105. package/docs/reference/authoring-surface/type-charttransition.md +22 -0
  106. package/docs/reference/authoring-surface/type-charttransitions.md +14 -0
  107. package/docs/reference/authoring-surface/type-chartvalue.md +15 -0
  108. package/docs/reference/authoring-surface/type-childat.md +12 -0
  109. package/docs/reference/authoring-surface/type-childslots.md +12 -0
  110. package/docs/reference/authoring-surface/type-commandenv.md +15 -0
  111. package/docs/reference/authoring-surface/type-commandinput.md +19 -0
  112. package/docs/reference/authoring-surface/type-commandoptions.md +78 -0
  113. package/docs/reference/authoring-surface/type-commandoutput.md +104 -0
  114. package/docs/reference/authoring-surface/type-customize.md +109 -0
  115. package/docs/reference/authoring-surface/type-dispatchdefinition.md +39 -0
  116. package/docs/reference/authoring-surface/type-dispatchinput.md +14 -0
  117. package/docs/reference/authoring-surface/type-dispatchopenparts.md +14 -0
  118. package/docs/reference/authoring-surface/type-dispatchoutput.md +65 -0
  119. package/docs/reference/authoring-surface/type-duration.md +15 -0
  120. package/docs/reference/authoring-surface/type-effectbinding.md +93 -0
  121. package/docs/reference/authoring-surface/type-effectlogic.md +12 -0
  122. package/docs/reference/authoring-surface/type-envopenparts.md +14 -0
  123. package/docs/reference/authoring-surface/type-eventaudience.md +27 -0
  124. package/docs/reference/authoring-surface/type-eventdef.md +45 -0
  125. package/docs/reference/authoring-surface/type-eventfrom.md +16 -0
  126. package/docs/reference/authoring-surface/type-eventsemantics.md +26 -0
  127. package/docs/reference/authoring-surface/type-execoutcome.md +34 -0
  128. package/docs/reference/authoring-surface/type-failslot.md +14 -0
  129. package/docs/reference/authoring-surface/type-failure.md +19 -0
  130. package/docs/reference/authoring-surface/type-failureinput.md +20 -0
  131. package/docs/reference/authoring-surface/type-filebody.md +41 -0
  132. package/docs/reference/authoring-surface/type-frominput.md +14 -0
  133. package/docs/reference/authoring-surface/type-hostinjectedinput.md +58 -0
  134. package/docs/reference/authoring-surface/type-httpdefinition.md +101 -0
  135. package/docs/reference/authoring-surface/type-httpinput.md +12 -0
  136. package/docs/reference/authoring-surface/type-httpmethod.md +14 -0
  137. package/docs/reference/authoring-surface/type-httpopenparts.md +12 -0
  138. package/docs/reference/authoring-surface/type-httpoutput.md +89 -0
  139. package/docs/reference/authoring-surface/type-httprequestparts.md +33 -0
  140. package/docs/reference/authoring-surface/type-jr2config.md +126 -0
  141. package/docs/reference/authoring-surface/type-lends.md +38 -0
  142. package/docs/reference/authoring-surface/type-logicat.md +12 -0
  143. package/docs/reference/authoring-surface/type-markedif.md +12 -0
  144. package/docs/reference/authoring-surface/type-mergedactors.md +12 -0
  145. package/docs/reference/authoring-surface/type-namedsecret.md +25 -0
  146. package/docs/reference/authoring-surface/type-noslotspec.md +26 -0
  147. package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +32 -0
  148. package/docs/reference/authoring-surface/type-openbodydeclaration.md +40 -0
  149. package/docs/reference/authoring-surface/type-openbodylogic.md +14 -0
  150. package/docs/reference/authoring-surface/type-openbodyoutput.md +42 -0
  151. package/docs/reference/authoring-surface/type-openbodyslot.md +18 -0
  152. package/docs/reference/authoring-surface/type-opencommandinput.md +17 -0
  153. package/docs/reference/authoring-surface/type-openeffect.md +12 -0
  154. package/docs/reference/authoring-surface/type-openeffectslots.md +12 -0
  155. package/docs/reference/authoring-surface/type-openof.md +12 -0
  156. package/docs/reference/authoring-surface/type-opensecret.md +23 -0
  157. package/docs/reference/authoring-surface/type-opensecretmark.md +12 -0
  158. package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +31 -0
  159. package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +32 -0
  160. package/docs/reference/authoring-surface/type-poolmachine.md +28 -0
  161. package/docs/reference/authoring-surface/type-poolspec.md +60 -0
  162. package/docs/reference/authoring-surface/type-pushinput.md +15 -0
  163. package/docs/reference/authoring-surface/type-pushoptions.md +79 -0
  164. package/docs/reference/authoring-surface/type-pushoutput.md +124 -0
  165. package/docs/reference/authoring-surface/type-pushwireoutput.md +12 -0
  166. package/docs/reference/authoring-surface/type-quantity.md +14 -0
  167. package/docs/reference/authoring-surface/type-reached.md +12 -0
  168. package/docs/reference/authoring-surface/type-reposlot.md +70 -0
  169. package/docs/reference/authoring-surface/type-reposlotsof.md +16 -0
  170. package/docs/reference/authoring-surface/type-sandboxoptions.md +77 -0
  171. package/docs/reference/authoring-surface/type-secret.md +25 -0
  172. package/docs/reference/authoring-surface/type-sender.md +19 -0
  173. package/docs/reference/authoring-surface/type-sentbyguards.md +12 -0
  174. package/docs/reference/authoring-surface/type-size.md +85 -0
  175. package/docs/reference/authoring-surface/type-slothandles.md +32 -0
  176. package/docs/reference/authoring-surface/type-sourcenextresult.md +17 -0
  177. package/docs/reference/authoring-surface/type-sourcespec.md +44 -0
  178. package/docs/reference/authoring-surface/type-syncstate.md +12 -0
  179. package/docs/reference/authoring-surface/type-text.md +19 -0
  180. package/docs/reference/authoring-surface/type-textpart.md +24 -0
  181. package/docs/reference/authoring-surface/type-thinkinglevel.md +39 -0
  182. package/docs/reference/authoring-surface/type-toparameterizedobject.md +14 -0
  183. package/docs/reference/authoring-surface/type-toprovidedactor.md +14 -0
  184. package/docs/reference/authoring-surface/type-unknown.md +21 -0
  185. package/docs/reference/authoring-surface/type-usercontainer.md +77 -0
  186. package/docs/reference/authoring-surface/type-withoutemptyindex.md +20 -0
  187. package/docs/reference/authoring-surface/type-workflowevent.md +46 -0
  188. package/docs/reference/authoring-surface/type-workspaceaccess.md +16 -0
  189. package/docs/reference/authoring-surface/type-workspaced.md +94 -0
  190. package/docs/reference/authoring-surface/type-workspacehandles.md +23 -0
  191. package/docs/reference/authoring-surface/type-workspacemachine.md +29 -0
  192. package/docs/reference/authoring-surface/type-workspaceof.md +13 -0
  193. package/docs/reference/authoring-surface/type-workspaceoptions.md +39 -0
  194. package/docs/reference/authoring-surface/type-workspacespec.md +37 -0
  195. package/docs/reference/authoring-surface/workspace.md +259 -0
  196. package/docs/reference/authoring-surface.md +197 -0
  197. package/docs/reference/cli.md +433 -0
  198. package/docs/reference/config.md +756 -0
  199. package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +113 -0
  200. package/docs/reference/decisions/0002-duplex-streaming-actor.md +110 -0
  201. package/docs/reference/decisions/0004-worktree-provisioning-model.md +124 -0
  202. package/docs/reference/decisions/0005-sandbox-pod-composition.md +230 -0
  203. package/docs/reference/decisions/0006-agent-control-surface.md +47 -0
  204. package/docs/reference/decisions/0007-durable-machine-state.md +109 -0
  205. package/docs/reference/decisions/0008-orchestrator-as-deployed-app.md +60 -0
  206. package/docs/reference/decisions/0009-cli-and-instance-interface.md +275 -0
  207. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +160 -0
  208. package/docs/reference/decisions/0011-workflow-defined-events.md +141 -0
  209. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +103 -0
  210. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +157 -0
  211. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +95 -0
  212. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +265 -0
  213. package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +124 -0
  214. package/docs/reference/decisions/0017-pool-over-a-source-port.md +69 -0
  215. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +111 -0
  216. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +275 -0
  217. package/docs/reference/decisions/0020-private-ca-trust-is-a-named-concept.md +60 -0
  218. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +100 -0
  219. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +103 -0
  220. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +176 -0
  221. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +439 -0
  222. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +65 -0
  223. package/docs/reference/decisions/0026-a-turn-that-is-over-is-never-prompted.md +62 -0
  224. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +237 -0
  225. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +89 -0
  226. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +154 -0
  227. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +100 -0
  228. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +190 -0
  229. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +61 -0
  230. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +127 -0
  231. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +57 -0
  232. package/docs/reference/decisions/0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md +92 -0
  233. package/docs/reference/decisions/0036-a-turn-compacts-its-own-context-mid-flight.md +138 -0
  234. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +196 -0
  235. package/docs/reference/decisions/0038-jr2-up-builds-every-image-it-deploys.md +170 -0
  236. package/docs/reference/decisions/0039-image-garbage-collects-by-reachability.md +115 -0
  237. package/docs/reference/decisions/0041-a-build-the-host-already-holds-is-not-spent-again.md +64 -0
  238. package/docs/reference/decisions/0042-ready-is-not-routable-an-admission-retries-its-connection.md +142 -0
  239. package/docs/reference/decisions/0043-the-kit-is-tested-as-installed-a-local-registry-stands-in-for-npm.md +170 -0
  240. package/docs/reference/decisions/0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md +85 -0
  241. package/docs/reference/decisions/0045-the-platform-joins-the-image-address-and-the-cluster-chooses-it.md +82 -0
  242. package/docs/reference/decisions/0046-a-converge-failure-carries-its-diagnosis.md +34 -0
  243. package/docs/reference/decisions/0047-the-git-ssh-key-source-is-the-users-choice.md +47 -0
  244. package/docs/reference/decisions/0048-the-orchestrator-boots-without-its-repos.md +36 -0
  245. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +126 -0
  246. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +73 -0
  247. package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +227 -0
  248. package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +81 -0
  249. package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +111 -0
  250. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +126 -0
  251. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +107 -0
  252. package/docs/reference/decisions/0056-a-global-jr2-hands-off-to-the-instances-own-and-an-instance-has-one-kit-version.md +62 -0
  253. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +152 -0
  254. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +106 -0
  255. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +384 -0
  256. package/docs/reference/decisions/0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md +111 -0
  257. package/docs/reference/decisions/0061-a-memory-kill-ends-the-agents-processes-not-the-harness.md +83 -0
  258. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +212 -0
  259. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +83 -0
  260. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +79 -0
  261. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +65 -0
  262. package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +550 -0
  263. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +430 -0
  264. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +464 -0
  265. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +180 -0
  266. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +323 -0
  267. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +183 -0
  268. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +144 -0
  269. package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +122 -0
  270. package/docs/reference/decisions/0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md +76 -0
  271. package/docs/reference/decisions.md +83 -0
  272. package/docs/reference/glossary.md +648 -0
  273. package/docs/reference/http-api.md +1348 -0
  274. package/docs/reference/machines.md +190 -0
  275. package/docs/reference/mechanism-events.md +72 -0
  276. package/docs/why.md +206 -0
  277. package/package.json +5 -3
  278. package/src/actor.ts +541 -79
  279. package/src/agent.ts +114 -38
  280. package/src/ambient.ts +2 -1
  281. package/src/assembly-checks.ts +189 -7
  282. package/src/cause.ts +234 -0
  283. package/src/chart.ts +169 -37
  284. package/src/charts.ts +34 -6
  285. package/src/config.ts +430 -178
  286. package/src/custodian.ts +74 -22
  287. package/src/customize.ts +188 -46
  288. package/src/dispatch-effect.ts +945 -0
  289. package/src/door-compat.ts +217 -0
  290. package/src/durability.ts +11 -4
  291. package/src/effect.ts +341 -44
  292. package/src/effector-wire.ts +160 -11
  293. package/src/fail.ts +209 -0
  294. package/src/harness-client.ts +541 -47
  295. package/src/http-effect.ts +495 -53
  296. package/src/http.ts +427 -16
  297. package/src/index.ts +60 -82
  298. package/src/instance.ts +68 -8
  299. package/src/internal.ts +84 -0
  300. package/src/machine-doc.ts +21 -3
  301. package/src/mechanism-routes.ts +168 -0
  302. package/src/model-keys.generated.ts +53 -0
  303. package/src/model-keys.ts +18 -58
  304. package/src/notices.ts +66 -13
  305. package/src/open-body.ts +159 -23
  306. package/src/open-effect.ts +81 -8
  307. package/src/open.ts +59 -6
  308. package/src/parts.ts +184 -13
  309. package/src/party.ts +68 -2
  310. package/src/pick.ts +463 -11
  311. package/src/pool.ts +247 -36
  312. package/src/registration.ts +83 -4
  313. package/src/repos.ts +2 -1
  314. package/src/run-host.ts +565 -31
  315. package/src/run-output.ts +38 -0
  316. package/src/sandbox-effect.ts +273 -38
  317. package/src/sandbox-kube.ts +3 -1
  318. package/src/secret-placement.ts +52 -17
  319. package/src/server.ts +3 -0
  320. package/src/setup.ts +239 -56
  321. package/src/size.ts +121 -9
  322. package/src/snapshot-store.ts +54 -10
  323. package/src/stub-harness.ts +44 -16
  324. package/src/timers.ts +14 -1
  325. package/src/vocabulary.ts +69 -14
  326. package/src/wire.ts +90 -16
  327. package/src/workspace.ts +697 -175
@@ -0,0 +1,38 @@
1
+ // The output of a run (ADR-0015). xstate v5 gives a Machine one output: the `output` at its root,
2
+ // beside `id` and `states`. The `output` of a final state directly below the root goes to that root
3
+ // `output` as `event.output`, and to nothing else. Thus a Machine that writes an `output` on such a
4
+ // final state and no root `output` ends with no output: its run status shows none, and the `onDone`
5
+ // of an invoke of it gets `event.output` as `undefined`. `types.output` does not stop this, so the
6
+ // load walk refuses the shape and the refusal gives the fix.
7
+ //
8
+ // A final state inside a compound state is legal: its `output` is the `event.output` of the `onDone`
9
+ // of that compound state. A Machine with no `output` anywhere is legal too: its result is in its
10
+ // `context`, and it says nothing false about an output.
11
+
12
+ import type { AnyStateMachine, StateNode } from "xstate";
13
+
14
+ /**
15
+ * Refuse a Machine whose root has no `output` while a final state directly below the root declares
16
+ * one (see the module comment). Thrown at build by `jr2Setup.createMachine` and at load by
17
+ * `partsOf`, so a Machine built with xstate's own `createMachine` is refused where `jr2 check`,
18
+ * `jr2 up` and the Orchestrator read it.
19
+ */
20
+ export function refuseLostOutput(machine: AnyStateMachine): void {
21
+ const root = machine.root as StateNode<any, any>;
22
+ if (root.config.output !== undefined) return;
23
+ const lost = Object.values(root.states as Record<string, StateNode<any, any>>).filter(
24
+ (node) => node.type === "final" && node.config.output !== undefined,
25
+ );
26
+ if (!lost.length) return;
27
+ const names = lost.map((node) => `"${node.id}"`).join(", ");
28
+ const many = lost.length > 1;
29
+ const [states, declare, reach, write, keep, pass] = many
30
+ ? ["final states", "declare", "these outputs reach", "one", "them on the final states", "them"]
31
+ : ["final state", "declares", "this output reaches", "it", "it on the final state", "it"];
32
+ throw new Error(
33
+ `machine "${machine.id}": ${states} ${names} ${declare} an output, and the root has no output — xstate v5 ` +
34
+ `gives a run only its root output, so ${reach} nothing and the run ends with none: write ${write} at the ` +
35
+ `root, as output: ({ context }) => ..., or keep ${keep} and pass ${pass} on with output: ({ event }) => ` +
36
+ `event.output at the root`,
37
+ );
38
+ }
@@ -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 { lastPushOf, needsASlot, pushPairsOf } from "./effect.ts";
57
57
  import {
58
58
  createEffectorClient,
59
59
  EffectorForged,
@@ -126,12 +126,30 @@ function slotOf(kind: string, slot: string, named: string | undefined, ambient:
126
126
  return named;
127
127
  }
128
128
  if (slots.length === 1) return slots[0]!;
129
+ const why = slots.length === 0 ? needsASlot(kind) : undefined;
130
+ if (why !== undefined)
131
+ throw new Error(`${kind} "${slot}" runs under a Workspace with no Repo Slot: ${why} (ADR-0051)`);
129
132
  throw new Error(
130
133
  `${kind} "${slot}" runs under a Workspace carrying ${slots.length === 0 ? "no" : "more than one"} Repo Slot ` +
131
134
  `(${slots.join(", ") || "none"}) — say which: { slot: "<slot>" } (ADR-0051)`,
132
135
  );
133
136
  }
134
137
 
138
+ /** Where a file body's path is read from, relative to the work root: the Repo Slot's Worktree, or
139
+ * the work root itself (`.`) in a Workspace with no Repo Slot, whose steps share `/work` (ADR-0051).
140
+ * The Effector opens the file beneath it either way (ADR-0067). */
141
+ function fileRootOf(slot: string, named: string | undefined, ambient: AmbientHandles): string {
142
+ if (named === undefined && Object.keys(ambient.repos).length === 0) return ".";
143
+ return posix.relative(WORK_ROOT, ambient.repos[slotOf("http", slot, named, ambient)]!);
144
+ }
145
+
146
+ /** The branch of a Workspace with Repo Slots. A kind that reads it resolved a slot first, and a
147
+ * Workspace with a slot always has a branch, so its absence is a jr2 defect. */
148
+ function branchOf(ambient: AmbientHandles): string {
149
+ if (ambient.branch === undefined) throw new Error("a Workspace with a Repo Slot has no branch — a jr2 defect");
150
+ return ambient.branch;
151
+ }
152
+
135
153
  /** The slot's repositories, relative to the work root: the Agent's common git dir is the pod-local
136
154
  * clone's, beside the branch Worktree (ADR-0004); the cache is the Binding's key (ADR-0051). */
137
155
  export function repoRefOf(ambient: AmbientHandles, repoSlot: string): RepoRef & { url: string } {
@@ -151,36 +169,159 @@ export function repoRefOf(ambient: AmbientHandles, repoSlot: string): RepoRef &
151
169
 
152
170
  // --- The definitions -------------------------------------------------------------------------------
153
171
 
154
- /** `push()`'s options (ADR-0067). */
172
+ /** The options of `push()`. */
155
173
  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. */
174
+ // A pinned commit the Agent pruned is refused by the remote, never substituted (ADR-0067).
175
+ /**
176
+ * `true`: the input of the Effect names the commit to push (`{ sha }`, a full commit id, for
177
+ * example one that a Gate approved). Default: the tip of the Agent's branch when the Effect
178
+ * starts. When the Agent removed that commit from its branch, the remote refuses the push. jr2
179
+ * never pushes another commit in its place.
180
+ *
181
+ * When it starts, it refuses a `sha` that is not a full commit id: `push({ pin: true }): its
182
+ * input's sha is not a full commit id (got <sha>)`.
183
+ */
159
184
  pin?: boolean;
160
- /** Push signed copies; the matching `git.credentials` entry's `sign` names the key. */
185
+ /**
186
+ * `true`: push signed copies of the commits. The `sign` field of the `git.credentials` entry that
187
+ * the Binding matches names the key. Default `false`.
188
+ *
189
+ * At `jr2 up`, it refuses an entry with no `sign` field: `<push> signed, and git.credentials
190
+ * entry "<match>" holds no sign field — add sign: { sshKey: "<a Secret with identity>", name:
191
+ * "<committer name>", email: "<committer email>" } to it; the email must belong to the forge
192
+ * account that holds the key (ADR-0051, ADR-0067)`. `<push>` names the push: `workflow
193
+ * "<workflow>": push "<slot>" pushes Repo Slot "<name>" (<url>)`.
194
+ */
161
195
  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). */
196
+ /**
197
+ * `true`: push one commit that holds all the changes. The input of the Effect gives its message
198
+ * (`{ message }`). Signed, its committer is the signer. Unsigned, it is the `committer` of the
199
+ * `git.credentials` entry that the Binding matches. Default `false`.
200
+ *
201
+ * When it starts, it refuses an input with no message: `push({ squash: true }): its input
202
+ * carries no message`.
203
+ *
204
+ * At `jr2 up`, it refuses an unsigned squash whose entry has no `sign` and no `committer`:
205
+ * `<push> squashed without sign, and git.credentials entry "<match>" holds neither sign nor
206
+ * committer — the squashed commit is new and needs a committer; add committer: { name:
207
+ * "<committer name>", email: "<committer email>" } to it, or sign the push — push({ squash:
208
+ * true, sign: true }) — and add sign: { sshKey: "<a Secret with identity>", name: "<committer
209
+ * name>", email: "<committer email>" } to it (ADR-0051, ADR-0067)`.
210
+ */
165
211
  squash?: boolean;
166
- /** Replace what the remote holds. */
212
+ /** `true`: replace what the remote branch holds. Default `false`. */
167
213
  force?: boolean;
168
- /** The Repo Slot pushed — needed only where the Workspace carries several. */
214
+ /**
215
+ * The Repo Slot to push. Write it only when the Workspace has more than one.
216
+ *
217
+ * When it starts, it refuses:
218
+ * - no `slot` under a Workspace with more than one Repo Slot: `push "<slot>" runs under a
219
+ * Workspace carrying more than one Repo Slot (<slots>) — say which: { slot: "<slot>" }
220
+ * (ADR-0051)`
221
+ * - a `slot` that the Workspace does not have: `push "<slot>" names Repo Slot "<name>", which
222
+ * its Workspace does not carry (<slots>)`
223
+ * - a Workspace with no Repo Slot: `push "<slot>" runs under a Workspace with no Repo Slot: a
224
+ * push publishes a Repo Slot's branch, and there is none — declare one: repos: { app: "<url>" }
225
+ * (ADR-0051)`. `jr2 up` refuses it first, with the same reason.
226
+ */
169
227
  slot?: string;
170
- /** Default 5m. Past it with no answer: output `unknown`. */
228
+ /**
229
+ * How long jr2 waits for the push: a number and a unit, such as `"90s"` or `"5m"`. Default
230
+ * `"5m"`. With no answer in that time, the output is `unknown`. It refuses a value that is not a
231
+ * positive duration: `timeout "<timeout>" is not a positive duration (like "30s", "5m")`.
232
+ */
171
233
  timeout?: Duration;
172
234
  };
173
235
 
174
- /** A push's input: what `pin` and `squash` add. */
236
+ /** The input of a `push` Effect: `{ sha }` with `pin`, and `{ message }` with `squash`. */
175
237
  export type PushInput<O extends PushOptions> = (O["pin"] extends true ? { sha: string } : unknown) &
176
238
  (O["squash"] extends true ? { message: string } : unknown);
177
239
 
178
- /** The sync's answer as a push's output carries it: a field beside the outcome, never the outcome. */
240
+ /**
241
+ * The sync's answer as a push's output carries it: a field beside the outcome, never the outcome.
242
+ *
243
+ * @internal
244
+ */
179
245
  export type SyncState = SyncAnswer["sync"];
180
246
 
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. */
247
+ // The sync's bundle never reaches the Machine: `deliver` drops it from the output (ADR-0067).
248
+ /**
249
+ * The output of a `push` Effect: what the `onDone` of its state gets as `event.output`. Every
250
+ * outcome is output, so route on `outcome`:
251
+ *
252
+ * - `pushed`: the remote has the commit. `published` is the commit on the remote, and `source` is
253
+ * the Agent's commit that it comes from. They are the same commit, unless the push was signed or
254
+ * squashed. `previous` is what the remote had before, and `unchanged` is `true` when the remote
255
+ * had the commit already. `squashed` is `signed` or `unsigned` when the commit on the remote is
256
+ * one squashed commit. `via` tells how jr2 found the new commits after a signed push:
257
+ * `ancestry`, `pair`, `cherry` or `force`.
258
+ * - `rejected`: git or the remote refused the push. `stage` tells where, `reason` tells why, and
259
+ * `detail` has the words of git.
260
+ * - `diverged`: the commit on the remote is not in the history of the Agent's branch, and jr2
261
+ * could not pair it with a commit there. Nothing was pushed. `reason` tells why, and `published`
262
+ * and `source` name the two commits.
263
+ * - `unknown`: jr2 cannot tell what the push did. `reason` tells why.
264
+ *
265
+ * Beside `pushed`, `sync` tells what jr2 did to the Agent's branch after the push:
266
+ *
267
+ * - `synced`: the commit on the remote is not the Agent's own, and jr2 moved the branch to the
268
+ * published commits.
269
+ * - `already`: the branch has the published commit already.
270
+ * - `skipped`: the remote has the Agent's own commit, so the branch does not move.
271
+ * - `busy`: a rebase, merge, cherry-pick or revert of the branch is in progress. Nothing changed.
272
+ * - `not-synced`: the branch did not move.
273
+ *
274
+ * With `busy` or `not-synced`, `syncReason` tells why, and jr2 tries again before the next push.
275
+ *
276
+ * @example
277
+ * ```ts
278
+ * import { fail, jr2Setup, push, type PushOutput, workspace, type Workspaced } from "@jr2/orchestrator";
279
+ *
280
+ * type Input = Workspaced<{ sha: string }, "app">;
281
+ *
282
+ * // A push that did not happen (`rejected` or `diverged`) is a different problem from one jr2
283
+ * // cannot see (`unknown`).
284
+ * const notPushed = (output: PushOutput): boolean =>
285
+ * output.outcome === "rejected" || output.outcome === "diverged";
286
+ *
287
+ * const body = jr2Setup({
288
+ * types: {} as { context: Input; input: Input },
289
+ * actors: { publish: push({ pin: true }), fail: fail() },
290
+ * }).createMachine({
291
+ * id: "release",
292
+ * context: ({ input }) => input,
293
+ * on: { "workspace.lost": ".lost" },
294
+ * initial: "publishing",
295
+ * states: {
296
+ * publishing: {
297
+ * invoke: {
298
+ * src: "publish",
299
+ * input: ({ context }) => ({ sha: context.sha }),
300
+ * onDone: [
301
+ * { guard: ({ event }) => event.output.outcome === "pushed", target: "published" },
302
+ * { guard: ({ event }) => notPushed(event.output), target: "notPushed" },
303
+ * { target: "unknown" },
304
+ * ],
305
+ * },
306
+ * },
307
+ * published: { type: "final" },
308
+ * // Not final states: each one fails the run, with its own reason.
309
+ * notPushed: { invoke: { src: "fail", input: { reason: "the remote refused the push" } } },
310
+ * unknown: { invoke: { src: "fail", input: { reason: "jr2 cannot tell if the push happened" } } },
311
+ * lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
312
+ * },
313
+ * });
314
+ *
315
+ * export const machine = workspace(body, {
316
+ * repos: { app: { url: "https://github.com/you/app.git" } },
317
+ * spec: () => ({ branch: "release" }),
318
+ * });
319
+ * ```
320
+ *
321
+ * @see docs/guides/push-and-sign-a-branch.md
322
+ * @see docs/concepts/effects.md
323
+ * @see docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md
324
+ */
184
325
  export type PushOutput =
185
326
  | Exclude<PushWireOutput, { outcome: "pushed" }>
186
327
  | (Omit<Extract<PushWireOutput, { outcome: "pushed" }>, "bundle"> & { sync: SyncState; syncReason?: string });
@@ -232,8 +373,8 @@ export function pushStartOf(
232
373
  };
233
374
  }
234
375
 
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. */
376
+ /** A file-body http's start for one run (ADR-0067): the definition's parts, the url's segments, and
377
+ * the Worktree file as the body. */
237
378
  export function fileStartOf(
238
379
  parts: HttpParts,
239
380
  worktree: string,
@@ -248,7 +389,8 @@ export function fileStartOf(
248
389
  return { ...http, body: { file: { worktree, path: run.path, ...(contentType ? { contentType } : {}) } } };
249
390
  }
250
391
 
251
- /** A credentialed command's env as its definition names it. */
392
+ /** The `env` of a `command`: for each variable, `secret("name")` for the value of an Effect
393
+ * secret, or `secret("name").path` for the path of a file that holds it. */
252
394
  export type CommandEnv = Readonly<Record<string, { readonly secret: string; readonly path: unknown }>>;
253
395
 
254
396
  export function commandEnvParts(env: CommandEnv): Pick<CommandParts, "env" | "secrets" | "openSecret"> {
@@ -579,7 +721,8 @@ async function drive<I, O>(
579
721
  }
580
722
  }
581
723
 
582
- /** The settled row's Repo Slot, or none when the Workspace does not name one the kind can run on. */
724
+ /** The settled row's Repo Slot, or none when the Workspace does not name one the kind can run on.
725
+ */
583
726
  function repoField(spec: KindSpec, ambient: AmbientHandles, slot: string): { repo?: string } {
584
727
  try {
585
728
  return { repo: spec.repoOf(ambient, slot) };
@@ -660,9 +803,10 @@ function stoppedText(state: EffectState | undefined): string {
660
803
  }
661
804
 
662
805
  /** 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. */
806
+ * (ADR-0067): an http `refused`; a push `rejected` at stage `start` — nothing pushed; a
807
+ * credentialed command `exited` 127 with the reason in its tail, the Effector's own form for a
808
+ * command that could not start. An Agent's pick parameter — a squash message, an argv part — can
809
+ * reach the bound. */
666
810
  function tooLarge(req: StartRequest): HttpOutput | PushWireOutput | CommandWireOutput | undefined {
667
811
  const bytes = Buffer.byteLength(JSON.stringify(req), "utf8");
668
812
  if (bytes <= MAX_START_BYTES) return undefined;
@@ -728,7 +872,7 @@ export function pushWith<const O extends PushOptions>(
728
872
  build: (input, ambient, slot, pinned, extra) => {
729
873
  const repoSlot = slotOf("push", slot, parts.slot, ambient);
730
874
  const ref = repoRefOf(ambient, repoSlot);
731
- const req = pushStartOf(parts, ref, ambient.branch, input as { sha?: string; message?: string }, extra?.pairs);
875
+ const req = pushStartOf(parts, ref, branchOf(ambient), input as { sha?: string; message?: string }, extra?.pairs);
732
876
  if (pinned) req.push.pin = pinned;
733
877
  return req;
734
878
  },
@@ -746,7 +890,7 @@ export function pushWith<const O extends PushOptions>(
746
890
  const answer = await syncOf(ctx).sync(
747
891
  {
748
892
  worktree: worktreeOf(ctx),
749
- branch: ctx.ambient.branch,
893
+ branch: branchOf(ctx.ambient),
750
894
  published: output.published,
751
895
  source: output.source,
752
896
  ...(typeof output.bundle === "string" ? { bundle: output.bundle } : {}),
@@ -772,7 +916,7 @@ export function pushWith<const O extends PushOptions>(
772
916
  const answer = await syncOf(ctx).sync(
773
917
  {
774
918
  worktree: worktreeOf(ctx),
775
- branch: ctx.ambient.branch,
919
+ branch: branchOf(ctx.ambient),
776
920
  published: o.published,
777
921
  source: o.source,
778
922
  ...(o.bundle !== undefined ? { bundle: o.bundle } : {}),
@@ -800,8 +944,9 @@ function syncFields(answer: SyncAnswer): { sync: SyncState; syncReason?: string
800
944
 
801
945
  export { lastPushOf };
802
946
 
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. */
947
+ /** An http whose body is a Worktree file (ADR-0067), over an injected port factory — the seam
948
+ * `http()` and the unit tests share. It runs in the Sandbox Effector of the Workspace whose body
949
+ * invokes it. */
805
950
  export function httpFileWith<
806
951
  S extends z.ZodType | undefined = undefined,
807
952
  const U extends HttpDefinition<S>["url"] = string,
@@ -827,7 +972,10 @@ export function httpFileWith<S extends z.ZodType | undefined = undefined>(
827
972
  kind: "http",
828
973
  idempotent: parts.idempotent,
829
974
  repeatsPinned: false,
830
- label: () => `http ${parts.method} ${parts.target.scheme}://${parts.target.host} (a Worktree file)`,
975
+ // With no Repo Slot the file is in `/work`, which is no Worktree (ADR-0051).
976
+ label: (ambient) =>
977
+ `http ${parts.method} ${parts.target.scheme}://${parts.target.host} ` +
978
+ (Object.keys(ambient.repos).length === 0 && body.slot === undefined ? "(a file of /work)" : "(a Worktree file)"),
831
979
  repoOf: (ambient, slot) => slotOf("http", slot, body.slot, ambient),
832
980
  build: (input, ambient, slot) => {
833
981
  let value: unknown = input;
@@ -836,8 +984,7 @@ export function httpFileWith<S extends z.ZodType | undefined = undefined>(
836
984
  if (!parsed.success) throw new Error(`http "${slot}": its input does not match the definition's input schema`);
837
985
  value = parsed.data;
838
986
  }
839
- const repoSlot = slotOf("http", slot, body.slot, ambient);
840
- const worktree = posix.relative(WORK_ROOT, ambient.repos[repoSlot]!);
987
+ const worktree = fileRootOf(slot, body.slot, ambient);
841
988
  const segments = fromInput<readonly string[]>(body.segments, value);
842
989
  return fileStartOf(parts, worktree, {
843
990
  path: fromInput<string>(body.path, value)!,
@@ -847,7 +994,8 @@ export function httpFileWith<S extends z.ZodType | undefined = undefined>(
847
994
  });
848
995
  }
849
996
 
850
- /** A command whose env names a secret: it runs in the Sandbox Effector against the pinned commit. */
997
+ /** A command whose env names a secret: it runs in the Sandbox Effector against the pinned commit.
998
+ */
851
999
  export function credentialedCommandWith(
852
1000
  portFactory: EffectorPortFactory,
853
1001
  parts: CommandParts,
@@ -871,7 +1019,7 @@ export function credentialedCommandWith(
871
1019
  parts,
872
1020
  argvOf(input, slot),
873
1021
  ref,
874
- ambient.branch,
1022
+ branchOf(ambient),
875
1023
  pinned ?? (approved as string | undefined),
876
1024
  );
877
1025
  },
@@ -879,12 +1027,99 @@ export function credentialedCommandWith(
879
1027
  }
880
1028
 
881
1029
  /**
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`.
1030
+ * Make a `push` Effect: it pushes the branch of the Workspace to the Binding of its Repo Slot.
1031
+ * Put it in `actors`, and invoke it from the body of a `workspace()`: `actors: { publish: push({
1032
+ * sign: true }) }`.
1033
+ *
1034
+ * It runs in the Sandbox Effector of the Workspace, with the push credential of the
1035
+ * `git.credentials` entry that the Binding matches. The Agent never reads that credential. It
1036
+ * pushes one commit: the tip of the Agent's branch when the Effect starts, or, with `pin`, the
1037
+ * commit that the input names. After an Orchestrator restart, jr2 pushes only that same commit
1038
+ * again. With `sign` or `squash`, the commit on the remote is not the Agent's own. Then jr2 moves
1039
+ * the Agent's branch to it.
1040
+ *
1041
+ * Every outcome is output (`PushOutput`): `pushed`, `rejected`, `diverged` or `unknown`. Each
1042
+ * field of {@link PushOptions} tells what it refuses. When it starts, it also refuses a push
1043
+ * outside a Workspace: `push "<slot>" runs in the Sandbox Effector of a Workspace, and its state is
1044
+ * not inside a workspace() — invoke it from a Workspace's body (ADR-0067)`.
1045
+ *
1046
+ * @param options - Optional. Each field tells what it does.
1047
+ * @returns An Effect, for `actors`.
1048
+ *
1049
+ * @example
1050
+ * ```ts
1051
+ * import { z } from "zod";
1052
+ * import {
1053
+ * agent,
1054
+ * defineEvent,
1055
+ * fail,
1056
+ * jr2Setup,
1057
+ * push,
1058
+ * workspace,
1059
+ * type Workspaced,
1060
+ * } from "@jr2/orchestrator";
1061
+ *
1062
+ * const committed = defineEvent({
1063
+ * name: "committed",
1064
+ * description: "Say that the fix is committed.",
1065
+ * input: z.object({}),
1066
+ * });
1067
+ *
1068
+ * type Input = Workspaced<{}, "app">;
1069
+ *
1070
+ * const body = jr2Setup({
1071
+ * types: {} as { context: Input; input: Input },
1072
+ * events: [committed],
1073
+ * actors: {
1074
+ * coder: agent({
1075
+ * model: "anthropic/claude-haiku-5-5",
1076
+ * instructions: "You are a careful engineer.",
1077
+ * }),
1078
+ * publish: push({ sign: true }),
1079
+ * fail: fail(),
1080
+ * },
1081
+ * }).createMachine({
1082
+ * id: "fix-test",
1083
+ * context: ({ input }) => input,
1084
+ * on: { "workspace.lost": ".failed", "agent.fault": ".failed" },
1085
+ * initial: "coding",
1086
+ * states: {
1087
+ * coding: {
1088
+ * invoke: { src: "coder", input: { prompt: "Fix the failing test, and commit the fix." } },
1089
+ * // The Agent moves the Machine on with a pick. An Agent's actor never ends, so `onDone`
1090
+ * // never fires.
1091
+ * on: { committed: "publishing" },
1092
+ * },
1093
+ * publishing: {
1094
+ * invoke: {
1095
+ * src: "publish",
1096
+ * onDone: [
1097
+ * { guard: ({ event }) => event.output.outcome === "pushed", target: "done" },
1098
+ * { target: "failed" },
1099
+ * ],
1100
+ * },
1101
+ * },
1102
+ * done: { type: "final" },
1103
+ * // Not a final state: the run ends `failed`, and the Workspace deletes the Sandbox.
1104
+ * failed: { invoke: { src: "fail", input: { reason: "the fix was not pushed" } } },
1105
+ * },
1106
+ * });
1107
+ *
1108
+ * export const machine = workspace(body, {
1109
+ * repos: { app: { url: "https://github.com/you/app.git" } },
1110
+ * spec: () => ({ branch: "fix-test" }),
1111
+ * });
1112
+ * ```
1113
+ *
1114
+ * @see docs/guides/push-and-sign-a-branch.md
1115
+ * @see docs/concepts/effects.md
1116
+ * @see docs/guides/give-the-cluster-git-credentials.md
1117
+ * @see docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md
1118
+ * @see docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md
886
1119
  */
887
1120
  export function push<const O extends PushOptions>(options?: O): EffectLogic<PushInput<O> | void, PushOutput> {
1121
+ // After an Orchestrator restart, a push is sent again with only the commit its intent row holds
1122
+ // (ADR-0067): a run is a function of the commit, never of the Agent's tip now.
888
1123
  return pushWith(sandboxEffectorPort, options);
889
1124
  }
890
1125
 
@@ -1394,7 +1394,9 @@ export function kubeSandbox(opts: KubeSandboxOptions = {}): SandboxPort {
1394
1394
  const endpoint = sandbox?.status?.endpoint;
1395
1395
  if (!endpoint) throw new Error(`Sandbox "${req.name}" has no endpoint to attach through — is it gone?`);
1396
1396
  if (req.repos.length === 0) {
1397
- throw new Error("the attach names no Repo Slot — nothing to attach (a workspace() declares at least one)");
1397
+ throw new Error(
1398
+ "the attach names no Repo Slot — nothing to attach (a Workspace with no Repo Slot skips the attach, ADR-0051)",
1399
+ );
1398
1400
  }
1399
1401
  const body: AttachRequest = {
1400
1402
  slots: req.repos.map((r) => {
@@ -6,7 +6,8 @@
6
6
  // - the Instance Harness: the model keys of the `"none"` Agents it hosts, and nothing else (they
7
7
  // cannot execute code, ADR-0031, so nothing else is theirs to use);
8
8
  // - an Effector: what its Effects name with `secret("name")` — the Instance Effector what its
9
- // `http` Effects name; a Workspace's Sandbox Effector what its file-body `http`s and
9
+ // `http` Effects name and the token of each `dispatch` with a target (ADR-0071; a dispatch with
10
+ // no target runs in the Orchestrator's own process and names nothing); a Workspace's Sandbox Effector what its file-body `http`s and
10
11
  // credentialed `command`s name. The push credential its `push`es spend is not placed here: the provision
11
12
  // names the entry the slot's url matches on the Sandbox, by reference (git-push.ts, ADR-0051).
12
13
  //
@@ -27,6 +28,7 @@ import {
27
28
  type HeldLookups,
28
29
  } from "./held-secrets.ts";
29
30
  import type { JR2Config } from "./config.ts";
31
+ import { needsASlot } from "./effect.ts";
30
32
  import { declaredPushKinds, pushRefusal } from "./git-push.ts";
31
33
  import { INSTANCE_HARNESS_HELD_POD } from "./names.ts";
32
34
  import { routeText, type CarriedEffect, type WorkspacePod } from "./parts.ts";
@@ -140,7 +142,29 @@ export function refuseEffectSecrets(config: JR2Config, walk: PlacementWalk): voi
140
142
  // An empty value is refused later with the list's own rule (`sourceOf`): it is an unset variable.
141
143
  if (namedBy.has(e.name) && e.value) shortRefusal(e, e.value.length);
142
144
  }
143
- for (const ws of walk.workspaces) pushesOf(config, ws);
145
+ for (const ws of walk.workspaces) {
146
+ refuseWithNoSlot(ws);
147
+ pushesOf(config, ws);
148
+ }
149
+ }
150
+
151
+ /**
152
+ * A Workspace with no Repo Slot (ADR-0051) has no branch and no commit, so the two Sandbox Effector kinds that
153
+ * run on a commit of a slot's branch have none: a `push`, and a `command` with an Effect secret
154
+ * (ADR-0067). Refused here, before anything is built, as the invoke would refuse them mid-run. A
155
+ * file-body `http` reads `/work` and stays.
156
+ */
157
+ function refuseWithNoSlot(ws: PlacedWorkspace): void {
158
+ if (!ws.pod.noSlot) return;
159
+ for (const effect of ws.pod.effects) {
160
+ // Every `command` placed in a Sandbox Effector names a secret: the others run beside the Agent.
161
+ const why = needsASlot(effect.kind);
162
+ if (why === undefined) continue;
163
+ throw new Error(
164
+ `workflow "${ws.workflow}": ${effect.kind} "${effect.name}" runs in ${workspaceLabel(ws)}, which has no Repo ` +
165
+ `Slot: ${why} (ADR-0051)`,
166
+ );
167
+ }
144
168
  }
145
169
 
146
170
  /**
@@ -153,7 +177,7 @@ export function secretPlacementsOf(
153
177
  walk: PlacementWalk,
154
178
  lookups: Pick<HeldLookups, "secretKeys" | "valueLength"> = {},
155
179
  ): SecretPlacements {
156
- const { entries, namedBy, http } = namesOf(config, walk);
180
+ const { entries, namedBy, instancePlaced } = namesOf(config, walk);
157
181
  const { pods, redundant } = heldPodsOf(config, walk, new Set(entries.map((e) => e.name)));
158
182
  const claimed = new Set(pods.flatMap((p) => p.names));
159
183
 
@@ -207,12 +231,12 @@ export function secretPlacementsOf(
207
231
  report.push(`${label}: secrets lists ${name}, which its Agents' provider claims already — redundant`);
208
232
  }
209
233
 
210
- const instanceSecrets = new Set(http.flatMap((p) => p.effect.secrets));
234
+ const instanceSecrets = new Set(instancePlaced.flatMap((p) => p.effect.secrets));
211
235
  const instance =
212
- http.length === 0
236
+ instancePlaced.length === 0
213
237
  ? undefined
214
238
  : effectorPlan(
215
- http,
239
+ instancePlaced,
216
240
  secrets.filter((s) => instanceSecrets.has(s.name)),
217
241
  values,
218
242
  );
@@ -267,6 +291,7 @@ function sandboxPlanOf(
267
291
  x.scheme === t.scheme && x.host === t.host && x.port === t.port && x.secrets.join() === t.secrets.join();
268
292
  if (!targets.some(same)) targets.push(t);
269
293
  }
294
+ refuseWithNoSlot(ws);
270
295
  const pushes = pushesOf(config, ws);
271
296
  const commands = ws.pod.effects.some((e) => e.kind === "command" && (e.secrets?.length ?? 0) > 0);
272
297
  return {
@@ -322,10 +347,12 @@ function pushesOf(config: JR2Config, ws: PlacedWorkspace): SandboxPush[] {
322
347
  return out;
323
348
  }
324
349
 
325
- function effectorPlan(http: HttpPlaced[], secrets: EffectSecret[], values: Record<string, string>): EffectorPlan {
350
+ function effectorPlan(placed: InstancePlaced[], secrets: EffectSecret[], values: Record<string, string>): EffectorPlan {
326
351
  const targets: EffectorTargetConfig[] = [];
327
- for (const { effect } of http) {
328
- // An Open url has no target, so it allows nothing: `jr2 up` refuses it before this (ADR-0054).
352
+ for (const { effect } of placed) {
353
+ // An Open url or an Open dispatch target has no target, so it allows nothing: `jr2 up` refuses
354
+ // it before this (ADR-0054). A dispatch's target is its origin, its one secret the token
355
+ // (ADR-0071).
329
356
  if (effect.target === undefined) continue;
330
357
  const t = { ...effect.target, secrets: [...effect.secrets].sort() };
331
358
  const same = (x: EffectorTargetConfig) =>
@@ -337,12 +364,17 @@ function effectorPlan(http: HttpPlaced[], secrets: EffectSecret[], values: Recor
337
364
  secrets,
338
365
  values,
339
366
  targets,
340
- longestTimeoutMs: Math.max(...http.map((p) => p.effect.timeoutMs)),
341
- effects: http.map((p) => `${p.workflow}/${p.effect.name}`),
367
+ // The grace a rollout gives a replica covers the http Effects alone (ADR-0071): a dispatch is
368
+ // idempotent by construction, so a `wait` a rollout cuts is sent again under its key and its
369
+ // poll resumes, and a wait of hours never stretches the Effector's grace.
370
+ longestTimeoutMs: Math.max(0, ...placed.flatMap((p) => (p.effect.kind === "http" ? [p.effect.timeoutMs] : []))),
371
+ effects: placed.map((p) => `${p.workflow}/${p.effect.name}`),
342
372
  };
343
373
  }
344
374
 
345
- type HttpPlaced = { workflow: string; effect: Extract<CarriedEffect, { method: string }> };
375
+ /** An Effect the Instance Effector runs: an http one with no Worktree file, or a dispatch with a
376
+ * target (ADR-0068, ADR-0071). */
377
+ type InstancePlaced = { workflow: string; effect: Extract<CarriedEffect, { kind: "http" | "dispatch" }> };
346
378
 
347
379
  /** A Workspace as a human reads it: its workflow and the placement route to it, an inline hop as
348
380
  * `invoked inline in state <id>`. */
@@ -423,11 +455,14 @@ function namesOf(config: JR2Config, walk: PlacementWalk) {
423
455
  }
424
456
  byName.set(e.name, e);
425
457
  }
426
- // Every Effect that names a secret names it, whatever its kind: an `http` one in the Instance
427
- // Effector, a file-body `http` and a credentialed `command` in their Workspace's Sandbox Effector.
458
+ // Every Effect that names a secret names it, whatever its kind: an `http` one and a `dispatch`'s
459
+ // token in the Instance Effector, a file-body `http` and a credentialed `command` in their
460
+ // Workspace's Sandbox Effector. A dispatch with no target is placed "orchestrator" and names none
461
+ // (ADR-0071).
428
462
  const namedBy = new Map<string, Array<{ label: string; where: string }>>();
429
- const http = walk.effects.filter(
430
- (p): p is HttpPlaced => p.effect.kind === "http" && p.effect.placement === "instance",
463
+ const instancePlaced = walk.effects.filter(
464
+ (p): p is InstancePlaced =>
465
+ (p.effect.kind === "http" || p.effect.kind === "dispatch") && p.effect.placement === "instance",
431
466
  );
432
467
  // Keyed by the Effect's placement route and slot name, as workspacePodsOf keys a pod's Effects:
433
468
  // two slots of one name in one workflow — one in a workspace(), one outside it — are two Effects,
@@ -463,7 +498,7 @@ function namesOf(config: JR2Config, walk: PlacementWalk) {
463
498
  namedBy.set(name, [...(namedBy.get(name) ?? []), { label, where }]);
464
499
  }
465
500
  }
466
- return { entries, namedBy, http };
501
+ return { entries, namedBy, instancePlaced };
467
502
  }
468
503
 
469
504
  /** An Effect secret shorter than 8 characters is refused, and the refusal says why. */
package/src/server.ts CHANGED
@@ -295,6 +295,9 @@ export async function serverMain(opts: ServerMainOptions): Promise<RunningInstan
295
295
  instanceHarness: namespace
296
296
  ? `http://${INSTANCE_HARNESS_SERVICE}.${namespace}.svc:${INSTANCE_HARNESS_PORT}`
297
297
  : undefined,
298
+ // What a foreign dispatch names this Orchestrator by at the callee's door (ADR-0071): its
299
+ // namespace, which is stable and not secret — never a run's Instance ID.
300
+ ...(namespace ? { orchestratorName: namespace } : {}),
298
301
  // Where an http Effect runs (ADR-0068): the Instance Effector's headless Service. `jr2 up`
299
302
  // converges it whenever a registered Machine carries an http Effect.
300
303
  ...(namespace ? { instanceEffector: `${INSTANCE_EFFECTOR}.${namespace}.svc:${EFFECTOR_PORT}` } : {}),