@jr2/orchestrator 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/console/components/app.ts +55 -15
  2. package/console/components/bottom-drawer.ts +259 -0
  3. package/console/components/drawer.ts +14 -6
  4. package/console/components/fleet.ts +36 -13
  5. package/console/components/machine-pane.ts +61 -5
  6. package/console/components/nav.ts +54 -8
  7. package/console/conversation.ts +161 -0
  8. package/console/login.ts +123 -0
  9. package/console/main.ts +600 -60
  10. package/console/store.ts +611 -64
  11. package/console/style.css +231 -0
  12. package/docs/agents-block.md +2 -2
  13. package/docs/concepts/agents-and-turns.md +142 -29
  14. package/docs/concepts/architecture.md +60 -34
  15. package/docs/concepts/callers-and-hooks.md +205 -0
  16. package/docs/concepts/charts.md +13 -8
  17. package/docs/concepts/composition.md +10 -1
  18. package/docs/concepts/converge.md +7 -2
  19. package/docs/concepts/durable-runs.md +43 -16
  20. package/docs/concepts/effects.md +133 -50
  21. package/docs/concepts/gates.md +95 -25
  22. package/docs/concepts/machines-and-workflows.md +14 -4
  23. package/docs/concepts/pools-and-sources.md +193 -17
  24. package/docs/concepts/secrets-and-trust.md +186 -62
  25. package/docs/concepts/the-menu.md +14 -0
  26. package/docs/concepts/workspaces.md +135 -56
  27. package/docs/get-started/a-model-decides.md +14 -13
  28. package/docs/get-started/an-agent-in-a-sandbox.md +46 -35
  29. package/docs/get-started/connect-a-model.md +1 -1
  30. package/docs/get-started/create-an-instance.md +17 -14
  31. package/docs/get-started/have-your-assistant-write-a-machine.md +111 -40
  32. package/docs/get-started/install.md +4 -0
  33. package/docs/guides/add-tools-to-the-sandbox.md +72 -15
  34. package/docs/guides/answer-a-gate-from-a-forge.md +304 -0
  35. package/docs/guides/answer-a-gate.md +74 -37
  36. package/docs/guides/bind-open-parts.md +29 -12
  37. package/docs/guides/call-an-mcp-tool-from-a-machine.md +572 -0
  38. package/docs/guides/cancel-a-run.md +26 -16
  39. package/docs/guides/cut-a-worktree.md +560 -0
  40. package/docs/guides/fix-a-failed-converge.md +19 -7
  41. package/docs/guides/give-a-caller-a-scope.md +203 -0
  42. package/docs/guides/give-an-agent-mcp-tools.md +422 -0
  43. package/docs/guides/iterate-on-a-machine-on-kind.md +10 -9
  44. package/docs/guides/let-an-agent-write-a-chart.md +14 -7
  45. package/docs/guides/look-inside-a-sandbox.md +115 -42
  46. package/docs/guides/move-from-kind-to-a-shared-cluster.md +16 -0
  47. package/docs/guides/open-the-console.md +63 -14
  48. package/docs/guides/push-and-sign-a-branch.md +67 -21
  49. package/docs/guides/read-a-conversation.md +225 -0
  50. package/docs/guides/run-a-chart-by-name.md +17 -16
  51. package/docs/guides/run-a-command-or-call-a-url.md +84 -41
  52. package/docs/guides/run-one-worker-per-item.md +34 -27
  53. package/docs/guides/set-an-agents-access-and-dials.md +69 -22
  54. package/docs/guides/size-a-workspace.md +19 -6
  55. package/docs/guides/start-a-run-for-each-alert.md +290 -0
  56. package/docs/guides/start-a-run-from-a-run.md +18 -16
  57. package/docs/guides/start-a-run.md +24 -18
  58. package/docs/guides/steer-an-agent.md +332 -0
  59. package/docs/guides/use-a-hosted-provider.md +7 -1
  60. package/docs/guides/watch-a-run.md +146 -117
  61. package/docs/guides/write-a-hook-kind.md +238 -0
  62. package/docs/guides/write-a-tool-as-an-mcp-server.md +351 -0
  63. package/docs/guides/write-a-workflow.md +18 -8
  64. package/docs/llms.txt +110 -23
  65. package/docs/reference/authoring-surface/agent.md +35 -12
  66. package/docs/reference/authoring-surface/answer.md +20 -4
  67. package/docs/reference/authoring-surface/arg.md +28 -6
  68. package/docs/reference/authoring-surface/basic.md +1 -1
  69. package/docs/reference/authoring-surface/bearer.md +1 -1
  70. package/docs/reference/authoring-surface/branchname.md +15 -0
  71. package/docs/reference/authoring-surface/carry.md +23 -0
  72. package/docs/reference/authoring-surface/chartschema.md +1 -1
  73. package/docs/reference/authoring-surface/chartschemafor.md +1 -1
  74. package/docs/reference/authoring-surface/command.md +37 -22
  75. package/docs/reference/authoring-surface/customize.md +4 -1
  76. package/docs/reference/authoring-surface/defineconfig.md +1 -1
  77. package/docs/reference/authoring-surface/defineevent.md +1 -1
  78. package/docs/reference/authoring-surface/dispatch.md +5 -4
  79. package/docs/reference/authoring-surface/fail.md +18 -5
  80. package/docs/reference/authoring-surface/file.md +37 -17
  81. package/docs/reference/authoring-surface/frompick.md +1 -1
  82. package/docs/reference/authoring-surface/gate.md +118 -0
  83. package/docs/reference/authoring-surface/group-agents.md +15 -0
  84. package/docs/reference/authoring-surface/group-charts.md +16 -0
  85. package/docs/reference/authoring-surface/group-composition.md +25 -0
  86. package/docs/reference/authoring-surface/group-config.md +17 -0
  87. package/docs/reference/authoring-surface/group-effects.md +48 -0
  88. package/docs/reference/authoring-surface/group-failure.md +13 -0
  89. package/docs/reference/authoring-surface/group-gates.md +16 -0
  90. package/docs/reference/authoring-surface/group-machine-and-events.md +15 -0
  91. package/docs/reference/authoring-surface/group-steering.md +12 -0
  92. package/docs/reference/authoring-surface/group-types-these-signatures-name.md +137 -0
  93. package/docs/reference/authoring-surface/group-workspaces-and-repos.md +15 -0
  94. package/docs/reference/authoring-surface/http.md +109 -9
  95. package/docs/reference/authoring-surface/isfailure.md +1 -1
  96. package/docs/reference/authoring-surface/isready.md +14 -0
  97. package/docs/reference/authoring-surface/jr2setup.md +14 -4
  98. package/docs/reference/authoring-surface/mcp.md +107 -0
  99. package/docs/reference/authoring-surface/open.md +41 -8
  100. package/docs/reference/authoring-surface/openbody.md +1 -1
  101. package/docs/reference/authoring-surface/paramsof.md +23 -5
  102. package/docs/reference/authoring-surface/pool.md +79 -11
  103. package/docs/reference/authoring-surface/push.md +31 -13
  104. package/docs/reference/authoring-surface/readyof.md +21 -0
  105. package/docs/reference/authoring-surface/reread.md +19 -0
  106. package/docs/reference/authoring-surface/secret.md +26 -5
  107. package/docs/reference/authoring-surface/sentby.md +1 -1
  108. package/docs/reference/authoring-surface/skip.md +22 -0
  109. package/docs/reference/authoring-surface/source.md +59 -4
  110. package/docs/reference/authoring-surface/steer.md +93 -0
  111. package/docs/reference/authoring-surface/text.md +1 -1
  112. package/docs/reference/authoring-surface/type-agentdefinition.md +16 -3
  113. package/docs/reference/authoring-surface/type-agentmcpserver.md +54 -0
  114. package/docs/reference/authoring-surface/type-agentslots.md +4 -1
  115. package/docs/reference/authoring-surface/type-anyframepath.md +14 -0
  116. package/docs/reference/authoring-surface/type-anyworktreepath.md +15 -0
  117. package/docs/reference/authoring-surface/type-bindactors.md +14 -0
  118. package/docs/reference/authoring-surface/type-binding.md +6 -7
  119. package/docs/reference/authoring-surface/type-caller.md +40 -0
  120. package/docs/reference/authoring-surface/type-callerrecord.md +32 -0
  121. package/docs/reference/authoring-surface/type-carry.md +19 -0
  122. package/docs/reference/authoring-surface/type-chainlink.md +3 -3
  123. package/docs/reference/authoring-surface/type-commandinput.md +4 -3
  124. package/docs/reference/authoring-surface/type-commandoptions.md +0 -22
  125. package/docs/reference/authoring-surface/type-commandoutput.md +23 -5
  126. package/docs/reference/authoring-surface/type-customize.md +20 -3
  127. package/docs/reference/authoring-surface/type-delivered.md +17 -0
  128. package/docs/reference/authoring-surface/type-deliveredevent.md +18 -0
  129. package/docs/reference/authoring-surface/type-dispatchdefinition.md +4 -3
  130. package/docs/reference/authoring-surface/type-dispatchoutput.md +9 -2
  131. package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
  132. package/docs/reference/authoring-surface/type-effectbinding.md +12 -2
  133. package/docs/reference/authoring-surface/type-filebody.md +4 -8
  134. package/docs/reference/authoring-surface/type-framepath.md +14 -0
  135. package/docs/reference/authoring-surface/type-gatebinding.md +15 -0
  136. package/docs/reference/authoring-surface/type-gatehooks.md +12 -0
  137. package/docs/reference/authoring-surface/type-gatelook.md +37 -0
  138. package/docs/reference/authoring-surface/type-gateoutcome.md +32 -0
  139. package/docs/reference/authoring-surface/type-gatereply.md +19 -0
  140. package/docs/reference/authoring-surface/type-gateslot.md +21 -0
  141. package/docs/reference/authoring-surface/type-gateslotinput.md +13 -0
  142. package/docs/reference/authoring-surface/type-gateslotparts.md +18 -0
  143. package/docs/reference/authoring-surface/type-gateslots.md +12 -0
  144. package/docs/reference/authoring-surface/type-gatetypesat.md +13 -0
  145. package/docs/reference/authoring-surface/type-hostinjectedinput.md +3 -5
  146. package/docs/reference/authoring-surface/type-httpdefinition.md +6 -3
  147. package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
  148. package/docs/reference/authoring-surface/type-httpoutput.md +8 -2
  149. package/docs/reference/authoring-surface/type-jr2config.md +37 -2
  150. package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
  151. package/docs/reference/authoring-surface/type-lends.md +5 -1
  152. package/docs/reference/authoring-surface/type-lookoutput.md +14 -0
  153. package/docs/reference/authoring-surface/type-mcpcalloutput.md +78 -0
  154. package/docs/reference/authoring-surface/type-mcpnotreturned.md +42 -0
  155. package/docs/reference/authoring-surface/type-mcpoptions.md +47 -0
  156. package/docs/reference/authoring-surface/type-mcpoutput.md +50 -0
  157. package/docs/reference/authoring-surface/type-mcpreadoutput.md +53 -0
  158. package/docs/reference/authoring-surface/type-mcpremoteserver.md +50 -0
  159. package/docs/reference/authoring-surface/type-mcpserver.md +41 -0
  160. package/docs/reference/authoring-surface/type-mcpstdioserver.md +42 -0
  161. package/docs/reference/authoring-surface/type-namedsecret.md +4 -4
  162. package/docs/reference/authoring-surface/type-namestart.md +77 -0
  163. package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +1 -6
  164. package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
  165. package/docs/reference/authoring-surface/type-openbodydeclaration.md +1 -1
  166. package/docs/reference/authoring-surface/type-openbodyoutput.md +1 -1
  167. package/docs/reference/authoring-surface/type-opencommandinput.md +7 -2
  168. package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +1 -6
  169. package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +5 -10
  170. package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
  171. package/docs/reference/authoring-surface/type-poolspec.md +11 -6
  172. package/docs/reference/authoring-surface/type-pushinput.md +22 -3
  173. package/docs/reference/authoring-surface/type-pushoutput.md +33 -8
  174. package/docs/reference/authoring-surface/type-pushsourcespec.md +38 -0
  175. package/docs/reference/authoring-surface/type-readsourcespec.md +47 -0
  176. package/docs/reference/authoring-surface/type-reposlot.md +6 -6
  177. package/docs/reference/authoring-surface/type-reread.md +18 -0
  178. package/docs/reference/authoring-surface/type-runsconfig.md +41 -0
  179. package/docs/reference/authoring-surface/type-sandboxoptions.md +11 -4
  180. package/docs/reference/authoring-surface/type-sender.md +26 -1
  181. package/docs/reference/authoring-surface/type-sendercallerfield.md +14 -0
  182. package/docs/reference/authoring-surface/type-size.md +1 -1
  183. package/docs/reference/authoring-surface/type-skip.md +19 -0
  184. package/docs/reference/authoring-surface/type-slothandles.md +5 -13
  185. package/docs/reference/authoring-surface/type-sourcehooks.md +23 -0
  186. package/docs/reference/authoring-surface/type-sourcein.md +25 -0
  187. package/docs/reference/authoring-surface/type-sourcereply.md +18 -0
  188. package/docs/reference/authoring-surface/type-sourcespec.md +26 -17
  189. package/docs/reference/authoring-surface/type-sourceverdict.md +49 -0
  190. package/docs/reference/authoring-surface/type-standingpoolmachine.md +12 -0
  191. package/docs/reference/authoring-surface/type-standingpoolspec.md +51 -0
  192. package/docs/reference/authoring-surface/type-steerargs.md +20 -0
  193. package/docs/reference/authoring-surface/type-thinkinglevel.md +1 -1
  194. package/docs/reference/authoring-surface/type-usercontainer.md +1 -1
  195. package/docs/reference/authoring-surface/type-workspaced.md +30 -9
  196. package/docs/reference/authoring-surface/type-workspacehandles.md +5 -6
  197. package/docs/reference/authoring-surface/type-workspaceoptions.md +1 -13
  198. package/docs/reference/authoring-surface/type-worktreefound.md +16 -0
  199. package/docs/reference/authoring-surface/type-worktreeinput.md +56 -0
  200. package/docs/reference/authoring-surface/type-worktreekey.md +14 -0
  201. package/docs/reference/authoring-surface/type-worktreeoptions.md +29 -0
  202. package/docs/reference/authoring-surface/type-worktreeoutput.md +45 -0
  203. package/docs/reference/authoring-surface/type-worktreepath.md +16 -0
  204. package/docs/reference/authoring-surface/type-worktreeready.md +37 -0
  205. package/docs/reference/authoring-surface/type-worktreerefused.md +21 -0
  206. package/docs/reference/authoring-surface/workspace.md +66 -38
  207. package/docs/reference/authoring-surface/worktree.md +71 -0
  208. package/docs/reference/authoring-surface.md +88 -147
  209. package/docs/reference/cli.md +413 -74
  210. package/docs/reference/config.md +248 -6
  211. package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +10 -9
  212. package/docs/reference/decisions/0002-duplex-streaming-actor.md +81 -14
  213. package/docs/reference/decisions/0004-worktree-provisioning-model.md +27 -21
  214. package/docs/reference/decisions/0005-sandbox-pod-composition.md +39 -25
  215. package/docs/reference/decisions/0006-agent-control-surface.md +6 -3
  216. package/docs/reference/decisions/0007-durable-machine-state.md +1 -1
  217. package/docs/reference/decisions/0009-cli-and-instance-interface.md +150 -49
  218. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +5 -3
  219. package/docs/reference/decisions/0011-workflow-defined-events.md +256 -12
  220. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +39 -32
  221. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +54 -30
  222. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +53 -27
  223. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +26 -16
  224. package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +9 -7
  225. package/docs/reference/decisions/0017-pool-over-a-source-port.md +226 -10
  226. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +15 -7
  227. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +53 -27
  228. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +16 -2
  229. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +4 -4
  230. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +124 -43
  231. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +36 -38
  232. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +10 -5
  233. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +16 -7
  234. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +36 -19
  235. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +18 -10
  236. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +3 -2
  237. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +23 -15
  238. package/docs/reference/decisions/0032-the-console-unlocks-with-a-callers-credential.md +101 -0
  239. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +22 -22
  240. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +6 -6
  241. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +13 -12
  242. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +23 -23
  243. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +6 -1
  244. package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +57 -46
  245. package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +5 -0
  246. package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +20 -8
  247. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +55 -40
  248. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +14 -13
  249. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +31 -24
  250. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +18 -10
  251. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +137 -86
  252. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +165 -91
  253. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +7 -7
  254. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +2 -2
  255. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md +109 -0
  256. package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +53 -23
  257. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +221 -153
  258. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +161 -96
  259. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +32 -9
  260. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +48 -33
  261. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +56 -30
  262. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +35 -25
  263. package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +1 -1
  264. package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +296 -0
  265. package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +347 -0
  266. package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
  267. package/docs/reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md +285 -0
  268. package/docs/reference/decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md +198 -0
  269. package/docs/reference/decisions.md +9 -4
  270. package/docs/reference/glossary.md +138 -45
  271. package/docs/reference/hooks.md +1077 -0
  272. package/docs/reference/http-api.md +988 -123
  273. package/docs/reference/machines.md +0 -1
  274. package/docs/why.md +7 -5
  275. package/package.json +10 -3
  276. package/src/actor.ts +194 -42
  277. package/src/agent.ts +56 -6
  278. package/src/ambient.ts +3 -5
  279. package/src/assembly-checks.ts +530 -4
  280. package/src/callers.ts +245 -0
  281. package/src/chart.ts +238 -26
  282. package/src/charts.ts +9 -2
  283. package/src/config.ts +566 -7
  284. package/src/conversations.ts +453 -0
  285. package/src/custodian.lua +17 -5
  286. package/src/custodian.ts +257 -77
  287. package/src/customize.ts +154 -7
  288. package/src/dispatch-effect.ts +142 -26
  289. package/src/durability.ts +2 -0
  290. package/src/duration.ts +20 -0
  291. package/src/effect-checks.ts +52 -4
  292. package/src/effect.ts +401 -132
  293. package/src/effector-wire.ts +278 -30
  294. package/src/fail.ts +17 -4
  295. package/src/gate-brand.ts +16 -0
  296. package/src/gate.ts +634 -48
  297. package/src/harness-client.ts +84 -35
  298. package/src/harness-env.ts +33 -0
  299. package/src/held-secrets.ts +197 -39
  300. package/src/holds.ts +5 -2
  301. package/src/hook-kinds/discord.ts +92 -0
  302. package/src/hook-kinds/door.ts +107 -0
  303. package/src/hook-kinds/github.ts +89 -0
  304. package/src/hook-kinds/gitlab.ts +72 -0
  305. package/src/hook-kinds/hook.ts +7 -0
  306. package/src/hook-kinds/index.ts +465 -0
  307. package/src/hook-kinds/slack.ts +151 -0
  308. package/src/hook-kinds/webhook.ts +113 -0
  309. package/src/hooks.ts +506 -0
  310. package/src/http-effect.ts +164 -70
  311. package/src/http.ts +933 -99
  312. package/src/index.ts +30 -5
  313. package/src/instance.ts +196 -26
  314. package/src/internal.ts +23 -3
  315. package/src/kube-client.ts +6 -3
  316. package/src/machine-doc.ts +29 -5
  317. package/src/mcp-effect.ts +424 -0
  318. package/src/model-keys.ts +24 -0
  319. package/src/mount-digest.ts +41 -7
  320. package/src/names.ts +82 -7
  321. package/src/notices.ts +576 -59
  322. package/src/open-body.ts +110 -30
  323. package/src/open-effect.ts +17 -4
  324. package/src/open.ts +40 -7
  325. package/src/parts.ts +125 -35
  326. package/src/party.ts +35 -1
  327. package/src/pick.ts +43 -9
  328. package/src/pool.ts +1172 -172
  329. package/src/registration.ts +249 -17
  330. package/src/repo-identity.ts +7 -3
  331. package/src/repos.ts +20 -2
  332. package/src/run-host.ts +1614 -190
  333. package/src/run-list.ts +191 -0
  334. package/src/sandbox-effect.ts +217 -66
  335. package/src/sandbox-kube.ts +116 -21
  336. package/src/secret-placement.ts +248 -35
  337. package/src/server.ts +53 -4
  338. package/src/setup.ts +158 -38
  339. package/src/snapshot-store.ts +565 -48
  340. package/src/steer.ts +111 -0
  341. package/src/stub-harness.ts +6 -2
  342. package/src/tokens.ts +362 -46
  343. package/src/vocabulary.ts +23 -4
  344. package/src/wire.ts +27 -10
  345. package/src/workspace.ts +314 -259
  346. package/src/worktree.ts +249 -0
  347. package/docs/reference/authoring-surface/type-noslotspec.md +0 -26
  348. package/docs/reference/authoring-surface/type-workspacespec.md +0 -37
  349. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +0 -61
  350. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +0 -65
package/src/parts.ts CHANGED
@@ -48,15 +48,18 @@
48
48
  import { basename } from "node:path";
49
49
  import { fileURLToPath } from "node:url";
50
50
  import type { AnyActorRef, AnyStateMachine, StateNode, UnknownActorLogic } from "xstate";
51
- import { isAgent, isOpenAgent, type AgentDeclaration } from "./agent.ts";
52
- import { argvText, isEffect, placementOf, type EffectParts, type Placement } from "./effect.ts";
51
+ import { isAgent, isOpenAgent, type AgentDeclaration, type AgentMcpServer, type WorkspaceAccess } from "./agent.ts";
52
+ import { argvText, isEffect, placementOf, type EffectParts, type EffectSlot, type Placement } from "./effect.ts";
53
+ import { isGateSlot } from "./gate.ts";
53
54
  import { effectBindingLine, openPartsOf, type OpenEffectPart } from "./open-effect.ts";
54
55
  import { isFailSlot } from "./fail.ts";
55
56
  import { isImageContext } from "./images.ts";
56
57
  import { isOpenBody, openBodyDeclarationOf, type OpenBodyDeclaration } from "./open-body.ts";
57
58
  import { open } from "./open.ts";
58
59
  import { repoIdentity } from "./repo-identity.ts";
60
+ import { refuseUnroutedOutcome } from "./effect-checks.ts";
59
61
  import { refuseUnroutedAgentFault } from "./mechanism-routes.ts";
62
+ import { refuseSlotActiveTwice } from "./party.ts";
60
63
  import { refuseLostOutput } from "./run-output.ts";
61
64
  import { userSeatOf, type Size, type UserContainer } from "./size.ts";
62
65
 
@@ -75,9 +78,9 @@ import { userSeatOf, type Size, type UserContainer } from "./size.ts";
75
78
  export { open } from "./open.ts";
76
79
 
77
80
  /**
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
+ * The repository of a Repo Slot: `{ url, ref }`. `url` is the Repo's git url. `ref` is the branch,
82
+ * the tag or the commit that the clone stays on, and that a new branch starts from; absent, the
83
+ * Repo's default branch. Where only a url is needed, a string is enough.
81
84
  *
82
85
  * `workspace()` and `customize()` refuse:
83
86
  * - an empty `url`: ``<where>: Repo Slot "<slot>" is bound to an empty url (got <value>) — a slot
@@ -102,7 +105,6 @@ export { open } from "./open.ts";
102
105
  *
103
106
  * export const machine = workspace(body, {
104
107
  * repos: { app },
105
- * spec: () => ({ branch: "jr2/hotfix" }),
106
108
  * });
107
109
  * ```
108
110
  *
@@ -113,8 +115,8 @@ export { open } from "./open.ts";
113
115
  export type Binding = {
114
116
  /** The Repo's git url. */
115
117
  url: string;
116
- /** The branch or commit that the branch Worktree starts from. Default: the Repo's default
117
- * branch. */
118
+ /** The branch, tag or commit that the clone stays on, and that a new branch starts from. Default:
119
+ * the Repo's default branch. */
118
120
  ref?: string;
119
121
  };
120
122
 
@@ -130,10 +132,11 @@ export type Binding = {
130
132
  * - a function of the run input, `({ input }) => url` or a {@link Binding}: each run chooses. The
131
133
  * url must match an entry of `git.credentials` in `jr2.config.ts`, or the run cannot use it.
132
134
  *
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
135
+ * The key of the slot is a directory name under `/work`. It starts with a letter, so never with
136
+ * `.`: `/work/.jr2/` is reserved for jr2. `workspace()` and `customize()` refuse a key that does
137
+ * not: ``<where>: Repo Slot key "<key>" is not a directory name — a slot becomes `/work/<slot>`,
138
+ * `/work/.jr2/` is reserved for jr2, and its order matters, so it starts with a letter and matches
139
+ * /^[A-Za-z][A-Za-z0-9._-]*$/ (ADR-0051, ADR-0075).`` They refuse a value of another
137
140
  * shape: ``<where>: Repo Slot "<slot>" is <what> — a slot is `open`, a url, `{ url, ref? }`, or a
138
141
  * mapper `({ input }) => url | { url, ref? }` over the door (ADR-0051).``
139
142
  *
@@ -163,7 +166,6 @@ export type Binding = {
163
166
  * docs: "https://github.com/you/docs.git",
164
167
  * config: open,
165
168
  * },
166
- * spec: ({ input }) => ({ branch: `deploy/${input.service}` }),
167
169
  * });
168
170
  * ```
169
171
  *
@@ -195,12 +197,13 @@ export function repoSlotState(value: RepoSlot<any>): RepoSlotState {
195
197
  * carry — and to what a prompt can name without quoting. It starts with a LETTER: the handles
196
198
  * keep declaration order, which a Machine may give a meaning to (`task`: the first slot is the
197
199
  * one the coder edits), and JS puts an integer-like key such as `"1"` ahead of every other key in
198
- * `Object.keys`, wherever the author wrote it. */
200
+ * `Object.keys`, wherever the author wrote it. So a key never starts with `.`, and `/work/.jr2/`
201
+ * stays jr2's (ADR-0075). */
199
202
  export const REPO_SLOT_KEY = /^[A-Za-z][A-Za-z0-9._-]*$/;
200
203
 
201
204
  /**
202
- * Refuse a malformed slot value BY NAME, at build time — the same derives-from-a-typo class the
203
- * spec guard catches for the branch, one build earlier. A bound url is also parsed here: an
205
+ * Refuse a malformed slot value BY NAME, at build time — a derives-from-a-typo fault, one build
206
+ * earlier. A bound url is also parsed here: an
204
207
  * identity the walk cannot derive (a relative path, an unknown scheme) is refused where the
205
208
  * author wrote it rather than at the converge that walks it. `where` names the caller
206
209
  * (`workspace()`, `customize()`).
@@ -209,8 +212,8 @@ export function assertRepoSlot(where: string, slot: string, value: unknown): ass
209
212
  if (!REPO_SLOT_KEY.test(slot)) {
210
213
  throw new Error(
211
214
  `${where}: Repo Slot key ${JSON.stringify(slot)} is not a directory name — a slot becomes ` +
212
- "`/work/<slot>` and its order matters, so it starts with a letter and matches " +
213
- "/^[A-Za-z][A-Za-z0-9._-]*$/ (ADR-0051).",
215
+ "`/work/<slot>`, `/work/.jr2/` is reserved for jr2, and its order matters, so it starts with a letter " +
216
+ "and matches /^[A-Za-z][A-Za-z0-9._-]*$/ (ADR-0051, ADR-0075).",
214
217
  );
215
218
  }
216
219
  if (value === open || typeof value === "function") return;
@@ -242,9 +245,8 @@ export function assertRepoSlot(where: string, slot: string, value: unknown): ass
242
245
  * to a concrete ref on the port's side, which is what keeps the Machine cluster-agnostic. The
243
246
  * Repos are Slots, each bound, open, or per-run.
244
247
  *
245
- * They are options rather than `WorkspaceSpec` fields because they are STATIC: `jr2 up` must find
246
- * them by walking the Machine, and a per-run spec is a function of run input that no walk can
247
- * evaluate.
248
+ * They are static options because `jr2 up` must find them by walking the Machine, and no walk can
249
+ * evaluate a function of run input.
248
250
  */
249
251
  export type SandboxParts = {
250
252
  /** The Sandbox Image. Absent → the Instance's `images/default`, then the stock Harness. */
@@ -258,6 +260,8 @@ export type SandboxParts = {
258
260
  /** The Size the Machine states (ADR-0060) — the `workspace()`'s, with any `customize()` layered
259
261
  * over it. Absent → the Instance's `sandbox.resources`, then the kit default. */
260
262
  resources?: Size;
263
+ /** The pod's work group (ADR-0005). Absent: the port's default. */
264
+ workGroup?: number;
261
265
  /** The Held secrets this Workspace's pod gets beside its Agents' model keys (ADR-0059): names of
262
266
  * the `secrets` list. Naming is placement — no other pod gets a name for being listed here. */
263
267
  secrets?: string[];
@@ -561,14 +565,15 @@ export type CarriedEffect = {
561
565
  (| {
562
566
  kind: "command";
563
567
  argv: string[];
564
- slot?: string;
565
568
  secrets?: string[];
566
569
  pin?: true;
567
570
  open?: Array<"argv" | "secret">;
571
+ /** A `worktree()`: its `expect`. A report prints it in place of the kit program's argv. */
572
+ cut?: { expect?: "new" | "existing" };
568
573
  }
569
574
  /** An http Effect (ADR-0068): its method, its fixed target, and the secrets it names — what
570
575
  * 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.
576
+ * file (ADR-0067), so it runs in its Workspace's Sandbox Effector.
572
577
  * An Open url has no target, and names only the secrets its headers and auth name. */
573
578
  | {
574
579
  kind: "http";
@@ -577,10 +582,9 @@ export type CarriedEffect = {
577
582
  secrets: string[];
578
583
  idempotent: boolean;
579
584
  file?: true;
580
- slot?: string;
581
585
  open?: Array<"url" | "secret">;
582
586
  }
583
- /** A push (ADR-0067, ADR-0051): the Workspace's branch to its Repo Slot's Binding. It names no
587
+ /** A push (ADR-0067, ADR-0075): a cut Worktree's branch to its Repo Slot's Binding. It names no
584
588
  * secret: `jr2 up` resolves its credential through `git.credentials`. */
585
589
  | { kind: "push"; slot?: string; sign: boolean; squash: boolean; force: boolean; pin: boolean; secrets: string[] }
586
590
  /** A dispatch (ADR-0071): the Workflow it starts a run of, as `jr2 run` names it, whether it
@@ -598,6 +602,20 @@ export type CarriedEffect = {
598
602
  input?: Record<string, unknown>;
599
603
  open?: Array<"target">;
600
604
  }
605
+ /** An mcp Effect (ADR-0079): what it asks of which server, by the server's name in the `mcp`
606
+ * block of `jr2.config.ts`. It names no secret of its own: `jr2 up` reads the server's url and
607
+ * secret from the config and places them in the Instance Effector. An Open server or tool is
608
+ * absent until bound. `timeoutMs` is the author's; 0 when the author set none, and the server's
609
+ * `timeout` then applies. */
610
+ | {
611
+ kind: "mcp";
612
+ op: "call" | "read" | "prompt";
613
+ server?: string;
614
+ tool?: string;
615
+ secrets: string[];
616
+ idempotent: boolean;
617
+ open?: Array<"server" | "tool">;
618
+ }
601
619
  );
602
620
 
603
621
  /** One state that can fail the run (ADR-0073): it invokes a `fail()` slot. `state` is the state's
@@ -646,9 +664,22 @@ export type CarriedParts = {
646
664
  /**
647
665
  * The Instance Harness is needed (ADR-0031): an Agent declares `workspace: "none"`, or an Open
648
666
  * 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.
667
+ * allowed ones. `servers` are the MCP servers those Agents name (ADR-0079), sorted, deduped.
668
+ * Absent when nothing needs it.
650
669
  */
651
- instanceHarness?: { models: string[] };
670
+ instanceHarness?: { models: string[]; servers: string[] };
671
+ /** Every Agent slot whose definition names an MCP server (ADR-0079), at each `customize()` route
672
+ * the walk reached it by, in walk order: what `jr2 up` checks against the config's `mcp` block. */
673
+ mcpAgents: CarriedMcpAgent[];
674
+ };
675
+
676
+ /** An Agent that names MCP servers (ADR-0079): its slot, the `customize()` route to its Machine,
677
+ * where its Turns run, and its `mcp` list as written. */
678
+ export type CarriedMcpAgent = {
679
+ slot: string;
680
+ path: OpenSlot["path"];
681
+ workspace: WorkspaceAccess;
682
+ mcp: readonly AgentMcpServer[];
652
683
  };
653
684
 
654
685
  /** A machine actor, told apart from a promise/callback/observable one by having a state tree.
@@ -739,6 +770,7 @@ export function partsOf(machines: Iterable<AnyStateMachine>): CarriedParts {
739
770
  const openEffects: CarriedParts["openEffects"] = [];
740
771
  const sizes: CarriedSize[] = [];
741
772
  const openBodies: CarriedParts["openBodies"] = [];
773
+ const mcpAgents: CarriedMcpAgent[] = [];
742
774
  // The Machine that declares each Open body: where its lent Agents are read (not carried data).
743
775
  const bodyWrappers = new Map<CarriedParts["openBodies"][number], AnyStateMachine>();
744
776
  let sandboxed = false;
@@ -808,6 +840,9 @@ export function partsOf(machines: Iterable<AnyStateMachine>): CarriedParts {
808
840
  refuseInlineParts(machine);
809
841
  refuseLostOutput(machine);
810
842
  refuseUnroutedAgentFault(machine);
843
+ refuseUnroutedOutcome(machine);
844
+ // Here too, for a Machine built with xstate's own setup: the Turns' identity is the runtime's.
845
+ refuseSlotActiveTwice(machine);
811
846
  const parts = sandboxPartsOf(machine);
812
847
  const user = userSeatOf(parts.user);
813
848
  collectImage(parts.image);
@@ -826,8 +861,11 @@ export function partsOf(machines: Iterable<AnyStateMachine>): CarriedParts {
826
861
  const body = wrapperBodyOf(machine);
827
862
  for (const { state, slot } of failingStatesOf(machine)) failures.push({ machine: machine.id, state, slot, path });
828
863
  for (const [name, logic] of Object.entries(machine.implementations.actors as Record<string, unknown>)) {
829
- if (isAgent(logic)) collectAgent(path, name, logic.definition);
830
- else if (isOpenBody(logic)) {
864
+ if (isAgent(logic)) {
865
+ collectAgent(path, name, logic.definition);
866
+ const { mcp, workspace } = logic.definition;
867
+ if (mcp?.length) mcpAgents.push({ slot: name, path, workspace: workspace ?? "write", mcp });
868
+ } else if (isOpenBody(logic)) {
831
869
  const body = { slot: name, declaration: openBodyDeclarationOf(logic), path };
832
870
  openBodies.push(body);
833
871
  bodyWrappers.set(body, machine);
@@ -836,6 +874,10 @@ export function partsOf(machines: Iterable<AnyStateMachine>): CarriedParts {
836
874
  effects.push(effect);
837
875
  if (effect.kind !== "push" && effect.open !== undefined)
838
876
  openEffects.push({ slot: name, path, parts: [...effect.open] });
877
+ } else if (lookReadOf(logic)) {
878
+ // A Gate slot's look reads with an http Effect (ADR-0011): the Instance Effector runs it, so
879
+ // `jr2 up` converges the Effector and places its secrets as for any Effect slot.
880
+ effects.push(carriedEffectOf(`${name}.look`, path, route, lookReadOf(logic)!.effect));
839
881
  } else {
840
882
  const child = asMachine(logic);
841
883
  if (child) walk(child, slotPath(path, name, body), name === body ? route : [...route, name]);
@@ -852,6 +894,7 @@ export function partsOf(machines: Iterable<AnyStateMachine>): CarriedParts {
852
894
  const noneBodies = openBodies.filter((b) => bodyTurnsRunIn(bodyWrappers.get(b)!, b.declaration, "instance"));
853
895
  const harnessModels = new Set<string>(menuOnly.map((a) => a.definition.model).filter(bound));
854
896
  for (const body of noneBodies) for (const m of body.declaration.allows.models) harnessModels.add(m);
897
+ const harnessServers = new Set(menuOnly.flatMap((a) => (a.definition.mcp ?? []).map((e) => e.server)));
855
898
  return {
856
899
  agents,
857
900
  failures,
@@ -865,7 +908,10 @@ export function partsOf(machines: Iterable<AnyStateMachine>): CarriedParts {
865
908
  composesSandbox: sandboxed,
866
909
  openBodies,
867
910
  models: [...models].sort(),
868
- ...(menuOnly.length || noneBodies.length ? { instanceHarness: { models: [...harnessModels].sort() } } : {}),
911
+ ...(menuOnly.length || noneBodies.length
912
+ ? { instanceHarness: { models: [...harnessModels].sort(), servers: [...harnessServers].sort() } }
913
+ : {}),
914
+ mcpAgents,
869
915
  };
870
916
  }
871
917
 
@@ -918,6 +964,11 @@ export function placedEffectsOf(root: AnyStateMachine): CarriedEffect[] {
918
964
  effects.push(carriedEffectOf(name, path, route, logic.effect));
919
965
  continue;
920
966
  }
967
+ const look = lookReadOf(logic);
968
+ if (look) {
969
+ effects.push(carriedEffectOf(`${name}.look`, path, route, look.effect));
970
+ continue;
971
+ }
921
972
  const child = asMachine(logic);
922
973
  if (child) walk(child, slotPath(path, name, body), name === body ? route : [...route, name], on);
923
974
  }
@@ -968,7 +1019,22 @@ export function carriedEffectOf(
968
1019
  idempotent: parts.idempotent,
969
1020
  timeoutMs: parts.timeoutMs,
970
1021
  ...(parts.file ? { file: true as const } : {}),
971
- ...(parts.file?.slot !== undefined ? { slot: parts.file.slot } : {}),
1022
+ };
1023
+ }
1024
+ if (parts.kind === "mcp") {
1025
+ return {
1026
+ name,
1027
+ path,
1028
+ route,
1029
+ kind: "mcp",
1030
+ placement,
1031
+ op: parts.op,
1032
+ ...(parts.server === open ? {} : { server: parts.server }),
1033
+ ...(parts.tool === undefined || parts.tool === open ? {} : { tool: parts.tool }),
1034
+ secrets: [],
1035
+ idempotent: parts.idempotent,
1036
+ timeoutMs: parts.timeoutMs ?? 0,
1037
+ ...(openParts.length ? { open: openParts as Array<"server" | "tool"> } : {}),
972
1038
  };
973
1039
  }
974
1040
  if (parts.kind === "push") {
@@ -988,7 +1054,7 @@ export function carriedEffectOf(
988
1054
  ...(slot !== undefined ? { slot } : {}),
989
1055
  };
990
1056
  }
991
- const { argv, timeoutMs, slot, secrets, pin } = parts;
1057
+ const { argv, timeoutMs, secrets, pin } = parts;
992
1058
  return {
993
1059
  name,
994
1060
  path,
@@ -997,10 +1063,10 @@ export function carriedEffectOf(
997
1063
  placement,
998
1064
  argv: argvText(argv),
999
1065
  timeoutMs,
1000
- ...(slot !== undefined ? { slot } : {}),
1001
1066
  ...(secrets?.length ? { secrets: [...secrets] } : {}),
1002
1067
  ...(pin ? { pin } : {}),
1003
1068
  ...(openParts.length ? { open: openParts as Array<"argv" | "secret"> } : {}),
1069
+ ...(parts.cut ? { cut: { ...parts.cut } } : {}),
1004
1070
  };
1005
1071
  }
1006
1072
 
@@ -1035,10 +1101,17 @@ export type WorkspacePod = {
1035
1101
  * down to the next `workspace()`, which is a pod of its own, and every model an Open body below
1036
1102
  * it allows a Chart's own Agents when it allows "write" or "read" (ADR-0070). Sorted, deduped. */
1037
1103
  models: string[];
1104
+ /** The MCP servers that the Agents whose Turns run in this pod name (ADR-0079), with the Agent
1105
+ * that names each and its allowlist: what places each server's secret in this pod, and what the
1106
+ * allowlist rule reads. Absent when none names one. */
1107
+ mcp?: Array<{ slot: string; path: OpenSlot["path"]; entry: AgentMcpServer }>;
1038
1108
  /** The Effects placed in this pod's Sandbox Effector (ADR-0068): every `push`, file-body `http` and
1039
1109
  * credentialed `command` slot below the `workspace()`, down to the next `workspace()` — read off
1040
1110
  * their plain data, statically. Empty → the pod gets no Sandbox Effector and no Secret. */
1041
1111
  effects: CarriedEffect[];
1112
+ /** The slot of each `worktree()` below the `workspace()`, down to the next `workspace()`. A cut runs
1113
+ * in the Harness container, so `effects` does not hold it. Absent when there is none. */
1114
+ cuts?: string[];
1042
1115
  /** The `workspace()`'s `effector: { image }`, as written. Absent → the Sandbox Image. */
1043
1116
  effector?: { image: string };
1044
1117
  /** The `workspace()`'s own `image`, as written — its Sandbox Image. Absent → `images/default`,
@@ -1067,6 +1140,8 @@ export function workspacePodsOf(root: AnyStateMachine): WorkspacePod[] {
1067
1140
  models: Set<string>,
1068
1141
  on: Set<AnyStateMachine>,
1069
1142
  effects: Map<string, CarriedEffect>,
1143
+ cuts: Set<string>,
1144
+ mcp: NonNullable<WorkspacePod["mcp"]>,
1070
1145
  path: OpenSlot["path"],
1071
1146
  route: PlacementRoute,
1072
1147
  ): void => {
@@ -1074,12 +1149,14 @@ export function workspacePodsOf(root: AnyStateMachine): WorkspacePod[] {
1074
1149
  on.add(machine);
1075
1150
  const body = wrapperBodyOf(machine);
1076
1151
  const visit = (child: AnyStateMachine, at: OpenSlot["path"], to: PlacementRoute) => {
1077
- if (!composesSandbox(child)) modelsIn(child, models, on, effects, at, to);
1152
+ if (!composesSandbox(child)) modelsIn(child, models, on, effects, cuts, mcp, at, to);
1078
1153
  };
1079
1154
  for (const [name, logic] of Object.entries(machine.implementations.actors as Record<string, unknown>)) {
1080
1155
  if (isAgent(logic)) {
1081
1156
  const { model, workspace } = logic.definition;
1082
1157
  if (workspace !== "none" && typeof model === "string") models.add(model);
1158
+ // A "none" Agent's servers are the Instance Harness's (ADR-0079), never this pod's.
1159
+ if (workspace !== "none") for (const entry of logic.definition.mcp ?? []) mcp.push({ slot: name, path, entry });
1083
1160
  continue;
1084
1161
  }
1085
1162
  // A Turn under an Open body may run in this pod with any model the body allows (ADR-0070):
@@ -1093,6 +1170,7 @@ export function workspacePodsOf(root: AnyStateMachine): WorkspacePod[] {
1093
1170
  if (isEffect(logic)) {
1094
1171
  const effect = carriedEffectOf(name, path, route, logic.effect);
1095
1172
  if (effect.placement === "sandbox") effects.set(JSON.stringify([route, name]), effect);
1173
+ if (logic.effect.kind === "command" && logic.effect.cut) cuts.add(name);
1096
1174
  continue;
1097
1175
  }
1098
1176
  const child = asMachine(logic);
@@ -1112,7 +1190,9 @@ export function workspacePodsOf(root: AnyStateMachine): WorkspacePod[] {
1112
1190
  if (composesSandbox(machine)) {
1113
1191
  const models = new Set<string>();
1114
1192
  const effects = new Map<string, CarriedEffect>();
1115
- modelsIn(machine, models, new Set(), effects, path, route);
1193
+ const cuts = new Set<string>();
1194
+ const mcp: NonNullable<WorkspacePod["mcp"]> = [];
1195
+ modelsIn(machine, models, new Set(), effects, cuts, mcp, path, route);
1116
1196
  const parts = sandboxPartsOf(machine);
1117
1197
  const bindings: Record<string, string | undefined> = {};
1118
1198
  if (parts.repos !== open) {
@@ -1127,6 +1207,8 @@ export function workspacePodsOf(root: AnyStateMachine): WorkspacePod[] {
1127
1207
  secrets: [...(parts.secrets ?? [])],
1128
1208
  models: [...models].sort(),
1129
1209
  effects: [...effects.values()],
1210
+ ...(cuts.size ? { cuts: [...cuts] } : {}),
1211
+ ...(mcp.length ? { mcp } : {}),
1130
1212
  ...(parts.effector ? { effector: { ...parts.effector } } : {}),
1131
1213
  ...(parts.image !== undefined ? { image: parts.image } : {}),
1132
1214
  bindings,
@@ -1145,6 +1227,14 @@ export function workspacePodsOf(root: AnyStateMachine): WorkspacePod[] {
1145
1227
  return pods;
1146
1228
  }
1147
1229
 
1230
+ /** The read of a Gate slot's bound look, when it is an Effect (ADR-0011); undefined otherwise. */
1231
+ function lookReadOf(logic: unknown): EffectSlot | undefined {
1232
+ if (!isGateSlot(logic)) return undefined;
1233
+ const look = logic.gateSlot.look;
1234
+ if (typeof look !== "object" || look === null) return undefined;
1235
+ return isEffect(look.read) ? look.read : undefined;
1236
+ }
1237
+
1148
1238
  /** Every state of `machine`'s own tree that invokes a `fail()` slot (ADR-0073), in document order,
1149
1239
  * with the slot key: a state that can fail the run. A child Machine's states are its own walk's. */
1150
1240
  export function failingStatesOf(machine: AnyStateMachine): Array<{ state: string; slot: string }> {
package/src/party.ts CHANGED
@@ -19,6 +19,7 @@
19
19
  import type { AnyActorRef, AnyEventObject, AnyMachineSnapshot, AnyStateMachine } from "xstate";
20
20
  import { SENDER_KEY, type Sender } from "@jr2/agent-protocol";
21
21
  import { isAgent } from "./agent.ts";
22
+ import { isGateInvoke } from "./gate-brand.ts";
22
23
  import { vocabularyOf } from "./vocabulary.ts";
23
24
 
24
25
  /** The guard type `sentBy` names: dotted, so no author guard can take the name. */
@@ -138,7 +139,7 @@ export function partiesAt(node: Node, machine: AnyStateMachine): Sender[] {
138
139
  const out: Sender[] = [];
139
140
  for (const inv of node.invoke) {
140
141
  if (typeof inv.src !== "string") continue;
141
- if (inv.src === "gate") out.push({ party: "gate", id: inv.id });
142
+ if (isGateInvoke(inv, actors)) out.push({ party: "gate", id: inv.id });
142
143
  else if (isAgent(actors[inv.src])) out.push({ party: "agent", id: inv.src });
143
144
  }
144
145
  return out;
@@ -422,6 +423,39 @@ function orthogonal(a: Node, b: Node): boolean {
422
423
  /** Can the two states be active at the same time? */
423
424
  const together = (a: Node, b: Node): boolean => isAncestorOrSelf(a, b) || isAncestorOrSelf(b, a) || orthogonal(a, b);
424
425
 
426
+ /**
427
+ * Refuse one Agent slot invoked by two states that can be active together (ADR-0015): in two
428
+ * parallel regions, one state inside the other, or twice by one state. A slot is one Agent of this
429
+ * Machine: its Turns share one sender, so `sentBy` cannot tell them apart and one pick matches both
430
+ * writings, and they share one conversation, so `continue` cannot hold two. Two Agents at once are
431
+ * two slots (one definition can fill both), a child Machine in each region, or a Pool (ADR-0075).
432
+ */
433
+ export function refuseSlotActiveTwice(machine: AnyStateMachine): void {
434
+ const bySlot = new Map<string, Node[]>();
435
+ for (const node of allNodes(machine))
436
+ for (const p of partiesAt(node, machine))
437
+ if (p.party === "agent") bySlot.set(p.id, [...(bySlot.get(p.id) ?? []), node]);
438
+ for (const [slot, nodes] of bySlot) {
439
+ for (let i = 0; i < nodes.length; i++) {
440
+ for (let j = i + 1; j < nodes.length; j++) {
441
+ const [a, b] = [nodes[i]!, nodes[j]!];
442
+ if (!together(a, b)) continue;
443
+ const how =
444
+ a === b
445
+ ? `state "${a.id}" invokes it twice`
446
+ : `"${a.id}" and "${b.id}" invoke it and can be active together ` +
447
+ (orthogonal(a, b) ? "(parallel regions)" : "(one inside the other)");
448
+ throw new Error(
449
+ `machine "${machine.id}": Agent "${slot}" would run two Turns at once: ${how}. A slot is one Agent, with ` +
450
+ "one sender and one conversation, so `sentBy` cannot tell its two Turns apart and `continue` cannot " +
451
+ "hold both. For two Agents at once, use two slots (one definition can fill both), a child Machine in " +
452
+ "each region, or a Pool (ADR-0075)",
453
+ );
454
+ }
455
+ }
456
+ }
457
+ }
458
+
425
459
  /** One writing: one `on` key on one state, with its candidates (an array writing has several). */
426
460
  type Writing = { node: Node; name: string; candidates: Transition[] };
427
461
 
package/src/pick.ts CHANGED
@@ -63,9 +63,11 @@ import type { PickAnswer } from "./wire.ts";
63
63
  * command,
64
64
  * defineEvent,
65
65
  * fail,
66
+ * isReady,
66
67
  * jr2Setup,
67
68
  * paramsOf,
68
69
  * workspace,
70
+ * worktree,
69
71
  * type Workspaced,
70
72
  * } from "@jr2/orchestrator";
71
73
  *
@@ -83,6 +85,8 @@ import type { PickAnswer } from "./wire.ts";
83
85
  *
84
86
  * type Input = Workspaced<{}, "app">;
85
87
  *
88
+ * const fixTree = (context: Input) => `${context.workspace.repos.app}/fix` as const;
89
+ *
86
90
  * const body = jr2Setup({
87
91
  * types: {} as { context: Input; input: Input },
88
92
  * events: [runTest, finished],
@@ -91,6 +95,7 @@ import type { PickAnswer } from "./wire.ts";
91
95
  * model: "anthropic/claude-haiku-5-5",
92
96
  * instructions: "You are a careful engineer.",
93
97
  * }),
98
+ * cut: worktree(),
94
99
  * test: command(["node", "--test", arg("file")]),
95
100
  * fail: fail(),
96
101
  * },
@@ -98,12 +103,22 @@ import type { PickAnswer } from "./wire.ts";
98
103
  * id: "fix-tests",
99
104
  * context: ({ input }) => input,
100
105
  * on: { "workspace.lost": ".lost", "agent.fault": ".failed" },
101
- * initial: "coding",
106
+ * initial: "cutting",
102
107
  * states: {
108
+ * cutting: {
109
+ * invoke: {
110
+ * src: "cut",
111
+ * input: ({ context }) => ({ path: fixTree(context), branch: "fix-tests" }),
112
+ * onDone: [{ guard: ({ event }) => isReady(event.output), target: "coding" }, { target: "notCut" }],
113
+ * },
114
+ * },
103
115
  * coding: {
104
116
  * invoke: {
105
117
  * src: "coder",
106
- * input: { prompt: "Make the tests pass. Run the tests of one module at a time." },
118
+ * input: ({ context }) => ({
119
+ * prompt: "Make the tests pass. Run the tests of one module at a time.",
120
+ * cwd: fixTree(context),
121
+ * }),
107
122
  * },
108
123
  * on: { finished: "done" },
109
124
  * initial: "ready",
@@ -113,13 +128,17 @@ import type { PickAnswer } from "./wire.ts";
113
128
  * invoke: {
114
129
  * src: "test",
115
130
  * // A plain mapper: the Machine makes the file name from the pick's `module`.
116
- * input: ({ event }) => ({ file: `test/${paramsOf(event, runTest).module}.test.js` }),
131
+ * input: ({ context, event }) => ({
132
+ * file: `test/${paramsOf(event, runTest).module}.test.js`,
133
+ * worktree: fixTree(context),
134
+ * }),
117
135
  * onDone: "ready",
118
136
  * },
119
137
  * },
120
138
  * },
121
139
  * },
122
140
  * done: { type: "final" },
141
+ * notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
123
142
  * failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
124
143
  * lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
125
144
  * },
@@ -127,7 +146,6 @@ import type { PickAnswer } from "./wire.ts";
127
146
  *
128
147
  * export const machine = workspace(body, {
129
148
  * repos: { app: { url: "https://github.com/you/app.git" } },
130
- * spec: () => ({ branch: "fix-tests" }),
131
149
  * });
132
150
  * ```
133
151
  *
@@ -406,8 +424,8 @@ export function fromPick<N extends string, I extends z.ZodObject, C, E extends E
406
424
  }
407
425
 
408
426
  /**
409
- * One reply since the pick, as the author reads it: an invoked actor's output, typed by its slot
410
- * (`src`); or an event that answered a state below the Agent's — a Gate's answer, a nested Agent's
427
+ * One reply since the pick, as the author reads it. It is an invoked actor's output, typed by its
428
+ * slot (`src`); or an event that answered a state below the Agent's — a Gate's answer, a nested Agent's
411
429
  * pick — typed by the Machine's events. A reply is from outside the Machine: the events it sends
412
430
  * itself are not links.
413
431
  */
@@ -640,8 +658,10 @@ export const isAnswer = (action: unknown): boolean =>
640
658
  * command,
641
659
  * defineEvent,
642
660
  * fail,
661
+ * isReady,
643
662
  * jr2Setup,
644
663
  * workspace,
664
+ * worktree,
645
665
  * type Workspaced,
646
666
  * } from "@jr2/orchestrator";
647
667
  *
@@ -659,6 +679,8 @@ export const isAnswer = (action: unknown): boolean =>
659
679
  *
660
680
  * type Input = Workspaced<{}, "app">;
661
681
  *
682
+ * const fixTree = (context: Input) => `${context.workspace.repos.app}/fix` as const;
683
+ *
662
684
  * const body = jr2Setup({
663
685
  * types: {} as { context: Input; input: Input },
664
686
  * events: [runTests, finished],
@@ -667,6 +689,7 @@ export const isAnswer = (action: unknown): boolean =>
667
689
  * model: "anthropic/claude-haiku-5-5",
668
690
  * instructions: "You are a careful engineer.",
669
691
  * }),
692
+ * cut: worktree(),
670
693
  * tests: command(["npm", "test"]),
671
694
  * fail: fail(),
672
695
  * },
@@ -674,10 +697,20 @@ export const isAnswer = (action: unknown): boolean =>
674
697
  * id: "fix-tests",
675
698
  * context: ({ input }) => input,
676
699
  * on: { "workspace.lost": ".lost", "agent.fault": ".failed" },
677
- * initial: "coding",
700
+ * initial: "cutting",
678
701
  * states: {
702
+ * cutting: {
703
+ * invoke: {
704
+ * src: "cut",
705
+ * input: ({ context }) => ({ path: fixTree(context), branch: "fix-tests" }),
706
+ * onDone: [{ guard: ({ event }) => isReady(event.output), target: "coding" }, { target: "notCut" }],
707
+ * },
708
+ * },
679
709
  * coding: {
680
- * invoke: { src: "coder", input: { prompt: "Make the tests pass." } },
710
+ * invoke: {
711
+ * src: "coder",
712
+ * input: ({ context }) => ({ prompt: "Make the tests pass.", cwd: fixTree(context) }),
713
+ * },
681
714
  * on: { finished: "done" },
682
715
  * initial: "ready",
683
716
  * states: {
@@ -685,6 +718,7 @@ export const isAnswer = (action: unknown): boolean =>
685
718
  * testing: {
686
719
  * invoke: {
687
720
  * src: "tests",
721
+ * input: ({ context }) => ({ worktree: fixTree(context) }),
688
722
  * onDone: {
689
723
  * target: "ready",
690
724
  * // The Agent reads this string, not the whole output of the command.
@@ -699,6 +733,7 @@ export const isAnswer = (action: unknown): boolean =>
699
733
  * },
700
734
  * },
701
735
  * done: { type: "final" },
736
+ * notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
702
737
  * failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
703
738
  * lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
704
739
  * },
@@ -706,7 +741,6 @@ export const isAnswer = (action: unknown): boolean =>
706
741
  *
707
742
  * export const machine = workspace(body, {
708
743
  * repos: { app: { url: "https://github.com/you/app.git" } },
709
- * spec: () => ({ branch: "fix-tests" }),
710
744
  * });
711
745
  * ```
712
746
  *