@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
package/src/customize.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  // `.provide()`, nothing to learn (ADR-0015's rule, restated by ADR-0049 — a `.with()` method and
10
10
  // a callable-machine hybrid were both rejected there).
11
11
  //
12
- // Four moves, and each is the smallest one that works:
12
+ // Five moves, and each is the smallest one that works:
13
13
  //
14
14
  // - An AGENT override is xstate's own `provide`: `machine.provide({ actors: { coder:
15
15
  // agent({ ...stock, ...override }) } })`. The slot already IS the Agent (ADR-0049), so
@@ -18,6 +18,8 @@
18
18
  // package left `open`, or whose auth, header or env names `secret(open)`, carries a binder,
19
19
  // which builds the Effect again with the composer's parts, so the bound slot is checked as a
20
20
  // written one is. Once — a bound part is fixed from then on.
21
+ // - A GATE binding is the same substitution (ADR-0011, ADR-0054): a Gate slot is built again from
22
+ // its parts, with the look bound and the handlers patched by kind and event key.
21
23
  // - A CHILD override is the same call one level down, recursing. Each level is exactly the
22
24
  // one-level reach ADR-0015 found `provide` has — held by the composer who owns the child
23
25
  // object, never host-side injection into somebody else's Machine.
@@ -45,6 +47,8 @@
45
47
  // its slot MAP Open (`repos: open`, ADR-0051) declared exactly this: every slot is the composer's
46
48
  // to name, so the map they write is the declaration, not an addition to one.
47
49
 
50
+ import type { CallbackActorLogic } from "xstate";
51
+ import type { AgentRunReceiveEvent } from "./actor.ts";
48
52
  import { StateMachine, type AnyStateMachine, type InputFrom, type ProvidedActor, type UnknownActorLogic } from "xstate";
49
53
  import type { AgentLogic } from "./actor.ts";
50
54
  import { isAgent, type AgentDefinition } from "./agent.ts";
@@ -78,6 +82,18 @@ import { isEffect, type Arg } from "./effect.ts";
78
82
  import { attachLent, copyOpenBody, lentIn, openBodiesOf, openBodyDeclarationOf } from "./open-body.ts";
79
83
  import { effectChecks } from "./effect-checks.ts";
80
84
  import type { NamedSecret, Text } from "./http-effect.ts";
85
+ import type { EventsOf, HookKinds, NoWildcardKeys } from "./hook-kinds/index.ts";
86
+ import {
87
+ gateSlotOf,
88
+ hooksTableFault,
89
+ isGateSlot,
90
+ lookFault,
91
+ type AnyHandler,
92
+ type GateIn,
93
+ type GateLook,
94
+ type GateSlot,
95
+ type GateSlotParts,
96
+ } from "./gate.ts";
81
97
  import {
82
98
  binderOf,
83
99
  effectBindingShape,
@@ -128,8 +144,10 @@ type LogicAt<M extends AnyStateMachine, K extends string> = Extract<SlotsOf<M>,
128
144
  *
129
145
  * @internal
130
146
  */
131
- type AgentSlots<M extends AnyStateMachine> = Extract<SlotsOf<M>, { logic: AgentLogic }>["src"];
132
-
147
+ type AgentSlots<M extends AnyStateMachine> = Extract<
148
+ SlotsOf<M>,
149
+ { logic: CallbackActorLogic<AgentRunReceiveEvent, any> }
150
+ >["src"];
133
151
  /** The Effect slots with an Open part — told by the phantom `OpenEffect` their constructor's
134
152
  * overload states for an `open` url or argv, or a `secret(open)` (open-effect.ts).
135
153
  *
@@ -137,6 +155,45 @@ type AgentSlots<M extends AnyStateMachine> = Extract<SlotsOf<M>, { logic: AgentL
137
155
  */
138
156
  type OpenEffectSlots<M extends AnyStateMachine> = Extract<SlotsOf<M>, { logic: OpenEffect<OpenEffectPart> }>["src"];
139
157
 
158
+ /** The Gate slots (`gate({ … })`, ADR-0011) — told by their brand.
159
+ *
160
+ * @internal
161
+ */
162
+ type GateSlots<M extends AnyStateMachine> = Extract<SlotsOf<M>, { logic: GateSlot<any, any> }>["src"];
163
+ /**
164
+ * The meta and the event type of the Gate slot under one key.
165
+ *
166
+ * @internal
167
+ */
168
+ type GateTypesAt<M extends AnyStateMachine, K extends string> =
169
+ LogicAt<M, K> extends GateSlot<infer Meta, infer Ev> ? { meta: Meta; ev: Ev } : never;
170
+
171
+ /**
172
+ * A patch of a Gate slot's handlers (ADR-0011, ADR-0054): by Hook kind id and event key, a handler
173
+ * that adds or replaces the one there, or `null` to remove it. A key that the kind's payloads do not
174
+ * name does not compile.
175
+ *
176
+ * @internal
177
+ */
178
+ export type GateHooksPatch<Meta, Ev> = {
179
+ readonly [Id in keyof HookKinds as HookKinds[Id] extends { readonly id: Id } ? Id : never]?: {
180
+ readonly [Key in keyof EventsOf<HookKinds[Id]>]?:
181
+ | ((d: GateIn<Meta> & { readonly payload: EventsOf<HookKinds[Id]>[Key] }) => Ev | undefined)
182
+ | null;
183
+ } & NoWildcardKeys<HookKinds[Id]>;
184
+ };
185
+
186
+ /**
187
+ * What `customize()` binds on one Gate slot: its look, and a patch of its handlers.
188
+ *
189
+ * @internal
190
+ */
191
+ export type GateBinding<Meta, Ev> = {
192
+ /** The look, bound once: the package left it `open`, or the composer replaces it. Or `"none"`. */
193
+ readonly look?: GateLook<Meta, Ev> | "none";
194
+ readonly hooks?: GateHooksPatch<Meta, Ev>;
195
+ };
196
+
140
197
  /** What one Open part binds to: a url, an argv, a secret's name, a dispatch's target — another
141
198
  * Orchestrator's origin and the secret that holds its Instance token, together (ADR-0071). */
142
199
  type PartBinding<P> = P extends "url"
@@ -147,7 +204,11 @@ type PartBinding<P> = P extends "url"
147
204
  ? { secret: string }
148
205
  : P extends "target"
149
206
  ? { target: { url: string; token: NamedSecret } }
150
- : never;
207
+ : P extends "server"
208
+ ? { server: string }
209
+ : P extends "tool"
210
+ ? { tool: string }
211
+ : never;
151
212
 
152
213
  /** A union of object types as their intersection: one binding object with every key. */
153
214
  type Intersected<U> = (U extends unknown ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
@@ -271,6 +332,19 @@ export type Customize<M extends AnyStateMachine> = {
271
332
  effects?: [OpenEffectSlots<Reached<M>>] extends [never]
272
333
  ? never
273
334
  : { [K in OpenEffectSlots<Reached<M>>]?: BindingOf<LogicAt<Reached<M>, K>> };
335
+ /** Bind the look of a Gate slot (`gate({ … })`) that a package left `open`, and add, replace or
336
+ * remove its handlers by Hook kind id and event key (`null` removes one), by slot name:
337
+ * `{ review: { look, hooks: { gitlab: { "note.MergeRequest": null } } } }`. At run time,
338
+ * `customize()` refuses a slot that holds no Gate slot: `customize(): machine "<id>" carries no
339
+ * Gate slot "<slot>" — its Gate slots are: <slots>`. */
340
+ gates?: [GateSlots<Reached<M>>] extends [never]
341
+ ? never
342
+ : {
343
+ [K in GateSlots<Reached<M>>]?: GateBinding<
344
+ GateTypesAt<Reached<M>, K>["meta"],
345
+ GateTypesAt<Reached<M>, K>["ev"]
346
+ >;
347
+ };
274
348
  /** Retune a Machine that this Machine composes, by slot name. The value has the same shape as
275
349
  * these parts, one level down. At run time, `customize()` refuses a slot that holds no Machine:
276
350
  * `customize(): machine "<id>" composes no Machine under "<slot>"`. */
@@ -311,13 +385,18 @@ export type Customize<M extends AnyStateMachine> = {
311
385
  * the Machine does not declare does not compile. Some Machines, such as `task`, leave their
312
386
  * whole slot map Open. Then you name every slot, and at least one. The keys are your names for
313
387
  * the Repos, and the Machine says what their order means. */
314
- repos?: [RepoSlotsOf<M>] extends [never] ? never : { [K in RepoSlotsOf<M>]?: RepoSlot<InputFrom<WorkspaceOf<M>>> };
388
+ // A Workspace with only optional fields can start with no input, but the wrapper gives a mapper `{}`,
389
+ // so its `input` is never `undefined`.
390
+ repos?: [RepoSlotsOf<M>] extends [never]
391
+ ? never
392
+ : { [K in RepoSlotsOf<M>]?: RepoSlot<Exclude<InputFrom<WorkspaceOf<M>>, undefined>> };
315
393
  };
316
394
 
317
395
  /** The same shape with the types erased — what the implementation walks. */
318
396
  type LooseParts = {
319
397
  agents?: Record<string, Partial<AgentDefinition> | undefined>;
320
398
  effects?: Record<string, EffectBinding | undefined>;
399
+ gates?: Record<string, { look?: unknown; hooks?: Record<string, Record<string, AnyHandler | null>> } | undefined>;
321
400
  actors?: Record<string, LooseParts | undefined>;
322
401
  image?: string;
323
402
  user?: string | UserContainer;
@@ -342,6 +421,9 @@ type LooseParts = {
342
421
  * - `customize(): machine "<id>" carries no Agent slot "<slot>" — its Agents are: <slots>`
343
422
  * - `customize(): machine "<id>" composes no Machine under "<slot>" — the Machines it composes are: <slots>`
344
423
  * - `customize(): machine "<id>" carries no Effect slot "<slot>" — its Effects are: <slots>`
424
+ * - `customize(): machine "<id>" carries no Gate slot "<slot>" — its Gate slots are: <slots>`
425
+ * - `` customize(): Gate slot "<slot>"'s look is bound once, to a look or "none" — not `open` again ``, and the
426
+ * refusals of a look that `gate()` makes
345
427
  * - `customize(): machine "<id>" declares no Repo Slot "<slot>" — its slots are: <slots>`
346
428
  * - `customize(): machine "<id>" declares its Repo Slots open as a map — name at least one`
347
429
  * - `` customize(): `repos` say what a SANDBOX is made of, how big it is and which Repos it
@@ -376,11 +458,11 @@ type LooseParts = {
376
458
  * @see docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md
377
459
  */
378
460
  export function customize<M extends AnyStateMachine>(machine: M, parts: Customize<M>): M {
379
- const { agents, effects, actors, image, user, repos, resources } = parts as LooseParts;
461
+ const { agents, effects, gates, actors, image, user, repos, resources } = parts as LooseParts;
380
462
  let out: AnyStateMachine = machine;
381
463
  // Order matters in one direction only: the Sandbox seats and slots REBUILD the wrapper, so they
382
464
  // go last and carry the retuned implementations with them.
383
- if (agents || effects || actors) out = retune(out, { agents, effects, actors });
465
+ if (agents || effects || gates || actors) out = retune(out, { agents, effects, gates, actors });
384
466
  const seats: Seats = {
385
467
  ...(image !== undefined ? { image } : {}),
386
468
  ...(user !== undefined ? { user } : {}),
@@ -450,6 +532,9 @@ function retune(machine: AnyStateMachine, parts: LooseParts): AnyStateMachine {
450
532
  }
451
533
  const bound = Object.entries(parts.effects ?? {}).filter(([, b]) => b !== undefined);
452
534
  for (const [name, binding] of bound) actors[name] = bindEffect(machine, name, binding!);
535
+ for (const [name, binding] of Object.entries(parts.gates ?? {})) {
536
+ if (binding) actors[name] = bindGate(machine, name, binding);
537
+ }
453
538
  for (const [name, childParts] of Object.entries(parts.actors ?? {})) {
454
539
  if (!childParts) continue;
455
540
  const child = asMachine((machine.implementations.actors as Record<string, unknown>)[name]);
@@ -539,6 +624,12 @@ function bindEffect(machine: AnyStateMachine, name: string, binding: EffectBindi
539
624
  );
540
625
  }
541
626
  }
627
+ if ((part === "server" || part === "tool") && (typeof value !== "string" || value === "")) {
628
+ throw new Error(
629
+ `customize(): the mcp Effect "${name}"'s ${part} is bound to a name, a string: ` +
630
+ `${effectBindingShape(part)} (ADR-0054, ADR-0079)`,
631
+ );
632
+ }
542
633
  if (part === "secret" && typeof value !== "string") {
543
634
  throw new Error(
544
635
  `customize(): the ${logic.effect.kind} Effect "${name}"'s secret is bound to a secret's name, a string — ` +
@@ -549,6 +640,62 @@ function bindEffect(machine: AnyStateMachine, name: string, binding: EffectBindi
549
640
  return binder(binding);
550
641
  }
551
642
 
643
+ /**
644
+ * Bind a Gate slot's look and patch its handlers (ADR-0011, ADR-0054). The slot is built again from
645
+ * its parts, with the same `meta`, `delivers` and `reply`, so a run of the import and a run of the
646
+ * bound Machine each have their own. A look is bound to a value once: `open` again is refused, and
647
+ * the bound look meets the checks that `gate()` runs.
648
+ */
649
+ function bindGate(
650
+ machine: AnyStateMachine,
651
+ name: string,
652
+ binding: NonNullable<NonNullable<LooseParts["gates"]>[string]>,
653
+ ): unknown {
654
+ const logic = (machine.implementations.actors as Record<string, unknown>)[name];
655
+ if (!isGateSlot(logic)) {
656
+ throw new Error(
657
+ `customize(): machine "${machine.id}" carries no Gate slot "${name}" — its Gate slots are: ` +
658
+ `${listed(slotsWhere(machine, isGateSlot))} (ADR-0011). Only declared parts can be bound.`,
659
+ );
660
+ }
661
+ const parts: GateSlotParts = logic.gateSlot;
662
+ let look = parts.look;
663
+ if (binding.look !== undefined) {
664
+ if (binding.look === open) {
665
+ throw new Error(
666
+ `customize(): Gate slot "${name}"'s look is bound once, to a look or "none" — not \`open\` again (ADR-0054)`,
667
+ );
668
+ }
669
+ const fault = lookFault(binding.look);
670
+ if (fault) throw new Error(`customize(): Gate slot "${name}": ${fault} (ADR-0011)`);
671
+ look = binding.look as GateSlotParts["look"];
672
+ }
673
+ const hooks: Record<string, Record<string, AnyHandler>> = {};
674
+ for (const [kind, keys] of Object.entries(parts.hooks)) hooks[kind] = { ...keys };
675
+ for (const [kind, keys] of Object.entries(binding.hooks ?? {})) {
676
+ if (typeof keys !== "object" || keys === null) {
677
+ throw new Error(`customize(): Gate slot "${name}": hooks.${kind} is { <event key>: handler | null } (ADR-0011)`);
678
+ }
679
+ const into = (hooks[kind] ??= {});
680
+ for (const [key, handler] of Object.entries(keys)) {
681
+ if (handler === null) delete into[key];
682
+ else if (handler !== undefined) into[key] = handler;
683
+ }
684
+ if (Object.keys(into).length === 0) delete hooks[kind];
685
+ }
686
+ const fault = hooksTableFault(hooks);
687
+ if (fault) throw new Error(`customize(): Gate slot "${name}": ${fault} (ADR-0011)`);
688
+ const frozen: Record<string, Readonly<Record<string, AnyHandler>>> = {};
689
+ for (const [kind, keys] of Object.entries(hooks)) frozen[kind] = Object.freeze(keys);
690
+ return gateSlotOf(
691
+ Object.freeze({
692
+ ...parts,
693
+ hooks: Object.freeze(frozen),
694
+ ...(look !== undefined ? { look } : {}),
695
+ }),
696
+ );
697
+ }
698
+
552
699
  const isSecret = (value: unknown): boolean =>
553
700
  typeof value === "object" && value !== null && typeof (value as { secret?: unknown }).secret === "string";
554
701
 
@@ -19,6 +19,9 @@
19
19
  // through the binding, foreign in the `jr2-dispatched-by` header), and the door answers a
20
20
  // repeat with the run the key already started. So a dispatch is idempotent by construction: a
21
21
  // restore that cannot tell whether its start arrived sends it again, and gets the same run.
22
+ // The key is asked before the slot's schema reads the body: in-process through the run host's
23
+ // lookup, foreign as a key-only ask (no body) when the slot's schema refuses the body. With no
24
+ // run under the key, the schema's refusal is the invoke's error, and nothing starts.
22
25
  // - THE RUN ON THE ROW. Once the start's answer names the run, the intent row is written again
23
26
  // with it, saved at the write: a restore with it settles `started` at once, or resumes the
24
27
  // wait on that run, and never starts another.
@@ -78,9 +81,10 @@ export type DispatchDefinition<S extends z.ZodObject | undefined = undefined> =
78
81
  wait?: boolean;
79
82
  /** How long the Effect waits: by default 30s for the start, and 10m with `wait`. */
80
83
  timeout?: Duration;
81
- /** A different Orchestrator: `url` is its origin, and `token` is the secret that holds its
82
- * Instance token. Or `open`, which the composer binds with `customize(<import>, { effects: {
83
- * <slot>: { target: { url, token } } } })`. Without it, the run starts in this Orchestrator. */
84
+ /** A different Orchestrator: `url` is its origin, and `token` is the secret that holds the token
85
+ * of a Caller of it with `start` for the Workflow, and `read` too when `wait` is true. Or `open`,
86
+ * which the composer binds with `customize(<import>, { effects: { <slot>: { target: { url, token
87
+ * } } } })`. Without it, the run starts in this Orchestrator. */
84
88
  target?: { url: string; token: NamedSecret } | typeof open;
85
89
  };
86
90
 
@@ -208,7 +212,9 @@ function targetOf(workflow: string, target: unknown): { url: string; token: stri
208
212
  );
209
213
  }
210
214
  if (typeof token !== "object" || token === null || typeof (token as { secret?: unknown }).secret !== "string") {
211
- throw new Error(`${where}: the token is a secret("<name>") holding the target's Instance token (ADR-0071)`);
215
+ throw new Error(
216
+ `${where}: the token is a secret("<name>") holding a Caller token of the target with start for the Workflow (ADR-0071)`,
217
+ );
212
218
  }
213
219
  const name = (token as { secret: string }).secret;
214
220
  if (name === OPEN_SECRET) {
@@ -255,23 +261,75 @@ export function openDispatchOf<S extends z.ZodObject | undefined>(
255
261
  }
256
262
 
257
263
  /** The wire request for one foreign start (ADR-0071): the target's origin, the Workflow, the body,
258
- * the token by name, and who dispatched it. */
264
+ * the token by name, who dispatched it, and the party it acts for. With no body (`undefined`), the
265
+ * start is a key-only ask: the door answers the run under the key, and starts nothing. */
259
266
  export function dispatchStartOf(
260
267
  parts: DispatchParts,
261
- input: Record<string, unknown>,
268
+ input: Record<string, unknown> | undefined,
262
269
  dispatchedBy: DispatchedBy & { instance: string },
270
+ onBehalfOf?: string,
263
271
  ): DispatchStartRequest {
264
272
  if (parts.target === undefined) throw new Error("dispatchStartOf: an in-process dispatch has no wire request");
265
273
  return {
266
274
  kind: "dispatch",
267
275
  url: [{ text: parts.target.url }],
268
276
  workflow: parts.workflow,
269
- input,
277
+ ...(input !== undefined ? { input } : { keyOnly: true as const }),
270
278
  auth: { bearer: { secret: parts.target.token } },
271
279
  dispatchedBy,
272
280
  wait: parts.wait,
273
281
  timeoutMs: parts.timeoutMs,
282
+ ...(onBehalfOf !== undefined ? { onBehalfOf } : {}),
283
+ };
284
+ }
285
+
286
+ /**
287
+ * What a foreign start sends as `onBehalfOf` for the first Caller of the calling run's chain
288
+ * (ADR-0071, ADR-0078): the party that Caller says it acts for, else the name that jr2 shows, else
289
+ * `<via>/<name>`. Undefined when jr2 started the chain, or the text is not printable ASCII of at
290
+ * most 256 characters, which a header cannot carry: the start then goes with no claim.
291
+ */
292
+ export function onBehalfOfFor(
293
+ caller: { via: string; name: string; shown?: string; onBehalfOf?: string } | undefined,
294
+ ): string | undefined {
295
+ if (!caller) return undefined;
296
+ const text = caller.onBehalfOf ?? caller.shown ?? `${caller.via}/${caller.name}`;
297
+ return /^[\x20-\x7e]{1,256}$/.test(text) ? text : undefined;
298
+ }
299
+
300
+ // --- A Pool's worker ------------------------------------------------------------------------------
301
+
302
+ /** What a Pool of runs gives its `dispatch` worker (ADR-0017): the run's body, and the Hook delivery
303
+ * that carried the item, when one did. The key is the worker's own spawn id: the item's id. */
304
+ export type PoolWorkerInput = { body: unknown; hook?: DispatchedBy["hook"] };
305
+
306
+ /** A dispatch's snapshot as a Pool's worker holds it: the key and the Hook delivery ride the
307
+ * persisted snapshot, so a restore sends the same key again. */
308
+ type KeyedSnapshot<O, I> = EffectSnapshot<O, I> & { key?: string; hook?: DispatchedBy["hook"] };
309
+
310
+ /**
311
+ * The `dispatch` slot of a Pool of runs, as the Pool spawns it (ADR-0017): the same Effect, whose
312
+ * idempotency key is the item's id — the spawn id — and not its ledger row id. So every Pool, every
313
+ * hand-over and every restore that gives the same item to the callee Workflow gets the same run
314
+ * (ADR-0071). The input is {@link PoolWorkerInput}.
315
+ *
316
+ * @internal
317
+ */
318
+ export function poolWorker(logic: EffectLogic<any, DispatchOutput>): EffectLogic<PoolWorkerInput, DispatchOutput> {
319
+ const keyed = Object.create(
320
+ Object.getPrototypeOf(logic) as object,
321
+ Object.getOwnPropertyDescriptors(logic),
322
+ ) as EffectLogic<PoolWorkerInput, DispatchOutput>;
323
+ const initial = logic.getInitialSnapshot.bind(logic);
324
+ keyed.getInitialSnapshot = (scope, input) => {
325
+ const given = (input ?? { body: undefined }) as PoolWorkerInput;
326
+ return {
327
+ ...initial(scope, given.body),
328
+ key: scope.self.id,
329
+ ...(given.hook !== undefined ? { hook: given.hook } : {}),
330
+ } as KeyedSnapshot<DispatchOutput, PoolWorkerInput> as never;
274
331
  };
332
+ return keyed;
275
333
  }
276
334
 
277
335
  // --- The actor logic -----------------------------------------------------------------------------
@@ -418,6 +476,9 @@ async function drive<I, O>(
418
476
  mark({ stage: "output", text: `${named}: run ${runId}${existing ? " (the key found it: started before)" : ""}` });
419
477
  };
420
478
  const deadline = Date.parse(at) + parts.timeoutMs;
479
+ // A Pool's worker carries the item's id as its key, and the delivery that carried the item
480
+ // (ADR-0017). Any other dispatch's key is its ledger row id.
481
+ const { key, hook } = snapshot as KeyedSnapshot<O, I>;
421
482
 
422
483
  if (prior?.run !== undefined && !parts.wait) {
423
484
  // Started before the restart, and the answer is the run: nothing to send.
@@ -440,22 +501,58 @@ async function drive<I, O>(
440
501
  await waitIn(prior.run);
441
502
  return;
442
503
  }
443
- // Fresh, or in flight with no run on the row: the key makes a second start the same run.
444
- const input = inputOf();
504
+ // The key first, as the door asks it (ADR-0071): a run of the callee that carries it is
505
+ // answered before the input is read, so a re-send across a change of the slot's schema answers
506
+ // `existing`, never an error. The slot's bound holds the lookup as it holds the start.
507
+ const startBound = prior ? Math.max(deadline, Date.now() + START_FLOOR_MS) : deadline;
508
+ const asked = host.lookup
509
+ ? await beforeDeadline(
510
+ host.lookup(parts.workflow, key ?? id).then((run) => ({ run })),
511
+ startBound,
512
+ signal,
513
+ )
514
+ : { run: undefined };
515
+ if (signal.aborted) return;
516
+ if (asked === undefined) {
517
+ settle(
518
+ unknown(
519
+ `the run host did not answer the start of ${parts.workflow} within the slot's timeout; a run may have started`,
520
+ ),
521
+ );
522
+ return;
523
+ }
524
+ const found = asked.run;
525
+ if (found) {
526
+ if (!prior) binding.recordEffect?.(id, intent());
527
+ pin(found.runId, true);
528
+ if (!parts.wait) {
529
+ settle({ outcome: "started", run: found.runId });
530
+ return;
531
+ }
532
+ await waitIn(found.runId);
533
+ return;
534
+ }
535
+ // Fresh, or in flight with no run on the row: the key makes a second start the same run. With
536
+ // the key asked above and no run under it, the body is a first send's, and the slot's schema
537
+ // refuses it as the invoke's error (ADR-0071). A host with no lookup asks the key at its start.
538
+ const input = inputOf(prior !== undefined && !host.lookup);
445
539
  if (!prior) binding.recordEffect?.(id, intent());
446
540
  mark({
447
541
  stage: "started",
448
542
  text: prior ? `${named} (sent again under its key: a run it started is answered)` : named,
449
543
  });
450
544
  // The depth is the callee's: one below this run, which the host records as sent (ADR-0071).
451
- const by: DispatchedBy = { run: binding.runId, slot, effect: id, depth: (binding.depth ?? 0) + 1 };
545
+ const by: DispatchedBy = {
546
+ run: binding.runId,
547
+ slot,
548
+ effect: id,
549
+ depth: (binding.depth ?? 0) + 1,
550
+ ...(key !== undefined ? { key } : {}),
551
+ ...(hook !== undefined ? { hook } : {}),
552
+ };
452
553
  // The slot's bound holds the start too (ADR-0071): a run host that does not answer in it is
453
554
  // `unknown` — the start may have been taken, and a restore's re-send under the key would find it.
454
- const answer = await beforeDeadline(
455
- host.start(parts.workflow, input, by),
456
- prior ? Math.max(deadline, Date.now() + START_FLOOR_MS) : deadline,
457
- signal,
458
- );
555
+ const answer = await beforeDeadline(host.start(parts.workflow, input, by), startBound, signal);
459
556
  if (signal.aborted) return;
460
557
  if (answer === undefined) {
461
558
  settle(
@@ -489,8 +586,13 @@ async function drive<I, O>(
489
586
  const port = portFactory(target);
490
587
  // The wait's stop goes through this port when its state exits (ADR-0071).
491
588
  run.port = port;
589
+ // The party the start acts for: the first Caller of this run's chain, read once (ADR-0071).
590
+ const onBehalfOf = onBehalfOfFor(await binding?.firstCaller?.());
591
+ if (signal.aborted) return;
492
592
  // Fresh: nothing was sent. The body is checked before the intent row, so a defect in the input is
493
- // the Machine's error and sends nothing.
593
+ // the Machine's error and sends nothing. A body the slot's schema refuses is sent as a key-only
594
+ // ask (ADR-0071): the door answers a run under the key, and with none the schema's refusal is the
595
+ // invoke's error.
494
596
  const req = prior ? undefined : requestFor(false);
495
597
  const big = req && tooLarge(req);
496
598
  if (big) {
@@ -567,18 +669,27 @@ async function drive<I, O>(
567
669
  expired();
568
670
  }
569
671
 
570
- /** The run's body: the invoke's input, parsed by the slot's schema when it has one. */
571
- function inputOf(): Record<string, unknown> {
672
+ /** Why the slot's schema refuses the invoke's input, or undefined when it takes it (or the slot
673
+ * has no schema). */
674
+ function refusedBody(): string | undefined {
675
+ if (def.input === undefined) return undefined;
676
+ const parsed = def.input.safeParse(snapshot.input ?? {});
677
+ if (parsed.success) return undefined;
678
+ return (
679
+ `dispatch "${slot}": its input does not match the slot's input schema — ` +
680
+ parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")
681
+ );
682
+ }
683
+
684
+ /** The run's body: the invoke's input, parsed by the slot's schema when it has one. `again`: an
685
+ * in-process re-send to a run host that asks the key only at its start; a body the slot's schema
686
+ * refuses then goes as it is, and the run host answers the run under the key (ADR-0071). */
687
+ function inputOf(again = false): Record<string, unknown> {
572
688
  let input: unknown = snapshot.input ?? {};
573
689
  if (def.input !== undefined) {
574
690
  const parsed = def.input.safeParse(input);
575
- if (!parsed.success) {
576
- throw new Error(
577
- `dispatch "${slot}": its input does not match the slot's input schema — ` +
578
- parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; "),
579
- );
580
- }
581
- input = parsed.data;
691
+ if (parsed.success) input = parsed.data;
692
+ else if (!again) throw new Error(refusedBody());
582
693
  }
583
694
  if (typeof input !== "object" || input === null || Array.isArray(input)) {
584
695
  throw new Error(`dispatch "${slot}": its input is the run's body, a JSON object (ADR-0071)`);
@@ -600,7 +711,9 @@ async function drive<I, O>(
600
711
  instance: binding?.orchestratorName ?? "an Orchestrator with no name",
601
712
  };
602
713
  const timeoutMs = again ? Math.max(1, Math.floor(deadline - Date.now())) : parts.timeoutMs;
603
- return dispatchStartOf({ ...parts, timeoutMs }, inputOf(), by);
714
+ // A body the slot's schema refuses is never sent: the start asks the key alone (ADR-0071).
715
+ const body = refusedBody() === undefined ? inputOf() : undefined;
716
+ return dispatchStartOf({ ...parts, timeoutMs }, body, by, onBehalfOf);
604
717
  }
605
718
 
606
719
  /** The in-process wait (ADR-0071): the run host tells when the run leaves `live`; the slot's
@@ -677,6 +790,9 @@ async function drive<I, O>(
677
790
  if (state.state === "settled" && state.output) {
678
791
  const parsed = DispatchOutputShape.safeParse(state.output);
679
792
  if (!parsed.success) throw new EffectorMalformed("the Effector's output for a dispatch is not a dispatch's");
793
+ // A key-only ask, and no run has the key: nothing was started, and the slot's schema refusal
794
+ // is the invoke's error (ADR-0071).
795
+ if (parsed.data.outcome === "absent") throw new Error(refusedBody() ?? "the door found no run under the key");
680
796
  const output = parsed.data as DispatchOutput;
681
797
  if (output.outcome === "started" || output.outcome === "settled") pin(output.run, false);
682
798
  settle(output);
package/src/durability.ts CHANGED
@@ -18,6 +18,8 @@ export type StoredSnapshot = {
18
18
  status: string;
19
19
  snapshot: unknown;
20
20
  reason?: string;
21
+ /** When the run started (ADR-0009): the row's `started_at`. */
22
+ startedAt?: string;
21
23
  };
22
24
 
23
25
  type ChildEntry = { snapshot?: { input?: unknown; [k: string]: unknown }; [k: string]: unknown };
@@ -0,0 +1,20 @@
1
+ // A duration as a person writes it in config or a query: a number and one unit, such as `"90m"`,
2
+ // `"36h"`, `"30d"` or `"2w"`. `runs.retain` (ADR-0065) and the `since` of `GET /runs` (ADR-0009)
3
+ // read it.
4
+
5
+ const DURATION = /^(\d+(?:\.\d+)?)(s|m|h|d|w)$/;
6
+ const UNIT_MS: Record<string, number> = {
7
+ s: 1000,
8
+ m: 60_000,
9
+ h: 3_600_000,
10
+ d: 86_400_000,
11
+ w: 7 * 86_400_000,
12
+ };
13
+
14
+ /** The duration in milliseconds, or undefined when `text` is not a positive duration. */
15
+ export function periodMs(text: string): number | undefined {
16
+ const m = DURATION.exec(text.trim());
17
+ if (!m) return undefined;
18
+ const ms = Number(m[1]) * UNIT_MS[m[2]!]!;
19
+ return ms > 0 && Number.isFinite(ms) ? ms : undefined;
20
+ }
@@ -27,6 +27,11 @@
27
27
  // Agent reads the Effect's output, not the answer. Everywhere else a settled child with no
28
28
  // `onDone` is Rest, and the one-line form is correct — silent on purpose.
29
29
  //
30
+ // - AN onDone WITH NO LAST ARM (ADR-0075, `refuseUnroutedOutcome`). Each Effect outcome is open,
31
+ // so an `onDone` that an output can miss holds the run in the state and shows no fault. A type
32
+ // cannot see the missing arm, so the walk refuses it. No `onDone` at all is Rest, as above: the
33
+ // author routes nothing, so no outcome can miss a route.
34
+ //
30
35
  // The probe reads, it never runs work: a mapper is called with a Proxy event whose reads are
31
36
  // recorded, and a context and self that survive any read. A guard and an assign are pure by
32
37
  // contract, and only they are probed; any other action may do work, and is never run.
@@ -34,12 +39,44 @@
34
39
  import type { AnyStateMachine, UnknownActorLogic } from "xstate";
35
40
  import type { EventDef } from "@jr2/agent-protocol";
36
41
  import { isAgent } from "./agent.ts";
37
- import { inputKeysOf, isEffect } from "./effect.ts";
42
+ import { isGateInvoke } from "./gate-brand.ts";
43
+ import { inputKeysOf, inputShapeOf, isEffect } from "./effect.ts";
38
44
  import { mayTake, partiesAt } from "./party.ts";
39
45
  import { isAnswer, isPickMapper } from "./pick.ts";
40
46
 
41
47
  type Node = AnyStateMachine["root"];
42
48
 
49
+ /**
50
+ * Refuse a Machine with a state that invokes an Effect when the last arm of that `onDone` has a
51
+ * guard (ADR-0075). The slots are the Machine's own `actors`. Thrown at build by
52
+ * `jr2Setup.createMachine`, so also at a Chart's door, and at load by `partsOf`, so a Machine built
53
+ * with xstate's own `createMachine` is refused where `jr2 check`, `jr2 up` and the Orchestrator read
54
+ * it. Throws one line that names each state and the arm to write.
55
+ */
56
+ export function refuseUnroutedOutcome(machine: AnyStateMachine): void {
57
+ const actors = (machine.implementations.actors ?? {}) as Record<string, unknown>;
58
+ const unrouted: string[] = [];
59
+ const visit = (node: Node): void => {
60
+ for (const inv of node.invoke) {
61
+ if (typeof inv.src !== "string" || !isEffect(actors[inv.src])) continue;
62
+ const arms = node.transitions.get(`xstate.done.actor.${inv.id}`);
63
+ if (arms !== undefined && (arms.length === 0 || arms.at(-1)!.guard !== undefined)) {
64
+ unrouted.push(`"${node.id}" (Effect "${inv.src}")`);
65
+ }
66
+ }
67
+ for (const child of Object.values(node.states)) visit(child);
68
+ };
69
+ visit(machine.root);
70
+ if (!unrouted.length) return;
71
+ const many = unrouted.length > 1;
72
+ throw new Error(
73
+ `machine "${machine.id}": ${many ? "states" : "state"} ${unrouted.join(", ")} ${many ? "have" : "has"} an ` +
74
+ `onDone with no unguarded last arm — an Effect's outcomes are open, and an output that no arm matches holds ` +
75
+ `the run in that state with no fault: add a last arm with no guard, as onDone: [..., { target: "<the state ` +
76
+ `that handles the rest>" }] (ADR-0075)`,
77
+ );
78
+ }
79
+
43
80
  /** What the walk found: refusals, and warnings. */
44
81
  export type EffectFindings = { errors: string[]; warnings: string[] };
45
82
 
@@ -47,6 +84,7 @@ export function effectChecks(
47
84
  machine: AnyStateMachine,
48
85
  actors: Record<string, UnknownActorLogic>,
49
86
  defs: Map<string, EventDef>,
87
+ chart = false,
50
88
  ): EffectFindings {
51
89
  const errors: string[] = [];
52
90
  const warnings: string[] = [];
@@ -220,6 +258,15 @@ export function effectChecks(
220
258
  };
221
259
  });
222
260
  const effects = invokes.filter((inv) => inv.effect !== undefined);
261
+ // xstate's `input` is optional for an actor: a kit Effect that takes a key refuses an invoke
262
+ // with no input when `jr2Setup` or `customize()` builds the Machine.
263
+ for (const inv of effects) {
264
+ const shape = inputShapeOf(inv.effect!);
265
+ const needs = [...(shape?.required ?? []), ...(shape?.oneOf ?? [])];
266
+ if (inv.mapper === undefined && needs.length > 0) {
267
+ errors.push(`state "${n.id}": Effect "${inv.slot}" has no \`input\`, and it takes ${needs.join(", ")}`);
268
+ }
269
+ }
223
270
  if (!effects.length) continue;
224
271
  const at = `state "${n.id}" (Effect${effects.length > 1 ? "s" : ""} ${effects.map((e) => `"${e.slot}"`).join(", ")})`;
225
272
  for (const [event, { pick, ...reads }] of entering.get(n) ?? []) {
@@ -276,7 +323,8 @@ export function effectChecks(
276
323
  });
277
324
  } catch (err) {
278
325
  probed = false;
279
- warnings.push(`${own}: could not probe the input mapper (${messageOf(err)}); its params are unchecked`);
326
+ if (!chart)
327
+ warnings.push(`${own}: could not probe the input mapper (${messageOf(err)}); its params are unchecked`);
280
328
  }
281
329
  }
282
330
  }
@@ -289,7 +337,7 @@ export function effectChecks(
289
337
  const picked = effects.filter((e) => isPickMapper(e.mapper));
290
338
  if (probed && lost.length && probe.unrun.length) {
291
339
  // Any other action may do work, so the probe never runs one; it may read the param.
292
- warnings.push(unrunWarning(where, lost, probe.unrun));
340
+ if (!chart) warnings.push(unrunWarning(where, lost, probe.unrun));
293
341
  } else if (probed && lost.length && picked.length) {
294
342
  const inputs =
295
343
  picked.length === 1
@@ -342,7 +390,7 @@ export function effectChecks(
342
390
  const below: Node[] = [];
343
391
  const descend = (p: Node): void => {
344
392
  for (const child of Object.values(p.states)) {
345
- if (invokesAgent(child) !== undefined || child.invoke.some((inv) => inv.src === "gate")) continue;
393
+ if (invokesAgent(child) !== undefined || child.invoke.some((inv) => isGateInvoke(inv, actors))) continue;
346
394
  below.push(child);
347
395
  descend(child);
348
396
  }