@jr2/orchestrator 0.6.0 → 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 (301) 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 +86 -9
  14. package/docs/concepts/architecture.md +41 -28
  15. package/docs/concepts/callers-and-hooks.md +205 -0
  16. package/docs/concepts/charts.md +7 -5
  17. package/docs/concepts/composition.md +10 -1
  18. package/docs/concepts/converge.md +4 -2
  19. package/docs/concepts/durable-runs.md +43 -16
  20. package/docs/concepts/effects.md +50 -22
  21. package/docs/concepts/gates.md +95 -25
  22. package/docs/concepts/machines-and-workflows.md +5 -4
  23. package/docs/concepts/pools-and-sources.md +187 -11
  24. package/docs/concepts/secrets-and-trust.md +73 -33
  25. package/docs/concepts/the-menu.md +11 -0
  26. package/docs/concepts/workspaces.md +6 -5
  27. package/docs/get-started/a-model-decides.md +14 -13
  28. package/docs/get-started/an-agent-in-a-sandbox.md +43 -34
  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/guides/add-tools-to-the-sandbox.md +55 -11
  32. package/docs/guides/answer-a-gate-from-a-forge.md +304 -0
  33. package/docs/guides/answer-a-gate.md +74 -37
  34. package/docs/guides/bind-open-parts.md +24 -8
  35. package/docs/guides/call-an-mcp-tool-from-a-machine.md +572 -0
  36. package/docs/guides/cancel-a-run.md +26 -16
  37. package/docs/guides/fix-a-failed-converge.md +18 -6
  38. package/docs/guides/give-a-caller-a-scope.md +203 -0
  39. package/docs/guides/give-an-agent-mcp-tools.md +422 -0
  40. package/docs/guides/iterate-on-a-machine-on-kind.md +10 -9
  41. package/docs/guides/let-an-agent-write-a-chart.md +9 -6
  42. package/docs/guides/look-inside-a-sandbox.md +101 -31
  43. package/docs/guides/open-the-console.md +63 -14
  44. package/docs/guides/read-a-conversation.md +225 -0
  45. package/docs/guides/run-a-chart-by-name.md +17 -16
  46. package/docs/guides/run-one-worker-per-item.md +15 -9
  47. package/docs/guides/set-an-agents-access-and-dials.md +2 -0
  48. package/docs/guides/start-a-run-for-each-alert.md +290 -0
  49. package/docs/guides/start-a-run-from-a-run.md +18 -16
  50. package/docs/guides/start-a-run.md +24 -18
  51. package/docs/guides/steer-an-agent.md +332 -0
  52. package/docs/guides/use-a-hosted-provider.md +1 -1
  53. package/docs/guides/watch-a-run.md +127 -121
  54. package/docs/guides/write-a-hook-kind.md +238 -0
  55. package/docs/guides/write-a-tool-as-an-mcp-server.md +351 -0
  56. package/docs/guides/write-a-workflow.md +17 -7
  57. package/docs/llms.txt +81 -15
  58. package/docs/reference/authoring-surface/agent.md +1 -1
  59. package/docs/reference/authoring-surface/answer.md +1 -1
  60. package/docs/reference/authoring-surface/arg.md +1 -1
  61. package/docs/reference/authoring-surface/basic.md +1 -1
  62. package/docs/reference/authoring-surface/bearer.md +1 -1
  63. package/docs/reference/authoring-surface/branchname.md +1 -1
  64. package/docs/reference/authoring-surface/carry.md +23 -0
  65. package/docs/reference/authoring-surface/chartschema.md +1 -1
  66. package/docs/reference/authoring-surface/chartschemafor.md +1 -1
  67. package/docs/reference/authoring-surface/command.md +1 -1
  68. package/docs/reference/authoring-surface/customize.md +4 -1
  69. package/docs/reference/authoring-surface/defineconfig.md +1 -1
  70. package/docs/reference/authoring-surface/defineevent.md +1 -1
  71. package/docs/reference/authoring-surface/dispatch.md +5 -4
  72. package/docs/reference/authoring-surface/fail.md +1 -1
  73. package/docs/reference/authoring-surface/file.md +1 -1
  74. package/docs/reference/authoring-surface/frompick.md +1 -1
  75. package/docs/reference/authoring-surface/gate.md +118 -0
  76. package/docs/reference/authoring-surface/group-agents.md +15 -0
  77. package/docs/reference/authoring-surface/group-charts.md +16 -0
  78. package/docs/reference/authoring-surface/group-composition.md +25 -0
  79. package/docs/reference/authoring-surface/group-config.md +17 -0
  80. package/docs/reference/authoring-surface/group-effects.md +48 -0
  81. package/docs/reference/authoring-surface/group-failure.md +13 -0
  82. package/docs/reference/authoring-surface/group-gates.md +16 -0
  83. package/docs/reference/authoring-surface/group-machine-and-events.md +15 -0
  84. package/docs/reference/authoring-surface/group-steering.md +12 -0
  85. package/docs/reference/authoring-surface/group-types-these-signatures-name.md +137 -0
  86. package/docs/reference/authoring-surface/group-workspaces-and-repos.md +15 -0
  87. package/docs/reference/authoring-surface/http.md +1 -1
  88. package/docs/reference/authoring-surface/isfailure.md +1 -1
  89. package/docs/reference/authoring-surface/isready.md +1 -1
  90. package/docs/reference/authoring-surface/jr2setup.md +13 -3
  91. package/docs/reference/authoring-surface/mcp.md +107 -0
  92. package/docs/reference/authoring-surface/open.md +5 -3
  93. package/docs/reference/authoring-surface/openbody.md +1 -1
  94. package/docs/reference/authoring-surface/paramsof.md +1 -1
  95. package/docs/reference/authoring-surface/pool.md +77 -9
  96. package/docs/reference/authoring-surface/push.md +1 -1
  97. package/docs/reference/authoring-surface/readyof.md +1 -1
  98. package/docs/reference/authoring-surface/reread.md +19 -0
  99. package/docs/reference/authoring-surface/secret.md +1 -1
  100. package/docs/reference/authoring-surface/sentby.md +1 -1
  101. package/docs/reference/authoring-surface/skip.md +22 -0
  102. package/docs/reference/authoring-surface/source.md +56 -1
  103. package/docs/reference/authoring-surface/steer.md +93 -0
  104. package/docs/reference/authoring-surface/text.md +1 -1
  105. package/docs/reference/authoring-surface/type-agentdefinition.md +14 -1
  106. package/docs/reference/authoring-surface/type-agentmcpserver.md +54 -0
  107. package/docs/reference/authoring-surface/type-anyframepath.md +1 -1
  108. package/docs/reference/authoring-surface/type-anyworktreepath.md +1 -1
  109. package/docs/reference/authoring-surface/type-binding.md +1 -1
  110. package/docs/reference/authoring-surface/type-caller.md +40 -0
  111. package/docs/reference/authoring-surface/type-callerrecord.md +32 -0
  112. package/docs/reference/authoring-surface/type-carry.md +19 -0
  113. package/docs/reference/authoring-surface/type-chainlink.md +3 -3
  114. package/docs/reference/authoring-surface/type-commandoutput.md +1 -1
  115. package/docs/reference/authoring-surface/type-customize.md +16 -1
  116. package/docs/reference/authoring-surface/type-delivered.md +17 -0
  117. package/docs/reference/authoring-surface/type-deliveredevent.md +18 -0
  118. package/docs/reference/authoring-surface/type-dispatchdefinition.md +4 -3
  119. package/docs/reference/authoring-surface/type-dispatchoutput.md +1 -1
  120. package/docs/reference/authoring-surface/type-effectbinding.md +12 -2
  121. package/docs/reference/authoring-surface/type-framepath.md +1 -1
  122. package/docs/reference/authoring-surface/type-gatebinding.md +15 -0
  123. package/docs/reference/authoring-surface/type-gatehooks.md +12 -0
  124. package/docs/reference/authoring-surface/type-gatelook.md +37 -0
  125. package/docs/reference/authoring-surface/type-gateoutcome.md +32 -0
  126. package/docs/reference/authoring-surface/type-gatereply.md +19 -0
  127. package/docs/reference/authoring-surface/type-gateslot.md +21 -0
  128. package/docs/reference/authoring-surface/type-gateslotinput.md +13 -0
  129. package/docs/reference/authoring-surface/type-gateslotparts.md +18 -0
  130. package/docs/reference/authoring-surface/type-gateslots.md +12 -0
  131. package/docs/reference/authoring-surface/type-gatetypesat.md +13 -0
  132. package/docs/reference/authoring-surface/type-hostinjectedinput.md +1 -1
  133. package/docs/reference/authoring-surface/type-httpoutput.md +1 -1
  134. package/docs/reference/authoring-surface/type-jr2config.md +37 -2
  135. package/docs/reference/authoring-surface/type-lateroutcome.md +1 -1
  136. package/docs/reference/authoring-surface/type-lends.md +5 -1
  137. package/docs/reference/authoring-surface/type-lookoutput.md +14 -0
  138. package/docs/reference/authoring-surface/type-mcpcalloutput.md +78 -0
  139. package/docs/reference/authoring-surface/type-mcpnotreturned.md +42 -0
  140. package/docs/reference/authoring-surface/type-mcpoptions.md +47 -0
  141. package/docs/reference/authoring-surface/type-mcpoutput.md +50 -0
  142. package/docs/reference/authoring-surface/type-mcpreadoutput.md +53 -0
  143. package/docs/reference/authoring-surface/type-mcpremoteserver.md +50 -0
  144. package/docs/reference/authoring-surface/type-mcpserver.md +41 -0
  145. package/docs/reference/authoring-surface/type-mcpstdioserver.md +42 -0
  146. package/docs/reference/authoring-surface/type-namedsecret.md +4 -4
  147. package/docs/reference/authoring-surface/type-openbodydeclaration.md +1 -1
  148. package/docs/reference/authoring-surface/type-openbodyoutput.md +1 -1
  149. package/docs/reference/authoring-surface/type-poolnextinput.md +1 -1
  150. package/docs/reference/authoring-surface/type-poolspec.md +11 -6
  151. package/docs/reference/authoring-surface/type-pushoutput.md +1 -1
  152. package/docs/reference/authoring-surface/type-pushsourcespec.md +38 -0
  153. package/docs/reference/authoring-surface/type-readsourcespec.md +47 -0
  154. package/docs/reference/authoring-surface/type-reposlot.md +1 -1
  155. package/docs/reference/authoring-surface/type-reread.md +18 -0
  156. package/docs/reference/authoring-surface/type-runsconfig.md +41 -0
  157. package/docs/reference/authoring-surface/type-sender.md +26 -1
  158. package/docs/reference/authoring-surface/type-sendercallerfield.md +14 -0
  159. package/docs/reference/authoring-surface/type-size.md +1 -1
  160. package/docs/reference/authoring-surface/type-skip.md +19 -0
  161. package/docs/reference/authoring-surface/type-sourcehooks.md +23 -0
  162. package/docs/reference/authoring-surface/type-sourcein.md +25 -0
  163. package/docs/reference/authoring-surface/type-sourcereply.md +18 -0
  164. package/docs/reference/authoring-surface/type-sourcespec.md +16 -7
  165. package/docs/reference/authoring-surface/type-sourceverdict.md +49 -0
  166. package/docs/reference/authoring-surface/type-standingpoolmachine.md +12 -0
  167. package/docs/reference/authoring-surface/type-standingpoolspec.md +51 -0
  168. package/docs/reference/authoring-surface/type-steerargs.md +20 -0
  169. package/docs/reference/authoring-surface/type-thinkinglevel.md +1 -1
  170. package/docs/reference/authoring-surface/type-usercontainer.md +1 -1
  171. package/docs/reference/authoring-surface/type-workspaced.md +1 -1
  172. package/docs/reference/authoring-surface/type-worktreefound.md +3 -3
  173. package/docs/reference/authoring-surface/type-worktreeinput.md +1 -1
  174. package/docs/reference/authoring-surface/type-worktreeoptions.md +1 -1
  175. package/docs/reference/authoring-surface/type-worktreeoutput.md +1 -1
  176. package/docs/reference/authoring-surface/type-worktreepath.md +1 -1
  177. package/docs/reference/authoring-surface/type-worktreeready.md +1 -1
  178. package/docs/reference/authoring-surface/type-worktreerefused.md +4 -4
  179. package/docs/reference/authoring-surface/workspace.md +1 -1
  180. package/docs/reference/authoring-surface/worktree.md +1 -1
  181. package/docs/reference/authoring-surface.md +88 -167
  182. package/docs/reference/cli.md +413 -74
  183. package/docs/reference/config.md +235 -2
  184. package/docs/reference/decisions/0002-duplex-streaming-actor.md +71 -11
  185. package/docs/reference/decisions/0005-sandbox-pod-composition.md +14 -10
  186. package/docs/reference/decisions/0006-agent-control-surface.md +4 -4
  187. package/docs/reference/decisions/0007-durable-machine-state.md +1 -1
  188. package/docs/reference/decisions/0009-cli-and-instance-interface.md +150 -49
  189. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +5 -3
  190. package/docs/reference/decisions/0011-workflow-defined-events.md +256 -12
  191. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +9 -2
  192. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +20 -14
  193. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +53 -27
  194. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +18 -14
  195. package/docs/reference/decisions/0017-pool-over-a-source-port.md +226 -12
  196. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +15 -7
  197. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +32 -18
  198. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +14 -1
  199. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +4 -4
  200. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +116 -34
  201. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +38 -37
  202. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +10 -5
  203. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +7 -6
  204. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +23 -7
  205. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +18 -10
  206. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +3 -2
  207. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +23 -18
  208. package/docs/reference/decisions/0032-the-console-unlocks-with-a-callers-credential.md +101 -0
  209. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +4 -3
  210. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +6 -6
  211. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +5 -4
  212. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +19 -19
  213. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +6 -1
  214. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +55 -40
  215. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +14 -13
  216. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +7 -0
  217. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +13 -7
  218. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +38 -30
  219. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +64 -17
  220. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +2 -1
  221. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +2 -2
  222. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md +109 -0
  223. package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +51 -23
  224. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +94 -54
  225. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +101 -47
  226. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +32 -9
  227. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +17 -13
  228. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +56 -30
  229. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +35 -25
  230. package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +4 -3
  231. package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +37 -10
  232. package/docs/reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md +285 -0
  233. 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
  234. package/docs/reference/decisions.md +6 -4
  235. package/docs/reference/glossary.md +96 -42
  236. package/docs/reference/hooks.md +1077 -0
  237. package/docs/reference/http-api.md +976 -149
  238. package/docs/why.md +2 -1
  239. package/package.json +10 -3
  240. package/src/actor.ts +36 -10
  241. package/src/agent.ts +50 -0
  242. package/src/assembly-checks.ts +530 -4
  243. package/src/callers.ts +245 -0
  244. package/src/chart.ts +52 -7
  245. package/src/config.ts +550 -1
  246. package/src/conversations.ts +453 -0
  247. package/src/custodian.ts +8 -2
  248. package/src/customize.ts +143 -4
  249. package/src/dispatch-effect.ts +142 -26
  250. package/src/durability.ts +2 -0
  251. package/src/duration.ts +20 -0
  252. package/src/effect-checks.ts +2 -1
  253. package/src/effect.ts +60 -7
  254. package/src/effector-wire.ts +272 -27
  255. package/src/gate-brand.ts +16 -0
  256. package/src/gate.ts +634 -48
  257. package/src/harness-client.ts +10 -4
  258. package/src/held-secrets.ts +72 -23
  259. package/src/holds.ts +2 -2
  260. package/src/hook-kinds/discord.ts +92 -0
  261. package/src/hook-kinds/door.ts +107 -0
  262. package/src/hook-kinds/github.ts +89 -0
  263. package/src/hook-kinds/gitlab.ts +72 -0
  264. package/src/hook-kinds/hook.ts +7 -0
  265. package/src/hook-kinds/index.ts +465 -0
  266. package/src/hook-kinds/slack.ts +151 -0
  267. package/src/hook-kinds/webhook.ts +113 -0
  268. package/src/hooks.ts +506 -0
  269. package/src/http-effect.ts +76 -38
  270. package/src/http.ts +889 -94
  271. package/src/index.ts +16 -5
  272. package/src/instance.ts +196 -26
  273. package/src/internal.ts +22 -3
  274. package/src/machine-doc.ts +26 -1
  275. package/src/mcp-effect.ts +424 -0
  276. package/src/mount-digest.ts +41 -7
  277. package/src/names.ts +64 -2
  278. package/src/notices.ts +138 -10
  279. package/src/open-body.ts +25 -11
  280. package/src/open-effect.ts +17 -4
  281. package/src/open.ts +4 -2
  282. package/src/parts.ts +87 -9
  283. package/src/party.ts +2 -1
  284. package/src/pick.ts +2 -2
  285. package/src/pool.ts +1142 -163
  286. package/src/registration.ts +203 -13
  287. package/src/run-host.ts +1509 -171
  288. package/src/run-list.ts +191 -0
  289. package/src/sandbox-kube.ts +53 -5
  290. package/src/secret-placement.ts +239 -33
  291. package/src/server.ts +53 -4
  292. package/src/setup.ts +72 -32
  293. package/src/snapshot-store.ts +565 -48
  294. package/src/steer.ts +111 -0
  295. package/src/stub-harness.ts +6 -2
  296. package/src/tokens.ts +263 -22
  297. package/src/wire.ts +17 -0
  298. package/src/workspace.ts +50 -6
  299. package/src/worktree.ts +3 -3
  300. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +0 -61
  301. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +0 -65
@@ -15,11 +15,13 @@ else.
15
15
 
16
16
  - **The rule.** A name is on the surface when an author writes it in a workflow module or in `jr2.config.ts`. No other
17
17
  name is. A name that an author only gets by inference stays off the surface: the event union that `jr2Setup` derives,
18
- a Gate's input, an Agent's Frame. The `gate` actor is off the surface too. `jr2Setup` registers it, and a state
19
- invokes it as `src: "gate"`, so no author writes the value. A type is on the surface when an author must write its
20
- name, for example `Workspaced`, `HostInjectedInput`, `AgentDefinition`, and the types of the parts that `customize()`
21
- takes. `JR2Config` is on the surface too: the `jr2.config.ts` Reference documents its fields, and an author names it
22
- to type a config that is built outside `defineConfig()`.
18
+ a Gate's input, an Agent's Frame. The plain `gate` actor is off the surface too. `jr2Setup` registers it, and a state
19
+ invokes it as `src: "gate"`, so no author writes the value. The `gate()` factory of a Gate slot that hears a Hook is
20
+ on the surface, because an author writes it in `actors` ([ADR-0011](0011-workflow-defined-events.md)); the walk knows
21
+ such a slot by identity, not only by `src === "gate"`. A type is on the surface when an author must write its name,
22
+ for example `Workspaced`, `HostInjectedInput`, `AgentDefinition`, and the types of the parts that `customize()` takes.
23
+ `JR2Config` is on the surface too: the `jr2.config.ts` Reference documents its fields, and an author names it to type
24
+ a config that is built outside `defineConfig()`.
23
25
  - **Named exports only.** `src/index.ts` names each export (`export { a } from …`, `export type { T } from …`), in
24
26
  groups by topic, and has no `export *`. Thus an Assistant that reads the file sees the full surface, and each change
25
27
  to the surface is a line in a diff.
@@ -73,14 +75,14 @@ another Agent slot (a nested Turn: its picks are its own) or a `gate` (its trans
73
75
  leaves out everything below that child; a child that runs anything else stays in. A state that invokes `gate` gets its
74
76
  accepted set from its own and its ancestors' transitions and those written on its plain children, by the same descent:
75
77
  position gives a Gate the writings below it that no other party answers for, as it gives an Agent its modes (the Pool's
76
- wake Gate, on its root, takes the writing on `parked`). The consumer names neither; MCP appears nowhere in workflow
77
- code. Mechanics: the **vocabulary** derivation is static, in `jr2Setup.createMachine` — a config walk wraps each
78
- invoke's `input` to append the derived names, so names still ride serializable input and the ADR-0007 restore path and
79
- invoke-time validation are unchanged. The same walk unions the derived sets of every state that invokes an Agent of one
80
- name into that Agent's **Menu** — its tools, fixed for a conversation so the provider's prompt cache survives a change
81
- of state. The walk reads transition _keys_ and so cannot see guards, which is why what a Turn may pick is not static on
82
- top of it: `agentSurface` asks the invoking machine's guards, and the Turn is told its **Allowed picks** — the derived
83
- set, less every event that the live snapshot, in its active mode, would not take
78
+ `source` Gate, on its root, takes the writing on `parked`). The consumer names neither, and the Menu's `mcp__jr2__` tool
79
+ names appear nowhere in workflow code. Mechanics: the **vocabulary** derivation is static, in `jr2Setup.createMachine` —
80
+ a config walk wraps each invoke's `input` to append the derived names, so names still ride serializable input and the
81
+ ADR-0007 restore path and invoke-time validation are unchanged. The same walk unions the derived sets of every state
82
+ that invokes an Agent of one name into that Agent's **Menu** — its tools, fixed for a conversation so the provider's
83
+ prompt cache survives a change of state. The walk reads transition _keys_ and so cannot see guards, which is why what a
84
+ Turn may pick is not static on top of it: `agentSurface` asks the invoking machine's guards, and the Turn is told its
85
+ **Allowed picks** — the derived set, less every event that the live snapshot, in its active mode, would not take
84
86
  ([ADR-0029](0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md) owns the rationale).
85
87
  Authoring is untouched by the split: a workflow names no tools in either leg. The same walk feeds `jr2 visualize` ("this
86
88
  state's agent can call X, Y"). Dotted names (`agent.*`, `workspace.lost`, `xstate.*`, `after`) are mechanically
@@ -89,7 +91,9 @@ excluded.
89
91
  **A transition belongs to the party that its state, or the nearest state above it, invokes.** The party is the Agent
90
92
  slot or the `gate` that state invokes, or both when one state invokes both; root transitions, with no such state above,
91
93
  belong to both. That is who may send an event whose def leaves `audience` unset
92
- (`defineEvent({ audience?: "agent" | "external" | "any" })`, no default):
94
+ (`defineEvent({ audience?: "agent" | "external" | "any" })`, no default). One case reads as `external`: an event in a
95
+ Gate slot's `delivers` whose def leaves the audience unset, so a Hook's event is never on the Menu of an Agent below
96
+ that Gate ([ADR-0011](0011-workflow-defined-events.md)):
93
97
 
94
98
  | Transition written on | Unset audience: who may send |
95
99
  | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
@@ -9,35 +9,219 @@ done — was ~90 lines of hand-rolled bookkeeping in the jr-parity exercise (`ac
9
9
  `spawnChild` top-level or visualize goes blind" footgun). No xstate-ecosystem primitive exists for this (verified: the
10
10
  canonical v5 pattern _is_ the hand-rolled loop), so jr2 ships one.
11
11
 
12
+ The webhook round of 2026-11-03 added the other half: work that starts from the outside. An alert service sends one
13
+ alert, and each alert needs one run of a triage Workflow. A forge labels an issue, and each labeled issue needs one run
14
+ of a ticket Workflow. A Hook ([ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md))
15
+ never starts a run itself. It reaches two things that exist: a Gate, which answers a run
16
+ ([ADR-0011](0011-workflow-defined-events.md)), and a Source, which brings items. So this ADR gives the Source a push
17
+ half, lets a Pool's worker be a run of another Workflow, and lets such a Pool stand.
18
+
12
19
  ## `pool(worker, spec)` — a machine factory, like `workspace()`
13
20
 
14
- `pool(worker, { source, itemId, cap, itemInput, onDrained, failures })` returns a plain machine that owns
21
+ `pool(worker, { source, itemId, cap, itemInput, onDrained, failures, standing })` returns a plain machine that owns
15
22
  spawn-under-cap, stable child identity, typed completion collection (`DoneActorEvent<Output, string>` + the
16
- `xstate.done.actor.*` wildcard — zero casts), `stopChild` bookkeeping, the wake gate, and the re-query timer. The one
17
- top-level `spawnChild` lives in jr2's pool code, once — the visualize footgun is dissolved by construction, and
23
+ `xstate.done.actor.*` wildcard — zero casts), `stopChild` bookkeeping, the Source's Gate, and the re-query timer. The
24
+ one top-level `spawnChild` lives in jr2's pool code, once — the visualize footgun is dissolved by construction, and
18
25
  everything durable is a registered string src (xstate cannot persist inline-src children, which independently forces
19
26
  this shape). The pool composes: it returns a plain machine and can itself be nested as a child. The typed idioms
20
27
  (wildcard done events, `assertEvent`, outcome-in-context root output) ship as documented patterns regardless, so
21
28
  outgrowing the pool means hand-rolling without penalty, not fighting a framework.
22
29
 
30
+ **The worker is a child Machine or a `dispatch(...)` slot, never a bare Workflow name.** A child Machine runs inside the
31
+ Pool's run, as it always did. A `dispatch` slot ([ADR-0071](0071-a-run-starts-a-run-through-a-dispatch-effect.md)) makes
32
+ each item a run of its own: `pool(dispatch("triage", { input: triageDoor, wait: true }), { … })`. The two worker kinds
33
+ differ in five places — a worker's defect, a cancel of the Pool, what `customize` reaches, the life of an item's key,
34
+ and where the Gates are — and each difference is one that ADR-0071 states between a child and a dispatched run. So they
35
+ belong to the worker kind, and the word `dispatch` at the call site shows them. `pool("triage", …)` would hide all five
36
+ behind a string, so jr2 does not offer it. Inside, one claim loop serves both kinds with two internal policies.
37
+
23
38
  ## The source port is generalized; Work Source is one adapter
24
39
 
25
- The pool draws from a **Source** port — `next({ active, settled, input }) → item | null`, plus an optional `wake` event
26
- def (webhook / `jr2 send` push seam) and `pollEvery` (the set mutates underneath us) — a queue, a generator, or a
27
- re-queried set. `input` is the Pool's input and `settled` holds the ids of the finished and failed items, so a Source
28
- can fan out over a plan with no state of its own. tk's claim actor is one adapter; GLOSSARY.md's **Work Source**
29
- reframes as _the Source adapter for tickets_, and the Ready-set doctrine (re-query, never materialize) survives as that
30
- adapter's semantics, not the primitive's.
40
+ The pool draws from a **Source** port in one of three forms:
41
+
42
+ - **Pull**: `next({ active, settled, input }) → item | null`, plus an optional `wake` event def (for `jr2 send`) and
43
+ `pollEvery` (the set changes under the Pool). `input` is the Pool's input and `settled` holds the ids of the finished
44
+ and failed items, so a Source can fan out over a plan with no state of its own.
45
+ - **Read**: `read`, an `http` Effect marked `idempotent: true` or a promise (`pool()` refuses another Effect,
46
+ [ADR-0067](0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md), as a Gate's look does),
47
+ and `items`, which turns its answer into the items in order (by default the answer itself, when it is a list). The
48
+ read's input is the Pool's input. It is the re-queried set, read through an Effect, so the read can spend a secret
49
+ that the Instance Effector holds ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)), which
50
+ a `next` in the Orchestrator cannot. A promise read holds no secret, and the default tier reads with one. A failed
51
+ read gives no items.
52
+ - **Push**: `item`, a zod schema, and `hooks` whose handlers carry each item. Nothing can be read again: each item comes
53
+ in a delivery, and the Pool holds it (below).
54
+
55
+ A pull or a read Source can hear a Hook too. Its `hooks` handlers give `reread()`: read the set again now. **When a
56
+ Source can read, the read is the truth**: a delivery only says "read again", and an item that lost its label while it
57
+ waited is not in the next read. A handler of a Source has the shape of a Gate's (by Hook kind, then event key) and reads
58
+ `{ payload, caller, delivery, input }`. It gives `reread()`, `carry(item)` or `skip(reason)`, and never a bare
59
+ `undefined`: a delivery that brings nothing has a reason that a person can read in the Hook's answer, the hook log and
60
+ `jr2 hooks`. A throw is a skip with the error as its reason. `skip` is for a Source's handlers only; a Gate's handler
61
+ gives an event or `undefined`. A verdict that the Source's form cannot take (`carry` from a pull or a read Source,
62
+ `reread` from a push Source) and an item that the `item` schema refuses are each a skip, with the reason. **An item
63
+ whose `itemId` throws or gives no id gets no worker, in each form of Source.** In a delivery to a push Source, it is a
64
+ skip with the reason. In a read, the read skips that item, with a `pool` mark `skipped` on the feed that says which item
65
+ and why, and it claims the rest. A read at each `pollEvery` finds the item again, so jr2 marks each such item one time
66
+ in a run's life. Else one item with no id would make each read give no items, with no mark, and no item of that Source
67
+ would start. A `next` gives one item at a time, so its item with no id ends the run `error`, and the streak below pauses
68
+ the Pool when that does not stop. Mechanically the push half is the Pool's `source` Gate, which heard only `wake`
69
+ before, so the Hook's fan-in reaches it as it reaches any open Gate (ADR-0011). A Source may also give
70
+ `reply(verdict, { caller })`: pure, from its own verdict to plain text or `undefined`, which the person who acted on a
71
+ chat service reads, and nobody else; a `skipped` text shows only when no other target gave one (ADR-0011). The verdict
72
+ is the line of `sources`, and for `held`, `existing` and `hold-full` also the parsed `item`. A paused Pool has no run,
73
+ so no `reply` runs for `paused`. The reply after the work is a plain `http` Effect of the Machine.
74
+
75
+ The claim loop has three paths: `next`, the read, and the held items. A `reread`, a `wake` or a settled worker that
76
+ comes during a read (or during a call of `next`) sets a latch, and the loop reads once more after that read, so no
77
+ change is lost. Reads coalesce: one read claims every free slot, and the loop does not read after each claim and each
78
+ settled item one by one. The Pool drops a known id before it claims, so a read that finds an active or settled item
79
+ starts nothing. For a Pool of runs, an item that started a run has one record of "settled": the store's key check
80
+ (below). An item that ended with no run (`start-refused`, `dispatch-error`, `unreachable`) has no row in the store, so
81
+ the Pool counts its key as known for the rest of its run's life: else the read that its own settle starts would claim it
82
+ again at once, with no pause and no end, and one item that the callee's door refuses would hold the Orchestrator's event
83
+ loop. The next run of the Pool (a hand-over or a resume) tries such an item again, so a publish that fixes the door or
84
+ the `itemInput` takes it. The Pool keeps no settled keys past its own run's life: it keeps the run and the status of
85
+ each item that ended in that life, which the bound of a standing run limits, and which a `next` reads as `settled`. A
86
+ `next` that gives an item whose run ended before gets that run (`existing`), whose wait ends at once, so the item
87
+ settles and is not asked for again.
31
88
 
32
- A Work Source — `tk`, GitHub issues, Jira, a task queue — is a behavior port, not a canonical data schema: verbs like
89
+ tk's claim actor is one adapter; GLOSSARY.md's **Work Source** reframes as _the Source adapter for tickets_, and the
90
+ Ready-set doctrine (re-query, never materialize) survives as that adapter's semantics, not the primitive's. A Work
91
+ Source — `tk`, GitHub issues, Jira, a task queue — is a behavior port, not a canonical data schema: verbs like
33
92
  `claimNext`, `updateStatus` and `comment`, each gated by a capability the adapter advertises. It owns the dependency
34
93
  graph and hands out the Ready-set, the currently-unblocked items in dependency order, so the Machine never encodes a
35
94
  dependency edge. The Ready-set mutates during execution — items can be reopened or created mid-run — so it is not a
36
- static FIFO queue.
95
+ static FIFO queue. An adapter can give both halves of a Source: its read and its Hook's handlers, so the mapping from
96
+ the service's data to an item is written once.
97
+
98
+ ## A pushed item is held until the Pool claims it
99
+
100
+ - **The Pool holds each carried item in its run's blob**, in the order of arrival, until it claims it. An item that
101
+ leaves for a worker is no longer held: the worker, by its key, is the record. The Hook's answer leaves after the save
102
+ that records the item, so a `held` answer is durable.
103
+ - **The key check comes before the hold.** An item whose key a held item, an active worker, or a run of the worker's
104
+ Workflow holds is `existing`, and the answer names the run when one holds it.
105
+ - **`hold` bounds the held items**, 1000 by default. Above it, the Hook's answer is 429 (for a chat command, 200 with a
106
+ text that says why, ADR-0011) and the hook log keeps the delivery. The 1000 is not measured.
107
+ - **What a push loses**: a delivery while the Orchestrator is down, while no Assembly is current (503), while the Pool
108
+ is paused (`paused`), and above `hold`. A read Source loses nothing, because the next read finds the item.
109
+ - **A 429 does not claim the delivery's id.** The hook log keeps it as `hold-full`, and a repeat is found only by a
110
+ delivery that the door claimed (ADR-0011), so the service's send again is heard: a Gate that took it passes it over by
111
+ the run's answers, and a Pool that holds its item answers `existing`.
112
+ - **A Source's verdict on a delivery** is one of `held`, `existing`, `hold-full`, `reread`, `skipped` and `paused`, and
113
+ it goes in the delivery's outcome as a line of `sources` (ADR-0011).
114
+
115
+ ## A Pool of runs, and a Pool that stands
116
+
117
+ A Pool whose worker is a `dispatch` slot keys each item's run by `itemId`, scoped to the callee Workflow
118
+ ([ADR-0071](0071-a-run-starts-a-run-through-a-dispatch-effect.md)): every Pool, every hand-over and every `jr2 run` that
119
+ gives the same key for that Workflow gets the same run while its row lives, live and for `runs.retain` after it ends
120
+ ([ADR-0065](0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md)). `cap` counts the live runs that
121
+ this Pool dispatched, so its slot must say `wait: true`: with `started` alone, the Pool cannot know when a run ends. A
122
+ run that ends in any way frees its slot. A run that ends `failed`, `error`, `cancelled`, `lost` or `drifted`, or a start
123
+ that the door refuses, is a failure of its item, and never the Pool's `error`: the run is not the Pool's child. A cancel
124
+ of the Pool never cancels its runs (ADR-0071), and `customize(pool, { agents })` reaches nothing in them
125
+ ([ADR-0049](0049-a-machine-carries-its-parts-and-composes-by-invoke.md)). The Pool spawns its slot with the item's id as
126
+ the spawn id, and the Effect sends that id as the key, not its ledger row id. The run's `dispatchedBy` gains `key`, and,
127
+ for a carried item, `hook: { name, delivery, onBehalfOf? }`, so `jr2 status` prints which Pool, key and delivery started
128
+ it. Neither is on the open band (ADR-0014): a key is the workflow's data. A run that a read claimed names no delivery:
129
+ the Pool's feed says why an item did or did not start, with a `pool` mark for each start (`started`), each end
130
+ (`settled`, with the run's status, `start-refused` or `dispatch-error`), each held item that a hand-over drops
131
+ (`carry-refused`) and each item of a read that has no id (`skipped`). A `wait` that the slot's timeout ends while the
132
+ run is still live (`status: "live"`) is sent again under its key, so the wait follows the run and `cap` still counts it.
133
+
134
+ **`standing: true` makes a Pool of runs stand.** It is two things:
135
+
136
+ - **Parking.** A standing Pool never ends by itself. It has no `drained` and no `deadlocked` end, no output and no
137
+ `failures` mode. When it has no work it waits for a delivery, a `reread` or its `pollEvery`. An item that ends is a
138
+ `settled` mark on the feed, with the run's status.
139
+ - **The standing run.** jr2 keeps one live run of the Workflow under the current Assembly
140
+ ([ADR-0069](0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md)). Nobody starts it: jr2
141
+ starts it at boot and at the publish that adds it, and `jr2 run` of it is refused, except to resume it (below). A
142
+ dispatch to a standing Workflow is refused 409. jr2 **hands it over** to a new run at every publish, at drift, after
143
+ `error` (with back-off), and when its blob passes its bound. At every publish, because the Assembly is one hash and a
144
+ handler has no stamp of its own. The bound is 500 items taken or one day: each item taken is a dispatch, which keeps a
145
+ row in the Effect ledger for the run's life (ADR-0067), about 217 bytes, so 500 is about 110 KB a save. The bound
146
+ counts the items that the run took, not the keys that a hand-over carried to it: each carried key is sent again, so a
147
+ count of the ledger's rows would hand a run with a `cap` at the bound over again at once. **A run at the bound in
148
+ which no item that it took itself started a run is handed over as a run that ended `error` is**: after the back-off,
149
+ and it counts in the streak below. An item that ends with no run (`start-refused`, `dispatch-error`, `unreachable`,
150
+ `unknown`) is known only for its run's life (above), so a backlog of as many such items as the bound fills the bound
151
+ again in each new run. A hand-over at once would then loop with no pause and no end, and would hold the event loop.
152
+ jr2 ends that run `done`, with no `handedOverTo`, and starts the next run after the back-off. A run in which one item
153
+ that it took started a run (or found its run under its key) is handed over at once, and the count starts again. A
154
+ carried key does not count: the hand-over sends it again and it finds a run that an earlier run started, so a callee
155
+ run that lives for hours would hide the loop for as long as it lives. While each item that the run took is still in
156
+ flight, jr2 does not yet know, so it waits for the save of the first answer. The back-off before the hand-over after
157
+ the `n`-th counted run in a row is 1 s, 4 s, then 16 s. The numbers are not measured. The hand-over carries the held
158
+ items, parsed by the new Assembly's `item` schema (an item that does not parse is dropped, with a `carry-refused`
159
+ mark), and the active keys: the new run sends each one again with `wait: true`, the door answers `existing`, and the
160
+ wait follows that run, so `cap` still counts it. A held item is mapped by the new `itemInput`. A hand-over ends the
161
+ old run `done`, and its record carries `handedOverTo: <new run id>`: a standing Pool has no output, so its stint is
162
+ done. The start of the new run and the end of the old one are one synchronous step. A delivery that a Source hears
163
+ across that step (its key lookup awaits the store) goes on to the new run's Source, so the door never claims a
164
+ delivery that no run holds. jr2 keeps the **head** of each standing Workflow in a table of the store: its newest run,
165
+ when that run started, how many counted runs in a row ended, and why it is paused. jr2 writes the head in the same
166
+ step as the start. The sweep of rows older than `runs.retain`
167
+ ([ADR-0065](0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md)) never deletes the run that a head
168
+ names, so a paused Pool keeps its held items for as long as it is paused. A Workflow that a publish removes, or that
169
+ no longer stands, gets no hand-over: its live run holds its Assembly and runs on, as any run does (ADR-0069), until a
170
+ person cancels it. A standing run has no Caller: its record's `startedBy` is `{ kind: "jr2", why }`, with `why` one of
171
+ `boot`, `publish`, `hand-over` and `resume`
172
+ ([ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md)).
173
+ - **A cancel is a pause.** `jr2 send <run> --event CANCEL` of a standing run ends that run `cancelled`
174
+ ([ADR-0025](0025-cancel-ends-a-run-stop-parks-it.md)) and pauses its Workflow: jr2 starts it again at no boot and no
175
+ publish until `jr2 run <workflow>` resumes it. `jr2 up` and `jr2 status` print the Pool as paused, with the number of
176
+ items it holds. They read it from `GET /assembly` and from the answer of the publish, whose `standing` list gives each
177
+ standing Workflow's state (`live`, `paused` or `waiting`), its run and its count of held items. It holds no key and no
178
+ item, so it is on the open band with the held Assemblies ([ADR-0014](0014-observation-is-open-run-state-is-not.md)). A
179
+ paused Pool has no live run: its held items and active keys stay in the blob of the cancelled run. A resume is a
180
+ hand-over from that run, so the new run takes them and claims under `cap`, and a read Source reads again. The new run
181
+ records the Caller of the `jr2 run` that resumed it as `resumedBy`. On the wire the resume is
182
+ `POST /workflows/<workflow>/runs`, which answers `201 { runId, instanceId, resumed: true }`, and 409 while the
183
+ Workflow is live or waits out a back-off. `jr2 run <workflow> --drop-held` (`?drop-held=true`) resumes with no held
184
+ item, so a person can remove a poison item or a queue that they paused on purpose; a read Source then finds again what
185
+ is still true. A delivery to a paused Pool gets `paused`, and nothing is held, so a push Source loses it and a read
186
+ Source finds it at the resume. A delivery to a standing Pool in its back-off (no live run, and not paused) makes the
187
+ door answer 503 (ADR-0011), so the service sends it again. **One rule counts a run in the streak**, for each end that
188
+ hands over after the back-off: `error`, `failed` (a `fail()` of the Machine,
189
+ [ADR-0073](0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md), which jr2 treats as
190
+ `error`) and the bound with no run started. A run in which an item that it took itself, not a carried key, started a
191
+ run starts the count again, and is handed over after 1 s. Any other run counts. So a run that took only items that the
192
+ callee's door refused, and then ended `error`, counts. After 3 hand-overs from counted runs in a row, jr2 pauses the
193
+ Pool when the next counted run ends, so a poison item, or a backlog that the callee's door refuses, cannot loop
194
+ without end. The pause says why: `errors` when the last run ended `error` or `failed`, and `bound` when it took its
195
+ bound; `jr2 status` and the Orchestrator's log line name that end. The 3 is not measured. The Pool's context keeps the
196
+ keys that the hand-over carried, so jr2 tells them from the items that the run took.
197
+
198
+ **What the load walk refuses**, with its fix in the message:
199
+
200
+ - `standing: true` on a Pool of child Machines: a child cannot be handed over.
201
+ - `standing: true` on a Pool that is not the top Machine of its Workflow: jr2 keeps one run for each standing Workflow,
202
+ so the Workflow is the Pool. The fix is to move the Pool to its own file in `workflows/`.
203
+ - A Pool of runs that does not stand. A batch of runs (a Pool of runs that a Caller starts and that ends) is not offered
204
+ now: its `itemId` is a key that lives for `runs.retain` past the run, so a second batch in that time does no work. To
205
+ add it later does not break anything.
206
+ - A Pool of runs whose `dispatch` slot has no `wait: true`, or has a `target`: a foreign worker is not designed.
207
+ - A Pool of runs with no `cap`. The author must choose it; a Pool of child Machines keeps its default of 1.
208
+ - A push Source on a Pool that ends when it drains, which would end `drained` at once. The types refuse it too.
209
+ - A worker that is a bare Workflow name.
210
+
211
+ **What `jr2 up` prints and warns.** For each standing Pool, it prints the chain from each Hook that reaches it to the
212
+ callee: `sentry → alerts → triage`. It warns when two standing Pools dispatch to one Workflow: `cap` is per Pool, so one
213
+ issue with two labels can take a slot in each, and the caps add.
214
+
215
+ **The callee must end the level.** A read Source re-reads a level, such as "the open issues with the label `jr2`". When
216
+ the callee's run ends and the label stays, the key keeps the item from a second run while the run's row lives.
217
+ `runs.retain` after the run ended (30 days by default), the row is swept, and the next read starts the item again. So
218
+ the callee (or the person) removes the label. A drifted callee holds its key for `runs.retain`, and nothing starts that
219
+ item again without a person.
37
220
 
38
221
  ## Terminal triage is three-valued, surfaced as status
39
222
 
40
- Forced by parking-is-retention (ADR-0012) plus workflows whose children park on gates (escalation, human review):
223
+ Forced by parking-is-retention (ADR-0012) plus workflows whose children park on gates (escalation, human review). It
224
+ holds for a Pool that does not stand:
41
225
 
42
226
  - **drained** — source empty and all children settled → final, or the run ends `failed` when items failed (below);
43
227
  - **deadlocked** — open-but-never-ready items with nothing progressing → surfaced distinctly (jr exit 2), not an
@@ -69,3 +253,33 @@ is a correct answer. The default is `"fail"` because `done` means that the work
69
253
  not exit 0 while every item failed. The deadlock is the usual end of a dependency backlog with a failure: a Source over
70
254
  a tracker keeps the failed item open, so its dependents never become ready. The `status` in the detail tells the two
71
255
  ends apart, and the two details have one shape.
256
+
257
+ ## Tests
258
+
259
+ The default e2e tier (`features/standing-pool.feature`) and `pool-sources.test.ts` prove the standing Pool of runs: a
260
+ push Source, the held items and `hold`, `cap`, `reread`, the latch and merged reads, a restart and a hand-over with no
261
+ item lost or started twice, a hand-over across a changed callee door, and the key per Workflow. They read with a
262
+ promise, because the default tier has no Instance Effector. The `@kind` tier proves a read through an `http` Effect.
263
+
264
+ ## Considered options
265
+
266
+ - **An Intake**: a kit-shipped `intake({ routes })`, each `route({ start, input, hooks, look, cap })`, that jr2 keeps as
267
+ a **Standing Workflow**. Rejected: it needs three concepts (Intake, route, Start) and two GLOSSARY terms, and reuses
268
+ nothing that an author knows about a Pool, though it is a Pool inside. It writes the mapping from a delivery to a
269
+ start twice, once in the handler and once in the look, and its look starts an issue that lost its label while it
270
+ waited. One intake run serves every route, so its fault stops every route; a standing Pool for each Source keeps a
271
+ fault in one Source.
272
+ - **A direct door**: a Hook that starts a run itself (`door()`, or a Listener's `startOn(machine, kind, …)`). Rejected:
273
+ it is the smallest form for one alert, but it cannot grow. A cap, a read of a level, a queue and an answer after a
274
+ start are each state over time, which a door does not have, and each needs a standing Machine. So an Instance gets two
275
+ ways to start a run. It also makes a Hook more than a door that checks and decides nothing (ADR-0078).
276
+ - **A router that the author writes**: a hand-written standing Machine with `dispatch({ key })`, regions and a table of
277
+ runs in flight. Rejected: about 90 lines of mechanism for 25 lines of intent. When a judge wrote one alert's router
278
+ from it, the result had no hand-over and no routes for a child: its table went stale at each publish, and the first
279
+ refused start faulted it. A run holds its Assembly, so a router keeps its old handlers after a publish; a router that
280
+ ends `error` has no open Gate, so each later delivery reaches nothing; and a Kit bump drifts it and loses its backlog.
281
+ - **Two factories over one Source port**: `pool()` for children and batches, and a second factory for standing Pools of
282
+ runs. Rejected: the split removes three rows from the table of differences, and costs a GLOSSARY term for the second
283
+ factory, which is the Intake again.
284
+ - **A batch Pool of runs now.** Rejected for now, as above: no scenario needs it, and its key makes a second batch
285
+ within `runs.retain` do nothing. It adds later without a break.
@@ -7,9 +7,9 @@ hands: the [ADR-0013](0013-the-agent-reaches-its-machine-through-a-container-it-
7
7
  Submission — forget it and the Agent is silently mute, the same footgun class ADR-0016 removed from workflows), the
8
8
  ADR-0004/0012 sandbox geography (working tools execute in the Harness container against `/work`), the dependency pin
9
9
  that must match the Orchestrator's wire, and the image contracts the operator imposes (numeric uid, `:8080` = Ready, git
10
- present). The only content a user genuinely owns is the persona: model + instructions (+ someday tools/skills).
11
- GLOSSARY.md already promised the split — an Agent is a definition jr2 maps into the Harness, and the Instance folder
12
- holds only what a Machine cannot carry (ADR-0049/0050: the definition is one of the things it can).
10
+ present). The only content a user genuinely owns is the Agent's definition: model + instructions, and the MCP servers it
11
+ uses. GLOSSARY.md already promised the split — an Agent is a definition jr2 maps into the Harness, and the Instance
12
+ folder holds only what a Machine cannot carry (ADR-0049/0050: the definition is one of the things it can).
13
13
 
14
14
  ## Decision
15
15
 
@@ -53,6 +53,12 @@ holds only what a Machine cannot carry (ADR-0049/0050: the definition is one of
53
53
  Stand-in. `harness.provider.apiKey` is one implicitly: the held secret `JR2_PROVIDER_API_KEY`, bound to `baseUrl`'s
54
54
  host, so it leaves the `jr2-harness-env` Secret and `baseUrl` must be `https://` when a key is set. A known model key
55
55
  in `harness.env`, or in an `envFrom` Secret, is refused.
56
+ - **MCP is the one extension contract for tools.** Beyond its Working tools and its Menu, an Agent gets tools only from
57
+ the MCP servers that its definition names (`mcp`), and jr2 has no tool API of its own
58
+ ([ADR-0079](0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md)). The
59
+ definition names a server by the name that the `mcp` block of `jr2.config.ts` gives it, never by its url or its
60
+ secret: as `harness` declares reach for models, the `mcp` block declares reach for tools, and the definition makes the
61
+ choice.
56
62
  - **A catalog provider can be moved to a gateway, and stays a catalog provider.** `harness.catalog.<id>.baseUrl`
57
63
  replaces the base URL of every model of one pi catalog provider — a LiteLLM gateway in front of Bedrock, for
58
64
  `anthropic/…`. The catalog keeps its limits, cost and thinking levels; only the endpoint moves. It is reach, not a
@@ -99,11 +105,13 @@ Definitions keep converge-time safety; overrides pay the later check for the fle
99
105
 
100
106
  ## Consequences
101
107
 
102
- - The definition contract is data-only: what an Agent may do to the Workspace is part of it (`access`, ADR-0028); custom
103
- tools/skills/subagents have no seat yet. When someone needs them, the contract grows deliberately — there is no eject
104
- hatch to a foreign harness project (ADR-0027 deleted it).
108
+ - The definition contract is data-only: what an Agent may do to the Workspace is part of it (`access`, ADR-0028), and so
109
+ are the MCP servers it uses (ADR-0079); an author's own tool is an MCP server. Skills and subagents have no seat yet.
110
+ When someone needs them, the contract grows deliberately — there is no eject hatch to a foreign harness project
111
+ (ADR-0027 deleted it).
105
112
  - Because definitions are plain data, stock ones compose with no API: a Machine carries `agent(coder)`, and a consumer
106
- retunes it with `customize(machine, { agents: { coder: { model: "…" } } })` (ADR-0049, ADR-0009).
113
+ retunes it with `customize(machine, { agents: { coder: { model: "…" } } })` (ADR-0049, ADR-0009), or rebinds its
114
+ servers with `{ mcp: [...] }` in the same place.
107
115
  - An invalid definition fails at admission, loudly, naming the slot: a model no registry resolves, a malformed field. A
108
116
  Machine with no Agent slots is valid — a `workspace()` body parking a Sandbox (ADR-0012) invokes none. Readiness is
109
117
  binding `:8080`, with no boot build ahead of it (ADR-0027).
@@ -66,11 +66,12 @@ loudly narrated, safe to re-run:
66
66
  It is an ordinary converge failure: it names each node whose agent reports none, the probe's reason, and the fix (keep
67
67
  the node out with `sandbox.nodeSelector` or a taint, ADR-0052, or enable Landlock); a run after the fix converges
68
68
  ([ADR-0077](0077-jr2-requires-landlock-on-every-sandbox-node.md)).
69
- - **Provider preflight**: `up` checks each provider that a carried model names. It checks from inside the cluster, with
70
- one trivial tool-call completion. An endpoint that the pods cannot reach, a wrong or missing key, or a vLLM without
71
- `--enable-auto-tool-choice` then fails at converge time, not as `agent.fault` in a run. Reachability is the user's
72
- concern (`localhost` never works from a pod; a LAN address does on kind). The check must take the path that the
73
- Harness takes. If it does not, a pass means nothing.
69
+ - **Provider preflight**: `up` checks each provider that a carried model names, and the tool list of each remote MCP
70
+ server that an Agent names (below). It checks from inside the cluster, with one trivial tool-call completion for a
71
+ model. An endpoint that the pods cannot reach, a wrong or missing key, or a vLLM without `--enable-auto-tool-choice`
72
+ then fails at converge time, not as `agent.fault` in a run. Reachability is the user's concern (`localhost` never
73
+ works from a pod; a LAN address does on kind). The check must take the path that the Harness takes. If it does not, a
74
+ pass means nothing.
74
75
  - **One path for every provider.** `up` checks the custom provider (`harness.provider`, ADR-0018) and a provider of
75
76
  pi's catalog (for example `anthropic/…`) in the same way. A key of either is a held secret, and only a Custodian can
76
77
  use it ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)). So no key enters a pod
@@ -106,8 +107,8 @@ loudly narrated, safe to re-run:
106
107
  model runs where the Instance Harness runs.
107
108
  - The held set: a pod that holds a secret sends its HTTPS through its Custodian, and a tunnel carries a host that no
108
109
  secret is bound to. A pod that holds no secret sends its HTTPS direct. So the check pod holds the held set of the
109
- first pod of its class, and the keys that the other pods of the class hold. A class that holds nothing gets a
110
- check pod that holds nothing.
110
+ first pod of its class, and the keys and the MCP server secrets that the other pods of the class hold. A class
111
+ that holds nothing gets a check pod that holds nothing.
111
112
 
112
113
  When no pod of the class holds a key of the provider, the check pod holds none. Then pi refuses the call
113
114
  (`not configured`), and the converge fails. A signing credential in `harness.env` reaches the check pod, as it
@@ -115,6 +116,16 @@ loudly narrated, safe to re-run:
115
116
  knows, and for which jr2 knows no key name, is checked with no held key, as a Turn runs it. The check pod has the
116
117
  priority class of the Sandbox tier, as the Harness pods have (ADR-0060).
117
118
 
119
+ - **MCP servers.** The check pod also checks each remote MCP server that an Agent of its class names
120
+ ([ADR-0079](0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md)). It
121
+ reads the server's `tools/list` through its Custodian, with the Stand-in of the server's secret, as a Turn reads it.
122
+ An allowlisted tool name that the list does not have fails the converge, and the result names the server and the
123
+ tool. A server that refuses the request (a 401 or a 403: a wrong or expired token) fails the converge, as a model
124
+ key that the provider refuses does. A server that does not answer is a warning: the first Turn of a conversation
125
+ fails with a named error when the server does not answer then. A stdio server is not checked: it runs as Workspace
126
+ code in the Sandbox, and the check pod runs none. Each server has 60 s for its whole listing, after the models. Two
127
+ literals set it, `PREFLIGHT_MCP_BUDGET_SECONDS` in `preflight-line.ts` and `CHECK_MCP_SECONDS` in `up.ts`, and a
128
+ test holds them equal. The pod writes the list, and `up` compares it with the allowlists.
118
129
  - **The request.** The check's request shares these with a Turn's: the model, its api, its endpoint and key, the
119
130
  completion limit (what pi-ai sends for a Turn), the retries, no forced tool, and the level of a definition with no
120
131
  `thinkingLevel` (`off`: no `reasoning` is sent). It does not share these: it has no system prompt, one tool, no
@@ -195,12 +206,12 @@ loudly narrated, safe to re-run:
195
206
 
196
207
  It never deletes the objects of another Instance.
197
208
 
198
- - **Skipping it.** `jr2 up --skip-provider-preflight` runs no check pod. A check can fail when the model works, for
199
- example when a model loads for longer than the check waits. That fault must not stop every converge until jr2 is
200
- fixed. The skip is never silent: `up` writes one warning line, which names each model that it did not check. `up`
201
- still deletes the leftovers of earlier runs, because their Secrets can hold a key. The flag has no config key,
202
- because no flag of `up` has one (CI gives `--yes` as a flag). A model that does not work then fails in a run, as
203
- `agent.fault`.
209
+ - **Skipping it.** `jr2 up --skip-provider-preflight` runs no check pod, so it checks no model and no MCP server's
210
+ tool list. A check can fail when the model works, for example when a model loads for longer than the check waits.
211
+ That fault must not stop every converge until jr2 is fixed. The skip is never silent: `up` writes one warning line,
212
+ which names each model and each MCP server that it did not check. `up` still deletes the leftovers of earlier runs,
213
+ because their Secrets can hold a key. The flag has no config key, because no flag of `up` has one (CI gives `--yes`
214
+ as a flag). A model that does not work then fails in a run, as `agent.fault`.
204
215
  - **A limit.** The check pod has labels of its own. It does not get the labels of a Harness pod: the Instance Harness
205
216
  Service selects its pods by label, and would send Turns to the check pod. So a NetworkPolicy that selects Harness
206
217
  pods by label does not select the check pod. When such a policy closes the egress of the Harness pods, the check can
@@ -229,8 +240,9 @@ takes the operator too.
229
240
  instance error with "not deployed here — right context?". This is the guardrail against silently pointing at the wrong
230
241
  cluster, without a state file that can itself go stale.
231
242
  - **CLI transport**: run-verbs port-forward the Orchestrator Service via the kube API for the duration of the command
232
- and read the Instance token from its Secret — kube RBAC is the real gate. `--url`/`JR2_URL` (+ token env) is the
233
- escape hatch for ingress-exposed instances and callers without cluster creds.
243
+ and read the Instance token from its Secret — kube RBAC is the real gate. `--url`/`JR2_URL` (+ token env, any Caller's
244
+ credential, [ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md)) is the escape
245
+ hatch for ingress-exposed instances and callers without cluster creds.
234
246
  - **A dead ADDRESS and an absent IDENTITY are two faults, and resolution names which.** They share a verb and a context
235
247
  but not a fix: one is the network, the other is `jr2 up`. A cluster that answered and holds no instance Secret is "not
236
248
  deployed here — right context?"; a cluster that could not answer at all is "cannot reach the cluster — context X: …",
@@ -274,9 +286,11 @@ from env.
274
286
 
275
287
  ## Consequences
276
288
 
277
- - The everyday story is two commands ever: `jr2 init`, `jr2 up`. The full CLI: `init`, `up`, `down`, `gc` (ADR-0039),
278
- `run`, `runs`, `status`, `logs`, `send`, `kit push` (ADR-0044), plus the workspace verbs `ls`, `ssh`, `rm` (ADR-0009;
279
- decided, not yet in the binary).
289
+ - The everyday story is two commands ever: `jr2 init`, `jr2 up`. The rest of the CLI is in ADR-0009's table, and the
290
+ workspace verbs `workspace ls|shell|forward|rm` are among them.
291
+ - **The workspace verbs are not run-verbs.** They address the Sandbox CRs of the current context with its kube
292
+ credentials alone: no port-forward to the Orchestrator, no Instance token and no Caller scope. So they work when the
293
+ Orchestrator is down, and kube RBAC on the Sandbox CRs and their pods is the whole gate.
280
294
  - **Simplification must not surprise**: `up` prompts exactly when meeting a cluster that isn't yet home, and run-verbs
281
295
  print the context they're talking to — the ambient-context magic stays visible.
282
296
  - The snapshot store (sqlite) moves onto a PVC in the instance's namespace; `.jr2/` shrinks to scratch.
@@ -57,6 +57,19 @@ end. And a restarted Harness answered a `continue` with no memory of the convers
57
57
  attached pod. Its lifetime is the state's lifetime, which xstate already manages: it re-invokes on snapshot restore,
58
58
  so restore-reconcile stops being a special case and becomes the first tick of the normal loop; and it stops on every
59
59
  exit — body final, run stopped, run faulted.
60
+ - **The Lease marks its hold on the CR.** The CR's first write sets the annotation `jr2.dev/hold: held`, and each
61
+ renewal writes it again. When the Lease stops for the body's final state, a fault, a cancel or a parent that leaves
62
+ the Workspace's state, it sets `jr2.dev/hold: released` and renews no more. A defect that a parent catches does not
63
+ stop the Lease, because xstate stops no child of a Machine that ended in error: at its next renewal the Lease sees
64
+ that its Workspace is over, and sets `released` then. A Lease does not set `released` on a name that a live Workspace
65
+ of the run holds: a parent that catches the defect can invoke the same Machine again, and the new Workspace gets the
66
+ same Sandbox name and a Lease of its own. A step of the wrapper from one state to the next writes nothing: the next
67
+ state has a Lease of its own. A park (`RunHost.stop`, [ADR-0025](0025-cancel-ends-a-run-stop-parks-it.md)) and a
68
+ process shutdown write nothing, so a crash keeps the mark `held` and the restored run's Lease writes it again. A
69
+ restore that ends a run `drifted` or `lost` without hosting it starts no Lease, so the Orchestrator sets `released` on
70
+ each CR with that run's `jr2.dev/run` label. So `jr2 workspace rm` tells a held Sandbox from a free one with the
71
+ cluster alone ([ADR-0009](0009-cli-and-instance-interface.md)): the keepalive's age cannot tell it, because a renewal
72
+ can be late by more than one interval and a few missed renewals are survivable.
60
73
 
61
74
  That last point deletes machinery rather than adding it. Three mechanisms with three different owners and lifetimes —
62
75
  the process-global heartbeat map, the one-shot probe, and `release(runId)` (which label-queried the cluster to stop
@@ -98,4 +111,4 @@ behavior `release()` was written to produce is emergent.
98
111
  independently. This falls out of invoking it beside the body rather than owning it at the host.
99
112
  - `SandboxPort` has no optional method: `renew` is a write, `continuity` is a subscription to the watch, and
100
113
  `memoryFault` (ADR-0061) answers undefined from a backend that cannot see the container. The `jr2.dev/run` label
101
- survives for `jr2 ls` and as the watch's selector, no longer load-bearing for lease bookkeeping.
114
+ survives for the `jr2 workspace` verbs and as the watch's selector, no longer load-bearing for lease bookkeeping.
@@ -34,8 +34,8 @@ can get away with fetching once, because a person can press a button; an API can
34
34
  so there is no gap to detect and no cursor to resume from. Same reconciliation idiom as the Lease (ADR-0021) and
35
35
  `jr2 up` (ADR-0019): say the whole truth, repeatedly, and let the reader converge.
36
36
  - **Two granularities, one vocabulary.** The per-run feed stays — tracking exactly one run is a legitimate thing to
37
- want, and it is what `jr2 run` and `jr2 logs -f` already do. Every run-scoped frame carries `runId` even where the
38
- route makes it redundant, so one parser serves both.
37
+ want, and it is what `jr2 run` and `jr2 watch` do. Every run-scoped frame carries `runId` even where the route makes
38
+ it redundant, so one parser serves both.
39
39
  - **`gone` is a fact, not an inference.** A run can leave the live set _without_ a terminal status: `stop()`
40
40
  deliberately leaves the stored status `"live"` so a later `restore()` picks the run back up. A client watching for
41
41
  `status === "done"` would show a stopped run as live forever, so departure is stated outright.
@@ -48,7 +48,7 @@ can get away with fetching once, because a person can press a button; an API can
48
48
  which is why it is wired to the handler's exit.
49
49
  - **A settled run is the client's own memory.** A run appears in the page's `settled` list iff _this page_ watched it
50
50
  leave. After a hard reload they are gone, and that is correct rather than a defect: serving a finished run's state to
51
- an unauthenticated reader is a read-through, which stays behind the Instance token on `/runs/:id` (ADR-0014).
51
+ an unauthenticated reader is a read-through, which stays behind the `read` scope on `/runs/:id` (ADR-0014).
52
52
  - **`jr2 visualize` is deleted.** Its job was to find an orchestrator, forward a port, and open a browser. With the page
53
53
  now useful on its own, that is one `kubectl port-forward` the reader already knows how to run — and the command's
54
54
  ephemeral-fallback path was quietly forwarding to a _different_ instance than the deployed one. Its one unique piece
@@ -61,7 +61,7 @@ can get away with fetching once, because a person can press a button; an API can
61
61
  tick, and a reader that keeps up drains them before any ping judges it. Closing is safe because the feed is
62
62
  level-triggered: the client reconnects and receives the whole current truth, having missed no state — only activity
63
63
  (Emits, retries) that fell in the gap. A page reconnects with EventSource's own retry; the CLI (`jr2 run`,
64
- `jr2 logs -f`) re-attaches any feed that ends before a terminal status, and never reads such an end as the run's —
64
+ `jr2 watch`) re-attaches any feed that ends before a terminal status, and never reads such an end as the run's —
65
65
  except `gone`: a run stopped before it settled (parked, ADR-0025) reads through as `active`, so its one-shot feed says
66
66
  `gone` after the status, and the CLI ends there instead of re-attaching in a loop. The server keeps no per-client
67
67
  queue and no slow-client accounting beyond counting the subscriber's writes (scaling review R10, 2026-09-30).