@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
@@ -19,7 +19,7 @@ the file as `workflows/review.ts` in your [Instance](../reference/glossary.md#in
19
19
  ```ts
20
20
  import { z } from "zod";
21
21
  import { assign } from "xstate";
22
- import { agent, defineEvent, fail, jr2Setup, workspace, type Workspaced } from "@jr2/orchestrator";
22
+ import { agent, defineEvent, fail, isReady, jr2Setup, workspace, worktree, type Workspaced } from "@jr2/orchestrator";
23
23
 
24
24
  const approve = defineEvent({
25
25
  name: "approve",
@@ -42,6 +42,10 @@ const runInput = z.object({ branch: z.string(), sha: z.string(), model: z.string
42
42
  type BodyInput = Workspaced<z.infer<typeof runInput>, "app">;
43
43
  type Context = BodyInput & { notes: string; summary: string };
44
44
 
45
+ // Two Worktrees of one clone: the coder's branch, and the commit that the reviewer reads.
46
+ const coderTree = (context: BodyInput) => `${context.workspace.repos.app}/work` as const;
47
+ const reviewTree = (context: BodyInput) => `${context.workspace.repos.app}/review` as const;
48
+
45
49
  const body = jr2Setup({
46
50
  types: {} as { input: BodyInput; context: Context },
47
51
  events: [approve, requestChanges, finished],
@@ -64,21 +68,38 @@ const body = jr2Setup({
64
68
  workspace: "none",
65
69
  thinkingLevel: "low",
66
70
  }),
71
+ cut: worktree(),
67
72
  fail: fail(),
68
73
  },
69
74
  }).createMachine({
70
75
  id: "review",
71
76
  context: ({ input }) => ({ ...input, notes: "", summary: "" }),
72
77
  on: { "workspace.lost": ".lost", "agent.fault": ".failed" },
73
- initial: "reviewing",
78
+ initial: "cutting",
74
79
  states: {
80
+ // The Workspace only clones. The Machine cuts the Worktree that the Agent works in.
81
+ cutting: {
82
+ invoke: {
83
+ src: "cut",
84
+ input: ({ context }) => ({ path: coderTree(context), branch: context.branch }),
85
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "detaching" }, { target: "notCut" }],
86
+ },
87
+ },
88
+ // The second cut has no branch: the Worktree holds the commit, detached.
89
+ detaching: {
90
+ invoke: {
91
+ src: "cut",
92
+ input: ({ context }) => ({ path: reviewTree(context), detach: context.sha }),
93
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "reviewing" }, { target: "notCut" }],
94
+ },
95
+ },
75
96
  reviewing: {
76
97
  invoke: {
77
98
  src: "reviewer",
78
99
  input: ({ context }) => ({
79
100
  prompt: `Review commit ${context.sha}.`,
80
- // The review Worktree holds the commit of `reviewSha`, detached from the branch.
81
- cwd: context.workspace.review?.app ?? context.workspace.repos.app,
101
+ // The review Worktree holds the commit, detached from the branch.
102
+ cwd: reviewTree(context),
82
103
  // A Dial: this Turn thinks harder than the default of the model.
83
104
  thinkingLevel: "high",
84
105
  }),
@@ -93,7 +114,7 @@ const body = jr2Setup({
93
114
  src: "coder",
94
115
  input: ({ context }) => ({
95
116
  prompt: `Change the code on the branch as the review asks:\n\n${context.notes}`,
96
- cwd: context.workspace.repos.app,
117
+ cwd: coderTree(context),
97
118
  // Each Turn of the coder continues the same conversation.
98
119
  continue: true,
99
120
  // A Dial: the model of this Turn, from the run input. Absent, the model of the definition.
@@ -115,6 +136,7 @@ const body = jr2Setup({
115
136
  },
116
137
  },
117
138
  done: { type: "final" },
139
+ notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
118
140
  failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
119
141
  lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
120
142
  },
@@ -123,7 +145,6 @@ const body = jr2Setup({
123
145
  export const machine = workspace(body, {
124
146
  input: runInput,
125
147
  repos: { app: "https://github.com/<you>/app.git" },
126
- spec: ({ input }) => ({ branch: input.branch, reviewSha: input.sha }),
127
148
  });
128
149
  ```
129
150
 
@@ -132,7 +153,7 @@ export const machine = workspace(body, {
132
153
  ```ts
133
154
  import { z } from "zod";
134
155
  import { assign } from "xstate";
135
- import { agent, defineEvent, fail, jr2Setup, workspace, type Workspaced } from "@jr2/orchestrator";
156
+ import { agent, defineEvent, fail, isReady, jr2Setup, workspace, worktree, type Workspaced } from "@jr2/orchestrator";
136
157
 
137
158
  const approve = defineEvent({
138
159
  name: "approve",
@@ -155,6 +176,10 @@ const runInput = z.object({ branch: z.string(), sha: z.string(), model: z.string
155
176
  type BodyInput = Workspaced<z.infer<typeof runInput>, "app">;
156
177
  type Context = BodyInput & { notes: string; summary: string };
157
178
 
179
+ // Two Worktrees of one clone: the coder's branch, and the commit that the reviewer reads.
180
+ const coderTree = (context: BodyInput) => `${context.workspace.repos.app}/work` as const;
181
+ const reviewTree = (context: BodyInput) => `${context.workspace.repos.app}/review` as const;
182
+
158
183
  const body = jr2Setup({
159
184
  types: {} as { input: BodyInput; context: Context },
160
185
  events: [approve, requestChanges, finished],
@@ -177,21 +202,38 @@ const body = jr2Setup({
177
202
  workspace: "none",
178
203
  thinkingLevel: "low",
179
204
  }),
205
+ cut: worktree(),
180
206
  fail: fail(),
181
207
  },
182
208
  }).createMachine({
183
209
  id: "review",
184
210
  context: ({ input }) => ({ ...input, notes: "", summary: "" }),
185
211
  on: { "workspace.lost": ".lost", "agent.fault": ".failed" },
186
- initial: "reviewing",
212
+ initial: "cutting",
187
213
  states: {
214
+ // The Workspace only clones. The Machine cuts the Worktree that the Agent works in.
215
+ cutting: {
216
+ invoke: {
217
+ src: "cut",
218
+ input: ({ context }) => ({ path: coderTree(context), branch: context.branch }),
219
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "detaching" }, { target: "notCut" }],
220
+ },
221
+ },
222
+ // The second cut has no branch: the Worktree holds the commit, detached.
223
+ detaching: {
224
+ invoke: {
225
+ src: "cut",
226
+ input: ({ context }) => ({ path: reviewTree(context), detach: context.sha }),
227
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "reviewing" }, { target: "notCut" }],
228
+ },
229
+ },
188
230
  reviewing: {
189
231
  invoke: {
190
232
  src: "reviewer",
191
233
  input: ({ context }) => ({
192
234
  prompt: `Review commit ${context.sha}.`,
193
- // The review Worktree holds the commit of `reviewSha`, detached from the branch.
194
- cwd: context.workspace.review?.app ?? context.workspace.repos.app,
235
+ // The review Worktree holds the commit, detached from the branch.
236
+ cwd: reviewTree(context),
195
237
  // A Dial: this Turn thinks harder than the default of the model.
196
238
  thinkingLevel: "high",
197
239
  }),
@@ -206,7 +248,7 @@ const body = jr2Setup({
206
248
  src: "coder",
207
249
  input: ({ context }) => ({
208
250
  prompt: `Change the code on the branch as the review asks:\n\n${context.notes}`,
209
- cwd: context.workspace.repos.app,
251
+ cwd: coderTree(context),
210
252
  // Each Turn of the coder continues the same conversation.
211
253
  continue: true,
212
254
  // A Dial: the model of this Turn, from the run input. Absent, the model of the definition.
@@ -228,6 +270,7 @@ const body = jr2Setup({
228
270
  },
229
271
  },
230
272
  done: { type: "final" },
273
+ notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
231
274
  failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
232
275
  lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
233
276
  },
@@ -236,7 +279,6 @@ const body = jr2Setup({
236
279
  export const machine = workspace(body, {
237
280
  input: runInput,
238
281
  repos: { app: "https://github.com/<you>/app.git" },
239
- spec: ({ input }) => ({ branch: input.branch, reviewSha: input.sha }),
240
282
  });
241
283
  ```
242
284
 
@@ -259,13 +301,13 @@ prompt and picks from its [Menu](../reference/glossary.md#menu). You can invoke
259
301
  the example does, or in a [Machine](../reference/glossary.md#machine) with no Workspace.
260
302
 
261
303
  A `"read"` Agent keeps bash, so a command can still change a file or the branch. To keep a reviewer away from the
262
- branch, give its Turn the review [Worktree](../reference/glossary.md#worktree):
304
+ branch, give its Turn a detached [Worktree](../reference/glossary.md#worktree):
263
305
 
264
- 1. In the `spec` of the `workspace()`, set `reviewSha` to the commit to review.
265
- 2. In the Turn, set `cwd` to `context.workspace.review?.<slot>`.
306
+ 1. Cut the Worktree with [`worktree()`](../reference/authoring-surface/worktree.md), with `{ path, detach }`.
307
+ 2. In the Turn, set `cwd` to that path.
266
308
 
267
- The review Worktree is `/work/<slot>/<branch>-review`, with each `/` in the branch changed to `-`. It holds the commit
268
- of `reviewSha`, detached from the branch. In the example, `<slot>` is `app`, and the commit comes from the run input.
309
+ The Worktree holds the commit, with no branch. In the example, the path is `/work/app/review`, and the commit comes from
310
+ the run input. [Cut a Worktree](cut-a-worktree.md) tells more.
269
311
 
270
312
  The access is a part of the identity of the Agent. Only the definition sets it. A Turn cannot change it.
271
313
 
@@ -290,7 +332,7 @@ Run [`jr2 up`](../reference/cli.md#up) to converge the new Workflow:
290
332
  jr2 up
291
333
  ```
292
334
 
293
- `jr2 up` prints no line that is special to this Workflow. The `seats:` lines show the picks of the three Agents.
335
+ The `effects:` lines show the cut. The `seats:` lines show the picks of the three Agents.
294
336
 
295
337
  To give a model to the coder's Turns, add `model` to the run input:
296
338
 
@@ -318,6 +360,8 @@ Dial. Thus:
318
360
  - `jr2 up` gives each pod the model key of the providers that the definitions of its Agents name. For an Agent in a
319
361
  Sandbox, use a Dial model of a provider that a definition in that Sandbox names. For a Menu-only Agent, use a provider
320
362
  that a Menu-only definition of the Instance names.
363
+ - In the same way, `jr2 up` gives each pod the secret of each MCP server that the definitions of its Agents name. The
364
+ [MCP tools](../reference/glossary.md#mcp-tools) of an Agent come from its definition only. A Turn cannot change them.
321
365
 
322
366
  ## Continue a conversation
323
367
 
@@ -337,9 +381,11 @@ task, also on a Turn that continues. In the example, each `fixing` prompt gives
337
381
  - jr2 refuses a Turn whose `input` has a key that a Turn does not have, such as `instructions` or `workspace`. Only the
338
382
  definition sets those two. `<keys>` is each key that is not correct, in backticks, with a comma between them:
339
383
  ``agent "<slot>": the Turn's input carries <keys>, which a Turn has no use for — its input is its Frame (`prompt`, `cwd`), its Dials (`model`, `thinkingLevel`), and `continue` (ADR-0057)``
340
- - Under a Workspace with more than one [Repo Slot](../reference/glossary.md#repo-slot), jr2 refuses a Turn of an Agent
341
- with Working tools that sets no `cwd`:
342
- ``turn <id>: agent "<slot>" works under a Workspace carrying more than one Repo Slot (<slots>) and its Turn framed no `cwd` — say which Worktree it works in (`cwd: context.workspace.repos.<slot>`, one of those), because the kit gives no slot a meaning (ADR-0051) and a Turn's Frame is where it works (ADR-0057)``
384
+ - Under a Workspace with [Repo Slots](../reference/glossary.md#repo-slot), jr2 refuses a Turn of an Agent with Working
385
+ tools that sets no `cwd`:
386
+ ``turn <id>: agent "<slot>" works under a Workspace with Repo Slots (<slots>) and its Turn framed no `cwd`: say where it works, a Worktree, or `cwd: "/work"` above them (ADR-0057)``
387
+ - `workspace()` refuses a body with an invoke that does not say where it works, when it can read the input:
388
+ ``workspace(): state "<state>": "<slot>" names no `<key>`, and a Workspace with Repo Slots says where each Turn and Effect works: a Worktree, or `cwd: "/work"` for a Turn (ADR-0057)``
343
389
  - The Harness refuses a `cwd` that is not an absolute path. The run gets `agent.fault`, with this reason:
344
390
  ``harness admission failed (400): agent "<slot>": the Turn's Frame carries a `cwd` that is not an absolute path — the Frame says WHERE this Turn works (ADR-0057), and a Turn rooted anywhere else would work in the wrong directory silently``
345
391
  - The Harness refuses a model that is not in its catalog and not a model of the custom provider. The run gets
@@ -359,7 +405,8 @@ task, also on a Turn that continues. In the example, each `fixing` prompt gives
359
405
  ## Read next
360
406
 
361
407
  - [Agents and Turns](../concepts/agents-and-turns.md): the definition, the Frame, the Dials and `continue`.
362
- - [Workspaces](../concepts/workspaces.md): the Worktrees and the review Worktree.
408
+ - [Workspaces](../concepts/workspaces.md): the Worktrees.
409
+ - [Cut a Worktree](cut-a-worktree.md): a detached Worktree for a reviewer.
363
410
  - [The Menu](../concepts/the-menu.md): the picks of an Agent.
364
411
  - [Bind a shipped Machine's Open parts](bind-open-parts.md): bind an Open model.
365
412
  - Reference: [`agent`](../reference/authoring-surface/agent.md),
@@ -14,26 +14,39 @@ it on a node, so the Sandbox always has it. Write it in the `resources` option o
14
14
 
15
15
  ```ts
16
16
  import { z } from "zod";
17
- import { command, fail, jr2Setup, workspace, type Workspaced } from "@jr2/orchestrator";
17
+ import { command, fail, isReady, jr2Setup, workspace, worktree, type Workspaced } from "@jr2/orchestrator";
18
18
 
19
19
  const runInput = z.object({ branch: z.string() });
20
20
  type BodyInput = Workspaced<z.infer<typeof runInput>, "app">;
21
21
 
22
+ // The Worktree that the tests run in.
23
+ const testTree = (context: BodyInput) => `${context.workspace.repos.app}/test` as const;
24
+
22
25
  const body = jr2Setup({
23
- types: {} as { input: BodyInput },
26
+ types: {} as { input: BodyInput; context: BodyInput },
24
27
  actors: {
28
+ cut: worktree(),
25
29
  // JR2_CPUS holds the cpu limit of the container. Playwright does not read that limit.
26
30
  e2e: command(["sh", "-c", "npx playwright test --workers=$JR2_CPUS"], { timeout: "30m" }),
27
31
  fail: fail(),
28
32
  },
29
33
  }).createMachine({
30
34
  id: "e2e",
35
+ context: ({ input }) => input,
31
36
  on: { "workspace.lost": ".lost" },
32
- initial: "testing",
37
+ initial: "cutting",
33
38
  states: {
39
+ cutting: {
40
+ invoke: {
41
+ src: "cut",
42
+ input: ({ context }) => ({ path: testTree(context), branch: context.branch }),
43
+ onDone: [{ guard: ({ event }) => isReady(event.output), target: "testing" }, { target: "notCut" }],
44
+ },
45
+ },
34
46
  testing: {
35
47
  invoke: {
36
48
  src: "e2e",
49
+ input: ({ context }) => ({ worktree: testTree(context) }),
37
50
  onDone: [
38
51
  {
39
52
  guard: ({ event }) => event.output.outcome === "exited" && event.output.code === 0,
@@ -46,6 +59,7 @@ const body = jr2Setup({
46
59
  passed: { type: "final" },
47
60
  // Not final states: each one fails the run, and the run ends `failed`.
48
61
  failed: { invoke: { src: "fail", input: { reason: "the Playwright tests did not pass" } } },
62
+ notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
49
63
  lost: { invoke: { src: "fail", input: { reason: "the Sandbox was lost" } } },
50
64
  },
51
65
  });
@@ -55,7 +69,6 @@ export const machine = workspace(body, {
55
69
  // The Size of the whole Sandbox: 4 GiB of memory and 2 cpus. jr2 reserves all of it on a node.
56
70
  resources: { limits: { memory: "4Gi", cpu: "2" } },
57
71
  repos: { app: "https://github.com/you/app.git" },
58
- spec: ({ input }) => ({ branch: input.branch }),
59
72
  });
60
73
  ```
61
74
 
@@ -160,8 +173,8 @@ The Harness can miss a fast increase. Then the kernel stops the whole container,
160
173
  memory limit (OOMKilled, limit <memory>): the Workspace's processes passed the Harness container's memory limit and the kernel killed the container, the conversation with it. Use fewer workers, or give the Workspace a larger Size (ADR-0060, ADR-0061).
161
174
  ```
162
175
 
163
- The next Agent in the Workspace gets a notice of the kill. When you see either text again and again, state a larger
164
- Size.
176
+ Each conversation that was in the Workspace at the kill gets a notice of it one time, and so does the next Turn of the
177
+ Agent whose Turn the kill cut. When you see either text again and again, state a larger Size.
165
178
 
166
179
  ## Rules
167
180
 
@@ -0,0 +1,290 @@
1
+ ---
2
+ title: Start a run for each alert
3
+ description: Make a standing Pool that starts one run for each alert that an outside service sends to a Hook.
4
+ ---
5
+
6
+ # Start a run for each alert
7
+
8
+ Start one run of a [Workflow](../reference/glossary.md#workflow) for each alert that an alert service sends to a
9
+ [Hook](../reference/glossary.md#hook).
10
+
11
+ The example has three Workflows and one Hook. The alert service sends each new issue to the Hook `sentry`. The Workflow
12
+ `alerts` is a standing [Pool](../reference/glossary.md#pool) of runs: its push Source holds each alert, and the Pool
13
+ starts one run of `triage` for each alert, with at most 8 live at one time. A `triage` run waits until a person takes
14
+ the alert or dismisses it.
15
+
16
+ `@jr2/orchestrator/hooks/webhook` has `webhook()`, which makes the kind of a service that jr2 ships no kind for. It is
17
+ part of `@jr2/orchestrator`, so your [Instance](../reference/glossary.md#instance) installs nothing more.
18
+
19
+ ## Write the kind of the service
20
+
21
+ The alert service signs the raw body with a shared secret, and puts the HMAC-SHA256 in hex in the header
22
+ `sentry-hook-signature`. The header `sentry-hook-resource` and the field `action` give the event key, such as
23
+ `issue.created`. The header `request-id` is the id of the delivery. Save the kind as `hooks/sentry.ts`, beside
24
+ `workflows/`, because it is not a Workflow:
25
+
26
+ ```ts
27
+ import { z } from "zod";
28
+ import { webhook } from "@jr2/orchestrator/hooks/webhook";
29
+
30
+ // An alert service that jr2 ships no kind for. The service signs the raw body with a shared secret
31
+ // (sha256, hex) in the header sentry-hook-signature. The event key is the resource and its action,
32
+ // such as issue.created. The module is in `hooks/`, not in `workflows/`, because it is not a Workflow.
33
+ export const sentry = webhook({
34
+ id: "sentry",
35
+ events: {
36
+ "issue.created": z.looseObject({
37
+ data: z.looseObject({
38
+ issue: z.looseObject({ id: z.string(), title: z.string(), level: z.string() }),
39
+ }),
40
+ }),
41
+ },
42
+ verify: { mac: { header: "sentry-hook-signature" } },
43
+ key: { name: { header: "sentry-hook-resource" }, action: { json: "action" } },
44
+ delivery: { header: "request-id" },
45
+ });
46
+
47
+ declare module "@jr2/orchestrator/hooks" {
48
+ interface HookKinds {
49
+ sentry: typeof sentry;
50
+ }
51
+ }
52
+ ```
53
+
54
+ The `declare module` block adds the kind to the types of the handlers. A handler for an event key that the kind does not
55
+ give does not compile.
56
+
57
+ ## Write the Workflow of one alert
58
+
59
+ Save `workflows/triage.ts`. Its door takes the issue id and the title of one alert:
60
+
61
+ ```ts
62
+ import { assign } from "xstate";
63
+ import { z } from "zod";
64
+ import { defineEvent, jr2Setup } from "@jr2/orchestrator";
65
+
66
+ // The two answers of the person on call.
67
+ const take = defineEvent({
68
+ name: "take",
69
+ description: "I take this alert.",
70
+ input: z.object({ who: z.string().min(1) }),
71
+ });
72
+ const dismiss = defineEvent({
73
+ name: "dismiss",
74
+ description: "This alert needs no work. Say why.",
75
+ input: z.object({ reason: z.string().min(1) }),
76
+ });
77
+
78
+ type Context = { issue: string; title: string; who?: string };
79
+
80
+ // One run for each alert. The run waits until a person takes the alert or dismisses it.
81
+ export const machine = jr2Setup({
82
+ types: {} as { input: { issue: string; title: string }; context: Context },
83
+ events: [take, dismiss],
84
+ }).createMachine({
85
+ id: "triage",
86
+ // The door: the alerts Pool sends this input for each alert.
87
+ input: z.object({ issue: z.string(), title: z.string() }),
88
+ context: ({ input }) => ({ issue: input.issue, title: input.title }),
89
+ initial: "waiting",
90
+ states: {
91
+ waiting: {
92
+ invoke: {
93
+ src: "gate",
94
+ input: ({ context }) => ({ meta: { issue: context.issue, title: context.title } }),
95
+ },
96
+ on: {
97
+ take: { target: "taken", actions: assign({ who: ({ event }) => event.who }) },
98
+ dismiss: "dismissed",
99
+ },
100
+ },
101
+ taken: { type: "final" },
102
+ dismissed: { type: "final" },
103
+ },
104
+ });
105
+ ```
106
+
107
+ ## Write the standing Pool
108
+
109
+ Save `workflows/alerts.ts`:
110
+
111
+ ```ts
112
+ import { z } from "zod";
113
+ import { carry, dispatch, pool, skip, source } from "@jr2/orchestrator";
114
+ import type {} from "../hooks/sentry.ts";
115
+
116
+ // The door of the triage Workflow: each alert is one run of it.
117
+ const triageDoor = z.object({ issue: z.string(), title: z.string() });
118
+
119
+ // A push Source: each Hook delivery carries an alert, and the Pool holds it until a slot is free.
120
+ const alerts = source({
121
+ item: z.object({ issue: z.string(), title: z.string(), level: z.string() }),
122
+ hooks: {
123
+ sentry: {
124
+ "issue.created": ({ payload }) => {
125
+ const issue = payload.data.issue;
126
+ if (issue.level !== "error" && issue.level !== "fatal") return skip(`level is ${issue.level}`);
127
+ return carry({ issue: issue.id, title: issue.title, level: issue.level });
128
+ },
129
+ },
130
+ },
131
+ });
132
+
133
+ // A standing Pool of runs: one triage run for each alert, at most 8 live at once.
134
+ export const machine = pool(dispatch("triage", { input: triageDoor, wait: true }), {
135
+ id: "alerts",
136
+ standing: true,
137
+ source: alerts,
138
+ itemId: (alert) => `sentry-${alert.issue}`,
139
+ cap: 8,
140
+ itemInput: (alert) => ({ issue: alert.issue, title: alert.title }),
141
+ });
142
+ ```
143
+
144
+ - The Source has `item` and `hooks`, and no read: it is a push Source. Its handler gives `carry(item)` for an alert of
145
+ the level `error` or `fatal`, and `skip(reason)` for the other levels.
146
+ - The worker is `dispatch("triage", { input, wait: true })`, so each alert is a run of its own of `triage`.
147
+ - `itemId` is the key of the run. A second delivery of the same alert finds the run of the first one, and starts no
148
+ second run.
149
+ - `standing: true`: jr2 starts the run of the Pool, and keeps one live run of it.
150
+
151
+ ## Declare the Hook
152
+
153
+ Add the Hook to `jr2.config.ts`, and its secret to the `secrets` list:
154
+
155
+ ```ts
156
+ import { defineConfig, secret } from "@jr2/orchestrator";
157
+ import { sentry } from "./hooks/sentry.ts";
158
+
159
+ export default defineConfig({
160
+ // The Hook's secret is an entry of the list. jr2 up places SENTRY_HOOK in the Instance Effector.
161
+ secrets: [{ name: "SENTRY_HOOK", value: process.env.SENTRY_HOOK }],
162
+ hooks: {
163
+ // POST /hooks/sentry: the alert service signs each delivery with SENTRY_HOOK. The Hook brings
164
+ // items to the Pool of the alerts Workflow, through its source Gate, and to nothing more.
165
+ sentry: sentry({ secret: secret("SENTRY_HOOK"), may: { answer: ["alerts"] } }),
166
+ },
167
+ });
168
+ ```
169
+
170
+ `may.answer` names `alerts`, because the Hook brings items to the Pool through the Pool's `source` Gate. Put the secret
171
+ in `.env`:
172
+
173
+ ```sh
174
+ SENTRY_HOOK=<secret>
175
+ ```
176
+
177
+ `<secret>` is a long random value. Give the alert service the same value.
178
+
179
+ ## Converge
180
+
181
+ ```sh
182
+ jr2 up
183
+ ```
184
+
185
+ ```text
186
+ …
187
+ standing: sentry → alerts → triage
188
+ …
189
+ secret SENTRY_HOOK: an Effect secret in the Instance Effector (hook sentry)
190
+ the Instance Effector: holds SENTRY_HOOK
191
+ …
192
+ standing alerts → triage: live, run 0d08bbbc-d34e-47fb-9b4b-8e788e328eb4, 0 held items
193
+ …
194
+ ```
195
+
196
+ `jr2 up` puts the secret in the [Instance Effector](../reference/glossary.md#instance-effector) only, which checks each
197
+ signature. jr2 starts the run of `alerts` at this publish. Your run id is different.
198
+
199
+ ## Set up the alert service
200
+
201
+ The address of the Hook is `POST /hooks/sentry` on the Orchestrator. `jr2 up` makes the Service `jr2-orchestrator` on
202
+ port 4000, with no address outside the cluster. Give the Service an address that the alert service can reach, such as an
203
+ ingress. Then give the service the URL `https://<host>/hooks/sentry` and the secret.
204
+
205
+ ## Send a test delivery
206
+
207
+ You can sign a delivery yourself. Forward a port to the Orchestrator, and keep this command open:
208
+
209
+ ```sh
210
+ kubectl -n <namespace> port-forward svc/jr2-orchestrator 4000:4000
211
+ ```
212
+
213
+ In a second terminal, sign a body with the secret and send it:
214
+
215
+ ```sh
216
+ BODY='{"action":"created","data":{"issue":{"id":"4505","title":"TypeError in checkout","level":"error"}}}'
217
+ SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SENTRY_HOOK" | sed 's/^.* //')
218
+ curl -s -X POST http://localhost:4000/hooks/sentry \
219
+ -H "Content-Type: application/json" \
220
+ -H "sentry-hook-resource: issue" \
221
+ -H "request-id: test-1" \
222
+ -H "sentry-hook-signature: $SIG" \
223
+ -d "$BODY"
224
+ ```
225
+
226
+ ```text
227
+ {"delivery":"test-1","seen":false,"delivered":[],"refused":[],"ignored":0,"sources":[{"pool":"0d08bbbc-d34e-47fb-9b4b-8e788e328eb4","workflow":"alerts","source":"source","verdict":"held","key":"sentry-4505"}]}
228
+ ```
229
+
230
+ The Pool holds the alert, and starts a run of `triage` for it when a slot is free. Send the same command again. The
231
+ answer has `"seen":true`, because the Hook got the id `test-1` before, and the Pool starts no second run.
232
+
233
+ ## Follow the alerts
234
+
235
+ [`jr2 runs`](../reference/cli.md#runs) lists the run of `triage` for the alert.
236
+ [`jr2 status <run id>`](../reference/cli.md#status) of that run prints, in its `dispatched by:` line, the run of the
237
+ Pool, the key `sentry-4505`, and the Hook delivery that carried the alert. The person on call answers its Gate:
238
+
239
+ ```sh
240
+ jr2 send <run id> --gate waiting --event take --input '{"who":"<you>"}'
241
+ ```
242
+
243
+ `<run id>` is the id of the `triage` run, and `<you>` is your name. To see what the door of the Hook did with each
244
+ delivery that jr2 keeps (`runs.retain`, 30 days by default), print its log:
245
+
246
+ ```sh
247
+ jr2 hooks sentry
248
+ ```
249
+
250
+ To stop the alerts for a time, cancel the run of `alerts`. A cancel of a standing Pool is a pause: the Hook's answer has
251
+ the verdict `paused`, and the Pool holds nothing. [`jr2 run alerts`](../reference/cli.md#run) resumes it.
252
+
253
+ ## Rules
254
+
255
+ - `pool()` refuses a Pool of runs that does not stand:
256
+ ``pool("<id>"): a Pool of runs stands: give it `standing: true`. A Pool of runs that a Caller starts and that ends is not offered: its itemId is a key that lives for runs.retain past the run, so a second batch in that time does no work (ADR-0017)``
257
+ - `pool()` refuses a `dispatch` slot without `wait: true`:
258
+ ``pool("<id>"): its dispatch slot for "<workflow>" has no `wait: true` — with `started` alone, the Pool cannot know when a run ends, and `cap` counts nothing (ADR-0017)``
259
+ - `pool()` refuses a `dispatch` slot with a `target`:
260
+ ``pool("<id>"): its dispatch slot for "<workflow>" has a `target` — a Pool of runs on another Orchestrator is not designed: remove the target (ADR-0017)``
261
+ - `pool()` refuses a Pool of runs without `cap`:
262
+ ``pool("<id>"): a Pool of runs states its `cap`, a whole number above 0: the most live runs at once (ADR-0017)``
263
+ - `pool()` refuses a push Source on a Pool that does not stand:
264
+ `pool("<id>"): a push Source needs a standing Pool of runs: a Pool that ends when it drains would end at once (ADR-0017)`
265
+ - `jr2 up` refuses a standing Pool that is not the top Machine of its Workflow:
266
+ `refusing: workflow "<workflow>": machine "<machine>" composes the standing Pool "<id>" — a standing Pool is the top Machine of its Workflow, because jr2 keeps one run of each standing Workflow: move the Pool to a file of its own in workflows/ (ADR-0017)`
267
+ - `jr2 up` warns when two standing Pools dispatch to one Workflow:
268
+ ``warning: the standing Pools "<pool>" and "<pool>" each dispatch to "<workflow>": `cap` is for each Pool, so one item that two Sources carry can take a slot in each, and the caps add (ADR-0017)``
269
+ - The Orchestrator refuses `jr2 run` of a standing Workflow whose run is live, with the status code `409`:
270
+ ``"<workflow>" is a standing Workflow, and its run <run id> is live: jr2 keeps its run, and `jr2 run` only resumes a paused one (ADR-0017)``
271
+ - The Orchestrator refuses a `dispatch` to a standing Workflow:
272
+ `"<workflow>" is a standing Workflow: jr2 starts its run, and a dispatch cannot (ADR-0017)`
273
+ - When the Pool holds `hold` items (1000 by default), the door answers 429 with the verdict `hold-full`, and the service
274
+ can send the delivery again.
275
+ - While the Pool waits for the back-off of a hand-over, it has no live run, and the door answers 503.
276
+ - A Hook whose `may.answer` does not name the Workflow of the Pool brings it nothing.
277
+
278
+ ## Read next
279
+
280
+ - [Pools and Sources](../concepts/pools-and-sources.md)
281
+ - [Callers and Hooks](../concepts/callers-and-hooks.md)
282
+ - [Write a Hook kind](write-a-hook-kind.md), for a service that `webhook()` cannot read
283
+ - [Start a run from a run](start-a-run-from-a-run.md)
284
+ - Reference: [`pool()`](../reference/authoring-surface/pool.md), [`source()`](../reference/authoring-surface/source.md),
285
+ [`carry()`](../reference/authoring-surface/carry.md), [`skip()`](../reference/authoring-surface/skip.md),
286
+ [`dispatch()`](../reference/authoring-surface/dispatch.md) and [`webhook()`](../reference/hooks.md#webhook)
287
+ - [ADR-0017](../reference/decisions/0017-pool-over-a-source-port.md): a standing Pool, and a Hook that brings items to
288
+ it
289
+ - [ADR-0078](../reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md): a
290
+ Hook, and `webhook()`