@jr2/orchestrator 0.3.0 → 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 (354) hide show
  1. package/README.md +20 -2
  2. package/bin/server.ts +7 -4
  3. package/console/canvas.ts +27 -14
  4. package/console/components/app.ts +12 -1
  5. package/console/components/fleet.ts +52 -6
  6. package/console/components/machine-pane.ts +128 -2
  7. package/console/components/nav.ts +1 -1
  8. package/console/components/schema-form.ts +12 -4
  9. package/console/json-field.ts +23 -0
  10. package/console/main.ts +122 -39
  11. package/console/store.ts +321 -21
  12. package/console/style.css +111 -0
  13. package/docs/agents-block.md +5 -0
  14. package/docs/concepts/agents-and-turns.md +442 -0
  15. package/docs/concepts/architecture.md +230 -0
  16. package/docs/concepts/charts.md +143 -0
  17. package/docs/concepts/composition.md +357 -0
  18. package/docs/concepts/converge.md +254 -0
  19. package/docs/concepts/durable-runs.md +189 -0
  20. package/docs/concepts/effects.md +603 -0
  21. package/docs/concepts/gates.md +208 -0
  22. package/docs/concepts/machines-and-workflows.md +289 -0
  23. package/docs/concepts/pools-and-sources.md +230 -0
  24. package/docs/concepts/secrets-and-trust.md +222 -0
  25. package/docs/concepts/the-menu.md +378 -0
  26. package/docs/concepts/workspaces.md +678 -0
  27. package/docs/get-started/a-model-decides.md +563 -0
  28. package/docs/get-started/an-agent-in-a-sandbox.md +381 -0
  29. package/docs/get-started/connect-a-model.md +248 -0
  30. package/docs/get-started/create-an-instance.md +194 -0
  31. package/docs/get-started/have-your-assistant-write-a-machine.md +922 -0
  32. package/docs/get-started/install.md +126 -0
  33. package/docs/guides/add-tools-to-the-sandbox.md +295 -0
  34. package/docs/guides/answer-a-gate.md +304 -0
  35. package/docs/guides/bind-open-parts.md +223 -0
  36. package/docs/guides/cancel-a-run.md +146 -0
  37. package/docs/guides/clean-up.md +206 -0
  38. package/docs/guides/fix-a-failed-converge.md +361 -0
  39. package/docs/guides/give-the-cluster-git-credentials.md +222 -0
  40. package/docs/guides/iterate-on-a-machine-on-kind.md +190 -0
  41. package/docs/guides/let-an-agent-write-a-chart.md +226 -0
  42. package/docs/guides/look-inside-a-sandbox.md +174 -0
  43. package/docs/guides/mirror-the-kit-images.md +176 -0
  44. package/docs/guides/move-from-kind-to-a-shared-cluster.md +302 -0
  45. package/docs/guides/open-the-console.md +91 -0
  46. package/docs/guides/publish-a-machine-as-an-npm-package.md +350 -0
  47. package/docs/guides/push-and-sign-a-branch.md +486 -0
  48. package/docs/guides/run-a-chart-by-name.md +270 -0
  49. package/docs/guides/run-a-command-or-call-a-url.md +544 -0
  50. package/docs/guides/run-one-worker-per-item.md +321 -0
  51. package/docs/guides/set-an-agents-access-and-dials.md +375 -0
  52. package/docs/guides/size-a-workspace.md +204 -0
  53. package/docs/guides/start-a-run-from-a-run.md +237 -0
  54. package/docs/guides/start-a-run.md +207 -0
  55. package/docs/guides/upgrade-the-kit-version.md +240 -0
  56. package/docs/guides/use-a-hosted-provider.md +208 -0
  57. package/docs/guides/use-a-local-model.md +313 -0
  58. package/docs/guides/watch-a-run.md +326 -0
  59. package/docs/guides/write-a-workflow.md +318 -0
  60. package/docs/index.md +44 -0
  61. package/docs/llms.txt +216 -0
  62. package/docs/reference/authoring-surface/agent.md +128 -0
  63. package/docs/reference/authoring-surface/answer.md +140 -0
  64. package/docs/reference/authoring-surface/arg.md +60 -0
  65. package/docs/reference/authoring-surface/basic.md +62 -0
  66. package/docs/reference/authoring-surface/bearer.md +53 -0
  67. package/docs/reference/authoring-surface/chartschema.md +23 -0
  68. package/docs/reference/authoring-surface/chartschemafor.md +48 -0
  69. package/docs/reference/authoring-surface/command.md +191 -0
  70. package/docs/reference/authoring-surface/customize.md +64 -0
  71. package/docs/reference/authoring-surface/defineconfig.md +41 -0
  72. package/docs/reference/authoring-surface/defineevent.md +114 -0
  73. package/docs/reference/authoring-surface/dispatch.md +126 -0
  74. package/docs/reference/authoring-surface/fail.md +83 -0
  75. package/docs/reference/authoring-surface/file.md +122 -0
  76. package/docs/reference/authoring-surface/frompick.md +277 -0
  77. package/docs/reference/authoring-surface/http.md +159 -0
  78. package/docs/reference/authoring-surface/isfailure.md +58 -0
  79. package/docs/reference/authoring-surface/jr2setup.md +237 -0
  80. package/docs/reference/authoring-surface/open.md +74 -0
  81. package/docs/reference/authoring-surface/openbody.md +86 -0
  82. package/docs/reference/authoring-surface/paramsof.md +125 -0
  83. package/docs/reference/authoring-surface/pool.md +186 -0
  84. package/docs/reference/authoring-surface/push.md +112 -0
  85. package/docs/reference/authoring-surface/secret.md +118 -0
  86. package/docs/reference/authoring-surface/sentby.md +83 -0
  87. package/docs/reference/authoring-surface/source.md +51 -0
  88. package/docs/reference/authoring-surface/text.md +62 -0
  89. package/docs/reference/authoring-surface/type-agentdeclaration.md +27 -0
  90. package/docs/reference/authoring-surface/type-agentdefinition.md +80 -0
  91. package/docs/reference/authoring-surface/type-agentlogic.md +15 -0
  92. package/docs/reference/authoring-surface/type-agentslots.md +12 -0
  93. package/docs/reference/authoring-surface/type-answeraction.md +12 -0
  94. package/docs/reference/authoring-surface/type-arg.md +19 -0
  95. package/docs/reference/authoring-surface/type-argnames.md +12 -0
  96. package/docs/reference/authoring-surface/type-binding.md +62 -0
  97. package/docs/reference/authoring-surface/type-bindingof.md +12 -0
  98. package/docs/reference/authoring-surface/type-bodyacceptsdoor.md +15 -0
  99. package/docs/reference/authoring-surface/type-bodyacceptsslots.md +17 -0
  100. package/docs/reference/authoring-surface/type-ceilings.md +41 -0
  101. package/docs/reference/authoring-surface/type-chainlink.md +19 -0
  102. package/docs/reference/authoring-surface/type-chartaction.md +46 -0
  103. package/docs/reference/authoring-surface/type-chartagent.md +28 -0
  104. package/docs/reference/authoring-surface/type-chartevent.md +20 -0
  105. package/docs/reference/authoring-surface/type-chartguard.md +20 -0
  106. package/docs/reference/authoring-surface/type-chartinvoke.md +28 -0
  107. package/docs/reference/authoring-surface/type-chartjson.md +14 -0
  108. package/docs/reference/authoring-surface/type-chartstate.md +46 -0
  109. package/docs/reference/authoring-surface/type-charttext.md +40 -0
  110. package/docs/reference/authoring-surface/type-charttransition.md +22 -0
  111. package/docs/reference/authoring-surface/type-charttransitions.md +14 -0
  112. package/docs/reference/authoring-surface/type-chartvalue.md +15 -0
  113. package/docs/reference/authoring-surface/type-childat.md +12 -0
  114. package/docs/reference/authoring-surface/type-childslots.md +12 -0
  115. package/docs/reference/authoring-surface/type-commandenv.md +15 -0
  116. package/docs/reference/authoring-surface/type-commandinput.md +19 -0
  117. package/docs/reference/authoring-surface/type-commandoptions.md +78 -0
  118. package/docs/reference/authoring-surface/type-commandoutput.md +104 -0
  119. package/docs/reference/authoring-surface/type-customize.md +109 -0
  120. package/docs/reference/authoring-surface/type-dispatchdefinition.md +39 -0
  121. package/docs/reference/authoring-surface/type-dispatchinput.md +14 -0
  122. package/docs/reference/authoring-surface/type-dispatchopenparts.md +14 -0
  123. package/docs/reference/authoring-surface/type-dispatchoutput.md +65 -0
  124. package/docs/reference/authoring-surface/type-duration.md +15 -0
  125. package/docs/reference/authoring-surface/type-effectbinding.md +93 -0
  126. package/docs/reference/authoring-surface/type-effectlogic.md +12 -0
  127. package/docs/reference/authoring-surface/type-envopenparts.md +14 -0
  128. package/docs/reference/authoring-surface/type-eventaudience.md +27 -0
  129. package/docs/reference/authoring-surface/type-eventdef.md +45 -0
  130. package/docs/reference/authoring-surface/type-eventfrom.md +16 -0
  131. package/docs/reference/authoring-surface/type-eventsemantics.md +26 -0
  132. package/docs/reference/authoring-surface/type-execoutcome.md +34 -0
  133. package/docs/reference/authoring-surface/type-failslot.md +14 -0
  134. package/docs/reference/authoring-surface/type-failure.md +19 -0
  135. package/docs/reference/authoring-surface/type-failureinput.md +20 -0
  136. package/docs/reference/authoring-surface/type-filebody.md +41 -0
  137. package/docs/reference/authoring-surface/type-frominput.md +14 -0
  138. package/docs/reference/authoring-surface/type-hostinjectedinput.md +58 -0
  139. package/docs/reference/authoring-surface/type-httpdefinition.md +101 -0
  140. package/docs/reference/authoring-surface/type-httpinput.md +12 -0
  141. package/docs/reference/authoring-surface/type-httpmethod.md +14 -0
  142. package/docs/reference/authoring-surface/type-httpopenparts.md +12 -0
  143. package/docs/reference/authoring-surface/type-httpoutput.md +89 -0
  144. package/docs/reference/authoring-surface/type-httprequestparts.md +33 -0
  145. package/docs/reference/authoring-surface/type-jr2config.md +126 -0
  146. package/docs/reference/authoring-surface/type-lends.md +38 -0
  147. package/docs/reference/authoring-surface/type-logicat.md +12 -0
  148. package/docs/reference/authoring-surface/type-markedif.md +12 -0
  149. package/docs/reference/authoring-surface/type-mergedactors.md +12 -0
  150. package/docs/reference/authoring-surface/type-namedsecret.md +25 -0
  151. package/docs/reference/authoring-surface/type-noslotspec.md +26 -0
  152. package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +32 -0
  153. package/docs/reference/authoring-surface/type-openbodydeclaration.md +40 -0
  154. package/docs/reference/authoring-surface/type-openbodylogic.md +14 -0
  155. package/docs/reference/authoring-surface/type-openbodyoutput.md +42 -0
  156. package/docs/reference/authoring-surface/type-openbodyslot.md +18 -0
  157. package/docs/reference/authoring-surface/type-opencommandinput.md +17 -0
  158. package/docs/reference/authoring-surface/type-openeffect.md +12 -0
  159. package/docs/reference/authoring-surface/type-openeffectslots.md +12 -0
  160. package/docs/reference/authoring-surface/type-openof.md +12 -0
  161. package/docs/reference/authoring-surface/type-opensecret.md +23 -0
  162. package/docs/reference/authoring-surface/type-opensecretmark.md +12 -0
  163. package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +31 -0
  164. package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +32 -0
  165. package/docs/reference/authoring-surface/type-poolmachine.md +28 -0
  166. package/docs/reference/authoring-surface/type-poolspec.md +60 -0
  167. package/docs/reference/authoring-surface/type-pushinput.md +15 -0
  168. package/docs/reference/authoring-surface/type-pushoptions.md +79 -0
  169. package/docs/reference/authoring-surface/type-pushoutput.md +124 -0
  170. package/docs/reference/authoring-surface/type-pushwireoutput.md +12 -0
  171. package/docs/reference/authoring-surface/type-quantity.md +14 -0
  172. package/docs/reference/authoring-surface/type-reached.md +12 -0
  173. package/docs/reference/authoring-surface/type-reposlot.md +70 -0
  174. package/docs/reference/authoring-surface/type-reposlotsof.md +16 -0
  175. package/docs/reference/authoring-surface/type-sandboxoptions.md +77 -0
  176. package/docs/reference/authoring-surface/type-secret.md +25 -0
  177. package/docs/reference/authoring-surface/type-sender.md +19 -0
  178. package/docs/reference/authoring-surface/type-sentbyguards.md +12 -0
  179. package/docs/reference/authoring-surface/type-size.md +85 -0
  180. package/docs/reference/authoring-surface/type-slothandles.md +32 -0
  181. package/docs/reference/authoring-surface/type-sourcenextresult.md +17 -0
  182. package/docs/reference/authoring-surface/type-sourcespec.md +44 -0
  183. package/docs/reference/authoring-surface/type-syncstate.md +12 -0
  184. package/docs/reference/authoring-surface/type-text.md +19 -0
  185. package/docs/reference/authoring-surface/type-textpart.md +24 -0
  186. package/docs/reference/authoring-surface/type-thinkinglevel.md +39 -0
  187. package/docs/reference/authoring-surface/type-toparameterizedobject.md +14 -0
  188. package/docs/reference/authoring-surface/type-toprovidedactor.md +14 -0
  189. package/docs/reference/authoring-surface/type-unknown.md +21 -0
  190. package/docs/reference/authoring-surface/type-usercontainer.md +77 -0
  191. package/docs/reference/authoring-surface/type-withoutemptyindex.md +20 -0
  192. package/docs/reference/authoring-surface/type-workflowevent.md +46 -0
  193. package/docs/reference/authoring-surface/type-workspaceaccess.md +16 -0
  194. package/docs/reference/authoring-surface/type-workspaced.md +94 -0
  195. package/docs/reference/authoring-surface/type-workspacehandles.md +23 -0
  196. package/docs/reference/authoring-surface/type-workspacemachine.md +29 -0
  197. package/docs/reference/authoring-surface/type-workspaceof.md +13 -0
  198. package/docs/reference/authoring-surface/type-workspaceoptions.md +39 -0
  199. package/docs/reference/authoring-surface/type-workspacespec.md +37 -0
  200. package/docs/reference/authoring-surface/workspace.md +259 -0
  201. package/docs/reference/authoring-surface.md +197 -0
  202. package/docs/reference/cli.md +433 -0
  203. package/docs/reference/config.md +756 -0
  204. package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +113 -0
  205. package/docs/reference/decisions/0002-duplex-streaming-actor.md +110 -0
  206. package/docs/reference/decisions/0004-worktree-provisioning-model.md +124 -0
  207. package/docs/reference/decisions/0005-sandbox-pod-composition.md +230 -0
  208. package/docs/reference/decisions/0006-agent-control-surface.md +47 -0
  209. package/docs/reference/decisions/0007-durable-machine-state.md +109 -0
  210. package/docs/reference/decisions/0008-orchestrator-as-deployed-app.md +60 -0
  211. package/docs/reference/decisions/0009-cli-and-instance-interface.md +275 -0
  212. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +160 -0
  213. package/docs/reference/decisions/0011-workflow-defined-events.md +141 -0
  214. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +103 -0
  215. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +157 -0
  216. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +95 -0
  217. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +265 -0
  218. package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +124 -0
  219. package/docs/reference/decisions/0017-pool-over-a-source-port.md +69 -0
  220. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +111 -0
  221. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +275 -0
  222. package/docs/reference/decisions/0020-private-ca-trust-is-a-named-concept.md +60 -0
  223. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +100 -0
  224. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +103 -0
  225. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +176 -0
  226. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +439 -0
  227. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +65 -0
  228. package/docs/reference/decisions/0026-a-turn-that-is-over-is-never-prompted.md +62 -0
  229. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +237 -0
  230. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +89 -0
  231. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +154 -0
  232. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +100 -0
  233. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +190 -0
  234. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +61 -0
  235. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +127 -0
  236. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +57 -0
  237. package/docs/reference/decisions/0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md +92 -0
  238. package/docs/reference/decisions/0036-a-turn-compacts-its-own-context-mid-flight.md +138 -0
  239. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +196 -0
  240. package/docs/reference/decisions/0038-jr2-up-builds-every-image-it-deploys.md +170 -0
  241. package/docs/reference/decisions/0039-image-garbage-collects-by-reachability.md +115 -0
  242. package/docs/reference/decisions/0041-a-build-the-host-already-holds-is-not-spent-again.md +64 -0
  243. package/docs/reference/decisions/0042-ready-is-not-routable-an-admission-retries-its-connection.md +142 -0
  244. package/docs/reference/decisions/0043-the-kit-is-tested-as-installed-a-local-registry-stands-in-for-npm.md +170 -0
  245. package/docs/reference/decisions/0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md +85 -0
  246. package/docs/reference/decisions/0045-the-platform-joins-the-image-address-and-the-cluster-chooses-it.md +82 -0
  247. package/docs/reference/decisions/0046-a-converge-failure-carries-its-diagnosis.md +34 -0
  248. package/docs/reference/decisions/0047-the-git-ssh-key-source-is-the-users-choice.md +47 -0
  249. package/docs/reference/decisions/0048-the-orchestrator-boots-without-its-repos.md +36 -0
  250. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +126 -0
  251. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +73 -0
  252. package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +227 -0
  253. package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +81 -0
  254. package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +111 -0
  255. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +126 -0
  256. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +107 -0
  257. package/docs/reference/decisions/0056-a-global-jr2-hands-off-to-the-instances-own-and-an-instance-has-one-kit-version.md +62 -0
  258. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +152 -0
  259. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +106 -0
  260. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +384 -0
  261. package/docs/reference/decisions/0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md +111 -0
  262. package/docs/reference/decisions/0061-a-memory-kill-ends-the-agents-processes-not-the-harness.md +83 -0
  263. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +212 -0
  264. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +83 -0
  265. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +79 -0
  266. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +65 -0
  267. 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
  268. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +430 -0
  269. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +464 -0
  270. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +180 -0
  271. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +323 -0
  272. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +183 -0
  273. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +144 -0
  274. package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +122 -0
  275. package/docs/reference/decisions/0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md +76 -0
  276. package/docs/reference/decisions.md +83 -0
  277. package/docs/reference/glossary.md +648 -0
  278. package/docs/reference/http-api.md +1348 -0
  279. package/docs/reference/machines.md +190 -0
  280. package/docs/reference/mechanism-events.md +72 -0
  281. package/docs/why.md +206 -0
  282. package/package.json +6 -4
  283. package/src/actor.ts +976 -61
  284. package/src/agent.ts +114 -38
  285. package/src/ambient.ts +4 -1
  286. package/src/assembly-checks.ts +347 -0
  287. package/src/assembly-tar.ts +494 -0
  288. package/src/assembly.ts +721 -0
  289. package/src/cause.ts +234 -0
  290. package/src/chart.ts +2182 -0
  291. package/src/charts.ts +748 -0
  292. package/src/config.ts +520 -162
  293. package/src/content-hash.ts +105 -0
  294. package/src/custodian.lua +6 -5
  295. package/src/custodian.ts +143 -39
  296. package/src/customize.ts +388 -55
  297. package/src/dispatch-effect.ts +945 -0
  298. package/src/door-compat.ts +217 -0
  299. package/src/durability.ts +11 -4
  300. package/src/effect-checks.ts +557 -0
  301. package/src/effect.ts +1202 -0
  302. package/src/effector-wire.ts +674 -0
  303. package/src/fail.ts +209 -0
  304. package/src/fingerprint.ts +4 -3
  305. package/src/gate.ts +8 -0
  306. package/src/git-clone.ts +65 -0
  307. package/src/git-push.ts +336 -0
  308. package/src/harness-client.ts +764 -48
  309. package/src/held-secrets.ts +82 -26
  310. package/src/holds.ts +156 -0
  311. package/src/http-effect.ts +1423 -0
  312. package/src/http.ts +916 -113
  313. package/src/images.ts +47 -21
  314. package/src/index.ts +62 -43
  315. package/src/instance.ts +505 -59
  316. package/src/internal.ts +84 -0
  317. package/src/kube-client.ts +333 -0
  318. package/src/machine-doc.ts +106 -6
  319. package/src/mechanism-routes.ts +168 -0
  320. package/src/model-keys.generated.ts +53 -0
  321. package/src/model-keys.ts +18 -58
  322. package/src/mount-digest.ts +53 -0
  323. package/src/names.ts +66 -8
  324. package/src/notices.ts +141 -0
  325. package/src/open-body.ts +1580 -0
  326. package/src/open-effect.ts +217 -0
  327. package/src/open.ts +59 -6
  328. package/src/parts.ts +709 -41
  329. package/src/party.ts +534 -0
  330. package/src/pick.ts +761 -0
  331. package/src/pool.ts +248 -37
  332. package/src/registration.ts +382 -19
  333. package/src/repo-fetch.ts +60 -85
  334. package/src/repos.ts +65 -75
  335. package/src/rest.ts +133 -0
  336. package/src/run-host.ts +2383 -115
  337. package/src/run-output.ts +38 -0
  338. package/src/sandbox-effect.ts +1126 -0
  339. package/src/sandbox-effector.ts +111 -0
  340. package/src/sandbox-kube.ts +1721 -0
  341. package/src/sandbox-watch.ts +260 -0
  342. package/src/secret-placement.ts +529 -0
  343. package/src/server.ts +284 -84
  344. package/src/setup.ts +819 -117
  345. package/src/size.ts +454 -0
  346. package/src/snapshot-store.ts +115 -20
  347. package/src/stub-harness.ts +49 -16
  348. package/src/sync-client.ts +54 -0
  349. package/src/timers.ts +191 -0
  350. package/src/tokens.ts +35 -4
  351. package/src/vocabulary.ts +135 -15
  352. package/src/wire.ts +238 -10
  353. package/src/workspace.ts +1195 -338
  354. package/src/sandbox-kubectl.ts +0 -1108
package/src/parts.ts CHANGED
@@ -16,8 +16,9 @@
16
16
  // Instance Harness converges when any of them declares `workspace: "none"`, every `file:`
17
17
  // image context a Machine ships is built and content-tagged, every BOUND Repo is known before
18
18
  // a run can ask for it, an OPEN part — a Repo Slot with no url, an Agent with no model
19
- // (ADR-0054) — is refused before anything is built, and whether any registered Machine
20
- // composes a Sandbox at all is the data-plane switch. None of these can read
19
+ // (ADR-0054) — is refused before anything is built, every Size a `workspace()` states is split
20
+ // and held to the Harness floor (ADR-0060), and whether any registered Machine composes a
21
+ // Sandbox at all is the data-plane switch. None of these can read
21
22
  // an invoke's `input` (it is a function — dials are not statically recoverable), and none
22
23
  // needs to: identity lives in the definition, the image and the slots are options, and all are
23
24
  // values ON the Machine. A per-run slot is a function too, and the walk reports nothing for
@@ -33,6 +34,13 @@
33
34
  // xstate rewrites to a generated key and keeps only on the raw config node. Nothing jr2 owns
34
35
  // arrives this way any more; the walk keeps it because an AUTHOR may still write one.
35
36
  //
37
+ // A PART written inline is refused, not walked (`refuseInlineParts`): an `agent()`, an Effect
38
+ // (ADR-0067) or a `workspace()` on an `invoke.src` has no author-written name. xstate's generated
39
+ // key (`xstate.invoke.<n>.<state id>`) moves when a state is renamed, so the ledger, the Console doc
40
+ // and `jr2 up`'s report would name it by a number, and `customize()` — xstate's `provide`, keyed on
41
+ // `actors` — could not reach it at all. A slot key is the one name every reader shares (ADR-0049),
42
+ // so a Part is a key of `actors`, and the refusal says how to write it.
43
+ //
36
44
  // Deliberately NOT a `provide()`-aware read: the walk runs on the registered Machine, which is the
37
45
  // object the run will start, so a `customize()` clone is walked as itself and a test seam's fake
38
46
  // (unbranded) contributes nothing.
@@ -41,9 +49,16 @@ import { basename } from "node:path";
41
49
  import { fileURLToPath } from "node:url";
42
50
  import type { AnyActorRef, AnyStateMachine, StateNode, UnknownActorLogic } from "xstate";
43
51
  import { isAgent, isOpenAgent, type AgentDeclaration } from "./agent.ts";
52
+ import { argvText, isEffect, placementOf, type EffectParts, type Placement } from "./effect.ts";
53
+ import { effectBindingLine, openPartsOf, type OpenEffectPart } from "./open-effect.ts";
54
+ import { isFailSlot } from "./fail.ts";
44
55
  import { isImageContext } from "./images.ts";
56
+ import { isOpenBody, openBodyDeclarationOf, type OpenBodyDeclaration } from "./open-body.ts";
45
57
  import { open } from "./open.ts";
46
58
  import { repoIdentity } from "./repo-identity.ts";
59
+ import { refuseUnroutedAgentFault } from "./mechanism-routes.ts";
60
+ import { refuseLostOutput } from "./run-output.ts";
61
+ import { userSeatOf, type Size, type UserContainer } from "./size.ts";
47
62
 
48
63
  // --- Repo Slots (ADR-0051) ---------------------------------------------------------------------
49
64
  // A `workspace()` names each Repo it attaches under a SLOT — the Machine's own word for it, the
@@ -51,7 +66,7 @@ import { repoIdentity } from "./repo-identity.ts";
51
66
  // is one of three states, and the walk tells them apart without evaluating anything. A Machine
52
67
  // whose body names no slot — it enumerates whatever is attached — declares the whole MAP Open
53
68
  // instead (`repos: open`): the composer names every slot, in an order the Machine may give a
54
- // meaning to. Open at the map is the same word as Open at a slot, one level up (CONTEXT.md).
69
+ // meaning to. Open at the map is the same word as Open at a slot, one level up (ADR-0051).
55
70
 
56
71
  // The sentinel itself lives in open.ts, a module with nothing else in it: since ADR-0054 it marks
57
72
  // an Agent's model too, and agent.ts must read it without importing this file (the walk imports
@@ -59,15 +74,103 @@ import { repoIdentity } from "./repo-identity.ts";
59
74
  // meets it first, and `import { open } from "@jr2/orchestrator"` is the only spelling anyone writes.
60
75
  export { open } from "./open.ts";
61
76
 
62
- /** What a Repo Slot resolves to (CONTEXT.md "Binding"): the url, and the base the branch
63
- * Worktree is cut from — absent, the Repo's own default branch. */
64
- export type Binding = { url: string; ref?: string };
77
+ /**
78
+ * The repository of a Repo Slot: `{ url, ref }`. `url` is the Repo's git url. `ref` is the branch
79
+ * or commit that the branch Worktree starts from; absent, the Repo's default branch. Where only a
80
+ * url is needed, a string is enough.
81
+ *
82
+ * `workspace()` and `customize()` refuse:
83
+ * - an empty `url`: ``<where>: Repo Slot "<slot>" is bound to an empty url (got <value>) — a slot
84
+ * is `open`, a url, `{ url, ref? }`, or a mapper `({ input }) => url | { url, ref? }` over the
85
+ * door (ADR-0051).``
86
+ * - an empty `ref`: the same text, with `bound with an empty ref` in place of `bound to an empty
87
+ * url`.
88
+ * - a url that names no Repo, such as a relative path: ``<where>: Repo Slot "<slot>" binds
89
+ * "<url>", which names no Repo — <reason> (ADR-0051).``
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * import { jr2Setup, workspace, type Binding } from "@jr2/orchestrator";
94
+ *
95
+ * const app: Binding = { url: "https://github.com/you/app.git", ref: "release-2" };
96
+ *
97
+ * const body = jr2Setup().createMachine({
98
+ * id: "body",
99
+ * initial: "done",
100
+ * states: { done: { type: "final" } },
101
+ * });
102
+ *
103
+ * export const machine = workspace(body, {
104
+ * repos: { app },
105
+ * spec: () => ({ branch: "jr2/hotfix" }),
106
+ * });
107
+ * ```
108
+ *
109
+ * @see docs/concepts/workspaces.md
110
+ * @see docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md
111
+ */
112
+ // What a Repo Slot resolves to (GLOSSARY.md "Binding").
113
+ export type Binding = {
114
+ /** The Repo's git url. */
115
+ url: string;
116
+ /** The branch or commit that the branch Worktree starts from. Default: the Repo's default
117
+ * branch. */
118
+ ref?: string;
119
+ };
65
120
 
121
+ // Bound and open are what `jr2 up` can see; per-run is the run's business, fenced at attach by
122
+ // `git.credentials` (ADR-0051).
66
123
  /**
67
- * One Repo Slot's value: BOUND (a url, or `{ url, ref? }`), OPEN (the {@link open} sentinel —
68
- * someone downstream binds it), or PER-RUN (a mapper over the wrapper's door, so a run input — a
69
- * ticket field — decides). Bound and open are what `jr2 up` can see; per-run is the run's business,
70
- * fenced at attach by `git.credentials` (ADR-0051).
124
+ * The value of one Repo Slot in `workspace({ repos })`. One of:
125
+ * - a url, or a {@link Binding} `{ url, ref }`: the Machine names the Repo.
126
+ * - {@link open}: the Instance that registers the Machine binds it with `customize()`. `jr2 up`
127
+ * refuses a Workflow that leaves it open: ``refusing: workflow "<name>" leaves Repo Slot
128
+ * "<slot>" open — bind it where the Machine is registered: export const machine =
129
+ * customize(<import>, …) (ADR-0051)``, with the whole `customize` line to paste.
130
+ * - a function of the run input, `({ input }) => url` or a {@link Binding}: each run chooses. The
131
+ * url must match an entry of `git.credentials` in `jr2.config.ts`, or the run cannot use it.
132
+ *
133
+ * The key of the slot is a directory name under `/work`. It starts with a letter, and
134
+ * `workspace()` and `customize()` refuse a key that does not: ``<where>: Repo Slot key "<key>" is
135
+ * not a directory name — a slot becomes `/work/<slot>` and its order matters, so it starts with a
136
+ * letter and matches /^[A-Za-z][A-Za-z0-9._-]*$/ (ADR-0051).`` They refuse a value of another
137
+ * shape: ``<where>: Repo Slot "<slot>" is <what> — a slot is `open`, a url, `{ url, ref? }`, or a
138
+ * mapper `({ input }) => url | { url, ref? }` over the door (ADR-0051).``
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * import { z } from "zod";
143
+ * import { jr2Setup, open, workspace, type RepoSlot, type Workspaced } from "@jr2/orchestrator";
144
+ *
145
+ * const runInput = z.object({ service: z.string() });
146
+ * type RunInput = z.infer<typeof runInput>;
147
+ *
148
+ * const service: RepoSlot<RunInput> = ({ input }) => `https://github.com/you/${input.service}.git`;
149
+ *
150
+ * const body = jr2Setup({
151
+ * types: {} as { input: Workspaced<RunInput, "service" | "docs" | "config"> },
152
+ * }).createMachine({
153
+ * id: "body",
154
+ * initial: "done",
155
+ * states: { done: { type: "final" } },
156
+ * });
157
+ *
158
+ * // A Machine for a package: the Instance that registers it binds `config`.
159
+ * export const deploy = workspace(body, {
160
+ * input: runInput,
161
+ * repos: {
162
+ * service,
163
+ * docs: "https://github.com/you/docs.git",
164
+ * config: open,
165
+ * },
166
+ * spec: ({ input }) => ({ branch: `deploy/${input.service}` }),
167
+ * });
168
+ * ```
169
+ *
170
+ * @see docs/concepts/workspaces.md
171
+ * @see docs/guides/bind-open-parts.md
172
+ * @see docs/guides/give-the-cluster-git-credentials.md
173
+ * @see docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md
71
174
  */
72
175
  export type RepoSlot<TInput = unknown> =
73
176
  | typeof open
@@ -146,11 +249,20 @@ export function assertRepoSlot(where: string, slot: string, value: unknown): ass
146
249
  export type SandboxParts = {
147
250
  /** The Sandbox Image. Absent → the Instance's `images/default`, then the stock Harness. */
148
251
  image?: string;
149
- /** The User Container's image (ADR-0005). Absent → the pod has no third container. */
150
- user?: string;
252
+ /** The User Container (ADR-0005): its image, or `{ image, resources }` when the Machine states
253
+ * its split of the Size (ADR-0060). Absent → the pod has no third container. */
254
+ user?: string | UserContainer;
151
255
  /** The Repo Slots, in declaration order — which the handles keep (ADR-0051) — or
152
256
  * {@link open} for a map the composer fills whole. */
153
257
  repos: Record<string, RepoSlot> | typeof open;
258
+ /** The Size the Machine states (ADR-0060) — the `workspace()`'s, with any `customize()` layered
259
+ * over it. Absent → the Instance's `sandbox.resources`, then the kit default. */
260
+ resources?: Size;
261
+ /** The Held secrets this Workspace's pod gets beside its Agents' model keys (ADR-0059): names of
262
+ * the `secrets` list. Naming is placement — no other pod gets a name for being listed here. */
263
+ secrets?: string[];
264
+ /** The Sandbox Effector's image (ADR-0068). Absent → the Sandbox Image. */
265
+ effector?: { image: string };
154
266
  };
155
267
 
156
268
  // The stamps below are written ON `machine.config` under `Symbol.for` keys, never held in a
@@ -178,11 +290,12 @@ export function sandboxPartsOf(machine: AnyStateMachine | undefined): SandboxPar
178
290
 
179
291
  /** Does this Machine COMPOSE a Sandbox — i.e. is it a `workspace()` wrapper? Distinct from
180
292
  * `sandboxPartsOf(m)` naming no image: a wrapper that names neither image still owns the two
181
- * seats, which is what `customize({ image })` retunes (ADR-0049) — and it always carries at least
182
- * one Repo Slot (ADR-0051). Also the data-plane switch, read off the walk: an Instance needs
183
- * Sandboxes exactly when a registered Machine composes one. */
293
+ * seats, which is what `customize({ image })` retunes (ADR-0049) — and it may carry no Repo Slot
294
+ * at all (ADR-0051). Also the data-plane switch, read off the walk: an Instance needs
295
+ * Sandboxes exactly when a registered Machine composes one. A logic with no `config` (an Effect's,
296
+ * ADR-0067) composes none. */
184
297
  export function composesSandbox(machine: AnyStateMachine): boolean {
185
- return (machine.config as Stamped)[SANDBOX_PARTS] !== undefined;
298
+ return (machine.config as Stamped | undefined)?.[SANDBOX_PARTS] !== undefined;
186
299
  }
187
300
 
188
301
  // The slot a jr2 WRAPPER is transparent to (ADR-0049): `workspace()`'s `body`, `pool()`'s
@@ -295,8 +408,43 @@ export type CarriedRepo = { url: string; ref?: string; identity: string; key: st
295
408
  */
296
409
  export type OpenSlot = { slot: string | undefined; path: readonly string[] | undefined };
297
410
 
298
- /** Which kind of Open part a fix line binds — the two `customize()` keys (ADR-0051, ADR-0054). */
299
- export type OpenPart = "repo" | "agent";
411
+ /**
412
+ * One hop of a placement route (ADR-0059, ADR-0068): the `actors` slot key a child is invoked by,
413
+ * or — for a plain child Machine written INLINE on a state's invoke — xstate's generated invoke
414
+ * src for that hop (`xstate.invoke.<index>.<state id>`), which the static walk computes from the
415
+ * state node and the run reads off the live child. An inline hop is an object, so it can never
416
+ * equal a slot key.
417
+ */
418
+ export type RouteHop = string | { readonly inline: string };
419
+
420
+ /**
421
+ * Where a placement sits: the chain of hops from the registered root down to it, a wrapper's
422
+ * transparent body skipped as {@link OpenSlot}'s `path` skips it. Unlike `path`, a route goes on
423
+ * past an inline Machine, so every placement — a Workspace's pod, an Effect — has a route of its
424
+ * own, and the pod keys of two Workspaces never collide. Where `path` is defined the route is that
425
+ * same list of slot keys, so a pod key reached by slot keys alone is what it was.
426
+ */
427
+ export type PlacementRoute = readonly RouteHop[];
428
+
429
+ /** The hop xstate gives the `index`-th invoke of state node `stateId` when its `src` is inline. */
430
+ export const inlineHop = (index: number, stateId: string): RouteHop => ({
431
+ inline: `xstate.invoke.${index}.${stateId}`,
432
+ });
433
+
434
+ /** A route as a human reads it, for a label: ` → "review"`, ` → invoked inline in state "w.a"`. */
435
+ export function routeText(route: PlacementRoute): string {
436
+ return route
437
+ .map((hop) =>
438
+ typeof hop === "string"
439
+ ? ` → "${hop}"`
440
+ : ` → invoked inline in state "${hop.inline.replace(/^xstate\.invoke\.\d+\./, "")}"`,
441
+ )
442
+ .join("");
443
+ }
444
+
445
+ /** Which kind of Open part a fix line binds — a Repo Slot (`repos`, ADR-0051), an Agent's model
446
+ * (`agents`, ADR-0054), or an Effect's url, argv or secret (`effects`, ADR-0054). */
447
+ export type OpenPart = "repo" | "agent" | OpenEffectPart;
300
448
 
301
449
  /**
302
450
  * The `customize` line that binds an Open part, given the identifier the composer holds the
@@ -305,22 +453,26 @@ export type OpenPart = "repo" | "agent";
305
453
  * `customize(top, { actors: { review: { repos: { target: "<url>" } } } })`. The same nesting
306
454
  * `customize()` accepts, so the line pastes.
307
455
  *
308
- * An Agent binds through `agents` instead, and the placeholder is the model spelling ADR-0018
456
+ * An Agent binds through `agents` instead, and the sample value is the model spelling ADR-0018
309
457
  * demands — `<provider>/<model>`, not a bare model id, since the prefix is what picks the
310
- * endpoint. `repo` is the default because a Repo Slot was the first Open part and reads as the
311
- * unmarked case. An Open MAP has no slot to print (`slot` undefined): the placeholder `<slot>`
458
+ * endpoint. An Effect binds through `effects`: its url, its argv, its secret, or one object with two
459
+ * of them for an Effect with two Open parts (open-effect.ts). `repo` is the
460
+ * default because a Repo Slot was the first Open part and reads as the
461
+ * unmarked case. An Open MAP has no slot to print (`slot` undefined): the sample `<slot>`
312
462
  * stands where the composer's own word goes, since choosing it is the composer's half of the line.
313
463
  */
314
464
  export function customizeLine(
315
465
  machine: string,
316
466
  path: readonly string[],
317
467
  slot: string | undefined,
318
- part: OpenPart = "repo",
468
+ part: OpenPart | readonly OpenEffectPart[] = "repo",
319
469
  ): string {
320
470
  const binds =
321
471
  part === "agent"
322
472
  ? `{ agents: { ${slot}: { model: "<provider>/<model>" } } }`
323
- : `{ repos: { ${slot ?? "<slot>"}: "<url>" } }`;
473
+ : part === "repo"
474
+ ? `{ repos: { ${slot ?? "<slot>"}: "<url>" } }`
475
+ : effectBindingLine(slot ?? "<slot>", part);
324
476
  const inner = path.reduceRight((parts, key) => `{ actors: { ${key}: ${parts} } }`, binds);
325
477
  return `customize(${machine}, ${inner})`;
326
478
  }
@@ -329,8 +481,10 @@ export function customizeLine(
329
481
  * The runtime twin of the walk's `path`: the actor-slot chain from a run's root actor down to
330
482
  * `actor` (its own `src` included), read off the live actor tree — each `src` is the slot key it
331
483
  * was invoked or spawned as, and a wrapper's transparent body is skipped by the same record
332
- * ({@link wrapperBodyOf}) the walk skips it by. `undefined` past an inline-invoked actor, whose
333
- * `src` is the logic itself and names no slot.
484
+ * ({@link wrapperBodyOf}) the walk skips it by. `undefined` past an inline-invoked actor, as the
485
+ * walk's `path` is: such an actor names no slot. xstate gives an inline `invoke.src` a generated
486
+ * string (`xstate.invoke.<n>.<state id>`) that is no key of its parent's `actors`, so the test is
487
+ * that registry, not the type of `src`; inline logic passed to `spawn` keeps the logic as `src`.
334
488
  */
335
489
  export function actorSlotPath(actor: AnyActorRef | undefined): string[] | undefined {
336
490
  const path: string[] = [];
@@ -338,14 +492,128 @@ export function actorSlotPath(actor: AnyActorRef | undefined): string[] | undefi
338
492
  const src = (node as { src?: unknown }).src;
339
493
  if (typeof src !== "string") return undefined;
340
494
  const parent = asMachine((node._parent as { logic?: unknown }).logic);
495
+ if (parent && !Object.hasOwn(parent.implementations.actors, src)) return undefined;
341
496
  if (!parent || wrapperBodyOf(parent) !== src) path.unshift(src);
342
497
  }
343
498
  return path;
344
499
  }
345
500
 
501
+ /**
502
+ * The runtime twin of the walk's `route` (ADR-0059): the placement route from a run's root actor
503
+ * down to `actor`, read off the live actor tree. A child invoked by a slot key adds that key, as
504
+ * {@link actorSlotPath} does; a plain Machine invoked inline adds xstate's generated src as an
505
+ * inline hop, which the walk computes from the state node — so `jr2 up` and the provision derive
506
+ * one pod key. Never undefined: an actor the walk cannot see — a Machine object handed to
507
+ * `spawnChild`, whose `src` is the logic itself — is refused with the slot form.
508
+ */
509
+ export function actorRoute(actor: AnyActorRef | undefined): RouteHop[] {
510
+ const route: RouteHop[] = [];
511
+ for (let node = actor; node?._parent; node = node._parent) {
512
+ const src = (node as { src?: unknown }).src;
513
+ const parent = asMachine((node._parent as { logic?: unknown }).logic);
514
+ if (typeof src === "string" && (!parent || Object.hasOwn(parent.implementations.actors, src))) {
515
+ if (!parent || wrapperBodyOf(parent) !== src) route.unshift(src);
516
+ } else if (typeof src === "string" && /^xstate\.invoke\.\d+\./.test(src)) {
517
+ route.unshift({ inline: src });
518
+ } else {
519
+ throw new Error(
520
+ `actor "${node.id}" was spawned from a logic object, which no walk can see, so jr2 up placed nothing ` +
521
+ "for what it composes — a Part is reached through keys of actors, so it has a name that the ledger, " +
522
+ 'the report and customize() share: actors: { <name>: <machine> }, then spawnChild("<name>") (ADR-0059)',
523
+ );
524
+ }
525
+ }
526
+ return route;
527
+ }
528
+
529
+ /**
530
+ * One `workspace()` a Machine composes, and the Size it states (ADR-0060) — what `jr2 up` resolves
531
+ * against the Instance default and splits, so a split that leaves the Harness below its floor is
532
+ * refused at the converge rather than at the first provision, and a Size no Sandbox node could hold
533
+ * is warned about. Located like an {@link OpenSlot}: `path` is the `customize()` route to it.
534
+ */
535
+ export type CarriedSize = {
536
+ path: readonly string[] | undefined;
537
+ /** The placement route to it (ADR-0059). */
538
+ route: PlacementRoute;
539
+ /** The Machine's Size, as stated; absent when it states none. */
540
+ resources?: Size;
541
+ /** The User Container's split, as stated; absent when it states none. */
542
+ user?: Size;
543
+ };
544
+
545
+ /** One Effect slot a Machine carries (ADR-0067), by its slot key and the `customize()` route to its
546
+ * Machine, with the plain data `jr2 up` prints: the kind, where it runs, a command's argv (an argv
547
+ * slot reads `<name>`), its timeout, and the Repo Slot whose Worktree is its cwd, when it names one.
548
+ * `open` names the parts a package left Open (ADR-0054) — an http url, which then has no target
549
+ * yet, or a command's argv, which reads `<open>`, and a secret its auth, a header or its env names
550
+ * as `secret(open)`, which names no secret yet — and the walk reports them for `jr2 up` to refuse;
551
+ * once `customize()` binds them, the slot carries the bound Effect and `open` is absent. */
552
+ export type CarriedEffect = {
553
+ name: string;
554
+ path: readonly string[] | undefined;
555
+ /** The placement route to its Machine (ADR-0068): what its placement key reads, never `path`. */
556
+ route: PlacementRoute;
557
+ placement: Placement;
558
+ timeoutMs: number;
559
+ } &
560
+ /** A command; one whose env names a secret runs in the Sandbox Effector (ADR-0067). */
561
+ (| {
562
+ kind: "command";
563
+ argv: string[];
564
+ slot?: string;
565
+ secrets?: string[];
566
+ pin?: true;
567
+ open?: Array<"argv" | "secret">;
568
+ }
569
+ /** An http Effect (ADR-0068): its method, its fixed target, and the secrets it names — what
570
+ * places each secret, and what its Effector's config allows. `file`: its body is a Worktree
571
+ * file (ADR-0067), so it runs in its Workspace's Sandbox Effector, on Repo Slot `slot` when named.
572
+ * An Open url has no target, and names only the secrets its headers and auth name. */
573
+ | {
574
+ kind: "http";
575
+ method: string;
576
+ target?: { scheme: "http" | "https"; host: string; port: number };
577
+ secrets: string[];
578
+ idempotent: boolean;
579
+ file?: true;
580
+ slot?: string;
581
+ open?: Array<"url" | "secret">;
582
+ }
583
+ /** A push (ADR-0067, ADR-0051): the Workspace's branch to its Repo Slot's Binding. It names no
584
+ * secret: `jr2 up` resolves its credential through `git.credentials`. */
585
+ | { kind: "push"; slot?: string; sign: boolean; squash: boolean; force: boolean; pin: boolean; secrets: string[] }
586
+ /** A dispatch (ADR-0071): the Workflow it starts a run of, as `jr2 run` names it, whether it
587
+ * waits, and — toward another Orchestrator — the target's origin as an http target and its
588
+ * token as the one secret (placed in the Instance Effector beside the http Effects). No target:
589
+ * it runs in the Orchestrator's own process and names no secret. `input`: the slot's input
590
+ * schema as JSON Schema, what the walk compares with the callee's door; absent, the door alone
591
+ * checks. An Open target has no target and no secret yet. */
592
+ | {
593
+ kind: "dispatch";
594
+ workflow: string;
595
+ wait: boolean;
596
+ target?: { scheme: "http" | "https"; host: string; port: number };
597
+ secrets: string[];
598
+ input?: Record<string, unknown>;
599
+ open?: Array<"target">;
600
+ }
601
+ );
602
+
603
+ /** One state that can fail the run (ADR-0073): it invokes a `fail()` slot. `state` is the state's
604
+ * id, `machine` the id of its Machine, `slot` the slot key it invokes, and `path` the `customize()`
605
+ * route to its Machine, as an {@link OpenSlot}'s. Static, as a Gate or an Effect is: the walk reads
606
+ * it, and `jr2 up`, `jr2 check` and the Machine doc list it. */
607
+ export type CarriedFailure = { machine: string; state: string; slot: string; path: OpenSlot["path"] };
608
+
346
609
  /** Everything the registered Machines carry that a converge or a boot must act on. */
347
610
  export type CarriedParts = {
348
611
  agents: CarriedAgent[];
612
+ /** Every state that invokes a `fail()` slot (ADR-0073), in walk order, nested Machines too. */
613
+ failures: CarriedFailure[];
614
+ /** Every Effect slot, in walk order — one per Machine and slot key, at the first route the walk
615
+ * reached its Machine by. The placement reads {@link placedEffectsOf}: one per route. */
616
+ effects: CarriedEffect[];
349
617
  images: CarriedImage[];
350
618
  /** Every bound Repo, deduped by identity, in walk order. */
351
619
  repos: CarriedRepo[];
@@ -355,8 +623,32 @@ export type CarriedParts = {
355
623
  /** Every Agent whose model is still Open (ADR-0054), in walk order — the same refusal, by the
356
624
  * same route, and the reason these are two lists rather than one: the fix lines differ. */
357
625
  openAgents: OpenSlot[];
626
+ /** Every Effect whose url, argv or secret is still Open (ADR-0054), in walk order — the same
627
+ * refusal, by the same route; `parts` says which line binds it: one object for both of two. */
628
+ openEffects: Array<OpenSlot & { parts: OpenEffectPart[] }>;
629
+ /** Every `workspace()` reached, with the Size it states, in walk order (ADR-0060). */
630
+ sizes: CarriedSize[];
358
631
  /** Whether any Machine reached, at any depth, composes a Sandbox — the data-plane switch. */
359
632
  composesSandbox: boolean;
633
+ /**
634
+ * Every Open body slot (ADR-0070), in walk order, with its declaration: what it lends, what a Chart
635
+ * may define, its ceilings. The one Open part a run binds, not a composer, so it is never in an
636
+ * Open list: an Open body whose lent names did not resolve never reaches the walk (createMachine
637
+ * refused it).
638
+ */
639
+ openBodies: Array<{ slot: string; declaration: OpenBodyDeclaration; path: OpenSlot["path"] }>;
640
+ /**
641
+ * Every model a key is placed and a provider preflighted for: each bound Agent's, and each model
642
+ * an Open body allows a Chart's own Agents (ADR-0070: a minted model is a door refusal, never a
643
+ * 401 mid-run). Deduped, sorted.
644
+ */
645
+ models: string[];
646
+ /**
647
+ * The Instance Harness is needed (ADR-0031): an Agent declares `workspace: "none"`, or an Open
648
+ * body allows it. `models` are the models its Turns may run: those Agents', and those Open bodies'
649
+ * allowed ones. Absent when nothing needs it.
650
+ */
651
+ instanceHarness?: { models: string[] };
360
652
  };
361
653
 
362
654
  /** A machine actor, told apart from a promise/callback/observable one by having a state tree.
@@ -366,16 +658,49 @@ export function asMachine(logic: unknown): AnyStateMachine | undefined {
366
658
  return (logic as AnyStateMachine | undefined)?.root ? (logic as AnyStateMachine) : undefined;
367
659
  }
368
660
 
369
- /** Every machine object written INLINE on one state's invokes. A named `src` resolves through
661
+ /** Every machine object written INLINE on one state's invokes, with the hop xstate gives it (its
662
+ * generated src, from the invoke's index on the state). A named `src` resolves through
370
663
  * `implementations.actors` instead and is walked there. */
371
- function inlineMachines(node: StateNode<any, any>): AnyStateMachine[] {
664
+ function inlineMachines(node: StateNode<any, any>): Array<{ machine: AnyStateMachine; hop: RouteHop }> {
372
665
  const config = [node.config.invoke ?? []].flat() as Array<{ src?: unknown }>;
373
- return config.flatMap((inv) => {
666
+ return config.flatMap((inv, index) => {
374
667
  const machine = asMachine(inv?.src);
375
- return machine ? [machine] : [];
668
+ return machine ? [{ machine, hop: inlineHop(index, node.id) }] : [];
376
669
  });
377
670
  }
378
671
 
672
+ /**
673
+ * Refuse a Part written inline on an `invoke.src` — an `agent()`, an Effect, a `fail()` or a `workspace()` that
674
+ * is no key of the Machine's `actors` (see the module comment). Thrown at build by
675
+ * `jr2Setup.createMachine` and at load by {@link partsOf}, so a Machine built with xstate's own
676
+ * `createMachine` is refused where `jr2 check`, `jr2 up` and the Orchestrator read it. A plain
677
+ * child Machine written inline is not a Part: it stays walked, with no `customize()` route (its
678
+ * `path` is `undefined`).
679
+ */
680
+ export function refuseInlineParts(machine: AnyStateMachine): void {
681
+ const partOf = (src: unknown): { part: string; written: string } | undefined => {
682
+ if (isAgent(src)) return { part: "an Agent", written: "agent(...)" };
683
+ if (isEffect(src)) return { part: "an Effect", written: `${src.effect.kind}(...)` };
684
+ if (isFailSlot(src)) return { part: "a fail() Part", written: "fail()" };
685
+ const child = asMachine(src);
686
+ if (child && composesSandbox(child)) return { part: "a Workspace", written: "workspace(...)" };
687
+ return undefined;
688
+ };
689
+ const visit = (node: StateNode<any, any>): void => {
690
+ for (const inv of [node.config.invoke ?? []].flat() as Array<{ src?: unknown }>) {
691
+ const found = partOf(inv?.src);
692
+ if (found === undefined) continue;
693
+ throw new Error(
694
+ `machine "${machine.id}": state "${node.id}" invokes ${found.part} inline — a Part is a key of actors, so ` +
695
+ `it has a name that the ledger, the report and customize() share: actors: { <name>: ${found.written} }, ` +
696
+ `then invoke: { src: "<name>" }`,
697
+ );
698
+ }
699
+ for (const child of Object.values(node.states as Record<string, StateNode<any, any>>)) visit(child);
700
+ };
701
+ visit(machine.root);
702
+ }
703
+
379
704
  /** `JSON.stringify` with object keys sorted at every depth — a value's identity, not its spelling.
380
705
  * Symbols are spelled out rather than dropped: `JSON.stringify` silently omits a symbol-valued
381
706
  * key, which would make an Open Agent (ADR-0054) collapse into a bound one that differs in nothing
@@ -405,10 +730,17 @@ function canonical(value: unknown): string {
405
730
  */
406
731
  export function partsOf(machines: Iterable<AnyStateMachine>): CarriedParts {
407
732
  const agents: CarriedAgent[] = [];
733
+ const failures: CarriedFailure[] = [];
734
+ const effects: CarriedEffect[] = [];
408
735
  const images: CarriedImage[] = [];
409
736
  const repos: CarriedRepo[] = [];
410
737
  const openSlots: OpenSlot[] = [];
411
738
  const openAgents: OpenSlot[] = [];
739
+ const openEffects: CarriedParts["openEffects"] = [];
740
+ const sizes: CarriedSize[] = [];
741
+ const openBodies: CarriedParts["openBodies"] = [];
742
+ // The Machine that declares each Open body: where its lent Agents are read (not carried data).
743
+ const bodyWrappers = new Map<CarriedParts["openBodies"][number], AnyStateMachine>();
412
744
  let sandboxed = false;
413
745
  const seen = new Set<string>();
414
746
  // Cycle guard AND work saver: a Machine reached twice carries the same parts both times, and a
@@ -466,35 +798,371 @@ export function partsOf(machines: Iterable<AnyStateMachine>): CarriedParts {
466
798
  }
467
799
  };
468
800
 
469
- const walkStates = (node: StateNode<any, any>, visit: (machine: AnyStateMachine) => void): void => {
470
- for (const machine of inlineMachines(node)) visit(machine);
471
- for (const child of Object.values(node.states as Record<string, StateNode<any, any>>)) walkStates(child, visit);
472
- };
473
-
474
801
  // `path` is the `customize()` route from the registered root to `machine` (see `OpenSlot`):
475
802
  // a named child slot extends it, a wrapper's transparent body does not, and an inline invoke
476
803
  // ends it — nothing downstream of an actor without a slot key can be named by a composer.
477
- const walk = (machine: AnyStateMachine, path: OpenSlot["path"]): void => {
804
+ // `route` is the placement route, which an inline invoke extends by its hop (`PlacementRoute`).
805
+ const walk = (machine: AnyStateMachine, path: OpenSlot["path"], route: PlacementRoute): void => {
478
806
  if (walked.has(machine)) return;
479
807
  walked.add(machine);
808
+ refuseInlineParts(machine);
809
+ refuseLostOutput(machine);
810
+ refuseUnroutedAgentFault(machine);
480
811
  const parts = sandboxPartsOf(machine);
812
+ const user = userSeatOf(parts.user);
481
813
  collectImage(parts.image);
482
- collectImage(parts.user);
814
+ collectImage(user.image);
815
+ collectImage(parts.effector?.image);
483
816
  if (composesSandbox(machine)) {
484
817
  sandboxed = true;
485
818
  collectRepos(path, parts.repos);
819
+ sizes.push({
820
+ path,
821
+ route,
822
+ ...(parts.resources !== undefined ? { resources: parts.resources } : {}),
823
+ ...(user.resources !== undefined ? { user: user.resources } : {}),
824
+ });
486
825
  }
487
826
  const body = wrapperBodyOf(machine);
827
+ for (const { state, slot } of failingStatesOf(machine)) failures.push({ machine: machine.id, state, slot, path });
488
828
  for (const [name, logic] of Object.entries(machine.implementations.actors as Record<string, unknown>)) {
489
829
  if (isAgent(logic)) collectAgent(path, name, logic.definition);
490
- else {
830
+ else if (isOpenBody(logic)) {
831
+ const body = { slot: name, declaration: openBodyDeclarationOf(logic), path };
832
+ openBodies.push(body);
833
+ bodyWrappers.set(body, machine);
834
+ } else if (isEffect(logic)) {
835
+ const effect = carriedEffectOf(name, path, route, logic.effect);
836
+ effects.push(effect);
837
+ if (effect.kind !== "push" && effect.open !== undefined)
838
+ openEffects.push({ slot: name, path, parts: [...effect.open] });
839
+ } else {
491
840
  const child = asMachine(logic);
492
- if (child) walk(child, path === undefined ? undefined : name === body ? path : [...path, name]);
841
+ if (child) walk(child, slotPath(path, name, body), name === body ? route : [...route, name]);
493
842
  }
494
843
  }
495
- walkStates(machine.root, (inline) => walk(inline, undefined));
844
+ walkStates(machine.root, (inline, hop) => walk(inline, undefined, [...route, hop]));
496
845
  };
497
846
 
498
- for (const machine of machines) walk(machine, []);
499
- return { agents, images, repos, openSlots, openAgents, composesSandbox: sandboxed };
847
+ for (const machine of machines) walk(machine, [], []);
848
+ const bound = (m: unknown): m is string => typeof m === "string";
849
+ const models = new Set<string>(agents.map((a) => a.definition.model).filter(bound));
850
+ for (const body of openBodies) for (const m of body.declaration.allows.models) models.add(m);
851
+ const menuOnly = agents.filter((a) => a.definition.workspace === "none");
852
+ const noneBodies = openBodies.filter((b) => bodyTurnsRunIn(bodyWrappers.get(b)!, b.declaration, "instance"));
853
+ const harnessModels = new Set<string>(menuOnly.map((a) => a.definition.model).filter(bound));
854
+ for (const body of noneBodies) for (const m of body.declaration.allows.models) harnessModels.add(m);
855
+ return {
856
+ agents,
857
+ failures,
858
+ effects,
859
+ images,
860
+ repos,
861
+ openSlots,
862
+ openAgents,
863
+ openEffects,
864
+ sizes,
865
+ composesSandbox: sandboxed,
866
+ openBodies,
867
+ models: [...models].sort(),
868
+ ...(menuOnly.length || noneBodies.length ? { instanceHarness: { models: [...harnessModels].sort() } } : {}),
869
+ };
870
+ }
871
+
872
+ /**
873
+ * Can a Turn under an Open body of `wrapper` run where `where` says, with a model the body allows
874
+ * (ADR-0070: a minted model needs a key the Assembly placed, a door refusal and never a 401)? A
875
+ * Chart's own Agent runs where its Workspace access says, so a body that allows "write" or "read"
876
+ * runs Turns in a pod, and one that allows "none" at the Instance Harness. A lent Agent runs where
877
+ * its own access says, and a Chart may set its `model` dial to an allowed model: a lent Agent that
878
+ * is not "none" is a pod Turn, and a "none" one an Instance Harness Turn.
879
+ */
880
+ function bodyTurnsRunIn(wrapper: AnyStateMachine, decl: OpenBodyDeclaration, where: "pod" | "instance"): boolean {
881
+ const atInstance = (w: string | undefined) => w === "none";
882
+ if (decl.allows.workspace.some((w) => (where === "instance" ? atInstance(w) : !atInstance(w)))) return true;
883
+ const actors = wrapper.implementations.actors as Record<string, unknown>;
884
+ return (decl.lends.agents ?? []).some((slot) => {
885
+ const logic = actors[slot];
886
+ if (!isAgent(logic)) return false;
887
+ const access = (logic.definition as { workspace?: string }).workspace;
888
+ return where === "instance" ? atInstance(access) : !atInstance(access);
889
+ });
890
+ }
891
+
892
+ /** The `customize()` route to a child of slot `name`: a wrapper's body does not extend it. */
893
+ function slotPath(path: OpenSlot["path"], name: string, body: string | undefined): OpenSlot["path"] {
894
+ return path === undefined ? undefined : name === body ? path : [...path, name];
895
+ }
896
+
897
+ /**
898
+ * Every Effect the registered Machine `root` places, at every route it is reached by — one entry
899
+ * per route, as {@link workspacePodsOf} lists pods: a Machine invoked under two slot keys places
900
+ * its Effects twice, and each is checked where it is. What the placement walk reads (ADR-0068),
901
+ * never {@link partsOf}'s list, which holds one entry per Machine. A cycle (a recursive pool
902
+ * worker) stops at the Machine on its own route.
903
+ */
904
+ export function placedEffectsOf(root: AnyStateMachine): CarriedEffect[] {
905
+ const effects: CarriedEffect[] = [];
906
+ const walk = (
907
+ machine: AnyStateMachine,
908
+ path: OpenSlot["path"],
909
+ route: PlacementRoute,
910
+ on: Set<AnyStateMachine>,
911
+ ): void => {
912
+ if (on.has(machine)) return;
913
+ on.add(machine);
914
+ const body = wrapperBodyOf(machine);
915
+ for (const [name, logic] of Object.entries(machine.implementations.actors as Record<string, unknown>)) {
916
+ if (isAgent(logic)) continue;
917
+ if (isEffect(logic)) {
918
+ effects.push(carriedEffectOf(name, path, route, logic.effect));
919
+ continue;
920
+ }
921
+ const child = asMachine(logic);
922
+ if (child) walk(child, slotPath(path, name, body), name === body ? route : [...route, name], on);
923
+ }
924
+ walkStates(machine.root, (inline, hop) => walk(inline, undefined, [...route, hop], on));
925
+ on.delete(machine);
926
+ };
927
+ walk(root, [], [], new Set());
928
+ return effects;
929
+ }
930
+
931
+ /** One Effect slot's plain data, as `jr2 up` and the placement read it. */
932
+ export function carriedEffectOf(
933
+ name: string,
934
+ path: OpenSlot["path"],
935
+ route: PlacementRoute,
936
+ parts: EffectParts,
937
+ ): CarriedEffect {
938
+ const placement = placementOf(parts);
939
+ const openParts = openPartsOf(parts);
940
+ if (parts.kind === "dispatch") {
941
+ const input = (parts as { [DISPATCH_INPUT]?: Record<string, unknown> })[DISPATCH_INPUT];
942
+ return {
943
+ name,
944
+ path,
945
+ route,
946
+ kind: "dispatch",
947
+ placement,
948
+ workflow: parts.workflow,
949
+ wait: parts.wait,
950
+ timeoutMs: parts.timeoutMs,
951
+ ...(parts.target === undefined || parts.target === open ? {} : { target: originTarget(parts.target.url) }),
952
+ secrets: [...parts.secrets],
953
+ ...(input !== undefined ? { input } : {}),
954
+ ...(openParts.length ? { open: openParts as Array<"target"> } : {}),
955
+ };
956
+ }
957
+ if (parts.kind === "http") {
958
+ return {
959
+ name,
960
+ path,
961
+ route,
962
+ kind: "http",
963
+ placement,
964
+ method: parts.method,
965
+ ...(parts.target === open ? {} : { target: { ...parts.target } }),
966
+ ...(openParts.length ? { open: openParts as Array<"url" | "secret"> } : {}),
967
+ secrets: [...parts.secrets],
968
+ idempotent: parts.idempotent,
969
+ timeoutMs: parts.timeoutMs,
970
+ ...(parts.file ? { file: true as const } : {}),
971
+ ...(parts.file?.slot !== undefined ? { slot: parts.file.slot } : {}),
972
+ };
973
+ }
974
+ if (parts.kind === "push") {
975
+ const { sign, squash, force, pin, timeoutMs, slot } = parts;
976
+ return {
977
+ name,
978
+ path,
979
+ route,
980
+ kind: "push",
981
+ placement,
982
+ sign,
983
+ squash,
984
+ force,
985
+ pin,
986
+ secrets: [],
987
+ timeoutMs,
988
+ ...(slot !== undefined ? { slot } : {}),
989
+ };
990
+ }
991
+ const { argv, timeoutMs, slot, secrets, pin } = parts;
992
+ return {
993
+ name,
994
+ path,
995
+ route,
996
+ kind: "command",
997
+ placement,
998
+ argv: argvText(argv),
999
+ timeoutMs,
1000
+ ...(slot !== undefined ? { slot } : {}),
1001
+ ...(secrets?.length ? { secrets: [...secrets] } : {}),
1002
+ ...(pin ? { pin } : {}),
1003
+ ...(openParts.length ? { open: openParts as Array<"argv" | "secret"> } : {}),
1004
+ };
1005
+ }
1006
+
1007
+ /** The key a dispatch's slot schema rides its parts under (dispatch-effect.ts): `Symbol.for`, so this
1008
+ * walk reads it off a Machine built by another copy of the module (ADR-0071). */
1009
+ const DISPATCH_INPUT = Symbol.for("jr2.dispatch.input");
1010
+
1011
+ /** A dispatch target's origin as the Effector's fence reads an http target: scheme, host, port. The
1012
+ * origin was checked when the slot was built, so it parses. */
1013
+ function originTarget(url: string): { scheme: "http" | "https"; host: string; port: number } {
1014
+ const u = new URL(url);
1015
+ const scheme = u.protocol === "http:" ? "http" : "https";
1016
+ // As an http target's: brackets off an IPv6 literal, lower case (http-effect.ts `targetOf`).
1017
+ const host = u.hostname.replace(/^\[|\]$/g, "").toLowerCase();
1018
+ return { scheme, host, port: u.port ? Number(u.port) : scheme === "http" ? 80 : 443 };
1019
+ }
1020
+
1021
+ /**
1022
+ * One Workspace a Machine composes, as its pod's Held secrets are placed (ADR-0059): what the pod
1023
+ * names. Located like a {@link CarriedSize} — `path` is the `customize()` route to its
1024
+ * `workspace()`, and `route` its placement route, the same route the run host reads off the live
1025
+ * actor at provision (`actorRoute`), so `jr2 up` and the provision name one pod alike.
1026
+ */
1027
+ export type WorkspacePod = {
1028
+ path: readonly string[] | undefined;
1029
+ /** The pod's placement route: what its pod key reads (`workspaceHeldPod`). */
1030
+ route: PlacementRoute;
1031
+ /** The `workspace()`'s own `secrets: [...]`, as written. */
1032
+ secrets: string[];
1033
+ /** The model of every bound Agent whose Turn runs in this pod: each Agent below the
1034
+ * `workspace()` that is not `workspace: "none"` (those run at the Instance Harness, ADR-0031),
1035
+ * down to the next `workspace()`, which is a pod of its own, and every model an Open body below
1036
+ * it allows a Chart's own Agents when it allows "write" or "read" (ADR-0070). Sorted, deduped. */
1037
+ models: string[];
1038
+ /** The Effects placed in this pod's Sandbox Effector (ADR-0068): every `push`, file-body `http` and
1039
+ * credentialed `command` slot below the `workspace()`, down to the next `workspace()` — read off
1040
+ * their plain data, statically. Empty → the pod gets no Sandbox Effector and no Secret. */
1041
+ effects: CarriedEffect[];
1042
+ /** The `workspace()`'s `effector: { image }`, as written. Absent → the Sandbox Image. */
1043
+ effector?: { image: string };
1044
+ /** The `workspace()`'s own `image`, as written — its Sandbox Image. Absent → `images/default`,
1045
+ * then the stock Harness (ADR-0037). */
1046
+ image?: string;
1047
+ /** Each Repo Slot's Binding url as the Machine binds it, by slot, in order; undefined for an open
1048
+ * or per-run slot, whose url `jr2 up` cannot read. A push resolves its credential through it. */
1049
+ bindings: Record<string, string | undefined>;
1050
+ /** The `workspace()` declares no Repo Slot (ADR-0051): it has only `/work`, which its steps
1051
+ * share. Absent for one with slots, and for an Open map, whose slots the composer names. */
1052
+ noSlot?: true;
1053
+ };
1054
+
1055
+ /**
1056
+ * Every Workspace a Machine composes, at every route it is reached by — one entry per route, so a
1057
+ * `workspace()` composed under two slot keys is two pods. Not deduped like {@link partsOf}: a pod
1058
+ * is a route, not a Machine. A cycle (a recursive pool worker) stops at the Machine on its own
1059
+ * route.
1060
+ */
1061
+ export function workspacePodsOf(root: AnyStateMachine): WorkspacePod[] {
1062
+ const pods: WorkspacePod[] = [];
1063
+ /** The bound models of the Agents in `machine` and the Machines it composes, short of a
1064
+ * `workspace()`. */
1065
+ const modelsIn = (
1066
+ machine: AnyStateMachine,
1067
+ models: Set<string>,
1068
+ on: Set<AnyStateMachine>,
1069
+ effects: Map<string, CarriedEffect>,
1070
+ path: OpenSlot["path"],
1071
+ route: PlacementRoute,
1072
+ ): void => {
1073
+ if (on.has(machine)) return;
1074
+ on.add(machine);
1075
+ const body = wrapperBodyOf(machine);
1076
+ const visit = (child: AnyStateMachine, at: OpenSlot["path"], to: PlacementRoute) => {
1077
+ if (!composesSandbox(child)) modelsIn(child, models, on, effects, at, to);
1078
+ };
1079
+ for (const [name, logic] of Object.entries(machine.implementations.actors as Record<string, unknown>)) {
1080
+ if (isAgent(logic)) {
1081
+ const { model, workspace } = logic.definition;
1082
+ if (workspace !== "none" && typeof model === "string") models.add(model);
1083
+ continue;
1084
+ }
1085
+ // A Turn under an Open body may run in this pod with any model the body allows (ADR-0070):
1086
+ // a Chart's own Agent, when the body allows a Workspace access other than "none", and a lent
1087
+ // Agent that is not "none", whose `model` dial a Chart may set to an allowed model.
1088
+ if (isOpenBody(logic)) {
1089
+ const decl = openBodyDeclarationOf(logic);
1090
+ if (bodyTurnsRunIn(machine, decl, "pod")) for (const m of decl.allows.models) models.add(m);
1091
+ continue;
1092
+ }
1093
+ if (isEffect(logic)) {
1094
+ const effect = carriedEffectOf(name, path, route, logic.effect);
1095
+ if (effect.placement === "sandbox") effects.set(JSON.stringify([route, name]), effect);
1096
+ continue;
1097
+ }
1098
+ const child = asMachine(logic);
1099
+ if (child) visit(child, slotPath(path, name, body), name === body ? route : [...route, name]);
1100
+ }
1101
+ walkStates(machine.root, (inline, hop) => visit(inline, undefined, [...route, hop]));
1102
+ on.delete(machine);
1103
+ };
1104
+ const walk = (
1105
+ machine: AnyStateMachine,
1106
+ path: OpenSlot["path"],
1107
+ route: PlacementRoute,
1108
+ on: Set<AnyStateMachine>,
1109
+ ): void => {
1110
+ if (on.has(machine)) return;
1111
+ on.add(machine);
1112
+ if (composesSandbox(machine)) {
1113
+ const models = new Set<string>();
1114
+ const effects = new Map<string, CarriedEffect>();
1115
+ modelsIn(machine, models, new Set(), effects, path, route);
1116
+ const parts = sandboxPartsOf(machine);
1117
+ const bindings: Record<string, string | undefined> = {};
1118
+ if (parts.repos !== open) {
1119
+ for (const [slot, value] of Object.entries(parts.repos)) {
1120
+ const state = repoSlotState(value);
1121
+ bindings[slot] = state.kind === "bound" ? state.binding.url : undefined;
1122
+ }
1123
+ }
1124
+ pods.push({
1125
+ path,
1126
+ route,
1127
+ secrets: [...(parts.secrets ?? [])],
1128
+ models: [...models].sort(),
1129
+ effects: [...effects.values()],
1130
+ ...(parts.effector ? { effector: { ...parts.effector } } : {}),
1131
+ ...(parts.image !== undefined ? { image: parts.image } : {}),
1132
+ bindings,
1133
+ ...(parts.repos !== open && Object.keys(parts.repos).length === 0 ? { noSlot: true as const } : {}),
1134
+ });
1135
+ }
1136
+ const body = wrapperBodyOf(machine);
1137
+ for (const [name, logic] of Object.entries(machine.implementations.actors as Record<string, unknown>)) {
1138
+ const child = isAgent(logic) || isEffect(logic) ? undefined : asMachine(logic);
1139
+ if (child) walk(child, slotPath(path, name, body), name === body ? route : [...route, name], on);
1140
+ }
1141
+ walkStates(machine.root, (inline, hop) => walk(inline, undefined, [...route, hop], on));
1142
+ on.delete(machine);
1143
+ };
1144
+ walk(root, [], [], new Set());
1145
+ return pods;
1146
+ }
1147
+
1148
+ /** Every state of `machine`'s own tree that invokes a `fail()` slot (ADR-0073), in document order,
1149
+ * with the slot key: a state that can fail the run. A child Machine's states are its own walk's. */
1150
+ export function failingStatesOf(machine: AnyStateMachine): Array<{ state: string; slot: string }> {
1151
+ const actors = machine.implementations.actors as Record<string, unknown>;
1152
+ const out: Array<{ state: string; slot: string }> = [];
1153
+ const visit = (node: StateNode<any, any>): void => {
1154
+ for (const inv of node.invoke) {
1155
+ if (typeof inv.src === "string" && isFailSlot(actors[inv.src])) out.push({ state: node.id, slot: inv.src });
1156
+ }
1157
+ const children = Object.values(node.states as Record<string, StateNode<any, any>>);
1158
+ for (const child of children.sort((a, b) => a.order - b.order)) visit(child);
1159
+ };
1160
+ visit(machine.root);
1161
+ return out;
1162
+ }
1163
+
1164
+ /** Every machine object written inline below `node`, at any depth of its state tree, with its hop. */
1165
+ function walkStates(node: StateNode<any, any>, visit: (machine: AnyStateMachine, hop: RouteHop) => void): void {
1166
+ for (const { machine, hop } of inlineMachines(node)) visit(machine, hop);
1167
+ for (const child of Object.values(node.states as Record<string, StateNode<any, any>>)) walkStates(child, visit);
500
1168
  }