@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
@@ -58,7 +58,7 @@ lifecycle stay implemented once, in the registration table.
58
58
 
59
59
  ## External side: the same primitive over HTTP (`gate`), and each gate is a resource
60
60
 
61
- `gate` is the symmetric actor for **every non-agent caller** — humans (`jr2 send`, a UI), webhook translators, CI, other
61
+ `gate` is the symmetric actor for **every non-agent caller** — humans (`jr2 send`, the Console), a Hook, CI, other
62
62
  systems. "Human" is policy, not mechanism, so the actor is not named for one caller. Each invocation is an addressable
63
63
  **gate**: input is `{ gate?, meta? }` — `gate` an optional authored id (the id is derived when absent — below), `meta`
64
64
  serializable caller/integration context (PR URL, title); its accepted set derives from the gated state's transitions,
@@ -72,11 +72,29 @@ scopes event resolution), so `gate: "F-12"` in two concurrent runs cannot collid
72
72
  Gate-as-resource is forced by concurrency: several children park simultaneously (three features in `humanReview` is the
73
73
  _normal_ case), all accepting `approve` — a run-level POST is ambiguous. And every real external surface already thinks
74
74
  in gates: a CLI listing shows pending decisions and acts on one; a UI inbox renders one card per gate (schemas drive the
75
- forms, `meta` drives the copy); a forge webhook translator finds its gate by matching `meta` (e.g. `prUrl`), which is
76
- why `meta` exists. Generalizing beyond humans also closes a hole hard-coded event types left: a push seam like "a
77
- work-source webhook wakes discovery" is just an idle state holding a gate accepting `work_ready` (the `wake` seam of
78
- ADR-0017). Two obvious later additions, both deferred: a cross-run inbox (`GET /gates`), and CLI segment-matching over
79
- derived gate ids (`--gate F-12` resolving an unambiguous segment, git-style like run-id prefixes).
75
+ forms, `meta` drives the copy); a hooked Gate's own handler matches a forge's delivery against its `meta` (e.g. `pr`,
76
+ below), which is why `meta` exists. Generalizing beyond humans also closes a hole hard-coded event types left: a push
77
+ seam like "a Hook brings an item" is a Pool's `source` Gate ([ADR-0017](0017-pool-over-a-source-port.md)). The cross-run
78
+ inbox is `GET /gates`: the open Gates of every live run that the Caller may answer, oldest first, with a `workflow`
79
+ filter, each entry `{ runId, workflow, gate, path, accepts, acceptsNow, meta, openedAt, hook? }` (`hook` names the Hook
80
+ kinds that the Gate hears, when it hears one) and never the run's `context`
81
+ ([ADR-0014](0014-observation-is-open-run-state-is-not.md)). A Gate that takes no event from a Caller, as a Pool's
82
+ `source` Gate that hears only Hooks, is not in it: no person can answer it. `jr2 gates [--workflow]` prints it, and the
83
+ Console's Gate cards read it. A stream of it is a later addition. CLI segment-matching over derived gate ids
84
+ (`--gate F-12` resolving an unambiguous segment, git-style like run-id prefixes) is deferred.
85
+
86
+ **The `answer` scope can name single Gates**, in the form of
87
+ [ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md). It names a Gate by its invoke
88
+ id, the one static name a Gate has in the Machine that invokes it, and the id that its events carry as their sender
89
+ (below): the invoke `id` that the author wrote, or else the key path of its gated state below the root of that Machine
90
+ (`body.humanReview`), with `.0`, `.1` after it when that state invokes more than one unnamed Gate. So `body.humanReview`
91
+ names that Gate in every child of a Pool, and two gated states with one key at two paths of one Machine have two names.
92
+ The name is not unique across the Workflow: a Workflow composes Machines by invoke
93
+ ([ADR-0049](0049-a-machine-carries-its-parts-and-composes-by-invoke.md)), and two composed Machines that each gate a
94
+ state `approve` give that name to two Gates. One per-Gate name covers every Gate of the Workflow that has that invoke
95
+ id, and `jr2 up` refuses a per-Gate name that matches Gates in more than one Machine of the Workflow, so a scope never
96
+ opens a Gate that its author did not see; the author writes an invoke `id` on one of them to tell them apart. It is not
97
+ the caller-facing gate id, which is different in each child and which a workflow may compute.
80
98
 
81
99
  **The gate id is derived; authoring one is the exception.** Absent an authored `gate`, the id is the gate actor's own
82
100
  path below the run root, and jr2Setup's menu-derivation walk names an unnamed gate invoke with its state key path so the
@@ -102,15 +120,202 @@ vocabulary — workflows speak only `defineEvent` / `agentRun` / `gate`.
102
120
 
103
121
  **Every delivered event carries its sender.** The table writes `sender: { party: "agent" | "gate", id }` on each event
104
122
  it delivers, after the schema parse and from the registration alone: `id` is the Agent's slot, or the Gate's invoke id
105
- (static in the Machine, the same after a restore — not the caller-facing gate id, which a workflow may compute). It is
106
- plain data on the event, so it rides the mailbox, a delayed event the run host persists beside the snapshot, and a
107
- restore; a restored registration writes it again from the persisted input and the invoke id. The Machine's own events
108
- (`raise`, `sendTo` itself) carry none. `defineEvent` refuses an input that declares `sender`, and both surfaces refuse a
109
- body that carries it (ADR-0013): no caller and no def can write it. What an author does with it is ADR-0015's.
123
+ (static in the Machine, the same after a restore — not the caller-facing gate id, which a workflow may compute). A
124
+ Gate's event also carries the **Caller** that answered it
125
+ ([ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md)): `via`, `name` and
126
+ `onBehalfOf`, with `checked` naming the fields that jr2 verified. `via` and `name` are always checked, because an
127
+ authenticator made them; `onBehalfOf` is checked only for a Hook, whose signature covers the payload it comes from. The
128
+ run records the same Caller as the answer's `answeredBy`. An event that a Gate's `look` gives (below) has the Gate as
129
+ its Sender and no Caller. The Sender is plain data on the event, so it rides the mailbox, a delayed event the run host
130
+ persists beside the snapshot, and a restore; a restored registration writes it again from the persisted input and the
131
+ invoke id. The Machine's own events (`raise`, `sendTo` itself) carry none. `defineEvent` refuses an input that declares
132
+ `sender`, and both surfaces refuse a body that carries it (ADR-0013): no caller and no def can write it. What an author
133
+ does with it is ADR-0015's.
110
134
 
111
135
  `CANCEL` stays reserved as the run-level infra interrupt. jr's "exit 3 and let the human drive the bash script" becomes
112
136
  "park in a durable state that advertises its human surface."
113
137
 
138
+ ## A Gate hears a Hook, and looks before it waits
139
+
140
+ A Hook ([ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md)) is a door for one
141
+ outside service. Its delivery is not an event of any Machine: a Gate makes one of it, or ignores it. So a hooked Gate is
142
+ an extension of the Gate, not a second concept. It has the same derived id, accepted set, Sender, Console card and
143
+ `jr2 send` path as any Gate. The Hook checks the delivery and decides nothing; only a Gate's handler or its look makes
144
+ an event for a run.
145
+
146
+ - **A Gate slot declares what it hears.** `actors: { review: gate({ meta, delivers, hooks, look, reply }) }`, and the
147
+ state invokes it by its key:
148
+ `invoke: { src: "review", input: ({ context }) => ({ meta: { pr: context.pr, sha: context.sha } }) }`. A plain Gate
149
+ stays `src: "gate"`. `meta` is a zod schema of the meta that the invoking state sets. The Gate checks the meta when it
150
+ opens, and the handlers and the look read it typed. It is a schema and not a type argument, because TypeScript does
151
+ not infer `delivers` when an author names one type argument.
152
+ - **`delivers` lists the events that the handlers and the look can give.** The load walk refuses one that is not in the
153
+ Gate's derived accepted set. The Chart door refuses one that is not in the Chart's Vocabulary
154
+ ([ADR-0070](0070-a-machine-lends-its-parts-to-a-chart.md)). An event in `delivers` is `audience: "external"` unless
155
+ its def sets an audience, so an Agent below the Gate never has it on its Menu, and the author does not have to know
156
+ this ([ADR-0015](0015-authoring-surface-absorbs-the-mechanism.md)). This default can change what a Menu shows, so the
157
+ change that builds it runs the `@model` tier
158
+ ([ADR-0066](0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md)).
159
+ - **A handler for each Hook kind and event key.** `hooks: { github: { "pull_request_review.submitted": handler } }`. The
160
+ key is the kind's id and the event key that the kind reads from the delivery (ADR-0078), never a Hook's name: two
161
+ Hooks of one kind (two organizations, two secrets) reach the same handlers. The table is typed at compile time: each
162
+ kind module that the program imports adds its id and its payload types to `interface HookKinds` by declaration
163
+ merging, and a key that no kind in scope gives does not compile. There is no registry at run time: the door matches a
164
+ delivery by the kind's id and the event key, exactly. A kind's schema key can end in `.*` (`"block_actions.*"`) to
165
+ type each key with that prefix, but a handler names the full key (`"block_actions.approve"`): a handler key that holds
166
+ `*` does not compile, and `gate()` and `source()` refuse it, so the load walk refuses its Workflow. No handler hears a
167
+ handshake. A handler reads `{ payload, meta, caller }`, where `payload` is the decoded body after the kind's schema
168
+ parsed it (below). Its type is the schema's output, so a handler reads only the fields that the schema names. It is
169
+ pure and synchronous, as a guard is: no I/O, no Promise, no clock. It matches the payload against the Gate's `meta`
170
+ (for a review, the PR's url and the pushed commit) and gives one plain event of `delivers`, such as
171
+ `{ type: "approve", by }`, typed by that list, or `undefined` to ignore the delivery. The Orchestrator parses the
172
+ event with its def's schema and asks the run whether it takes it from this Gate now, as it does for `jr2 send`
173
+ (ADR-0013). There are no correlation keys: each delivery calls every open handler of its kind and event key.
174
+ - **A look reads the fact that the Gate waits for.** `look: { read, input, every?, until }`. `read` is an `http` Effect
175
+ marked `idempotent: true`, or a promise actor, which a Machine can invoke anyway; `gate()` refuses another Effect
176
+ ([ADR-0067](0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md)). The promise is for
177
+ the default test tier, which has no Instance Effector, and for a fact inside the Instance. A read of a service outside
178
+ the Instance is an `http` read, because the Orchestrator makes no call out of the Instance for a run. The read is a
179
+ child of the Gate in the run's actor system, so an `http` read goes through the run's Instance Effector and its
180
+ ledger, and `jr2 up` places it as it places an Effect slot. The look runs once after the Gate registers: a change
181
+ before the read is in its answer, and a change after the Gate registered reaches the Gate as a delivery. It runs again
182
+ after each restore, and every `every` (10 minutes by default) while the Gate is open. `until` gives an event of
183
+ `delivers`, or `undefined` for "not yet". So a delivery that came before the Gate opened, a delivery lost while the
184
+ Orchestrator was down, and a restart all come to the same read, and the author writes no idiom for them. The run takes
185
+ a looked event only when it takes it from this Gate now, as for `jr2 send`. A read that fails, an `until` that throws,
186
+ and an event that the run does not take now are each a line in the Orchestrator's log, and the look reads again at the
187
+ next `every`: a look never faults the run. A looked event has the Gate as its Sender and no `answeredBy`: the look is
188
+ the Machine's own read, not a Caller. The per-run feed marks it `looked`, behind the token as a Turn marker is, before
189
+ the delivery, because a delivery can end the run in its own send. The look stops after the run takes its event, until
190
+ the Gate opens again, because a look reads a level: a Gate that stays open after its event would get the same event
191
+ every `every`. So a state that goes back to its own Gate must change the `meta` (a new `sha`), or it loops. A restore
192
+ opens the Gate again, so a Gate that stayed open after its event looks one more time after a restore.
193
+ - **`look: "none"` for a Gate that hears a command.** A `/retry` comment is an edge, not a level, so there is no fact to
194
+ read. A lost delivery of a command stays lost.
195
+ - **`jr2 up` warns about a hooked Gate with no look and no `after` on its state**, and does not refuse: a delivery lost
196
+ in an outage then stops the run until a person answers the Gate. The fix is one line, a look or `look: "none"`.
197
+ - **A bad delivery marks the feed and never faults the run.** A handler that throws, an event that is not in `delivers`,
198
+ and an event that the run does not take now each put a `hook-refused` mark on the run's feed and a line in the
199
+ answer's `refused`. The run goes on. A refusal that the author wants the person to read is a throw in the handler,
200
+ with the reason as its message.
201
+ - **`reply` gives the text that the person who acted reads**, for a kind whose answer a person reads (`slack`,
202
+ `discord`; not `github`, and not a Slack event). `reply(outcome, { meta, caller })` is pure and synchronous, as a
203
+ handler is. `outcome` is that Gate's own: `delivered` with the event, or `refused` with the reason. It gives plain
204
+ text or `undefined`, never a service's JSON, and the kind escapes the text and puts it in its envelope (below). It
205
+ runs after the run answers, because only then is `refused` known, so the text is not in the handler's return. A Gate
206
+ whose handler gave `undefined` was not a target, and its `reply` does not run: a click on one message does not make
207
+ the other open Gates of that key speak. A throw gives no text, and the hook log keeps the error. Only the person who
208
+ acted sees the text. A message that everyone sees is a change of shared state, so it is an Effect of the Machine. So
209
+ is the reply after the work: a plain `http` Effect with a bot token, such as Slack's `chat.postMessage`, never the
210
+ delivery's `response_url`, which takes 5 posts in 30 minutes.
211
+ - **A packaged Machine writes its handlers and leaves its look Open.** A forge's payload is public, so a package writes
212
+ the handlers for each forge that it supports, and handlers are never Open. It imports the kinds that it hears, so
213
+ their payload types are in scope where it is compiled. A look needs a url and a token, so a package leaves
214
+ `look: open` ([ADR-0054](0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md)). The composer
215
+ binds it with `customize(m, { gates: { review: { look, hooks } } })`, which also adds or replaces a handler by kind
216
+ and event key; `null` removes one. An Open look that nobody binds gets the warning above, not a refusal, so a person
217
+ can still answer the Gate with `jr2 send`. A forge part gives the handlers and the look for one fact (a review
218
+ verdict, CI on a commit). It takes event defs and secrets, never a Hook's name, and each verdict compares the commit,
219
+ not only the PR. It types its handlers by the kind's schemas, so a field that it reads is a field that the kind
220
+ parses.
221
+ - **A parked run keeps the handlers, the look and the `reply` of the Assembly that it holds**
222
+ ([ADR-0069](0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md)): they are code in the
223
+ Assembly, and the Gate's input stays data, so a restore registers the Gate again as before.
224
+
225
+ ## The Hook's door
226
+
227
+ - **`POST /hooks/:name`.** Each Hook that `jr2.config.ts` declares has one stable address, which a service is set up
228
+ with once. The Hook's verifier authenticates the route, outside the bands of ADR-0014 (ADR-0078). A delivery that
229
+ fails it, or whose signed timestamp is more than 300 seconds from the door's clock, gets 401, and nothing else
230
+ happens. The window is jr2's, the same for every kind. A name that the current config does not declare gets 404. With
231
+ no current Assembly the door answers 503 before the verifier, because no config says which Hooks exist. A body above 2
232
+ MiB gets 413, after the name and before the verifier. The door reads no more of the body than the limit, because this
233
+ is the one route with no bearer token that takes a body, and a `mac` check sends the body to the Instance Effector as
234
+ base64, which its wire limits to 4 MiB. Without the limit, a large delivery that is correctly signed would get a
235
+ false 503. The hook log keeps a `too-large` line, which claims no delivery id. A `mac` check that the Instance
236
+ Effector cannot answer (none is deployed, or no replica answers) is 503 too, so the service can send again: the
237
+ Orchestrator holds no secret to check with
238
+ ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)).
239
+ - **The kind's handshake comes after the verifier.** A Slack `url_verification` gets `{ "challenge": … }`, and a Discord
240
+ `PING` gets `{ "type": 1 }`. A handshake reaches no handler, and the hook log keeps it as `handshake`. A request that
241
+ the kind cannot read (its `read` throws, or gives no id or no event key) gets 400, and the hook log keeps the error as
242
+ `unreadable`.
243
+ - **The door parses the payload before the fan-out.** A Hook kind gives a zod schema for each event key, and the door
244
+ parses the decoded body with the schema of the delivery's key. Each object in a schema is a loose object, and a schema
245
+ names only the fields that handlers need. So a field that the service adds breaks nothing, and a named field that is
246
+ missing or of the wrong type fails the parse. A failed parse reaches no handler. The hook log keeps a
247
+ `payload-refused` line with zod's issues, and the answer names the kind and the event key in
248
+ `payloadRefused: { kind, key, issues }`. The status is 200, because a second try fails the same way. A key that the
249
+ kind gives no schema for is not parsed, and it reaches nothing, because no handler can hear it.
250
+ - **A repeat is found by the delivery id AND the body's digest.** A delivery whose id or digest the hook log holds gets
251
+ 200 with `seen: true`, and nothing is delivered. The check is for each Hook, because two Hooks can get one body (an
252
+ organization's webhook and a repository's). Only a delivery that the door claimed counts: a handshake, a
253
+ `payload-refused` line, an `unreadable` line, a `hold-full` line (a 429, below) and a `no-live-run` line (a 503,
254
+ below) never make a repeat, so a refused payload that comes again is parsed again, and a delivery that every Source
255
+ was too full to hold is heard again. The check and the claim are one synchronous step, so of two copies that come at
256
+ once, one is delivered. A service may or may not send a delivery again; the design depends on neither, because the
257
+ look finds what a lost delivery said.
258
+ - **The fan-in reaches open Gates only.** A delivery goes to every open Gate, in every live run of a Workflow that the
259
+ Hook may answer, whose handlers hear its kind and event key, and to nothing else. A Pool's `source` Gate is one of
260
+ them ([ADR-0017](0017-pool-over-a-source-port.md)). So a Hook answers a Gate of a run, or brings items to a Source. It
261
+ never starts a run, and it never names one.
262
+ - **The answer is the outcome.** 200 with
263
+ `{ delivery, seen, delivered: [{ run, workflow, gate, event }], refused: [{ run, gate, event?, reason }], ignored, sources, payloadRefused? }`:
264
+ `ignored` counts the open Gates whose handler gave nothing, and `sources` holds each Source's verdict (ADR-0017). A
265
+ delivery that reached nothing gets 200 with `delivered: []`. After the verifier, the fan-out always runs first. Then
266
+ the answer is 429 when every Source's verdict is `hold-full`, and 503 only in two cases: a standing Pool that hears
267
+ this kind and event key has no live run and is not paused, or the delivery followed a standing Pool's hand-overs to
268
+ the new run's Source 3 times and the Pool handed its run over again (ADR-0017). In the second case the Pool can have a
269
+ live run: the delivery stops following it, so that it cannot follow without end. Neither 503 claims the delivery's id,
270
+ so the service's send again is heard. The 503s that come before the verifier are in the door's rule above. So a Pool
271
+ in back-off does not stop the review Gates of other runs. This is the answer of a kind whose answer no person reads: a
272
+ forge, or a Slack event, which keeps its 429 and 503 so that Slack sends it again.
273
+ - **A person reads the answer to a chat command or click.** For a Slack command or click and a Discord interaction, the
274
+ kind's envelope takes the place of the outcome, and the hook log keeps the outcome. The text joins the `reply` of
275
+ every target: Sources first (what the act brought), then Gates (what the act answered), and in each by Workflow name,
276
+ then by run id. Empty texts and exact repeats are dropped. A text from a `skipped` verdict shows only when no other
277
+ target gave a text, because a skip says "not for me". Every target speaks, and not one winner, because the outcome is
278
+ already a list and the load walk cannot see which Gates will be open. When no target gives a text, the door writes one
279
+ from the outcome: a repeat, a delivery that reached nothing, a payload that the parse refused, or what the fan-in did
280
+ (a Gate that took an event, a Pool that is paused). For a 429 or a 503 of the fan-in, the door adds a line with the
281
+ cause after the targets' texts, so the text always says why. This text is the same for every service, so the kind
282
+ never sees the outcome. A paused Pool has no run, so no `reply` of it runs: the door's text says that it is paused.
283
+ The 503s and the 404 that come before the verifier stay as they are, because the door cannot know who reads the answer
284
+ before the kind reads a verified request. The door cuts the text to the limit that the kind gives (Discord: 2000
285
+ characters), and the kind puts it in its envelope. The envelope shows the text to the person who acted, and to nobody
286
+ else: Slack `{ "response_type": "ephemeral", "text": … }`, Discord
287
+ `{ "type": 4, "data": { "content": …, "flags": 64 } }`. The text is plain: no Block Kit blocks, no Discord embeds and
288
+ no `in_channel` in v1. A rich reply adds later without a break.
289
+ - **Such an answer is 200.** Where the rules above give 429 or 503, a delivery whose answer a person reads gets 200, and
290
+ its text says why. A non-200 shows the person the service's own error, which says nothing about what jr2 did. The hook
291
+ log keeps the status that the rules give. A failed verifier stays 401.
292
+ - **The reply to a Slack click goes to its `response_url`.** Slack shows no text from the answer to a Block Kit click:
293
+ that answer can only be an empty 200. So the `slack` kind's envelope for a click is an empty 200 and a follow-up, as
294
+ data: a post of the text to the click's `response_url`. The door answers the empty 200, and then makes the post
295
+ through the Instance Effector's `http` call
296
+ ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)): to a host that the kind declares
297
+ (`hooks.slack.com`), with no secret (the url is in the verified payload), one try. The door refuses a url that does
298
+ not start with one of the kind's `posts`, and posts nothing. A failed or refused post is a `follow-up` line in the
299
+ hook log. A `reply` that threw is in its delivery's line, as `replyErrors`. It is the answer by another road, with no
300
+ slot, no ledger row and no run. No Machine can post it, because a click that reaches nothing has no run.
301
+ - **No deferred answer.** The text is a verdict or an outcome, which is ready when the answer leaves. The work never is.
302
+ Discord's deferred answer (type 5) would make the run's result the answer, but its token lives 15 minutes, a run is
303
+ often longer, and a deferred answer that nobody edits stays "thinking". When the door misses the service's 3 seconds,
304
+ the person sees the service's error, the hook log keeps what jr2 did, and a held item still runs.
305
+ - **The hook log keeps each delivery for `runs.retain`**
306
+ ([ADR-0065](0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md)): its Hook, id, digest, outcome and
307
+ the status that the rules give, a `payload-refused` delivery with zod's issues, a Source's skip with its reason, a
308
+ `reply` that threw, and a failed follow-up post. `jr2 hooks <name>` prints it, newest first, from `GET /hooks/:name`,
309
+ which takes the Instance token alone ([ADR-0014](0014-observation-is-open-run-state-is-not.md)): a line can hold a
310
+ payload's fields and a handler's words. A run's blob keeps its `(delivery, gate)` pairs as the `delivery` of each
311
+ answer that a Hook gave (the run's last 100 answers, ADR-0078), so a restore never gives one delivery twice to one
312
+ Gate.
313
+ - **Tests.** The default tier drives `POST /hooks/:name` with a `token` and an `ed25519` verifier against hooked Gates
314
+ and a Pool's Source, with the handshakes, the parse, the join of the replies and the envelopes. It has no Instance
315
+ Effector, so the `@kind` tier drives a `mac` verifier through the Instance Effector, and a look whose `http` read runs
316
+ there ([ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md) lists the facts of
317
+ the services that the build must check).
318
+
114
319
  ## Static imports; live objects constructed per-invocation from input
115
320
 
116
321
  Everything above is reachable by plain `import`. We rejected two alternatives:
@@ -126,6 +331,42 @@ The doctrine that makes static import work, sharpening ADR-0007: **actor logic i
126
331
  per-invocation from serializable input** — the Harness client from an endpoint, the kube client from ambient config.
127
332
  Tests mock by `machine.provide()` at the layer under test.
128
333
 
334
+ ## Considered options for a Hook's delivery
335
+
336
+ - **Correlation keys**: a Gate states keys (`{ pr }`, `{ sha }`), a Hook event carries them, and the door routes by
337
+ them. Not now: the fan-out calls every open handler of the kind and event key, and keys can come later as an index,
338
+ without a break, when a measurement shows the need.
339
+ - **A kept buffer**: the door keeps each Hook event for a time and gives it to a Gate that opens later. Rejected: it
340
+ gives edges again in the order that they arrived, so an old verdict can come after a newer one. The look reads the
341
+ level instead.
342
+ - **A Listener**: an actor at a Machine's root, `listen(kind, { delivers, on, hold })`, that turns each delivery into
343
+ one of its own events and holds it until the Machine takes it. Rejected: it is a second concept beside the Gate, and a
344
+ third party of Sender, outside the position rule of ADR-0015.
345
+ - **A Lookout**: a delivery only wakes an Effect slot, which reads the fact with an idempotent read and settles on it,
346
+ so the payload never decides. Rejected as the only form: it is the soundest, and the hardest to write. Each wait needs
347
+ a read and a schema, and a service with nothing to read needs a second form. Its read is the Gate's `look`.
348
+ - **Payloads typed only**: a kind gives a TypeScript type for each event key, and the door parses nothing. Rejected: the
349
+ type then promises what nothing checks, and a fault in it shows in the handler of every Workflow, as a `hook-refused`
350
+ mark far from its cause, instead of at the kind.
351
+ - **Handlers as a list of `on(kind, handlers)` calls**, which needs no `HookKinds`. Rejected: TypeScript lost the Gate's
352
+ `meta` type through the nested generic call, and the handler's `meta` was `never`.
353
+ - **A brand on a handler's return** (`deliver(approve, { by })`). Rejected: a plain event typed by `delivers` gets the
354
+ same checks from the compiler and the walk, with one name less.
355
+ - **The reply text in the handler's return.** Rejected: the handler runs before the run answers, so it cannot know
356
+ `refused`.
357
+ - **`reply` in the Hook's config**, a function of the whole outcome. Rejected: the config is not the Assembly, it knows
358
+ no Workflow, and a publish does not hand it over.
359
+ - **The author's envelope** (`{ response_type, text }`, a Discord type), or a `reply` for each kind. Rejected: every
360
+ author learns two services for one text, and a message that the channel sees is shared state, which is an Effect.
361
+ - **A reply that changes the message** (Discord type 7, Slack `replace_original`). Rejected: a click that counts for
362
+ nothing would remove the buttons for everyone. The Machine's update of the message follows its state.
363
+ - **One winner** among the targets' texts (the first target, or a load-walk refusal of two). Rejected: it hides what
364
+ happened at the other targets, and the walk cannot see which Gates will be open.
365
+ - **No text for a Slack click**, only the Machine's update. Rejected: a click that reaches nothing, or that the run
366
+ refuses, then shows nothing at all.
367
+ - **A deadline fallback** (a deferred answer at 2.5 s, and the text posted later). Not now: the door is fast by design,
368
+ and the fallback adds a timer and a second road for a rare case.
369
+
129
370
  ## Consequences
130
371
 
131
372
  - Addresses are computed by jr2, never by the workflow — iids always (ADR-0016): fresh per invocation by default (the
@@ -138,4 +379,7 @@ Tests mock by `machine.provide()` at the layer under test.
138
379
  only**, which pass `endpoint` directly in run input — e2e drives their Machines by playing the Harness's Menu against
139
380
  `/agents/<iid>/*` with no cluster. Workflows that invoke `workspace()` always get real Sandboxes (the data plane is
140
381
  never faked — ADR-0009); their e2e tier requires kind.
141
- - Unsolicited mid-turn steering remains the residual hard case (ADR-0002/0006).
382
+ - A Caller's free text to an Agent is a Steering message ([ADR-0002](0002-duplex-streaming-actor.md)), never a workflow
383
+ event: it is in no Vocabulary, and no Gate takes it.
384
+ - Not set yet: a time budget for the handlers, which run in the Orchestrator's event loop for each open Gate of their
385
+ kind and event key.
@@ -88,8 +88,15 @@ the same contract as a temp directory.
88
88
 
89
89
  - The body chooses how many Agents work in its Workspace (ADR-0004, ADR-0075). The wrapper has no lock machinery for
90
90
  them.
91
- - The workspace labels the CR with its run (`jr2.dev/run`, `jr2.dev/workflow`), which is what `jr2 ls` groups by
92
- (ADR-0009).
91
+ - The workspace labels the CR with its run, its Workflow and its path (`jr2.dev/run`, `jr2.dev/workflow`,
92
+ `jr2.dev/workspace`), and writes the exact path in the annotation `jr2.dev/workspace-path`. The path is the workspace
93
+ actor's path in the run's Machine: actor ids joined by `/`, and a Pool child's id is its item's id, which can hold any
94
+ character. A label value holds at most 63 characters from `[A-Za-z0-9._-]`, so the label holds a form of the path for
95
+ a selector only: each other character becomes `-`, leading and trailing characters that are not alphanumeric are
96
+ removed (a label value must start and end with one), and a value above 63 characters is cut and ends with a hash, as
97
+ the CR's name does. A person selects by it with `kubectl -l`; no jr2 reader does. The `jr2 workspace` verbs list the
98
+ CRs by `jr2.dev/run` and compare a handle, `<run-prefix>/<workspace path>`, with the annotation, with no Orchestrator
99
+ (ADR-0009). They do not select by a run prefix, because a label selector matches only a whole value.
93
100
  - **Lost workspace: the wrapper emits, the body decides.** While the body runs, a lease actor beside it renews the
94
101
  Sandbox's keepalive and follows the Sandbox watch for whether the workspace it attached to is still there (ADR-0021,
95
102
  ADR-0063) — continuously, and on snapshot restore as the first tick of that same loop rather than a separate
@@ -32,7 +32,7 @@ is needed either (ADR-0006).
32
32
  ([ADR-0076](0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md)). The fetch token's Stand-in
33
33
  is public, `jr2-held-JR2_SANDBOX_TOKEN`, and `jr2-upload-pack` holds it as a constant. The Harness reaches the
34
34
  Orchestrator only through the Custodian: the agent routes on its control socket, `/run/jr2/custodian/control.sock`,
35
- which no confined program can replace (ADR-0076), and the fetch on `127.0.0.1:8081`. The Custodian forwards five
35
+ which no confined program can replace (ADR-0076), and the fetch on `127.0.0.1:8081`. The Custodian forwards six
36
36
  routes, with each route's own token put in place of its Stand-in, and answers every other route itself:
37
37
 
38
38
  ```
@@ -40,6 +40,7 @@ is needed either (ADR-0006).
40
40
  POST /agents/<iid>/events a pick → the Orchestrator
41
41
  GET /agents/<iid>/deliveries/<id> an outstanding pick's answer → the Orchestrator
42
42
  POST /agents/<iid>/notices a step boundary's notices heard → the Orchestrator
43
+ POST /agents/<iid>/conversation a batch of the conversation's entries (ADR-0023) → the Orchestrator
43
44
  POST /fetch the ask (ADR-0053), for THIS pod's Sandbox → POST /sandboxes/<name>/fetch
44
45
  anything else 404
45
46
  ```
@@ -47,10 +48,10 @@ is needed either (ADR-0006).
47
48
  So the Agent's sole control-plane peer is a container it can reach but whose credential it cannot read. That is what
48
49
  makes ADR-0006's "the Agent never steers the workflow" **enforced** rather than advertised.
49
50
 
50
- - **The Harness presents the Menu to its model itself.** There is no MCP server and no MCP client. `menu.ts` turns each
51
- accepted event into a Menu item, with the event's JSON Schema as its parameters, and turns a pick into one
52
- `POST …/events` — and, for an outstanding pick, the polls of its delivery, inside the same tool call. The engine runs
53
- one model answer's calls in the model's order
51
+ - **The Harness presents the Menu to its model itself.** No MCP server serves the Menu, and no MCP client reads it.
52
+ `menu.ts` turns each accepted event into a Menu item, with the event's JSON Schema as its parameters, and turns a pick
53
+ into one `POST …/events` — and, for an outstanding pick, the polls of its delivery, inside the same tool call. The
54
+ engine runs one model answer's calls in the model's order
54
55
  ([ADR-0027](0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md)), and only the first pick of a model
55
56
  answer runs. It imports nothing from pi: `menu-tools.ts` gives each item to pi as a tool. A harness that only speaks
56
57
  MCP would get a second presenter over the same `menu.ts`, not a second Menu. The receipt comes back as prose
@@ -60,15 +61,16 @@ is needed either (ADR-0006).
60
61
  keeps two thin HTTP surfaces, neither of them MCP:
61
62
 
62
63
  ```
63
- # human / webhook / CI — the Gate resource of ADR-0011, authenticated
64
- GET /runs/:id → open gates: accepts + schemas + acceptsNow + meta [Instance token]
65
- POST /runs/:id/gates/:gate/events → validate + deliver, or 409 [Instance token]
64
+ # human / Hook / CI — the Gate resource of ADR-0011, a Caller's (ADR-0078)
65
+ GET /runs/:id → open gates: accepts + schemas + acceptsNow + meta [answer]
66
+ POST /runs/:id/gates/:gate/events → validate + deliver, or 409 [answer]
66
67
 
67
68
  # the Agent's, through its pod's Custodian
68
69
  GET /agents/:iid/surface → Menu + Allowed picks + schemas + semantics [control token]
69
70
  POST /agents/:iid/events → validate + deliver → the receipt, or 409 [control token]
70
71
  GET /agents/:iid/deliveries/:id → an outstanding pick's answer, long-polled [control token]
71
72
  POST /agents/:iid/notices → the notices a step boundary appended, heard (ADR-0062) [control token]
73
+ POST /agents/:iid/conversation → a batch of the conversation's entries (ADR-0023) [control token]
72
74
  ```
73
75
 
74
76
  The receipt is `{ delivered, event, moved, turnComplete, allowed, deliveryId }`. A pick the workflow is still running
@@ -100,7 +102,10 @@ is needed either (ADR-0006).
100
102
  accepts each token only on its own routes. Neither reaches a Gate: a Gate refuses every principal of an Agent.
101
103
  Neither reaches another Sandbox. A Stand-in is refused on every route.
102
104
  - **Instance token** — the human/CLI credential for Gates and run control. Minted into an in-cluster Secret at
103
- `jr2 up`; the CLI reads it over the kube API (RBAC is the gate) while port-forwarding the Service (ADR-0019).
105
+ `jr2 up`; the CLI reads it over the kube API (RBAC is the gate) while port-forwarding the Service (ADR-0019). It is
106
+ one Caller among others: a named Caller token, a JWT and a Hook reach Gates within their scopes
107
+ ([ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md)). No Caller is a
108
+ principal of an Agent.
104
109
  - **The Instance-token door.** The Instance token may also read any Agent's surface and deliver to it —
105
110
  `GET /agents/:iid/surface`, `POST /agents/:iid/events` — because it is the operator. That is the operator speaking
106
111
  AS the Agent: it reads the Agent's Menu and Allowed picks and its picks are judged as the Agent's — they carry the
@@ -143,7 +148,7 @@ is needed either (ADR-0006).
143
148
  - **The Orchestrator serves MCP, and the Custodian forwards it.** Rejected: it keeps the MCP transport and its session
144
149
  handling in the control plane and on both ends, to carry two plain HTTP calls.
145
150
  - **A uniform `/registrations/:address` API** for every caller. Rejected: it dissolves the run-scoped, discoverable Gate
146
- resource ADR-0011 designed on purpose (a webhook translator finds its Gate by `meta.prUrl`; `GET /runs/:id` lists open
151
+ resource ADR-0011 designed on purpose (a hooked Gate's handler matches its own `meta.pr`; `GET /runs/:id` lists open
147
152
  decisions) in exchange for aesthetic symmetry.
148
153
 
149
154
  ## Consequences
@@ -151,7 +156,9 @@ is needed either (ADR-0006).
151
156
  - **The operator needs no change.** ADR-0001 made sidecars generic container fragments the operator schedules without
152
157
  understanding; the Custodian is exactly that. `kubeSandbox` composes it (plus the token Secret, written right after
153
158
  the CR with the CR as its owner from birth, so Kubernetes reaps it with the Sandbox and no path leaks it — ADR-0001).
154
- - **Nothing in jr2 speaks MCP.** The Orchestrator, the Harness and the CLI carry no MCP dependency.
159
+ - **The control plane speaks no MCP.** The Orchestrator and the CLI carry no MCP dependency, and no MCP carries the
160
+ Menu. The Harness and the Instance Effector speak MCP only as clients of an author's servers
161
+ ([ADR-0079](0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md)).
155
162
  - **One container holds the Sandbox's tokens and every model key.** A fault in the Custodian's engine is a fault in the
156
163
  one container that holds both. ADR-0059 records the trade.
157
164
  - **The `@kind` e2e tier owns the pod → Orchestrator leg** — the pod's Harness actually calls its tool through the
@@ -170,6 +177,5 @@ is needed either (ADR-0006).
170
177
  Custodian in every Harness pod must reach it and no policy can tell the Custodian's packets from the Agent's.
171
178
  - **The Harness wire is authenticated in the other direction.** The Orchestrator bears a token derived for the placement
172
179
  on every Harness call, and the Harness holds only its digest (ADR-0058). Same signed-name idea, reversed caller.
173
- - **Open: authn for non-kube callers.** The Instance token rides kube RBAC for anyone with cluster creds; what an
174
- ingress-exposed jr2 Application uses for human/webhook callers without them (`--url` mode) is out of scope here
175
- (ADR-0014 inherits the same question).
180
+ - **Callers without kube credentials are ADR-0078's.** The Instance token rides kube RBAC for anyone with cluster creds;
181
+ a Caller without them (`--url` mode, a browser, a Hook) holds a named token, a JWT or a Hook's signature.
@@ -1,4 +1,4 @@
1
- # Observation is open; run state and control need the Instance token
1
+ # Observation is open; run state and control need a Caller's scope
2
2
 
3
3
  [ADR-0013](0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md) minted two tokens and scoped them
4
4
  for **delivery** — a Sandbox token may deliver to its own agent registrations, an Instance token may deliver to gates.
@@ -16,20 +16,44 @@ Building `jr2 visualize` (the Machine renderer and its live run highlighting) fo
16
16
 
17
17
  ```
18
18
  open /workflows, /workflows/:name/machine, /viz/* structure
19
+ /console/config (the login's issuers, ADR-0032) public values
19
20
  /workflows/:name/runs, …/runs/:runId/events OBSERVATION (new)
20
21
  authenticated /agents/:iid/* the Agent's surface [Sandbox token]
21
- instanceOnly /runs, /runs/:id, /runs/:id/events (GET + POST) run state + control [Instance token]
22
- POST /workflows/:name/runs starting a run (ADR-0058)
23
- /runs/:id/gates/:gate/events gates (ADR-0011/0013)
22
+ guarded each route below, opened by one scope of a Caller run state + control [a Caller, ADR-0078]
24
23
  ```
25
24
 
26
- - **Run state and run control are the Instance token's alone.** `authenticated` accepts _any_ token we minted, so a
27
- Sandbox token — the one credential that sits in a pod an Agent shares a network namespace with — could read **every**
28
- run's context (other features' branches, ticket bodies, review verdicts, Harness endpoints) and **CANCEL any run**.
29
- Neither is on the Agent's surface, no more than a gate is: ADR-0013 scopes that token to delivering into registrations
30
- recorded against its _own_ Sandbox. This closes the same class of hole ADR-0013 closed for gates, on the routes it did
31
- not name. The band is named for the credential it requires, not for a role — "operator" in this repo is the Kubernetes
32
- operator (ADR-0001).
25
+ - **Run state and run control are a Caller's, and each route needs one scope.** `authenticated` accepts _any_ token we
26
+ minted, so a Sandbox token — the one credential that sits in a pod an Agent shares a network namespace with — could
27
+ read **every** run's context (other features' branches, ticket bodies, review verdicts, Harness endpoints) and
28
+ **CANCEL any run**. Neither is on the Agent's surface, no more than a gate is: ADR-0013 scopes that token to
29
+ delivering into registrations recorded against its _own_ Sandbox. This closes the same class of hole ADR-0013 closed
30
+ for gates, on the routes it did not name. The guarded band takes only a Caller
31
+ ([ADR-0078](0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md)): a Sandbox token is never
32
+ one. Each route of the band needs one scope, checked against the Workflow of the run or the door that it addresses:
33
+
34
+ ```
35
+ start POST /workflows/:name/runs, POST /workflows/:wrapper/charts/:name/runs
36
+ read GET /runs (summaries of the runs of its Workflows, live and ended), GET /runs/:id (the whole status),
37
+ GET /runs/:id/events, GET /runs/:id/conversations[/:iid[/events]] (the kept conversations, ADR-0023)
38
+ answer POST /runs/:id/gates/:gate/events, GET /gates (the open Gates it may answer), and the `gates` of GET /runs/:id
39
+ cancel POST /runs/:id/events (CANCEL)
40
+ steer POST /runs/:id/steering, GET /runs/:id/agents (the slot list { agent, open, steerable }, ADR-0002)
41
+ ```
42
+
43
+ `answer` alone opens a run's `gates` and their routing, and never its `context`: a person who answers a Gate reads the
44
+ Gate's `meta`, which the author wrote for that person. So `GET /runs/:id` gives `read` the status and `answer` the
45
+ `gates`; with `answer` alone the answer is the run's observation (below) and its `gates`. `GET /runs` leaves out the
46
+ runs of the Workflows that the Caller may not read; it refuses no run, and refuses only a Caller with no `read` scope
47
+ at all (ADR-0078). `answer` can name single Gates of a Workflow (ADR-0078). Then the gate POST refuses another Gate of
48
+ that Workflow, and `GET /gates` (its entries are ADR-0011's) and the `gates` of `GET /runs/:id` list only the Gates
49
+ that the Caller may answer. Neither gives the run's `context`. `GET /runs/resolve` answers ids to a Caller with any
50
+ scope, for the Workflows of its scopes. `GET /whoami` answers any Caller. A route that no scope names stays the
51
+ Instance token's: the runtime Chart routes (`PUT` and `DELETE /workflows/:wrapper/charts/:name`), `GET /repos`, a
52
+ Hook's log (`GET /hooks/:name`, which can hold a payload's fields and a handler's words), and the Instance-token door
53
+ on `/agents/:iid/*` (ADR-0013) except the conversation route, which takes only a control token
54
+ ([ADR-0023](0023-the-harness-prints-the-conversation.md)). `POST /hooks/:name` is outside the bands: the Hook's own
55
+ verifier authenticates it ([ADR-0011](0011-workflow-defined-events.md)). The band is named for what it checks, not for
56
+ a role — "operator" in this repo is the Kubernetes operator (ADR-0001).
33
57
 
34
58
  - **The observer gets a PROJECTION, not a credential.** `RunObservation` is
35
59
  `{ runId, workflow, status, value, children }` — identity, and where in the Machine the run is. What is absent is the
@@ -48,16 +72,15 @@ instanceOnly /runs, /runs/:id, /runs/:id/events (GET + POST) run state + c
48
72
 
49
73
  ## Considered options
50
74
 
51
- - **Give the page a token.** It would have to be the **Instance** token — gates, cancel, every run's context — handed to
52
- whatever can load a URL. Putting it in `jr2.config.ts` (checked in) is worse still, and the token is minted per boot,
53
- so the page would not have it anyway. Rejected: the visualizer needs to _see_ a run, and seeing is not a subset of the
54
- credential that steers it. That mismatch is the whole reason a projection exists.
75
+ - **Give the page a token.** It would be a credential — gates, cancel, every run's context — handed to whatever can load
76
+ a URL. Putting it in `jr2.config.ts` (checked in) is worse still, and the token is minted per boot, so the page would
77
+ not have it anyway. Rejected: the visualizer needs to _see_ a run, and seeing is not a subset of the credential that
78
+ steers it. That mismatch is the whole reason a projection exists.
55
79
  - **Keep `/runs*` on `authenticated` and filter in the handler.** Rejected: the guard would then depend on every future
56
80
  handler remembering to redact. The band is a middleware precisely so the default is refusal.
57
81
  - **A fourth "observer" token.** Rejected: it buys nothing today (the observation surface carries no secret) and costs a
58
82
  minting, distribution, and revocation story. If observation ever needs to be _private_ — a deployed jr2 Application on
59
- a shared network — that is the ADR to write, and it is the same open question ADR-0013 left about authn for non-kube
60
- callers.
83
+ a shared network — the guarded band and ADR-0078's scopes are already where it goes.
61
84
 
62
85
  ## Consequences
63
86
 
@@ -66,11 +89,11 @@ instanceOnly /runs, /runs/:id, /runs/:id/events (GET + POST) run state + c
66
89
  now **403**. The test pinned the hole.
67
90
  - **The API is a cluster Service** (ADR-0019), so "open" means open to whoever can reach it — in-cluster peers, a
68
91
  port-forward, or an ingress — deliberately, since what is open is structure plus lit states. Anything that would
69
- embarrass a user if seen belongs behind `instanceOnly`, and that is where it is.
70
- - **The CLI is unaffected.** It holds the Instance token from the in-cluster Secret (ADR-0019) and reads the guarded
71
- routes, as before.
72
- - **Open, inherited from ADR-0013:** what a deployed jr2 Application uses for human callers. Whatever it is, the band
73
- split survives it — only the credential in the `instanceOnly` band changes.
92
+ embarrass a user if seen belongs in the guarded band, and that is where it is.
93
+ - **The CLI is unaffected.** It holds the Instance token from the in-cluster Secret (ADR-0019), which has every scope,
94
+ and reads the guarded routes, as before.
95
+ - **A Caller without kube credentials is ADR-0078's:** a named token, a JWT, or a browser login. The band split holds
96
+ for every Caller; what the guarded band checks is one scope for each route.
74
97
 
75
98
  - **One scoped exception, and it does not live on this surface at all (ADR-0023).** The Harness prints each Agent's
76
99
  conversation — prompts, assistant text, thinking, tool calls, and the answer jr2 gave each Menu pick — to container
@@ -87,9 +110,12 @@ instanceOnly /runs, /runs/:id, /runs/:id/events (GET + POST) run state + c
87
110
  form; then it prints there as the model read it. The answer also carries an Effect's output as the model read it: a
88
111
  `command` Effect's output prints there up to the bound, and a Held value an upstream echoed into that output prints
89
112
  with it (ADR-0023, ADR-0059). What remains, deliberately, is that on a shared cluster the population who can read an
90
- Agent's reasoning, and what its picks answered, is whoever can read pod logs. If that becomes unacceptable, the fix is
91
- the read-through ADR-0023 declined to build — `instanceOnly`, on this surface, where the band already works.
113
+ Agent's reasoning, and what its picks answered, is whoever can read pod logs. The Orchestrator's kept copy of the same
114
+ conversation is not part of the exception: it is on this surface, behind `read`, and `jr2 logs` and the Console read
115
+ it there (ADR-0023). Thus `read` opens the same residual of a pick's answer for as long as the run is kept
116
+ (`runs.retain`, ADR-0065). If the pod-log audience becomes unacceptable, the print is what to turn off, and the record
117
+ stays.
92
118
 
93
- Evidence: `packages/orchestrator/src/http.ts` (the three middlewares), `run-host.ts` (`observe` / `RunObservation` /
94
- `RunChild`), `test/auth.test.ts` (the boundary), `test/http-viz.test.ts` (a planted secret never reaches the feed, at
95
- any depth of the child tree).
119
+ Evidence: `packages/orchestrator/src/http.ts` (the middlewares), `run-host.ts` (`observe` / `RunObservation` /
120
+ `RunChild`), `test/auth.test.ts` (the boundary), `test/callers.test.ts` (each scope on its routes),
121
+ `test/http-viz.test.ts` (a planted secret never reaches the feed, at any depth of the child tree).