@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
@@ -119,7 +119,6 @@ import { body, type TaskInput } from "@jr2/machines";
119
119
 
120
120
  const fixed = workspace(body, {
121
121
  repos: { app: { url: "https://github.com/you/app.git" } },
122
- spec: ({ input }: { input: TaskInput }) => ({ branch: input.branch ?? "jr2/fix" }),
123
122
  });
124
123
 
125
124
  export const machine = customize(fixed, {
package/docs/why.md CHANGED
@@ -47,7 +47,8 @@ cannot be the right one for each of them.
47
47
 
48
48
  The You band of the diagram holds the clients of jr2. The [Orchestrator](reference/glossary.md#orchestrator) is an HTTP
49
49
  API. The [jr2 CLI](reference/glossary.md#jr2-cli) and the [Console](reference/glossary.md#console) are its clients, and
50
- your own app, such as a webhook, can be one too. For example, a client can start a run with
50
+ your own app, such as a CI job or a bridge to a chat service, can be one too. An outside service, such as a forge, sends
51
+ its deliveries to a [Hook](reference/glossary.md#hook). For example, a client can start a run with
51
52
  [`POST /workflows/:name/runs`](reference/http-api.md#post-workflows-name-runs). In the diagram, a
52
53
  [Gate](reference/glossary.md#gate) in the state `approval` asks you, and you answer from any of these clients, with
53
54
  [`POST /runs/:runId/gates/:gate/events`](reference/http-api.md#post-runs-runid-gates-gate-events).
@@ -115,8 +116,8 @@ It talks to the model, and it runs the tools of the Agent. The Agent can run eac
115
116
  Agent that needs only its Menu has no Sandbox. Its Turns run on the
116
117
  [Instance Harness](reference/glossary.md#instance-harness).
117
118
 
118
- The Harness container holds no credential, so a command that an Agent runs cannot read one. jr2 keeps each credential in
119
- one of the two containers beside it:
119
+ The Harness runs each command of an Agent confined with Landlock, so the command cannot read a credential, and it cannot
120
+ send a pick for a different Agent. jr2 keeps each credential in one of the two containers beside the Harness:
120
121
 
121
122
  - The [Custodian](reference/glossary.md#custodian) holds each [Held secret](reference/glossary.md#held-secret): a key
122
123
  that the Agents can use, but cannot read. The Harness holds only a [Stand-in](reference/glossary.md#stand-in) for it.
@@ -192,8 +193,9 @@ These steps follow one run of the example Machine in the diagram. You write a Ma
192
193
  1. **You start a run.** You run `jr2 run feature`, with the ticket as the input of the run. The Machine starts in its
193
194
  first state, `code`.
194
195
  2. **The coder works in a Sandbox.** The state `code` is in a Workspace, so jr2 makes a Sandbox for it. The Sandbox
195
- holds a [Worktree](reference/glossary.md#worktree) of your Repo, on the branch of the run. The coder edits the files,
196
- runs the tests and commits. Then it picks `finish`, and the Machine moves to `review`.
196
+ holds a clone of your Repo, and the Machine cuts a [Worktree](reference/glossary.md#worktree) on the branch of the
197
+ run. The coder edits the files, runs the tests and commits. Then it picks `finish`, and the Machine moves to
198
+ `review`.
197
199
  3. **The reviewer gets a Turn.** The state `review` gives the reviewer a prompt and its Menu: `approved` and `changes`.
198
200
  The reviewer reads the change in the same Sandbox.
199
201
  4. **The reviewer picks.** If it picks `approved`, the Machine moves to `approval`. If it picks `changes`, the Machine
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jr2/orchestrator",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "description": "The jr2 Orchestrator: xstate runtime, HTTP API and Console, and the kit pieces a Workflow imports.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -31,6 +31,12 @@
31
31
  "exports": {
32
32
  ".": "./src/index.ts",
33
33
  "./internal": "./src/internal.ts",
34
+ "./hooks": "./src/hook-kinds/index.ts",
35
+ "./hooks/github": "./src/hook-kinds/github.ts",
36
+ "./hooks/gitlab": "./src/hook-kinds/gitlab.ts",
37
+ "./hooks/slack": "./src/hook-kinds/slack.ts",
38
+ "./hooks/discord": "./src/hook-kinds/discord.ts",
39
+ "./hooks/webhook": "./src/hook-kinds/webhook.ts",
34
40
  "./tsconfig.instance.json": "./tsconfig.instance.json"
35
41
  },
36
42
  "types": "./src/index.ts",
@@ -38,15 +44,16 @@
38
44
  "@hono/node-server": "^2.0.6",
39
45
  "elkjs": "^0.11.1",
40
46
  "hono": "^4.12.27",
47
+ "jose": "^6.2.3",
41
48
  "preact": "^10.29.8",
42
49
  "ts-blank-space": "^0.9.0",
43
50
  "xstate": "^5.18.0",
44
51
  "zod": "^4.4.3",
45
- "@jr2/agent-protocol": "0.5.1"
52
+ "@jr2/agent-protocol": "0.7.0"
46
53
  },
47
54
  "devDependencies": {
48
55
  "@types/node": "^26.0.1",
49
- "@jr2/harness": "0.5.1"
56
+ "@jr2/harness": "0.7.0"
50
57
  },
51
58
  "files": [
52
59
  "bin",
package/src/actor.ts CHANGED
@@ -50,14 +50,21 @@ import {
50
50
  type ThinkingLevel,
51
51
  type WorkspaceAccess,
52
52
  } from "./agent.ts";
53
+ import type { AnyFramePath } from "./worktree.ts";
53
54
  import { ambientHandlesFor, type AmbientHandles } from "./ambient.ts";
54
55
  import type { HarnessRestarts, MemoryKill } from "./workspace.ts";
55
56
  import { INSTANCE_HARNESS_SERVICE } from "./names.ts";
56
57
  import { SENDER_KEY } from "@jr2/agent-protocol";
57
- import { agentAddress, continuedIid, allowedPicks, resolveAccepts, runBindingOf } from "./registration.ts";
58
- import type { NoticeScope } from "./notices.ts";
58
+ import { agentAddress, agentSlot, continuedIid, allowedPicks, resolveAccepts, runBindingOf } from "./registration.ts";
59
+ import type { LedgerBody, NoticeScope } from "./notices.ts";
59
60
  import { restOf } from "./rest.ts";
60
- import { SUBMISSION_HARNESS_RESTARTED, SUBMISSION_RUNAWAY, type Notice, type PodMemoryKill } from "./wire.ts";
61
+ import {
62
+ SUBMISSION_HARNESS_RESTARTED,
63
+ SUBMISSION_RUNAWAY,
64
+ type Notice,
65
+ type PodMemoryKill,
66
+ type SteeringMessage,
67
+ } from "./wire.ts";
61
68
  import type { AbortCause } from "./wire.ts";
62
69
 
63
70
  /**
@@ -122,6 +129,14 @@ export type AgentAdmission = {
122
129
  */
123
130
  agent?: string;
124
131
  endpoint?: string;
132
+ /**
133
+ * The pod and the slot of the conversation (ADR-0076) — stamped by the ACTOR at ledger time, like
134
+ * `agent`. The kept conversation's route takes entries for this conversation only from this pod's
135
+ * control token, for this slot (ADR-0023). It reads the ledger and not the live registration,
136
+ * because the ledger outlives the Turn and the run.
137
+ */
138
+ sandbox?: string;
139
+ slot?: string;
125
140
  };
126
141
 
127
142
  /**
@@ -143,15 +158,15 @@ export type AgentTurnInput = {
143
158
  /**
144
159
  * The other half of the FRAME (ADR-0057) — WHERE this Turn works: the absolute directory the
145
160
  * Harness roots the Working tools at. Per Turn by nature, which is why no definition names one:
146
- * a Worktree path (`/work/<slot>/<branch>`) exists only once a run has a branch.
161
+ * a Worktree path (`/work/<slot>/<name>`) exists only once a Machine has cut it (ADR-0075).
147
162
  *
148
- * Absent, the actor resolves it before admission: a `workspace: "none"` Agent frames no
149
- * directory (it has no Working tools to root — ADR-0028), a Workspace with ONE Repo Slot frames
150
- * that slot's Worktree, a Workspace with more REFUSES the Turn (the kit gives no slot a meaning
151
- * — ADR-0051 — so the state must say which), and a run with no Workspace at all frames nothing
152
- * and the Harness keeps `/work`. A relative path is refused at admission, by the Harness.
163
+ * Required under Repo Slots: a Turn with no `cwd` there is REFUSED before admission, and a Turn
164
+ * that wants `/work` states it. A `workspace: "none"` Agent frames no directory (it has no Working
165
+ * tools to root — ADR-0028), and a Workspace with no Repo Slot, or a run with no Workspace at all,
166
+ * frames nothing and the Harness keeps `/work`. A relative path is refused at admission, by the
167
+ * Harness.
153
168
  */
154
- cwd?: string;
169
+ cwd?: AnyFramePath;
155
170
  /**
156
171
  * This turn's DIALS (ADR-0018) — how hard to run, layered over the definition's own
157
172
  * values. One definition value may be carried by several Machines, so the same persona
@@ -358,10 +373,16 @@ export interface AgentRunPort {
358
373
  * Machine edit that changed it. */
359
374
  export type AgentAdmitOptions = {
360
375
  definition: AgentDefinition;
376
+ /** This Agent's slot (ADR-0076): the actor path of its Machine and its agent name — the
377
+ * registration's. The Harness keeps it with the conversation and states it on each control
378
+ * request; it never derives it from an Instance ID, whose actor ids may hold a `/`. */
379
+ slot: string;
361
380
  signal?: AbortSignal;
362
381
  /** The pending notices in this admission's scope (ADR-0062), for the Briefing's Turn part. Not
363
382
  * on the persisted input either: they are the Orchestrator's, and delivered once. */
364
383
  notices?: Notice[];
384
+ /** The Steering messages held for this Agent slot (ADR-0002), delivered by the rules of a notice. */
385
+ steering?: SteeringMessage[];
365
386
  /** The admission is for a conversation that `holds` said the Harness keeps (ADR-0062): the
366
387
  * re-admission after a cut, and a continued Turn. That "kept" can be a timeout's, so a Harness
367
388
  * that keeps none refuses it and seats none; `admit` then rejects with an error whose `notKept`
@@ -555,20 +576,15 @@ const READMIT_PROMPT =
555
576
  * an ABSENT one, and a directory a state named is never second-guessed — a Menu-only Agent handed
556
577
  * one simply has no Working tools to root there (ADR-0028/0031).
557
578
  *
558
- * Absent, a Menu-only Agent frames no directory; a Workspace with ONE Repo Slot frames that
559
- * slot's Worktree, which privileges nothing because there is nothing to choose between (ADR-0051);
560
- * a Workspace with more REFUSES the Turn, naming the slots in declaration order and the line that
561
- * ends it, because the kit gives no slot a meaning and a wrong guess is the silent failure
562
- * ADR-0057 was written for; and a Workspace with no Repo Slot, or a run outside any Workspace,
563
- * frames nothing, so the Harness keeps `/work`, the folder a slotless Workspace's steps share.
579
+ * Absent, a Menu-only Agent frames no directory, and a Workspace with no Repo Slot, or a run outside
580
+ * any Workspace, frames nothing, so the Harness keeps `/work`, the folder a slotless Workspace's
581
+ * steps share. Under Repo Slots the Workspace only clones, so no Worktree exists for a default to
582
+ * name, and the kit gives no slot a meaning: an absent `cwd` is REFUSED, because a wrong guess is
583
+ * the silent failure ADR-0057 was written for (ADR-0075).
564
584
  *
565
- * Resolution counts SLOTS, and a slot's Worktree is the writable branch checkout. The detached
566
- * review Worktree (`AmbientHandles.review`, ADR-0028's reviewer seat) is never resolved to: it is
567
- * per review round, so only the reviewing state knows which one, and it frames it (ADR-0028 hands
568
- * the reviewer its path as "cwd and prompt"). A `workspace: "read"` Agent that frames nothing
569
- * under a one-slot Workspace therefore works in the branch checkout with `write`/`edit` withheld
570
- * and `bash` kept — the tool layer's hint, not containment (ADR-0028): containment is the
571
- * detached review Worktree, and only a reviewing state that requested one can frame it.
585
+ * `workspace: "read"` withholds `write`/`edit` and keeps `bash`: the tool layer's hint, not
586
+ * containment (ADR-0028). The containment is a detached Worktree that the state cuts and names as
587
+ * its `cwd`.
572
588
  *
573
589
  * The refusal throws at invoke, like the Open-model fence: loudly, at the first Turn, before a
574
590
  * surface is registered or a pod is spent.
@@ -583,12 +599,11 @@ function frameCwd(
583
599
  if (workspace === "none") return undefined;
584
600
  const slots = Object.keys(ambient?.repos ?? {});
585
601
  if (slots.length === 0) return undefined;
586
- if (slots.length === 1) return ambient!.repos[slots[0]!];
602
+ // A default here is ADR-0057's incident: an Agent's search hits `default/`, and a relative write
603
+ // lands outside each Worktree. An author who wants `/work` states it.
587
604
  throw new Error(
588
- `turn ${instanceId}: agent "${input.agentName}" works under a Workspace carrying more than one ` +
589
- `Repo Slot (${slots.join(", ")}) and its Turn framed no \`cwd\` — say which Worktree it works ` +
590
- `in (\`cwd: context.workspace.repos.<slot>\`, one of those), because the kit gives no slot a ` +
591
- `meaning (ADR-0051) and a Turn's Frame is where it works (ADR-0057)`,
605
+ `turn ${instanceId}: agent "${input.agentName}" works under a Workspace with Repo Slots (${slots.join(", ")}) ` +
606
+ 'and its Turn framed no `cwd`: say where it works, a Worktree, or `cwd: "/work"` above them (ADR-0057)',
592
607
  );
593
608
  }
594
609
 
@@ -701,7 +716,7 @@ export function agentActorWith(
701
716
 
702
717
  // The Frame's other half (ADR-0057): WHERE this Turn works. Resolved HERE, before any surface
703
718
  // is registered and before a pod is spent, because the answer is a fact about the run — a
704
- // Worktree path exists only once a run has a branch, so no definition could have named it.
719
+ // Worktree exists only once a run has cut it, so no definition could have named it.
705
720
  const cwd = frameCwd(instanceId, input, workspace, ambient);
706
721
  const framed: AgentRunInput = cwd === undefined ? input : { ...input, cwd };
707
722
 
@@ -732,8 +747,18 @@ export function agentActorWith(
732
747
  // This Agent's continued conversation in this Machine (ADR-0057) — the base id the epoch ledger
733
748
  // burns. This Turn is PART of it only when it continues it: a fresh Turn is new by definition.
734
749
  const conversation = continuedIid(self._parent, binding.runId, input.agentName);
750
+ // This Agent's slot (ADR-0076): the same identity without the run id. The Harness states it on
751
+ // every control request, and the Orchestrator speaks here only for it.
752
+ const slot = agentSlot(self._parent, input.agentName);
735
753
  // The iids whose surface the Harness has read (see `carried` below).
736
754
  const served = new Set<string>();
755
+ // Per iid, the notices a step boundary's surface read reserved and the Harness has not yet
756
+ // acknowledged (ADR-0062): by the id the read named, every id it stands for — equal notices
757
+ // are said once — and what each was raised as, for `reraiseUnheard`.
758
+ const boundary = new Map<
759
+ string,
760
+ Map<string, Array<{ id: string; raised: { notice: LedgerBody; source?: string } }>>
761
+ >();
737
762
  const registerSurface = (iid: string) =>
738
763
  disposers.push(
739
764
  binding.table.register({
@@ -744,6 +769,9 @@ export function agentActorWith(
744
769
  defs,
745
770
  menu,
746
771
  sandbox,
772
+ slot,
773
+ boundaryNotices: () => readBoundary(iid),
774
+ noticesHeard: (ids) => boundaryHeard(iid, ids),
747
775
  deliver: (event) => {
748
776
  signaled = true;
749
777
  // The settlement-pick marker (ADR-0023), BEFORE the delivery moves the Machine: the pick
@@ -824,7 +852,8 @@ export function agentActorWith(
824
852
  // LIVE conversation stamped on the record, so a restore settle-follows the live submission
825
853
  // AND re-addresses it (see `currentIid` above). The notices the admission carried are
826
854
  // delivered by this same write (ADR-0062).
827
- const ledger = (admission: AgentAdmission, delivered: readonly string[]) =>
855
+ // The conversation is a member of its Workspace's audiences from this save on (ADR-0062).
856
+ const ledger = (admission: AgentAdmission, delivered: readonly string[], taker: string) =>
828
857
  binding.recordAdmission?.(
829
858
  instanceId,
830
859
  {
@@ -832,9 +861,15 @@ export function agentActorWith(
832
861
  instanceId: currentIid,
833
862
  agent: input.agentName,
834
863
  endpoint,
864
+ ...(sandbox === undefined ? {} : { sandbox }),
865
+ slot,
835
866
  ...(input.turnId === undefined ? {} : { turnId: input.turnId }),
836
867
  },
837
868
  delivered,
869
+ taker,
870
+ noticeWorkspace === undefined
871
+ ? undefined
872
+ : { workspace: noticeWorkspace, slot, continued: Boolean(input.continuation) && taker === conversationIid() },
838
873
  );
839
874
 
840
875
  // Where this Turn hears notices (ADR-0062). Its continued conversation's — the base id the
@@ -845,7 +880,11 @@ export function agentActorWith(
845
880
  const noticeScope: NoticeScope = {
846
881
  ...(input.continuation ? { conversation } : {}),
847
882
  ...(noticeWorkspace !== undefined ? { workspace: noticeWorkspace } : {}),
883
+ slot,
848
884
  };
885
+ // The Instance ID of the continued conversation this Turn rides — its epoch's spelling, which
886
+ // the minted `instanceId` is. A fresh Turn has none.
887
+ const conversationIid = (): string | undefined => (input.continuation ? instanceId : undefined);
849
888
  // No `agent`: the notice names no Turn — what the pod's list says (below).
850
889
  // No `limit`: the sizes read were not sizes, and the notice says none.
851
890
  const memoryLimit = ({ limit, peak, agent }: { limit?: string; peak?: string; agent?: string }): Notice => ({
@@ -859,12 +898,15 @@ export function agentActorWith(
859
898
  // Turn's Agent. A stopped actor's last poll names nothing: the state that asked for the Turn has
860
899
  // exited, and the pod's list carries the kill. Keyed by the kill's id, so a second running
861
900
  // conversation's stream, or the pod's list, adds nothing.
901
+ // The kill cut this Turn (D4 rule 2): its Agent's next Turn retries the broken work, and hears it.
862
902
  const onMemoryLimit = (kill: StreamedKill): void => {
863
903
  if (stopped || noticeWorkspace === undefined) return;
864
904
  binding.raiseNotice?.(
865
905
  memoryLimit({ limit: kill.limit, peak: kill.peak, agent: input.agentName }),
866
906
  noticeWorkspace,
867
907
  kill.legacy ? kill.id : guardSource(noticeWorkspace, kill.id),
908
+ [],
909
+ { retry: { slot, cut: currentIid }, at: new Date().toISOString() },
868
910
  );
869
911
  };
870
912
  // …but a stream is not every kill's path. A kill while no Submission ran — a process an earlier
@@ -906,6 +948,8 @@ export function agentActorWith(
906
948
  memoryLimit({ limit: kill.limit }),
907
949
  noticeWorkspace,
908
950
  kernelSource(noticeWorkspace, kill.at),
951
+ [],
952
+ { at: kill.at },
909
953
  );
910
954
  };
911
955
  let killMark = input.attach?.killMark;
@@ -937,7 +981,9 @@ export function agentActorWith(
937
981
  noticeWorkspace,
938
982
  guardSource(noticeWorkspace, newest.id),
939
983
  kills.slice(0, -1).map((kill) => guardSource(noticeWorkspace, kill.id)),
984
+ { at: newest.at, told: newest.told.map((t) => t.instanceId) },
940
985
  );
986
+ applyTold(kills);
941
987
  const toldHere = (kill: ListedKill): boolean =>
942
988
  kill.told.some((t) => t.agent === input.agentName && t.instanceId === currentIid);
943
989
  // The first kill after the Turn's mark. A mark no longer in the bounded list is older than
@@ -956,6 +1002,86 @@ export function agentActorWith(
956
1002
  };
957
1003
  };
958
1004
 
1005
+ // The conversations each listed kill's `bash` answer told leave its audience (D4 rule 3) — a
1006
+ // kill raised off a stream before its answer returned is found here too.
1007
+ const applyTold = (kills: readonly ListedKill[]): void => {
1008
+ if (noticeWorkspace === undefined) return;
1009
+ const told = kills
1010
+ .filter((kill) => kill.told.length > 0)
1011
+ .map((kill) => ({ source: guardSource(noticeWorkspace, kill.id), takers: kill.told.map((t) => t.instanceId) }));
1012
+ if (told.length > 0) binding.noticesTold?.(told);
1013
+ };
1014
+ // The step boundary's read (ADR-0062): this conversation's pending notices in their audience,
1015
+ // reserved for this Turn until the Harness acknowledges the append. A guard kill's audience
1016
+ // leaves out the conversations its `bash` answer told, which only the pod's list says — so when
1017
+ // one is among them, the list is read first, and a read that could not tell takes no guard kill.
1018
+ const readBoundary = async (iid: string): Promise<Array<{ id: string; notice: LedgerBody }>> => {
1019
+ if (stopped || iid !== currentIid || binding.takeNotices === undefined) return [];
1020
+ const scope: NoticeScope = { ...noticeScope, taker: iid };
1021
+ if (noticeWorkspace !== undefined) {
1022
+ const guard = guardSource(noticeWorkspace, "");
1023
+ if ((binding.peekNotices?.(scope) ?? []).some((p) => p.source?.startsWith(guard))) {
1024
+ let kills: ListedKill[] | undefined;
1025
+ try {
1026
+ kills = await client.memoryKills({
1027
+ signal: AbortSignal.any([controller.signal, AbortSignal.timeout(memoryKillsReadMs)]),
1028
+ });
1029
+ } catch {
1030
+ kills = undefined;
1031
+ }
1032
+ if (kills === undefined) scope.exceptPrefix = guard;
1033
+ else applyTold(kills);
1034
+ }
1035
+ }
1036
+ if (stopped || iid !== currentIid) return [];
1037
+ const taken = binding.takeNotices(scope);
1038
+ const held = boundary.get(iid) ?? new Map();
1039
+ const said = new Map<string, string>();
1040
+ const out: Array<{ id: string; notice: LedgerBody }> = [];
1041
+ taken.ids.forEach((id, i) => {
1042
+ const raised = taken.raised?.[i] ?? { notice: taken.notices[i]! };
1043
+ const key = JSON.stringify(raised.notice);
1044
+ const first = said.get(key);
1045
+ if (first === undefined) {
1046
+ said.set(key, id);
1047
+ held.set(id, [{ id, raised }]);
1048
+ out.push({ id, notice: raised.notice });
1049
+ } else held.get(first)!.push({ id, raised });
1050
+ });
1051
+ if (held.size > 0) boundary.set(iid, held);
1052
+ return out;
1053
+ };
1054
+ // A step boundary whose notices the Harness did not append — a stopped model with no Allowed
1055
+ // picks, or a Turn ending as it read (`between-steps.ts`) — holds them until the next admission
1056
+ // of the conversation, which takes them again: a wake or a nudge carries them (ADR-0062).
1057
+ const releaseBoundary = (iid: string): void => {
1058
+ const held = boundary.get(iid);
1059
+ if (held === undefined) return;
1060
+ boundary.delete(iid);
1061
+ binding.releaseNotices?.(
1062
+ [...held.values()].flatMap((hs) => hs.map((h) => h.id)),
1063
+ iid,
1064
+ );
1065
+ };
1066
+ // The Harness appended them: heard in this conversation, in one save. An id this read did not
1067
+ // name changes nothing.
1068
+ const boundaryHeard = (iid: string, ids: readonly string[]): number => {
1069
+ const held = boundary.get(iid);
1070
+ if (held === undefined) return 0;
1071
+ const heard = ids.flatMap((id) => held.get(id) ?? []);
1072
+ for (const id of ids) held.delete(id);
1073
+ if (held.size === 0) boundary.delete(iid);
1074
+ if (heard.length === 0) return 0;
1075
+ binding.acknowledgeNotices?.(
1076
+ heard.map((h) => h.id),
1077
+ iid,
1078
+ );
1079
+ const bucket = carriedBy.get(iid) ?? { heard: [], unheard: [], task: false, fresh: [] };
1080
+ bucket.heard.push(...heard.map((h) => h.raised));
1081
+ carriedBy.set(iid, bucket);
1082
+ return ids.filter((id) => heard.some((h) => h.id === id)).length;
1083
+ };
1084
+
959
1085
  // The Turn's admission and a reroll's carry the pending notices in scope — a reroll's
960
1086
  // conversation is fresh, and has heard nothing. A nudge's and a wake's carry only the guard
961
1087
  // kills of this Turn that came after its own `bash` call's last kill verdict (`hearPodKills`):
@@ -973,7 +1099,7 @@ export function agentActorWith(
973
1099
  // the Harness last read its surface, `heard` before that — and whether an unheard one carried
974
1100
  // the Turn's task. Collected across the Turn, so a re-admission onto a lost conversation tells
975
1101
  // the Agent all of them again.
976
- type Raised = { notice: Notice; source?: string };
1102
+ type Raised = { notice: LedgerBody; source?: string };
977
1103
  // `fresh`: a fresh Turn's "this conversation is new", which rides an admission as `extra` and so
978
1104
  // is kept in no ledger — an unread one is carried again by the next re-admission.
979
1105
  const carriedBy = new Map<string, { heard: Raised[]; unheard: Raised[]; task: boolean; fresh: Notice[] }>();
@@ -996,26 +1122,34 @@ export function agentActorWith(
996
1122
  ) => {
997
1123
  await hearKernelKill();
998
1124
  const { known, unsaid, said } = await hearPodKills();
1125
+ releaseBoundary(turn.instanceId);
1126
+ // The conversation that takes a Workspace notice (ADR-0062): the one this admission rides, its
1127
+ // Instance ID. A continued Turn's is the continued conversation's; a fresh Turn's, a reroll's
1128
+ // and a burned conversation's successor's are each a new conversation, which has heard none.
1129
+ // A Workspace notice is delivered once in each, so every conversation in the Workspace hears
1130
+ // it — each child of a Pool has its own — and no conversation hears it twice.
1131
+ const taker = turn.instanceId;
999
1132
  // A read that could not tell leaves the same Turn no way to know which kills its own `bash`
1000
1133
  // reported: it takes none, as a nudge takes none then, and they stay for the next Turn.
1001
1134
  const sameTurn: NoticeScope = known
1002
- ? { ...noticeScope, except: said }
1135
+ ? { ...noticeScope, taker, except: said }
1003
1136
  : {
1004
1137
  ...noticeScope,
1138
+ taker,
1005
1139
  ...(noticeWorkspace === undefined ? {} : { exceptPrefix: guardSource(noticeWorkspace, "") }),
1006
1140
  };
1007
1141
  const scope: NoticeScope | undefined =
1008
1142
  opts.notices === "turn"
1009
- ? noticeScope
1143
+ ? { ...noticeScope, taker, turn: true }
1010
1144
  : opts.notices === "same-turn"
1011
1145
  ? sameTurn
1012
1146
  : unsaid.length > 0
1013
- ? { workspace: noticeWorkspace, only: unsaid }
1147
+ ? { workspace: noticeWorkspace, taker, only: unsaid }
1014
1148
  : undefined;
1015
- const none = { ids: [] as string[], notices: [] as Notice[], raised: [] as Raised[] };
1149
+ const none = { ids: [] as string[], notices: [] as LedgerBody[], raised: [] as Raised[] };
1016
1150
  const restored =
1017
1151
  opts.include && opts.include.length > 0
1018
- ? (binding.takeNotices?.({ ...noticeScope, only: opts.include }) ?? none)
1152
+ ? (binding.takeNotices?.({ ...noticeScope, taker, only: opts.include }) ?? none)
1019
1153
  : none;
1020
1154
  const main = (scope ? binding.takeNotices?.(scope) : undefined) ?? none;
1021
1155
  const taken = {
@@ -1030,12 +1164,16 @@ export function agentActorWith(
1030
1164
  // cut pick never got (ADR-0031), whose one path to the model is this admission. One "this
1031
1165
  // conversation is new" is enough, whichever path brought it.
1032
1166
  let fresh = false;
1033
- const notices = [...taken.notices, ...(opts.extra ?? [])].filter((n) => {
1167
+ const bodies = [...taken.notices, ...(opts.extra ?? [])].filter((n) => {
1034
1168
  if (n.kind !== "conversation-new") return true;
1035
1169
  if (fresh) return false;
1036
1170
  fresh = true;
1037
1171
  return true;
1038
1172
  });
1173
+ // The two kinds of body ride the admission apart (ADR-0062): a notice is a fact, and a
1174
+ // Steering message an author's text, which the Harness frames by its author.
1175
+ const notices = bodies.filter((n): n is Notice => n.kind !== "steering");
1176
+ const steering = bodies.filter((n): n is SteeringMessage => n.kind === "steering");
1039
1177
  // `served` is per Instance ID, and a nudge or a wake reuses the Turn's: it must be read again
1040
1178
  // before this admission counts as heard (`reraiseUnheard`). An admission that fails puts the
1041
1179
  // mark back — what an earlier one carried was heard.
@@ -1045,12 +1183,14 @@ export function agentActorWith(
1045
1183
  try {
1046
1184
  admission = await client.admit(turn, {
1047
1185
  definition,
1186
+ slot,
1048
1187
  signal: controller.signal,
1049
1188
  ...(notices.length ? { notices } : {}),
1189
+ ...(steering.length ? { steering } : {}),
1050
1190
  ...(opts.kept ? { kept: true } : {}),
1051
1191
  });
1052
1192
  } catch (err) {
1053
- binding.releaseNotices?.(taken.ids);
1193
+ binding.releaseNotices?.(taken.ids, taker);
1054
1194
  if (heard) served.add(turn.instanceId);
1055
1195
  throw err;
1056
1196
  }
@@ -1071,7 +1211,7 @@ export function agentActorWith(
1071
1211
  ...(killMark === undefined ? {} : { killMark }),
1072
1212
  };
1073
1213
  }
1074
- ledger(admission, taken.ids);
1214
+ ledger(admission, taken.ids, taker);
1075
1215
  const bucket = carriedBy.get(turn.instanceId) ?? { heard: [], unheard: [], task: false, fresh: [] };
1076
1216
  if (heard) {
1077
1217
  bucket.heard.push(...bucket.unheard);
@@ -1099,10 +1239,12 @@ export function agentActorWith(
1099
1239
  if (gone) bucket.heard = [];
1100
1240
  if (!read) bucket.unheard = [];
1101
1241
  for (const { notice, source } of again) {
1102
- const to = notice.scope === "conversation" ? conversation : noticeWorkspace;
1242
+ const to = notice.scope === "conversation" ? conversation : notice.scope === "slot" ? slot : noticeWorkspace;
1103
1243
  if (to === undefined) continue;
1104
- if (binding.restoreNotice) binding.restoreNotice(notice, to, source);
1105
- else binding.raiseNotice?.(notice, to);
1244
+ // A Workspace notice is pending again for the conversation that carried it, which did not
1245
+ // hear it; the others it reached keep theirs.
1246
+ if (binding.restoreNotice) binding.restoreNotice(notice, to, source, iid);
1247
+ else if (notice.kind !== "steering") binding.raiseNotice?.(notice, to);
1106
1248
  if (source !== undefined) restored.push(source);
1107
1249
  }
1108
1250
  }
@@ -1122,7 +1264,14 @@ export function agentActorWith(
1122
1264
  // Pool cancelling a child. The one exception is the host stopping the run for its own
1123
1265
  // reasons, which ADR-0007's restore re-attaches to.
1124
1266
  if (binding.hostStopping) return;
1267
+ // A step boundary's notices the Harness never acknowledged are pending again (ADR-0062).
1268
+ for (const iid of [...boundary.keys()]) releaseBoundary(iid);
1125
1269
  reraiseUnheard();
1270
+ // The Turn ended: a fresh conversation leaves every audience in its Workspace, a continued one
1271
+ // waits for its next Turn (ADR-0062).
1272
+ if (noticeWorkspace !== undefined) {
1273
+ binding.noticeTurnEnded?.(noticeWorkspace, [...new Set([...carriedBy.keys(), currentIid])]);
1274
+ }
1126
1275
  // Fire-and-forget, and unreportable BY CONSTRUCTION: this actor is stopped, so there is no
1127
1276
  // `agent.fault` left to raise. An orphan that survives a failed abort 404s on every tool
1128
1277
  // call and settles on its own.
@@ -1548,10 +1697,13 @@ export function agentActorWith(
1548
1697
  const kill = lostHarness && sandbox !== undefined ? await memoryFault(sandbox) : undefined;
1549
1698
  if (stopped) return;
1550
1699
  if (kill !== undefined && noticeWorkspace !== undefined) {
1700
+ // The kill cut this Turn (D4 rule 2): its Agent's next Turn retries the work, and hears it.
1551
1701
  binding.raiseNotice?.(
1552
1702
  memoryLimit({ limit: kill.limit, agent: input.agentName }),
1553
1703
  noticeWorkspace,
1554
1704
  kill.at === undefined ? undefined : kernelSource(noticeWorkspace, kill.at),
1705
+ [],
1706
+ { retry: { slot, cut: currentIid }, ...(kill.at === undefined ? {} : { at: kill.at }) },
1555
1707
  );
1556
1708
  }
1557
1709
  // A restart the Harness came back from keeps the conversation (ADR-0031); a 404 does not.
package/src/agent.ts CHANGED
@@ -58,9 +58,47 @@ export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhi
58
58
  * tells what each value gives. */
59
59
  export type WorkspaceAccess = "write" | "read" | "none";
60
60
 
61
+ // One entry of an Agent's `mcp` list (ADR-0079): a server of the config's `mcp` block, by its name,
62
+ // and the allowlist. An object, so that `resources` can join `tools` later. The TYPE cannot say
63
+ // which servers take a list: that is the config's (remote or stdio), so `jr2 up` says it.
64
+ /**
65
+ * One MCP server that an Agent uses, and the tools that it may call there.
66
+ *
67
+ * - `server` is a name of the `mcp` block of `jr2.config.ts`. `jr2 up` refuses a name that the
68
+ * block does not declare, and prints the `customize()` line that binds the Agent to one it does.
69
+ * - `tools` is the allowlist. A remote server needs it: a list of tool names, or `"*"` for every
70
+ * tool. The Harness offers and runs only those tools. When Workspace code in the Agent's Sandbox
71
+ * can spend the server's secret, `jr2 up` takes only `"*"`, because a list would then not be a
72
+ * limit. A stdio server takes no `tools`: the Agent gets every tool of the server.
73
+ *
74
+ * The model reads each tool as `mcp__<server>__<tool>`. A tool name in the list must keep that name
75
+ * within `[A-Za-z0-9_-]` and 128 characters.
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * import { agent } from "@jr2/orchestrator";
80
+ *
81
+ * export const triage = agent({
82
+ * model: "anthropic/claude-haiku-5-5",
83
+ * instructions: "You read the ticket and post a summary to the channel.",
84
+ * workspace: "none",
85
+ * mcp: [{ server: "slack-acme", tools: ["post_message", "read_channel"] }],
86
+ * });
87
+ * ```
88
+ *
89
+ * @see docs/reference/decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md
90
+ */
91
+ export type AgentMcpServer = {
92
+ /** A server name of the `mcp` block of `jr2.config.ts`. */
93
+ server: string;
94
+ /** The allowlist: tool names of the server, or `"*"` for every tool. Required for a remote
95
+ * server; absent for a stdio server. */
96
+ tools?: readonly string[] | "*";
97
+ };
98
+
61
99
  // The plain-data Agent definition (ADR-0018): IDENTITY plus one default. A Turn's Frame — what it
62
- // is about and where it works — is not here and cannot be (ADR-0057): a Worktree path exists only
63
- // once a run has a branch, so no definition written before the run could name one.
100
+ // is about and where it works — is not here and cannot be (ADR-0057): a Worktree exists only
101
+ // once a run has cut it, so no definition written before the run could name one.
64
102
  //
65
103
  // Must stay JSON-serializable: the definition rides the Turn to the Harness (ADR-0049), so
66
104
  // anything non-serializable would be silently lost — grow this contract deliberately. ADR-0028
@@ -119,17 +157,29 @@ export type AgentDefinition = {
119
157
  * level in the invoke's `input`. */
120
158
  thinkingLevel?: ThinkingLevel;
121
159
  // The persona in one word (ADR-0028), deliberately not `tools` (that names the control-plane
122
- // Menu, what it may SAY). `"read"` states intent and stops the honest path: bash stays, and the
123
- // detached review worktree is the containment. `"none"` frames no directory — only Working tools
160
+ // Menu, what it may SAY). `"read"` states intent and stops the honest path: bash stays, and a
161
+ // detached Worktree is the containment. `"none"` frames no directory — only Working tools
124
162
  // consume one — and places the Turn on the Instance Harness (ADR-0031).
125
163
  /** What the Agent may do to the Workspace. Default: `"write"`.
126
164
  * - `"write"`: all the Working tools (read, write, edit, bash, grep, glob).
127
165
  * - `"read"`: no write or edit tool. The Agent keeps bash, so `"read"` does not stop a command
128
- * that writes. To keep the Agent from changing the branch, frame its Turn in the review
129
- * Worktree: `reviewSha` in the spec, then `cwd: context.workspace.review?.<slot>`.
166
+ * that writes. To keep the Agent from changing the branch, frame its Turn in a detached
167
+ * Worktree: cut it with `{ path, detach: sha }`, then give that `path` as `cwd`.
130
168
  * - `"none"`: no Working tools. The Agent only reads its prompt and picks from its Menu. Its Turn
131
169
  * runs on the Instance Harness, not in a Sandbox, so it needs no `workspace()` around it. */
132
170
  workspace?: WorkspaceAccess;
171
+ // Reach for tools (ADR-0079): names only. The url and the secret are the Instance's, in the
172
+ // config's `mcp` block, so a packaged Machine ships no credential and a composer rebinds the list.
173
+ /** The MCP servers whose tools the Agent gets, each with its allowlist: see
174
+ * {@link AgentMcpServer}. The tools join the Agent's tools, after its Working tools and before
175
+ * its Menu. A call to one is not a pick, and the Turn goes on after it. Default: none. A composer
176
+ * replaces the whole list with `customize(machine, { agents: { <slot>: { mcp: [...] } } })`. */
177
+ mcp?: readonly AgentMcpServer[];
178
+ // Read by the Orchestrator alone (ADR-0002): the admission strips it, and the model never reads it.
179
+ /** May a Caller send this Agent a Steering message? Default: `true`. With `false`,
180
+ * `POST /runs/:runId/steering` and `jr2 steer` refuse it, and `GET /runs/:runId/agents` says
181
+ * `steerable: false`. The Machine can still steer the Agent with the `steer` action. */
182
+ steerable?: boolean;
133
183
  };
134
184
 
135
185
  // The Agent as an author DECLARES it (ADR-0054): the definition, except that `model` may be Open —
package/src/ambient.ts CHANGED
@@ -21,14 +21,12 @@ import type { AnyActorRef } from "xstate";
21
21
  export type AmbientHandles = {
22
22
  endpoint: string;
23
23
  sandbox: string;
24
- /** Every Repo Slot's branch-worktree path, by slot (ADR-0051). */
24
+ /** Every Repo Slot's folder, `/work/<slot>`, by slot (ADR-0051). */
25
25
  repos: Record<string, string>;
26
26
  /** Every Repo Slot's Binding url, by slot (ADR-0051): where a push goes. */
27
27
  bindings?: Record<string, string>;
28
- /** The branch the Worktrees are on. Absent for a Workspace with no Repo Slot (ADR-0051). */
29
- branch?: string;
30
- /** Detached review-worktree paths, when the spec carried `reviewSha` (ADR-0028). */
31
- review?: Record<string, string>;
28
+ /** Each Repo Slot's Binding base, where the Binding names one: a cut with no `from` starts here. */
29
+ bases?: Record<string, string>;
32
30
  };
33
31
 
34
32
  const byRef = new WeakMap<AnyActorRef, AmbientHandles>();