@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
@@ -0,0 +1,304 @@
1
+ ---
2
+ title: Answer a Gate from a forge
3
+ description: Make a Gate that a review on GitHub or GitLab answers, and that looks for the review it waits for.
4
+ ---
5
+
6
+ # Answer a Gate from a forge
7
+
8
+ Make a [Gate](../reference/glossary.md#gate) that a review on a forge, such as GitHub, answers through a
9
+ [Hook](../reference/glossary.md#hook), and that looks for the review when no delivery comes.
10
+
11
+ A Gate slot is a Gate that hears a Hook. You declare it with `gate({ … })` in `actors`, and a state invokes it by its
12
+ key. It is the same Gate as `src: "gate"`: it has the same id, the same events, the same card in the
13
+ [Console](../reference/glossary.md#console), and `jr2 send` can answer it too. It adds four parts:
14
+
15
+ - `meta`: a zod object of the data that the state gives the Gate. The Gate checks the data when it opens.
16
+ - `delivers`: the events that the Gate can give from a delivery or from its look.
17
+ - `hooks`: a handler for each [Hook kind](../reference/glossary.md#hook-kind) and event key. A handler reads the payload
18
+ of a delivery, and gives one event of `delivers`, or `undefined` when the delivery is not for this Gate.
19
+ - `look`: a read of the fact that the Gate waits for, with an `http` Effect that is marked `idempotent: true`.
20
+
21
+ The example is the [Workflow](../reference/glossary.md#workflow) `review`. A run waits for a review of one commit of one
22
+ pull request. The `github` kind is `@jr2/orchestrator/hooks/github`, so your
23
+ [Instance](../reference/glossary.md#instance) installs nothing more.
24
+
25
+ Save the file as `workflows/review.ts`:
26
+
27
+ ```ts
28
+ // workflows/review.ts: a run that waits for a review of one pull request on GitHub. A review on
29
+ // GitHub answers the Gate, through a Hook of the `github` kind. The look reads the reviews of the
30
+ // pull request, so a review that came before the Gate opened, or while the Orchestrator was down,
31
+ // also answers it.
32
+
33
+ import { z } from "zod";
34
+ import type {} from "@jr2/orchestrator/hooks/github";
35
+ import { bearer, defineEvent, gate, http, jr2Setup, secret, type HttpOutput } from "@jr2/orchestrator";
36
+
37
+ const approve = defineEvent({
38
+ name: "approve",
39
+ description: "A reviewer approved the commit.",
40
+ input: z.object({ by: z.string() }),
41
+ });
42
+ const changes = defineEvent({
43
+ name: "changes",
44
+ description: "A reviewer asked for changes.",
45
+ input: z.object({ by: z.string(), notes: z.string() }),
46
+ });
47
+
48
+ type Meta = { owner: string; repo: string; pr: number; url: string; sha: string };
49
+
50
+ // The reviews of the pull request, from the GitHub API. The look reads them again, so the call is
51
+ // marked idempotent.
52
+ const reviews = http({
53
+ method: "GET",
54
+ url: "https://api.github.com",
55
+ auth: bearer(secret("github-token")),
56
+ headers: { accept: "application/vnd.github+json" },
57
+ idempotent: true,
58
+ input: z.object({ owner: z.string(), repo: z.string(), pr: z.number() }),
59
+ request: ({ owner, repo, pr }) => ({ path: ["repos", owner, repo, "pulls", String(pr), "reviews"] }),
60
+ });
61
+
62
+ /** The last verdict on the commit, from the API's answer. */
63
+ function verdictOf(output: HttpOutput, meta: Meta) {
64
+ if (output.outcome !== "response" || output.status !== 200 || output.body === undefined) return undefined;
65
+ const list = JSON.parse(output.body) as Array<{
66
+ state: string;
67
+ commit_id: string;
68
+ body: string | null;
69
+ user: { login: string };
70
+ }>;
71
+ const last = list.filter((r) => r.commit_id === meta.sha && r.state !== "COMMENTED").at(-1);
72
+ if (last?.state === "APPROVED") return { type: "approve" as const, by: last.user.login };
73
+ if (last?.state === "CHANGES_REQUESTED")
74
+ return { type: "changes" as const, by: last.user.login, notes: last.body ?? "" };
75
+ return undefined;
76
+ }
77
+
78
+ const review = gate({
79
+ meta: z.object({ owner: z.string(), repo: z.string(), pr: z.number(), url: z.string(), sha: z.string() }),
80
+ delivers: [approve, changes],
81
+ hooks: {
82
+ github: {
83
+ "pull_request_review.submitted": ({ payload, meta }) => {
84
+ // A review of another pull request, or of an older commit, is not for this Gate.
85
+ if (payload.pull_request.html_url !== meta.url || payload.review.commit_id !== meta.sha) return undefined;
86
+ if (payload.review.state === "approved") return { type: "approve", by: payload.review.user.login };
87
+ if (payload.review.state === "changes_requested") {
88
+ return { type: "changes", by: payload.review.user.login, notes: payload.review.body ?? "" };
89
+ }
90
+ return undefined;
91
+ },
92
+ },
93
+ },
94
+ look: {
95
+ read: reviews,
96
+ input: (meta) => ({ owner: meta.owner, repo: meta.repo, pr: meta.pr }),
97
+ every: "10m",
98
+ until: verdictOf,
99
+ },
100
+ });
101
+
102
+ type Input = Meta;
103
+
104
+ export const machine = jr2Setup({
105
+ types: {} as { context: Input & { notes?: string }; input: Input },
106
+ events: [approve, changes],
107
+ actors: { review },
108
+ }).createMachine({
109
+ id: "review",
110
+ input: z.object({ owner: z.string(), repo: z.string(), pr: z.number(), url: z.string(), sha: z.string() }),
111
+ context: ({ input }) => input,
112
+ initial: "inReview",
113
+ states: {
114
+ inReview: {
115
+ invoke: {
116
+ src: "review",
117
+ input: ({ context }) => ({
118
+ meta: { owner: context.owner, repo: context.repo, pr: context.pr, url: context.url, sha: context.sha },
119
+ }),
120
+ },
121
+ on: { approve: "approved", changes: "changesAsked" },
122
+ },
123
+ approved: { type: "final" },
124
+ changesAsked: { type: "final" },
125
+ },
126
+ });
127
+ ```
128
+
129
+ The `import type {} from "@jr2/orchestrator/hooks/github"` line gives the handlers their types: the event key
130
+ `pull_request_review.submitted` and its payload. A key that no kind in scope gives does not compile. A handler reads
131
+ only the fields that the kind's schema names.
132
+
133
+ ## The handler
134
+
135
+ A delivery of the kind and the event key goes to each open Gate whose handlers hear them. Thus each handler compares the
136
+ payload with the Gate's `meta`. In the example, the handler gives `undefined` for a review of another pull request or of
137
+ an older commit. A handler is pure and synchronous: it does no I/O and reads no clock.
138
+
139
+ The event of a handler has the Gate as its [Sender](../reference/glossary.md#sender), with the Hook as its
140
+ [Caller](../reference/glossary.md#caller). The run takes the event only when it takes it from this Gate now, as for
141
+ `jr2 send`.
142
+
143
+ ## The look
144
+
145
+ A delivery can come before the Gate opens, or it can be lost while the
146
+ [Orchestrator](../reference/glossary.md#orchestrator) is down. The look reads the fact, so these cases come to the same
147
+ read:
148
+
149
+ - The look reads once after the Gate opens, again after each restart of the Orchestrator, and every `every` while the
150
+ Gate is open. The default `every` is `"10m"`.
151
+ - `until` gives one event of `delivers` from the read's output, or `undefined` for "not yet".
152
+ - When the run takes the event, the look stops until the Gate opens again. The run's feed shows `looked`. A looked event
153
+ has the Gate as its Sender and no Caller.
154
+ - The `http` Effect of the look is an Effect, so the secret that it names is an entry of the `secrets` list, as
155
+ `github-token` is in the example's `jr2.config.ts`. Put its value in `.env` as `GITHUB_TOKEN`.
156
+
157
+ A state that goes back to its own Gate must change the `meta`, for example to a new `sha`. If not, the look reads the
158
+ same verdict again.
159
+
160
+ Give `look: "none"` to a Gate that hears a command, such as a `/retry` comment. A command is not a fact that a read can
161
+ find, so a lost delivery of a command stays lost.
162
+
163
+ ## The Hook
164
+
165
+ The Gate hears a delivery only when the [Instance](../reference/glossary.md#instance) declares a Hook of its kind. Each
166
+ Hook is a name in the `hooks` block of `jr2.config.ts`, and its address is `POST /hooks/<name>`. Set up the service with
167
+ this address one time:
168
+
169
+ ```ts
170
+ import { defineConfig, secret } from "@jr2/orchestrator";
171
+ import { github } from "@jr2/orchestrator/hooks/github";
172
+ import { gitlab } from "@jr2/orchestrator/hooks/gitlab";
173
+
174
+ export default defineConfig({
175
+ // Each Hook's secret is an entry of the list. jr2 up places GH_HOOK in the Instance Effector, and
176
+ // gives the Orchestrator only the sha-256 of GL_HOOK.
177
+ secrets: [
178
+ { name: "GH_HOOK", value: process.env.GH_HOOK },
179
+ { name: "GL_HOOK", value: process.env.GL_HOOK },
180
+ // The look of review reads the reviews with this token. jr2 up places it in the Instance Effector.
181
+ { name: "github-token", value: process.env.GITHUB_TOKEN },
182
+ ],
183
+ hooks: {
184
+ // POST /hooks/forge: GitHub signs each delivery with the secret GH_HOOK. The Hook may answer
185
+ // the Gates of review alone.
186
+ forge: github({ secret: secret("GH_HOOK"), may: { answer: ["review"] } }),
187
+ // POST /hooks/lab: GitLab sends the secret GL_HOOK as a token in a header.
188
+ lab: gitlab({ secret: secret("GL_HOOK"), may: { answer: ["review"] } }),
189
+ },
190
+ });
191
+ ```
192
+
193
+ A Hook takes a credential and `may.answer`. `may.answer` is the list of Workflows whose Gates the Hook can answer. The
194
+ default is every Workflow. A Hook never starts a run. The kind tells how the service signs:
195
+
196
+ - `github` and `slack` sign with a shared secret (`mac`). The
197
+ [Instance Effector](../reference/glossary.md#instance-effector) holds the secret and checks the signature. The
198
+ Orchestrator does not hold the secret.
199
+ - `gitlab` sends a shared token in a header (`token`). The Orchestrator keeps only the sha-256 of the token, and removes
200
+ the header before the kind reads the delivery.
201
+ - `discord` signs with a key pair (`ed25519`). The config holds the public key, which cannot sign.
202
+
203
+ Each secret that a Hook names is an entry of the `secrets` list. For a `mac` Hook, [`jr2 up`](../reference/cli.md#up)
204
+ places the secret in the Instance Effector alone, as it places an
205
+ [Effect secret](../reference/glossary.md#effect-secret), and it converges the Instance Effector for the Hook. For a
206
+ `token` Hook, `jr2 up` writes the sha-256 of the secret into the Secret `jr2-hook-digests`, which the Orchestrator
207
+ mounts. The Orchestrator reads it again when it changes, so a new value needs no restart. The kubelet updates the mount
208
+ in about one minute, and `jr2 up` waits for it before it says `converged`. A kind that posts a follow-up, such as the
209
+ reply to a Slack click, declares the hosts of the post. `jr2 up` adds these hosts to the Instance Effector's list of
210
+ targets, with no secret. The output of `jr2 up` names the Hook as `hook forge`, in the line of the secret and in the
211
+ line of the Instance Effector.
212
+
213
+ When a delivery comes, the door does these steps in this sequence:
214
+
215
+ 1. The door refuses a body that is larger than 2 MiB. It reads no more of the body than this limit.
216
+ 2. The verifier checks the delivery. A signed time that is more than 300 seconds from the Orchestrator's clock fails the
217
+ check.
218
+ 3. The kind reads the delivery. A handshake (a GitHub `ping`, a Discord `PING`, a Slack `url_verification`) gets the
219
+ kind's answer and goes to no Gate.
220
+ 4. The kind's schema parses the payload.
221
+ 5. The door looks for a repeat: a delivery whose id or body this Hook got before.
222
+ 6. The delivery goes to each open Gate, in each live run of a Workflow that the Hook may answer, whose handlers hear its
223
+ kind and event key.
224
+
225
+ The answer is the outcome:
226
+ `{ delivery, seen, delivered: [{ run, workflow, gate, event }], refused: [{ run, gate, event?, reason }], ignored, sources }`.
227
+ `ignored` counts the open Gates whose handler gave `undefined`. A Gate whose handler throws, or gives an event that the
228
+ run does not take now, is in `refused`, and the run's feed shows `hook-refused`. The run continues.
229
+
230
+ To see what the door did, print the Hook's log. It keeps each delivery for `runs.retain` (30 days by default), newest
231
+ first:
232
+
233
+ ```sh
234
+ jr2 hooks forge
235
+ ```
236
+
237
+ ## A Gate in a package
238
+
239
+ A [packaged Machine](publish-a-machine-as-an-npm-package.md) writes its handlers, because a forge's payload is public.
240
+ It leaves its look `open`, because the look needs a url and a token. The composer binds the look with `customize()`, and
241
+ can add, replace or remove a handler by kind and event key, where `null` removes one:
242
+ `customize(machine, { gates: { review: { look, hooks: { gitlab: { "note.MergeRequest": null } } } } })`.
243
+
244
+ ## Rules
245
+
246
+ - The door refuses a delivery that fails the verifier, or whose signed time is more than 300 seconds from the
247
+ Orchestrator's clock, with 401. Nothing else occurs, and the Hook's log keeps no line.
248
+ - The door refuses a body that is larger than 2 MiB with 413, before the verifier:
249
+ `the delivery's body is larger than 2097152 bytes, the most the door reads` The Hook's log keeps a `too-large` line,
250
+ which does not claim the delivery's id.
251
+ - The door refuses a name that the config does not declare with 404:
252
+ `no Hook named "<name>" in the current Assembly's config`
253
+ - With no current Assembly, the door answers 503:
254
+ `the Orchestrator has no current Assembly, so no Hook is declared now — ask again`
255
+ - `jr2 up` refuses a `mac` Hook whose secret is not in the `secrets` list:
256
+ ``hooks.<name> names secret("<NAME>"), which the `secrets` list does not hold — add it: secrets: [{ name: "<NAME>", value: process.env.<NAME> }] (ADR-0068)``
257
+ It refuses a `token` Hook whose secret is not in the list with the same text, which ends with `(ADR-0078)`.
258
+ - `jr2 up` names the secret of a `token` Hook: `secret <NAME>: a digest in the Orchestrator (hook <name>)`
259
+ - A `mac` Hook whose check the Instance Effector cannot answer gets 503:
260
+ `the Hook's signature cannot be checked now: no Instance Effector (ADR-0068)`
261
+ - A request that the kind cannot read gets 400: `the <kind> kind cannot read the delivery: <error>`
262
+ - A payload that the kind's schema refuses gets 200, with `payloadRefused: { kind, key, issues }`. It goes to no Gate.
263
+ When the service sends it again, the door parses it again.
264
+ - A delivery whose id or body the Hook got before gets 200 with `seen: true`, and goes to no Gate.
265
+ - The config load refuses a Hook's name that is not lowercase letters, digits and `-`, at most 40 characters:
266
+ `<file> at hooks.<name>: a Hook's name is lowercase letters, digits and -, at most 40, starts with a letter and ends with a letter or digit (ADR-0078)`
267
+ - The config load refuses a value that no kind made:
268
+ `<file> at hooks.<name>: not a Hook — make it with its kind, such as github({ secret: secret("GH_HOOK") }) (ADR-0078)`
269
+ - `jr2 hooks <name>` needs the Instance token.
270
+
271
+ - `gate()` refuses a part that it does not know:
272
+ `gate(): "<key>" is not a part of a Gate slot (meta, delivers, hooks, look, reply) (ADR-0011)`
273
+ - `gate()` refuses an empty `delivers`: `gate(): its delivers lists at least one event that defineEvent made (ADR-0011)`
274
+ - `gate()` refuses a look whose read is a different Effect:
275
+ ``gate(): its look reads with <an http Effect that is not marked idempotent: true | a <kind> Effect> — a look reads again after each restore and every `every`, so its read is an http Effect marked idempotent: true, or a promise (ADR-0067) (ADR-0011)``
276
+ - `gate()` refuses an `every` that is not a duration:
277
+ `gate(): its look's every "<every>" is not a positive duration (like "90s", "10m", "2h") (ADR-0011)`
278
+ - `createMachine` refuses an event of `delivers` that the Machine does not declare:
279
+ `machine "<id>": Gate slot "<slot>" delivers "<event>", which no def in jr2Setup({ events }) declares (declared: <events>) — list it in events (ADR-0011)`
280
+ - `createMachine` refuses an event of `delivers` that the Gate does not take at the state that invokes it:
281
+ `machine "<id>": state "<state>" invokes Gate slot "<slot>", which delivers "<event>", and its Gate does not accept it (accepts: <events>) — no handler or look could deliver it: handle it on that state, a state above it or a plain child, or drop it from delivers (ADR-0011)`
282
+ - The Gate does not open when its `meta` does not agree with the schema, and the invoke fails:
283
+ `gate "<gate>": its meta does not match the Gate slot's meta schema — <path>: <message>`
284
+ - `customize()` refuses a slot that holds no Gate slot:
285
+ `customize(): machine "<id>" carries no Gate slot "<slot>" — its Gate slots are: <slots> (ADR-0011). Only declared parts can be bound.`
286
+ - `customize()` refuses `open` as a look:
287
+ ``customize(): Gate slot "<slot>"'s look is bound once, to a look or "none" — not `open` again (ADR-0054)``
288
+ - The Chart door refuses a Chart that invokes a lent Gate slot whose `delivers` holds an event that is not in the
289
+ Chart's `events`:
290
+ ``<path>: the Gate slot "<slot>" delivers "<event>", which is not in the Chart's `events` (events: <events>); declare it in `events` and handle it on this state``
291
+ - `jr2 up` and `jr2 check` warn, and do not refuse, about a Gate slot that hears a Hook and has no look and no `after`
292
+ on its state:
293
+ ``warning: workflow "<workflow>": Gate slot "<slot>" on "<state>" hears a Hook and has no look and no `after` on its state — a delivery lost in an outage stops the run until a person answers the Gate: give it a look, or look: "none" for a Gate that hears a command (ADR-0011)``
294
+ - `jr2 up` and `jr2 check` warn about a look that a package left `open` and that nobody bound:
295
+ `warning: workflow "<workflow>": Gate slot "<slot>" on "<state>" leaves its look open — a delivery lost in an outage stops the run until a person answers the Gate: bind it with customize(machine, { gates: { <slot>: { look } } }), or give it look: "none" (ADR-0011, ADR-0054)`
296
+
297
+ ## Read next
298
+
299
+ - [Gates](../concepts/gates.md) and [Callers and Hooks](../concepts/callers-and-hooks.md)
300
+ - [Write a Hook kind](write-a-hook-kind.md), for a service that jr2 ships no kind for
301
+ - [Answer a Gate](answer-a-gate.md) and [Bind Open parts](bind-open-parts.md)
302
+ - [ADR-0011](../reference/decisions/0011-workflow-defined-events.md): a Gate hears a Hook, and looks before it waits
303
+ - [ADR-0078](../reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md): a
304
+ Hook is a Caller that can only answer a Gate or bring items to a Source
@@ -84,7 +84,7 @@ jr2 run release --input '{"version":"1.4.0"}'
84
84
 
85
85
  ```text
86
86
  → context <context> / namespace <namespace>
87
- run 2319932e-02c1-4f9d-98db-3c5a410395da (release) — attached
87
+ run 0c948d65-fd6f-453a-87fa-71ec00c7fe68 (release) — attached
88
88
  → active "review"
89
89
  ```
90
90
 
@@ -96,12 +96,13 @@ next steps in a second terminal. [Start a run](start-a-run.md) gives the other w
96
96
  Run [`jr2 status`](../reference/cli.md#status) with the run id:
97
97
 
98
98
  ```sh
99
- jr2 status 2319
99
+ jr2 status 0c94
100
100
  ```
101
101
 
102
102
  ```text
103
103
  → context <context> / namespace <namespace>
104
- {"runId":"2319932e-02c1-4f9d-98db-3c5a410395da","workflow":"release","instanceId":"c898d8c7-e48b-4a56-8101-7148905b6aed","status":"active","value":"review","context":{"version":"1.4.0"},"children":[],"waiting":[],"held":[],"assembly":"83858e28b054","gates":[{"gate":"review","path":["review"],"accepts":[{"name":"approve","description":"Release this version.","input":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{},"additionalProperties":false}},{"name":"reject","description":"Do not release this version. Say why.","input":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"reason":{"type":"string","minLength":1}},"required":["reason"],"additionalProperties":false}}],"acceptsNow":["approve","reject"],"meta":{"version":"1.4.0"}}]}
104
+ {"runId":"0c948d65-fd6f-453a-87fa-71ec00c7fe68","workflow":"release","instanceId":"6db54032-0b87-4005-9583-049f7335b75f","startedAt":"2026-11-06T19:15:39.018Z","startedBy":{"kind":"caller","via":"token","name":"instance","onBehalfOf":"kubernetes-admin"},"status":"active","value":"review","context":{"version":"1.4.0"},"children":[],"waiting":[],"held":[],"assembly":"19d0bf0cbe10","gates":[{"gate":"review","path":["review"],"accepts":[{"name":"approve","description":"Release this version.","input":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{},"additionalProperties":false}},{"name":"reject","description":"Do not release this version. Say why.","input":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"reason":{"type":"string","minLength":1}},"required":["reason"],"additionalProperties":false}}],"acceptsNow":["approve","reject"],"meta":{"version":"1.4.0"}}]}
105
+ started by: token/instance on behalf of kubernetes-admin
105
106
  ```
106
107
 
107
108
  `gates` holds each open Gate of the run, as a [`GateView`](../reference/http-api.md#type-gateview):
@@ -114,18 +115,37 @@ jr2 status 2319
114
115
 
115
116
  A run that has no open Gate has `"gates":[]`.
116
117
 
118
+ To find the open Gates of all the live runs, run [`jr2 gates`](../reference/cli.md#gates). It prints the Gates that you
119
+ can answer, the oldest first. Give `--workflow <name>` to see only the Gates of the runs of one Workflow:
120
+
121
+ ```sh
122
+ jr2 gates --workflow release
123
+ ```
124
+
125
+ ```text
126
+ → context <context> / namespace <namespace>
127
+ [{"runId":"0c948d65-fd6f-453a-87fa-71ec00c7fe68","workflow":"release","gate":"review","path":["review"],"accepts":[{"name":"approve","description":"Release this version.","input":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{},"additionalProperties":false}},{"name":"reject","description":"Do not release this version. Say why.","input":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"reason":{"type":"string","minLength":1}},"required":["reason"],"additionalProperties":false}}],"acceptsNow":["approve","reject"],"meta":{"version":"1.4.0"},"openedAt":"2026-11-06T19:15:39.019Z"}]
128
+ ```
129
+
130
+ Each entry has the fields of a Gate in `gates`, and also `runId`, `workflow` and `openedAt`, the time when the Gate
131
+ opened. When the Gate hears a [Hook](../reference/glossary.md#hook), `hook` gives the
132
+ [Hook kinds](../reference/glossary.md#hook-kind). An entry never holds the `context` of the run. A Gate that takes no
133
+ event from a [Caller](../reference/glossary.md#caller), such as the `source` Gate of a
134
+ [Pool](../reference/glossary.md#pool) that hears only Hooks, is not in the list. `jr2 gates` reads
135
+ [`GET /gates`](../reference/http-api.md#get-gates), and it needs the `answer` scope.
136
+
117
137
  ## Answer from the CLI
118
138
 
119
139
  Run [`jr2 send`](../reference/cli.md#send). Give the Gate in `--gate`, the event in `--event`, and the input of the
120
140
  event as a JSON object in `--input`:
121
141
 
122
142
  ```sh
123
- jr2 send 2319 --gate review --event reject --input '{"reason":"The changelog is not ready."}'
143
+ jr2 send 0c94 --gate review --event reject --input '{"reason":"The changelog is not ready."}'
124
144
  ```
125
145
 
126
146
  ```text
127
147
  → context <context> / namespace <namespace>
128
- delivered reject to gate "review" on 2319932e-02c1-4f9d-98db-3c5a410395da
148
+ delivered reject to gate "review" on 0c948d65-fd6f-453a-87fa-71ec00c7fe68
129
149
  ```
130
150
 
131
151
  `jr2 send` prints `delivered` only when the run took the event. In the first terminal, the run ends. A `reject` ends the
@@ -136,7 +156,7 @@ ends `failed`. Its output is the [Failure](../reference/glossary.md#failure), an
136
156
  → active "rejected"
137
157
  → failed "rejected"
138
158
  failed: The changelog is not ready.
139
- {"runId":"2319932e-02c1-4f9d-98db-3c5a410395da","workflow":"release","instanceId":"c898d8c7-e48b-4a56-8101-7148905b6aed","status":"failed","value":"rejected","context":{"version":"1.4.0","reason":"The changelog is not ready."},"children":[],"waiting":[],"held":[],"assembly":"83858e28b054","output":{"reason":"The changelog is not ready."}}
159
+ {"runId":"0c948d65-fd6f-453a-87fa-71ec00c7fe68","workflow":"release","instanceId":"6db54032-0b87-4005-9583-049f7335b75f","startedAt":"2026-11-06T19:15:39.018Z","startedBy":{"kind":"caller","via":"token","name":"instance","onBehalfOf":"kubernetes-admin"},"status":"failed","value":"rejected","context":{"version":"1.4.0","reason":"The changelog is not ready."},"children":[],"waiting":[],"held":[],"answers":[{"gate":"review","type":"reject","at":"2026-11-06T19:15:52.901Z","answeredBy":{"via":"token","name":"instance","onBehalfOf":"kubernetes-admin"}}],"assembly":"19d0bf0cbe10","output":{"reason":"The changelog is not ready."}}
140
160
  ```
141
161
 
142
162
  The run ended, so its Gate closed. Each example below answers a run that waits. Before each example, start a new run in
@@ -154,24 +174,25 @@ jr2 send <run id> --gate review --event approve
154
174
 
155
175
  ```text
156
176
  → context <context> / namespace <namespace>
157
- delivered approve to gate "review" on c099e2dc-6dd2-45da-9358-0723c8b83c68
177
+ delivered approve to gate "review" on 342fef91-53a1-4640-87bf-6fb4395beebf
158
178
  ```
159
179
 
160
180
  `jr2 send` prints the full run id, also when you give a prefix.
161
181
 
162
182
  ## Answer from the Console
163
183
 
164
- You need the Instance token to answer a Gate from the Console. [Open the Console](open-the-console.md) tells how to open
165
- it and how to get the token.
184
+ You need a credential with the `answer` scope for the Workflow, such as the Instance token, to answer a Gate from the
185
+ Console. [Open the Console](open-the-console.md) tells how to open it, how to get the token and how to log in.
166
186
 
167
187
  1. Open the Console.
168
- 2. Type the Instance token in the `instance token` box in the nav.
188
+ 2. Type the token in the `Caller token` box in the nav, or click `log in`.
169
189
  3. Push Enter. The badge beside the box shows `…`, then `live`.
170
190
  4. Click the `Attention` tab in the drawer on the right. It shows a card for each open Gate of the Workflow that you
171
- selected. To see the Gates of all the Workflows, select `all workflows`. When you select no Workflow, it shows them
172
- all.
173
- 5. Find the card. Its top line shows `⚑` and the id of the Gate at the left. At the right, it shows the Workflow and the
174
- first 8 characters of the run id. Below it, the card shows the `meta` of the Gate.
191
+ selected and that your credential can answer, the oldest first. To see the Gates of all the Workflows, select
192
+ `all workflows`. When you select no Workflow, it shows them all. The Console reads the cards from
193
+ [`GET /gates`](../reference/http-api.md#get-gates).
194
+ 5. Find the card. Its top line shows `⚑` and the id of the Gate at the left. At the right, it shows the Workflow, the
195
+ first 8 characters of the run id and `open since <time>`. Below it, the card shows the `meta` of the Gate.
175
196
  6. The card has a form for each event that the Gate accepts. Fill in the form of the event that you send. A field that
176
197
  has `*` after its name is necessary.
177
198
  7. Click `send` in that form.
@@ -184,9 +205,13 @@ The `⚑` badge in the nav gives the number of open Gates in all the Workflows.
184
205
  ## Answer from a webhook or CI
185
206
 
186
207
  A webhook handler or a CI job answers a Gate with
187
- [`POST /runs/:runId/gates/:gate/events`](../reference/http-api.md#post-runs-runid-gates-gate-events) and the Instance
188
- token. jr2 has no webhook receiver of its own. Your handler or job reads its own event, finds the run and the Gate, and
189
- sends the call. The body is the event: `type` is the name of the event, and the other properties are its input.
208
+ [`POST /runs/:runId/gates/:gate/events`](../reference/http-api.md#post-runs-runid-gates-gate-events) and the token of a
209
+ named Caller with the `answer` scope for the Workflow. A CI job has no access to the cluster, so it cannot read the
210
+ Instance token, and it must not have it: the Instance token can do everything.
211
+ [Give a Caller a scope](give-a-caller-a-scope.md) makes the Caller token. A service that a Hook kind covers, such as a
212
+ forge, sends its webhook to a Hook: see [Answer a Gate from a forge](answer-a-gate-from-a-forge.md). Any other handler
213
+ or CI job uses the Gate route. It reads its own event, finds the run and the Gate, and sends the call. The body is the
214
+ event: `type` is the name of the event, and the other properties are its input.
190
215
 
191
216
  `jr2 up` makes the Service `jr2-orchestrator` on port 4000, with no address outside the cluster. Thus a caller outside
192
217
  the cluster uses a port forward, or an address that you make for the Service.
@@ -197,7 +222,8 @@ In a new terminal, forward a port to the Orchestrator. Keep this command open:
197
222
  kubectl -n <namespace> port-forward svc/jr2-orchestrator 4000:4000
198
223
  ```
199
224
 
200
- In a different terminal, get the Instance token into a variable:
225
+ In a different terminal, put a token into a variable. A CI job uses its Caller token. To try the route from your own
226
+ terminal, you can use the Instance token:
201
227
 
202
228
  ```sh
203
229
  TOKEN=$(kubectl -n <namespace> get secret jr2-instance -o jsonpath='{.data.JR2_INSTANCE_TOKEN}' | base64 -d)
@@ -217,9 +243,9 @@ curl -s -X POST http://localhost:4000/runs/<run id>/gates/review/events \
217
243
  ```
218
244
 
219
245
  A webhook handler often knows a fact, such as a version, but not the run id. It can find the run from the `meta` of the
220
- Gate. [`GET /runs`](../reference/http-api.md#get-runs) gives the live runs, and
221
- [`GET /runs/:runId`](../reference/http-api.md#get-runs-runid) gives the open Gates of one run. This script approves the
222
- `release` run that waits for one version. It needs `curl` and `jq`. Save it as `approve-release.sh`:
246
+ Gate. [`GET /gates`](../reference/http-api.md#get-gates) gives the open Gates of all the live runs that the token can
247
+ answer, each with its run id and its `meta`. It needs only the `answer` scope. This script approves the `release` run
248
+ that waits for one version. It needs `curl` and `jq`. Save it as `approve-release.sh`:
223
249
 
224
250
  ```sh
225
251
  #!/bin/sh
@@ -229,15 +255,13 @@ set -eu
229
255
 
230
256
  auth="Authorization: Bearer $JR2_TOKEN"
231
257
 
232
- # The live runs of the release Workflow.
233
- for run in $(curl -fsS -H "$auth" "$JR2_URL/runs" | jq -r '.[] | select(.workflow == "release") | .runId'); do
234
- # The open Gates of the run, with their meta.
235
- version=$(curl -fsS -H "$auth" "$JR2_URL/runs/$run" | jq -r '.gates[] | select(.gate == "review") | .meta.version')
236
- if [ "$version" = "$VERSION" ]; then
237
- curl -fsS -X POST -H "$auth" -H "Content-Type: application/json" \
238
- -d '{"type":"approve"}' "$JR2_URL/runs/$run/gates/review/events"
239
- echo
240
- fi
258
+ # The open Gates of the live release runs that this token can answer, with their meta.
259
+ runs=$(curl -fsS -H "$auth" "$JR2_URL/gates?workflow=release" |
260
+ jq -r --arg v "$VERSION" '.gates[] | select(.gate == "review" and .meta.version == $v) | .runId')
261
+ for run in $runs; do
262
+ curl -fsS -X POST -H "$auth" -H "Content-Type: application/json" \
263
+ -d '{"type":"approve"}' "$JR2_URL/runs/$run/gates/review/events"
264
+ echo
241
265
  done
242
266
  ```
243
267
 
@@ -258,15 +282,15 @@ JR2_URL=http://localhost:4000 JR2_TOKEN=$TOKEN jr2 send <run id> --gate review -
258
282
 
259
283
  ```text
260
284
  → http://localhost:4000
261
- delivered approve to gate "review" on c44c6e49-452d-4150-8ce4-4d036363d2a0
285
+ delivered approve to gate "review" on cad78069-0d8d-4c04-9cf4-3f771693274f
262
286
  ```
263
287
 
264
288
  ## Rules
265
289
 
266
290
  - The Orchestrator refuses a Gate that is not open on the run, with the status code `404`:
267
291
  `no open gate "<gate>" on run "<run id>" (open: <gates>)`. The part `(open: <gates>)` shows only when the run has an
268
- open Gate. When an event takes the run out of the state of the Gate, the Gate closes. Thus a second answer gets this
269
- refusal.
292
+ open Gate that the Caller can answer. When an event takes the run out of the state of the Gate, the Gate closes. Thus
293
+ a second answer gets this refusal.
270
294
  - The Orchestrator refuses an event that the Gate does not accept, with the status code `400`:
271
295
  `"<gate>" does not accept "<event>" (accepts: <events>)`
272
296
  - The Orchestrator refuses an input that does not agree with the schema of the event, with the status code `400`:
@@ -278,10 +302,23 @@ delivered approve to gate "review" on c44c6e49-452d-4150-8ce4-4d036363d2a0
278
302
  `event body must carry a string "type" (one of the gate's accepted names)`
279
303
  - The Orchestrator refuses a body that holds `sender`, with the status code `400`:
280
304
  `` `sender` is written by the Orchestrator on every delivered event; a caller cannot send it ``
281
- - The Orchestrator refuses an event without the Instance token, with the status code `401`: `unauthorized`.
305
+ - The Orchestrator refuses an event with no valid credential, with the status code `401`: `unauthorized`. A credential
306
+ is the Instance token, or that of a [Caller](../reference/glossary.md#caller) with the `answer` scope.
307
+ [Give a Caller a scope](give-a-caller-a-scope.md) makes one.
282
308
  - The Orchestrator refuses an event with the token of a [Sandbox](../reference/glossary.md#sandbox), with the status
283
- code `403`: `a Sandbox token cannot deliver to a gate — gates are not on the Agent's surface`. Thus an Agent cannot
284
- answer a Gate.
309
+ code `403`:
310
+ `this route needs a Caller, and a Sandbox token is not one: no Agent reaches run state, run control or a Gate`. Thus
311
+ an Agent cannot answer a Gate.
312
+ - The Orchestrator refuses an event from a Caller that has the `answer` scope for no Workflow, with the status code
313
+ `403`, before it reads the run: `the Caller <via>/<name> has no "answer" scope for any Workflow (ADR-0078)`. The body
314
+ also holds `scope`.
315
+ - The Orchestrator refuses an event from a Caller that does not have the `answer` scope for the Workflow of the run,
316
+ with the status code `403`: `the Caller <via>/<name> needs the "answer" scope for Workflow "<workflow>" (ADR-0078)`
317
+ - The Orchestrator refuses an event to an open Gate from a Caller whose `answer` scope names other Gates of the
318
+ Workflow, with the status code `403`:
319
+ `the Caller <via>/<name> needs the "answer" scope for Gate "<name>" of Workflow "<workflow>" (ADR-0078)`. `<name>` is
320
+ the name of the Gate, its invoke id. The body also holds `scope`, `workflow` and `gate`.
321
+ [Give a Caller a scope](give-a-caller-a-scope.md) tells how a scope names single Gates.
285
322
  - `jr2 send` prints each refusal of the Orchestrator as `error: <refusal>`, and exits with `1`. After a `404` refusal,
286
323
  it also prints the version of the Instance in one more line:
287
324
  ``instance: <version> (<hash>) — if it predates this CLI, `jr2 up` converges the cluster to this kit``. `<hash>` is
@@ -298,7 +335,7 @@ delivered approve to gate "review" on c44c6e49-452d-4150-8ce4-4d036363d2a0
298
335
  - [ADR-0011](../reference/decisions/0011-workflow-defined-events.md): a Gate, its events, and its `meta`.
299
336
  - [ADR-0013](../reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md): why an
300
337
  Agent cannot answer a Gate, and why a refused event is not delivered.
301
- - [ADR-0032](../reference/decisions/0032-the-console-unlocks-with-the-instance-token.md): the Instance token in the
338
+ - [ADR-0032](../reference/decisions/0032-the-console-unlocks-with-a-callers-credential.md): a Caller's credential in the
302
339
  Console.
303
340
  - [Start a run](start-a-run.md)
304
341
  - [Watch a run](watch-a-run.md)
@@ -121,9 +121,10 @@ jr2 run fix-app --input '{"prompt":"Make the failing test in test/greet.test.ts
121
121
  The key of each Repo Slot is the name of the slot, and the directory `/work/<slot>` in the
122
122
  [Sandbox](../reference/glossary.md#sandbox). The value is one of these:
123
123
 
124
- - a url: `"https://github.com/<you>/handbook.git"`. The [Worktree](../reference/glossary.md#worktree) starts from the
125
- default branch of the Repo.
126
- - a [Binding](../reference/glossary.md#binding): `{ url, ref }`. The Worktree starts from `ref`, a branch or a commit.
124
+ - a url: `"https://github.com/<you>/handbook.git"`. The clone stays on the default branch of the Repo, and a new
125
+ [Worktree](../reference/glossary.md#worktree) branch starts from it.
126
+ - a [Binding](../reference/glossary.md#binding): `{ url, ref }`. The clone stays on `ref`, a branch, a tag or a commit,
127
+ and a new branch starts from it.
127
128
  - a function of the run input: `({ input }) => url`, or `({ input }) => ({ url, ref })`. Each run chooses its Repo. The
128
129
  url must agree with an entry of [`git.credentials`](../reference/config.md#git-credentials) in `jr2.config.ts`.
129
130
  - `open`, to leave the slot Open for the Instance that registers your Machine.
@@ -136,7 +137,9 @@ others. A Machine that names its slots, such as `repos: { app: open }`, takes on
136
137
 
137
138
  `agents` takes the fields of the definition of an Agent, by slot. Each field goes over the field that the author wrote.
138
139
  Thus `{ model }` changes only the model, and the coder keeps the instructions of `task`. You can also change other
139
- fields, such as `thinkingLevel` or `instructions`.
140
+ fields, such as `thinkingLevel` or `instructions`. `{ mcp }` replaces the whole list of the MCP servers of the Agent.
141
+ `jr2 up` refuses a server name that the `mcp` block of `jr2.config.ts` does not declare, and prints the line with `mcp`
142
+ (see [Give an Agent MCP tools](give-an-agent-mcp-tools.md#bind-the-servers-of-a-packaged-machine)).
140
143
  [`AgentDefinition`](../reference/authoring-surface/type-agentdefinition.md) gives the fields.
141
144
 
142
145
  ## Bind the parts of a Machine that you compose
@@ -163,19 +166,29 @@ When `task` is a child of your Machine, give it `branch` in its input. Only the
163
166
 
164
167
  ## Bind the Open parts of an Effect
165
168
 
166
- A packaged Machine can leave the url of an `http` [Effect](../reference/glossary.md#effect), the argv of a `command`, or
167
- the name of a secret Open. Bind them with `effects`, by the slot of the Effect:
168
- `customize(machine, { effects: { notify: { url } } })`. One object binds all the Open parts of one Effect. You bind each
169
- part one time, to a value. No run chooses it. [`EffectBinding`](../reference/authoring-surface/type-effectbinding.md)
170
- gives the values, and [Run a command or call a URL with an Effect](run-a-command-or-call-a-url.md) shows an Effect.
169
+ A packaged Machine can leave the url of an `http` [Effect](../reference/glossary.md#effect), the argv of a `command`,
170
+ the name of a secret, or the server and the tool of an `mcp` Effect Open. Bind them with `effects`, by the slot of the
171
+ Effect: `customize(machine, { effects: { notify: { url } } })`, or
172
+ `customize(machine, { effects: { post: { server: "slack-acme", tool: "post_message" } } })`. One object binds all the
173
+ Open parts of one Effect. You bind each part one time, to a value. No run chooses it.
174
+ [`EffectBinding`](../reference/authoring-surface/type-effectbinding.md) gives the values, and
175
+ [Run a command or call a URL with an Effect](run-a-command-or-call-a-url.md) shows an Effect. A bound server is a name
176
+ of the `mcp` block. Its secret comes from that block, so an `mcp` Effect has no Open secret.
177
+
178
+ ## Bind the look of a Gate
179
+
180
+ A packaged Machine can leave the look of a Gate slot Open: `look: open`. Bind it with `gates`, by the slot of the Gate:
181
+ `customize(machine, { gates: { review: { look } } })`. The same key adds, replaces or removes a handler of the Gate, by
182
+ Hook kind and event key, where `null` removes one. `jr2 up` does not refuse an Open look. It warns, because a person can
183
+ still answer the Gate with `jr2 send`. [Answer a Gate from a forge](answer-a-gate-from-a-forge.md) shows a Gate slot.
171
184
 
172
185
  ## Rules
173
186
 
174
187
  - `jr2 up` refuses a registered Machine that leaves a part Open. It changes nothing in the cluster:
175
188
  `refusing: workflow "<name>" leaves <part> open — bind it where the Machine is registered: export const machine = customize(<import>, <binding>) (<adr>)`.
176
189
  `<part>` is one of `its Repo Slots`, `Repo Slot "<slot>"`, `Agent "<slot>"`, or `the <parts> of Effect "<slot>"`,
177
- where `<parts>` is such as `url` or `url and secret`. For a child Machine, `(on the Machine composed as "<slot>")`
178
- comes after `open`.
190
+ where `<parts>` is such as `url`, `url and secret`, `server` or `server and tool`. For a child Machine,
191
+ `(on the Machine composed as "<slot>")` comes after `open`.
179
192
  - `jr2 up` refuses an Open part on a Machine that a state writes in its `invoke.src`, because no `customize()` gets to
180
193
  it. Put that Machine under `actors`:
181
194
  `refusing: workflow "<name>" leaves <part> open on a Machine invoked inline — no customize() reaches an actor without a slot key; declare that Machine under setup({ actors }) and bind it there (ADR-0049, <adr>)`
@@ -188,10 +201,11 @@ gives the values, and [Run a command or call a URL with an Effect](run-a-command
188
201
  - `customize(): machine "<id>" composes no Machine under "<slot>" — the Machines it composes are: <slots> (ADR-0049).`
189
202
  - `customize(): machine "<id>" declares no Repo Slot "<slot>" — its slots are: <slots> (ADR-0051)`
190
203
  - `customize(): machine "<id>" carries no Effect slot "<slot>" — its Effects are: <slots> (ADR-0067). Only declared parts can be bound.`
204
+ - `customize(): machine "<id>" carries no Gate slot "<slot>" — its Gate slots are: <slots> (ADR-0011). Only declared parts can be bound.`
191
205
  - `customize()` refuses an Open map that you bind with no slot:
192
206
  ``customize(): machine "<id>" declares its Repo Slots open as a map — name at least one: `repos: { app: "https://…" }` (ADR-0051).``
193
207
  - `customize()` refuses a Repo Slot key that is not a directory name. The key becomes `/work/<slot>`:
194
- ``customize(): Repo Slot key "<slot>" is not a directory name — a slot becomes `/work/<slot>` and its order matters, so it starts with a letter and matches /^[A-Za-z][A-Za-z0-9._-]*$/ (ADR-0051).``
208
+ ``customize(): Repo Slot key "<slot>" is not a directory name — a slot becomes `/work/<slot>`, `/work/.jr2/` is reserved for jr2, and its order matters, so it starts with a letter and matches /^[A-Za-z][A-Za-z0-9._-]*$/ (ADR-0051, ADR-0075).``
195
209
  - `customize()` refuses an empty url:
196
210
  ``customize(): Repo Slot "<slot>" is bound to an empty url (got <value>) — a slot is `open`, a url, `{ url, ref? }`, or a mapper `({ input }) => url | { url, ref? }` over the door (ADR-0051).``
197
211
  For an empty `ref`, the text says `bound with an empty ref`.
@@ -200,6 +214,9 @@ gives the values, and [Run a command or call a URL with an Effect](run-a-command
200
214
  - `customize()` refuses `open` or a function as the value of an Effect's part:
201
215
  ``customize(): the <kind> Effect "<slot>"'s <part> is bound once, to a value — not `open` again: no run chooses an Effect's <part> (ADR-0054)``.
202
216
  For a function, the text says `a function of the run` in place of `` `open` again ``.
217
+ - `jr2 up` refuses an Agent that names an MCP server that the `mcp` block does not declare:
218
+ `refusing: workflow "<name>": Agent "<slot>" names the MCP server "<server>", which the mcp block declares "<names>" — bind the Agent to a declared server where the Machine is registered: export const machine = customize(<import>, { agents: { <slot>: { mcp: [<entries>] } } }); or declare it in jr2.config.ts: mcp: { "<server>": { url: "https://<host>/mcp", secret: "<SECRET>" } } (ADR-0079)`.
219
+ For a child Machine, the line names the child, and `customize()` goes through `actors`.
203
220
  - An Agent does not start a [Turn](../reference/glossary.md#turn) while its model is Open:
204
221
  ``agent "<slot>" has an Open model — nobody bound it; bind it where the Machine is registered (`customize(<machine>, { agents: { <slot>: { model: "<provider>/<model>" } } })`), because a packaged Machine cannot pick a model on your behalf (ADR-0054)``.
205
222
  `jr2 up` refuses this case first.