@jr2/orchestrator 0.4.1 → 0.5.1

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 (327) hide show
  1. package/README.md +20 -2
  2. package/console/components/app.ts +2 -0
  3. package/console/components/fleet.ts +14 -1
  4. package/console/components/machine-pane.ts +41 -2
  5. package/console/main.ts +13 -1
  6. package/console/store.ts +102 -6
  7. package/console/style.css +21 -0
  8. package/docs/agents-block.md +5 -0
  9. package/docs/concepts/agents-and-turns.md +442 -0
  10. package/docs/concepts/architecture.md +230 -0
  11. package/docs/concepts/charts.md +143 -0
  12. package/docs/concepts/composition.md +357 -0
  13. package/docs/concepts/converge.md +254 -0
  14. package/docs/concepts/durable-runs.md +189 -0
  15. package/docs/concepts/effects.md +603 -0
  16. package/docs/concepts/gates.md +208 -0
  17. package/docs/concepts/machines-and-workflows.md +289 -0
  18. package/docs/concepts/pools-and-sources.md +230 -0
  19. package/docs/concepts/secrets-and-trust.md +222 -0
  20. package/docs/concepts/the-menu.md +378 -0
  21. package/docs/concepts/workspaces.md +678 -0
  22. package/docs/get-started/a-model-decides.md +563 -0
  23. package/docs/get-started/an-agent-in-a-sandbox.md +381 -0
  24. package/docs/get-started/connect-a-model.md +248 -0
  25. package/docs/get-started/create-an-instance.md +194 -0
  26. package/docs/get-started/have-your-assistant-write-a-machine.md +922 -0
  27. package/docs/get-started/install.md +126 -0
  28. package/docs/guides/add-tools-to-the-sandbox.md +295 -0
  29. package/docs/guides/answer-a-gate.md +304 -0
  30. package/docs/guides/bind-open-parts.md +223 -0
  31. package/docs/guides/cancel-a-run.md +146 -0
  32. package/docs/guides/clean-up.md +206 -0
  33. package/docs/guides/fix-a-failed-converge.md +361 -0
  34. package/docs/guides/give-the-cluster-git-credentials.md +222 -0
  35. package/docs/guides/iterate-on-a-machine-on-kind.md +190 -0
  36. package/docs/guides/let-an-agent-write-a-chart.md +226 -0
  37. package/docs/guides/look-inside-a-sandbox.md +174 -0
  38. package/docs/guides/mirror-the-kit-images.md +176 -0
  39. package/docs/guides/move-from-kind-to-a-shared-cluster.md +302 -0
  40. package/docs/guides/open-the-console.md +91 -0
  41. package/docs/guides/publish-a-machine-as-an-npm-package.md +350 -0
  42. package/docs/guides/push-and-sign-a-branch.md +486 -0
  43. package/docs/guides/run-a-chart-by-name.md +270 -0
  44. package/docs/guides/run-a-command-or-call-a-url.md +544 -0
  45. package/docs/guides/run-one-worker-per-item.md +321 -0
  46. package/docs/guides/set-an-agents-access-and-dials.md +375 -0
  47. package/docs/guides/size-a-workspace.md +204 -0
  48. package/docs/guides/start-a-run-from-a-run.md +237 -0
  49. package/docs/guides/start-a-run.md +207 -0
  50. package/docs/guides/upgrade-the-kit-version.md +240 -0
  51. package/docs/guides/use-a-hosted-provider.md +208 -0
  52. package/docs/guides/use-a-local-model.md +313 -0
  53. package/docs/guides/watch-a-run.md +326 -0
  54. package/docs/guides/write-a-workflow.md +318 -0
  55. package/docs/index.md +44 -0
  56. package/docs/llms.txt +216 -0
  57. package/docs/reference/authoring-surface/agent.md +128 -0
  58. package/docs/reference/authoring-surface/answer.md +140 -0
  59. package/docs/reference/authoring-surface/arg.md +60 -0
  60. package/docs/reference/authoring-surface/basic.md +62 -0
  61. package/docs/reference/authoring-surface/bearer.md +53 -0
  62. package/docs/reference/authoring-surface/chartschema.md +23 -0
  63. package/docs/reference/authoring-surface/chartschemafor.md +48 -0
  64. package/docs/reference/authoring-surface/command.md +191 -0
  65. package/docs/reference/authoring-surface/customize.md +64 -0
  66. package/docs/reference/authoring-surface/defineconfig.md +41 -0
  67. package/docs/reference/authoring-surface/defineevent.md +114 -0
  68. package/docs/reference/authoring-surface/dispatch.md +126 -0
  69. package/docs/reference/authoring-surface/fail.md +83 -0
  70. package/docs/reference/authoring-surface/file.md +122 -0
  71. package/docs/reference/authoring-surface/frompick.md +277 -0
  72. package/docs/reference/authoring-surface/http.md +159 -0
  73. package/docs/reference/authoring-surface/isfailure.md +58 -0
  74. package/docs/reference/authoring-surface/jr2setup.md +237 -0
  75. package/docs/reference/authoring-surface/open.md +74 -0
  76. package/docs/reference/authoring-surface/openbody.md +86 -0
  77. package/docs/reference/authoring-surface/paramsof.md +125 -0
  78. package/docs/reference/authoring-surface/pool.md +186 -0
  79. package/docs/reference/authoring-surface/push.md +112 -0
  80. package/docs/reference/authoring-surface/secret.md +118 -0
  81. package/docs/reference/authoring-surface/sentby.md +83 -0
  82. package/docs/reference/authoring-surface/source.md +51 -0
  83. package/docs/reference/authoring-surface/text.md +62 -0
  84. package/docs/reference/authoring-surface/type-agentdeclaration.md +27 -0
  85. package/docs/reference/authoring-surface/type-agentdefinition.md +80 -0
  86. package/docs/reference/authoring-surface/type-agentlogic.md +15 -0
  87. package/docs/reference/authoring-surface/type-agentslots.md +12 -0
  88. package/docs/reference/authoring-surface/type-answeraction.md +12 -0
  89. package/docs/reference/authoring-surface/type-arg.md +19 -0
  90. package/docs/reference/authoring-surface/type-argnames.md +12 -0
  91. package/docs/reference/authoring-surface/type-binding.md +62 -0
  92. package/docs/reference/authoring-surface/type-bindingof.md +12 -0
  93. package/docs/reference/authoring-surface/type-bodyacceptsdoor.md +15 -0
  94. package/docs/reference/authoring-surface/type-bodyacceptsslots.md +17 -0
  95. package/docs/reference/authoring-surface/type-ceilings.md +41 -0
  96. package/docs/reference/authoring-surface/type-chainlink.md +19 -0
  97. package/docs/reference/authoring-surface/type-chartaction.md +46 -0
  98. package/docs/reference/authoring-surface/type-chartagent.md +28 -0
  99. package/docs/reference/authoring-surface/type-chartevent.md +20 -0
  100. package/docs/reference/authoring-surface/type-chartguard.md +20 -0
  101. package/docs/reference/authoring-surface/type-chartinvoke.md +28 -0
  102. package/docs/reference/authoring-surface/type-chartjson.md +14 -0
  103. package/docs/reference/authoring-surface/type-chartstate.md +46 -0
  104. package/docs/reference/authoring-surface/type-charttext.md +40 -0
  105. package/docs/reference/authoring-surface/type-charttransition.md +22 -0
  106. package/docs/reference/authoring-surface/type-charttransitions.md +14 -0
  107. package/docs/reference/authoring-surface/type-chartvalue.md +15 -0
  108. package/docs/reference/authoring-surface/type-childat.md +12 -0
  109. package/docs/reference/authoring-surface/type-childslots.md +12 -0
  110. package/docs/reference/authoring-surface/type-commandenv.md +15 -0
  111. package/docs/reference/authoring-surface/type-commandinput.md +19 -0
  112. package/docs/reference/authoring-surface/type-commandoptions.md +78 -0
  113. package/docs/reference/authoring-surface/type-commandoutput.md +104 -0
  114. package/docs/reference/authoring-surface/type-customize.md +109 -0
  115. package/docs/reference/authoring-surface/type-dispatchdefinition.md +39 -0
  116. package/docs/reference/authoring-surface/type-dispatchinput.md +14 -0
  117. package/docs/reference/authoring-surface/type-dispatchopenparts.md +14 -0
  118. package/docs/reference/authoring-surface/type-dispatchoutput.md +65 -0
  119. package/docs/reference/authoring-surface/type-duration.md +15 -0
  120. package/docs/reference/authoring-surface/type-effectbinding.md +93 -0
  121. package/docs/reference/authoring-surface/type-effectlogic.md +12 -0
  122. package/docs/reference/authoring-surface/type-envopenparts.md +14 -0
  123. package/docs/reference/authoring-surface/type-eventaudience.md +27 -0
  124. package/docs/reference/authoring-surface/type-eventdef.md +45 -0
  125. package/docs/reference/authoring-surface/type-eventfrom.md +16 -0
  126. package/docs/reference/authoring-surface/type-eventsemantics.md +26 -0
  127. package/docs/reference/authoring-surface/type-execoutcome.md +34 -0
  128. package/docs/reference/authoring-surface/type-failslot.md +14 -0
  129. package/docs/reference/authoring-surface/type-failure.md +19 -0
  130. package/docs/reference/authoring-surface/type-failureinput.md +20 -0
  131. package/docs/reference/authoring-surface/type-filebody.md +41 -0
  132. package/docs/reference/authoring-surface/type-frominput.md +14 -0
  133. package/docs/reference/authoring-surface/type-hostinjectedinput.md +58 -0
  134. package/docs/reference/authoring-surface/type-httpdefinition.md +101 -0
  135. package/docs/reference/authoring-surface/type-httpinput.md +12 -0
  136. package/docs/reference/authoring-surface/type-httpmethod.md +14 -0
  137. package/docs/reference/authoring-surface/type-httpopenparts.md +12 -0
  138. package/docs/reference/authoring-surface/type-httpoutput.md +89 -0
  139. package/docs/reference/authoring-surface/type-httprequestparts.md +33 -0
  140. package/docs/reference/authoring-surface/type-jr2config.md +126 -0
  141. package/docs/reference/authoring-surface/type-lends.md +38 -0
  142. package/docs/reference/authoring-surface/type-logicat.md +12 -0
  143. package/docs/reference/authoring-surface/type-markedif.md +12 -0
  144. package/docs/reference/authoring-surface/type-mergedactors.md +12 -0
  145. package/docs/reference/authoring-surface/type-namedsecret.md +25 -0
  146. package/docs/reference/authoring-surface/type-noslotspec.md +26 -0
  147. package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +32 -0
  148. package/docs/reference/authoring-surface/type-openbodydeclaration.md +40 -0
  149. package/docs/reference/authoring-surface/type-openbodylogic.md +14 -0
  150. package/docs/reference/authoring-surface/type-openbodyoutput.md +42 -0
  151. package/docs/reference/authoring-surface/type-openbodyslot.md +18 -0
  152. package/docs/reference/authoring-surface/type-opencommandinput.md +17 -0
  153. package/docs/reference/authoring-surface/type-openeffect.md +12 -0
  154. package/docs/reference/authoring-surface/type-openeffectslots.md +12 -0
  155. package/docs/reference/authoring-surface/type-openof.md +12 -0
  156. package/docs/reference/authoring-surface/type-opensecret.md +23 -0
  157. package/docs/reference/authoring-surface/type-opensecretmark.md +12 -0
  158. package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +31 -0
  159. package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +32 -0
  160. package/docs/reference/authoring-surface/type-poolmachine.md +28 -0
  161. package/docs/reference/authoring-surface/type-poolspec.md +60 -0
  162. package/docs/reference/authoring-surface/type-pushinput.md +15 -0
  163. package/docs/reference/authoring-surface/type-pushoptions.md +79 -0
  164. package/docs/reference/authoring-surface/type-pushoutput.md +124 -0
  165. package/docs/reference/authoring-surface/type-pushwireoutput.md +12 -0
  166. package/docs/reference/authoring-surface/type-quantity.md +14 -0
  167. package/docs/reference/authoring-surface/type-reached.md +12 -0
  168. package/docs/reference/authoring-surface/type-reposlot.md +70 -0
  169. package/docs/reference/authoring-surface/type-reposlotsof.md +16 -0
  170. package/docs/reference/authoring-surface/type-sandboxoptions.md +77 -0
  171. package/docs/reference/authoring-surface/type-secret.md +25 -0
  172. package/docs/reference/authoring-surface/type-sender.md +19 -0
  173. package/docs/reference/authoring-surface/type-sentbyguards.md +12 -0
  174. package/docs/reference/authoring-surface/type-size.md +85 -0
  175. package/docs/reference/authoring-surface/type-slothandles.md +32 -0
  176. package/docs/reference/authoring-surface/type-sourcenextresult.md +17 -0
  177. package/docs/reference/authoring-surface/type-sourcespec.md +44 -0
  178. package/docs/reference/authoring-surface/type-syncstate.md +12 -0
  179. package/docs/reference/authoring-surface/type-text.md +19 -0
  180. package/docs/reference/authoring-surface/type-textpart.md +24 -0
  181. package/docs/reference/authoring-surface/type-thinkinglevel.md +39 -0
  182. package/docs/reference/authoring-surface/type-toparameterizedobject.md +14 -0
  183. package/docs/reference/authoring-surface/type-toprovidedactor.md +14 -0
  184. package/docs/reference/authoring-surface/type-unknown.md +21 -0
  185. package/docs/reference/authoring-surface/type-usercontainer.md +77 -0
  186. package/docs/reference/authoring-surface/type-withoutemptyindex.md +20 -0
  187. package/docs/reference/authoring-surface/type-workflowevent.md +46 -0
  188. package/docs/reference/authoring-surface/type-workspaceaccess.md +16 -0
  189. package/docs/reference/authoring-surface/type-workspaced.md +94 -0
  190. package/docs/reference/authoring-surface/type-workspacehandles.md +23 -0
  191. package/docs/reference/authoring-surface/type-workspacemachine.md +29 -0
  192. package/docs/reference/authoring-surface/type-workspaceof.md +13 -0
  193. package/docs/reference/authoring-surface/type-workspaceoptions.md +39 -0
  194. package/docs/reference/authoring-surface/type-workspacespec.md +37 -0
  195. package/docs/reference/authoring-surface/workspace.md +259 -0
  196. package/docs/reference/authoring-surface.md +197 -0
  197. package/docs/reference/cli.md +433 -0
  198. package/docs/reference/config.md +756 -0
  199. package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +113 -0
  200. package/docs/reference/decisions/0002-duplex-streaming-actor.md +110 -0
  201. package/docs/reference/decisions/0004-worktree-provisioning-model.md +124 -0
  202. package/docs/reference/decisions/0005-sandbox-pod-composition.md +230 -0
  203. package/docs/reference/decisions/0006-agent-control-surface.md +47 -0
  204. package/docs/reference/decisions/0007-durable-machine-state.md +109 -0
  205. package/docs/reference/decisions/0008-orchestrator-as-deployed-app.md +60 -0
  206. package/docs/reference/decisions/0009-cli-and-instance-interface.md +275 -0
  207. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +160 -0
  208. package/docs/reference/decisions/0011-workflow-defined-events.md +141 -0
  209. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +103 -0
  210. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +157 -0
  211. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +95 -0
  212. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +265 -0
  213. package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +124 -0
  214. package/docs/reference/decisions/0017-pool-over-a-source-port.md +69 -0
  215. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +111 -0
  216. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +275 -0
  217. package/docs/reference/decisions/0020-private-ca-trust-is-a-named-concept.md +60 -0
  218. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +100 -0
  219. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +103 -0
  220. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +176 -0
  221. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +439 -0
  222. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +65 -0
  223. package/docs/reference/decisions/0026-a-turn-that-is-over-is-never-prompted.md +62 -0
  224. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +237 -0
  225. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +89 -0
  226. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +154 -0
  227. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +100 -0
  228. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +190 -0
  229. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +61 -0
  230. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +127 -0
  231. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +57 -0
  232. package/docs/reference/decisions/0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md +92 -0
  233. package/docs/reference/decisions/0036-a-turn-compacts-its-own-context-mid-flight.md +138 -0
  234. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +196 -0
  235. package/docs/reference/decisions/0038-jr2-up-builds-every-image-it-deploys.md +170 -0
  236. package/docs/reference/decisions/0039-image-garbage-collects-by-reachability.md +115 -0
  237. package/docs/reference/decisions/0041-a-build-the-host-already-holds-is-not-spent-again.md +64 -0
  238. package/docs/reference/decisions/0042-ready-is-not-routable-an-admission-retries-its-connection.md +142 -0
  239. package/docs/reference/decisions/0043-the-kit-is-tested-as-installed-a-local-registry-stands-in-for-npm.md +170 -0
  240. package/docs/reference/decisions/0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md +85 -0
  241. package/docs/reference/decisions/0045-the-platform-joins-the-image-address-and-the-cluster-chooses-it.md +82 -0
  242. package/docs/reference/decisions/0046-a-converge-failure-carries-its-diagnosis.md +34 -0
  243. package/docs/reference/decisions/0047-the-git-ssh-key-source-is-the-users-choice.md +47 -0
  244. package/docs/reference/decisions/0048-the-orchestrator-boots-without-its-repos.md +36 -0
  245. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +126 -0
  246. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +73 -0
  247. package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +227 -0
  248. package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +81 -0
  249. package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +111 -0
  250. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +126 -0
  251. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +107 -0
  252. package/docs/reference/decisions/0056-a-global-jr2-hands-off-to-the-instances-own-and-an-instance-has-one-kit-version.md +62 -0
  253. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +152 -0
  254. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +106 -0
  255. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +384 -0
  256. package/docs/reference/decisions/0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md +111 -0
  257. package/docs/reference/decisions/0061-a-memory-kill-ends-the-agents-processes-not-the-harness.md +83 -0
  258. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +212 -0
  259. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +83 -0
  260. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +79 -0
  261. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +65 -0
  262. package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +550 -0
  263. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +430 -0
  264. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +464 -0
  265. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +180 -0
  266. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +323 -0
  267. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +183 -0
  268. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +144 -0
  269. package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +122 -0
  270. package/docs/reference/decisions/0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md +76 -0
  271. package/docs/reference/decisions.md +83 -0
  272. package/docs/reference/glossary.md +648 -0
  273. package/docs/reference/http-api.md +1348 -0
  274. package/docs/reference/machines.md +190 -0
  275. package/docs/reference/mechanism-events.md +72 -0
  276. package/docs/why.md +206 -0
  277. package/package.json +5 -3
  278. package/src/actor.ts +541 -79
  279. package/src/agent.ts +114 -38
  280. package/src/ambient.ts +2 -1
  281. package/src/assembly-checks.ts +189 -7
  282. package/src/cause.ts +234 -0
  283. package/src/chart.ts +169 -37
  284. package/src/charts.ts +34 -6
  285. package/src/config.ts +430 -178
  286. package/src/custodian.ts +74 -22
  287. package/src/customize.ts +188 -46
  288. package/src/dispatch-effect.ts +945 -0
  289. package/src/door-compat.ts +217 -0
  290. package/src/durability.ts +11 -4
  291. package/src/effect.ts +341 -44
  292. package/src/effector-wire.ts +160 -11
  293. package/src/fail.ts +209 -0
  294. package/src/harness-client.ts +541 -47
  295. package/src/http-effect.ts +495 -53
  296. package/src/http.ts +427 -16
  297. package/src/index.ts +60 -82
  298. package/src/instance.ts +68 -8
  299. package/src/internal.ts +84 -0
  300. package/src/machine-doc.ts +21 -3
  301. package/src/mechanism-routes.ts +168 -0
  302. package/src/model-keys.generated.ts +53 -0
  303. package/src/model-keys.ts +18 -58
  304. package/src/notices.ts +66 -13
  305. package/src/open-body.ts +159 -23
  306. package/src/open-effect.ts +81 -8
  307. package/src/open.ts +59 -6
  308. package/src/parts.ts +184 -13
  309. package/src/party.ts +68 -2
  310. package/src/pick.ts +463 -11
  311. package/src/pool.ts +247 -36
  312. package/src/registration.ts +83 -4
  313. package/src/repos.ts +2 -1
  314. package/src/run-host.ts +565 -31
  315. package/src/run-output.ts +38 -0
  316. package/src/sandbox-effect.ts +273 -38
  317. package/src/sandbox-kube.ts +3 -1
  318. package/src/secret-placement.ts +52 -17
  319. package/src/server.ts +3 -0
  320. package/src/setup.ts +239 -56
  321. package/src/size.ts +121 -9
  322. package/src/snapshot-store.ts +54 -10
  323. package/src/stub-harness.ts +44 -16
  324. package/src/timers.ts +14 -1
  325. package/src/vocabulary.ts +69 -14
  326. package/src/wire.ts +90 -16
  327. package/src/workspace.ts +697 -175
@@ -0,0 +1,190 @@
1
+ # Menu-only Agents run on the Instance Harness
2
+
3
+ A workflow should be able to run an Agent whose whole job is a decision — read some inputs, pick the next event — with
4
+ no Workspace, no worktree, and no per-run pod: the statelyai/agent shape, at conversational latency, on any configured
5
+ model. jr2's Menu already _is_ that decision surface (the invoking state derives what the Agent may say —
6
+ [ADR-0015](0015-authoring-surface-absorbs-the-mechanism.md)/[ADR-0029](0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md)),
7
+ and the mechanism was already Sandbox-agnostic: `agentRun` needs only an endpoint URL, a registration may carry no
8
+ Sandbox (`tokens.ts`), and the stub Harness proved a Harness is "only a different URL". What was missing was the
9
+ hosting: the only real Harness anywhere was the one inside a Sandbox pod, so a Turn without a `workspace()` had nowhere
10
+ to run. Adopting statelyai/agent instead was rejected outright — it would be a second, weaker agent mechanism (no
11
+ Submission/Admission ledger, no re-attach, no Settlement, a menu that is not guard-narrowed) beside the one jr2 already
12
+ built.
13
+
14
+ ## Decision
15
+
16
+ - **One Instance Harness per instance, deployed by convention.** `jr2 up` converges a Harness StatefulSet + Service —
17
+ stock `jr2-harness:<kitversion>` image, same wire — whenever any Agent slot carried by a registered Machine
18
+ (ADR-0049's walk) declares `workspace: "none"`
19
+ ([ADR-0028](0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md)). The scan is static and
20
+ definition-level, deliberately not workflow-level (workflow internals are not statically recoverable — the same line
21
+ ADR-0018 drew for model preflight); a declared-but-never-invoked `"none"` Agent over-deploys, erring toward "the
22
+ convention works when you need it". No config key names, sizes, addresses, or enables it.
23
+ - **Placement is definition-wins, not nearest-wins.** A `workspace: "none"` Agent's Turn runs on the Instance Harness
24
+ _always_ — even invoked from inside a `workspace()`. It is ADR-0028's thesis extended one step: an Agent that touches
25
+ no Workspace has no reason to run in one, and a Sandbox — torn down at its Workspace's final state, replaced after an
26
+ eviction — is the wrong home for a conversation whose definition says it needs no Workspace. A conversation is an
27
+ Instance ID _on one Harness_ (the server holds the history), so placement must be a function of the definition, not of
28
+ where an invocation happens to sit; nearest-wins would make the Harness that holds a `"none"` conversation an accident
29
+ of authoring. Definition-wins also makes "which pod's logs" a function of the Agent alone.
30
+ - **The cohesive per-run log is solved by projection, not placement.** The cost of definition-wins — decisions vanishing
31
+ from the Workspace pod's log — is repaid by [ADR-0023](0023-the-harness-prints-the-conversation.md)'s echo: the
32
+ enclosing Workspace's Harness prints the run's narrative (admission + pick markers for remotely-hosted Turns, Emits,
33
+ the backfilled preamble). Instance Harness log = every `"none"` conversation, instance-wide, interleaved (the less
34
+ useful top-to-bottom read, accepted); Workspace log = everything that ran there plus the narrative of the run that
35
+ owns it.
36
+ - **The Instance Harness pod is Harness + Custodian — the one Harness shape, minus the Workspace.** No user container,
37
+ no `/work` volume, no attach step. The Custodian stays although `"none"` Agents cannot execute code (the
38
+ [ADR-0013](0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md) rationale technically lapses):
39
+ the Harness has exactly one route to its Menu — the Custodian on `localhost` — and forking that path for one pod buys
40
+ a divergence in the component this ADR keeps deliberately uniform. The pod is composed by the same function a
41
+ Sandbox's is ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)), so its Custodian
42
+ carries the Instance Harness's model calls the same way. It holds only the model keys of the `"none"` Agents it hosts
43
+ — the keys their providers read — and none of the held secrets a `workspace()` lists: a `"none"` Agent cannot execute
44
+ code, so it had no way to use the rest. Its held objects are its own (`held-instance.json`,
45
+ `jr2-held-secrets-instance`). Defense-in-depth is the bonus, not the reason.
46
+ - **The Instance Effector is the second Instance-level pod, beside it**
47
+ ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)), on the same convention: `jr2 up`
48
+ converges it when a registered Machine places an Effect there — an `http` Effect — and deletes it otherwise. It runs
49
+ the same `jr2-harness` image with `/opt/jr2/bin/effector` as its command, holds no conversation and runs no Agent, and
50
+ is a Deployment, not a StatefulSet: it keeps nothing, and a rollout must run the new pod beside the old one. No config
51
+ key names, sizes, addresses, or enables it.
52
+ - **No image config at all.** The Harness image was once configured as `sandbox.image` because the Sandbox pod was the
53
+ only place a Harness ran — a misnomer once the Instance Harness exists, and config is the wrong seat regardless:
54
+ `jr2 up` builds and resolves every image it deploys ([ADR-0038](0038-jr2-up-builds-every-image-it-deploys.md)), and
55
+ the per-Workspace images (Sandbox Image, User Container) are static `workspace()` options carried by the Machine
56
+ itself ([ADR-0049](0049-a-machine-carries-its-parts-and-composes-by-invoke.md),
57
+ [ADR-0037](0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md),
58
+ [ADR-0005](0005-sandbox-pod-composition.md)). `sandbox.image`, the per-component image keys, and `operator.image`
59
+ dissolve; the `sandbox` config section disappears until something genuinely pod-shaped and user-tunable exists.
60
+
61
+ ## The Instance Harness holds a conversation as long as its run, and frees the rest
62
+
63
+ The scaling review (R11, 2026-09-30) found the `conversations` map with no delete path: every Menu-only Agent instance
64
+ of every run stayed until the pod restarted, and the restart lost every conversation at once. The research that preceded
65
+ it checked Agent Substrate's own model against the primary sources: an actor is stateful, the platform snapshots its
66
+ memory and its `DurableDir` volumes and resumes it on any worker, and its control plane holds metadata and snapshot
67
+ pointers only; memory snapshots are tied to one template version, so the state with a documented path across a code
68
+ change is a volume the actor reloads on a cold boot. These decisions take that shape.
69
+
70
+ - **A conversation is persisted to a directory, and rebuilt from it on boot.** The Harness writes each conversation as
71
+ it goes: jr2's own record — the stream log, the Settlements, the history messages, in the wire's shapes — plus the
72
+ engine part, in a folder the engine names (`engine/<name>`), which the record does not read. Today it is jr2's
73
+ transcript (ADR-0027): `engine/pi/transcript.jsonl`, the pi-ai messages the model read and each Compaction. Rebuilding
74
+ reads jr2's record first; an engine that cannot read the engine part (another engine, another version) continues from
75
+ the history messages. The record is the contract; the engine part is not. A Submission in flight at the restart
76
+ settles `failed` (the typed error `harness_restarted`, which the Agent's Actor reads as "Turn lost") on the rebuilt
77
+ stream, so the Orchestrator reads a Settlement, not a 404, and the conversation's next Turn continues it.
78
+ - **A restart while the Agent's state is busy cuts the Turn and is no fault.** When the Agent's state is not at Rest —
79
+ its pick is outstanding, or work it started in runs below it — or its pick's answer was read and never reached the
80
+ model, the Agent's Actor raises no `agent.fault` for a restart: the Machine would route it and stop the work the Turn
81
+ did not want stopped (ADR-0024). It waits for Rest, then re-admits the Agent on the same conversation, which the
82
+ rebuilt Harness holds: a jr2 prompt ("Your previous turn was cut: the process that ran you restarted while the
83
+ workflow was busy. The workflow did not stop. Continue your task from here.") with the answer as a `pick-answer`
84
+ notice (`cut: true`) in the Briefing's Turn part, and the Allowed picks after it (ADR-0062). The re-admission asks the
85
+ rebuilt Harness for the conversation first, as a continued Turn does: one it cannot read gets a `conversation-new`
86
+ notice ahead of the answer, and the notices pending for the Turn ride along. In the rebuilt engine part the cut Menu
87
+ call has no result; the Harness answers it in the history with jr2's text — the pick reached the workflow, which does
88
+ not run it twice, and its result comes in the next message — before pi-ai would answer "No result provided", which
89
+ reads as a failed pick. That text prints once in the rebuilt Harness's log, before its first prompt (ADR-0023). The
90
+ engine runs a model answer's calls in order
91
+ ([ADR-0027](0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md)). So only the first call with no
92
+ result can have been out. Every call after it never started, a Working-tool call too. Each one gets jr2's text "This
93
+ call did not run: your turn ended before it started.", as an error result. A Menu call that an abort cut while the
94
+ model was still writing it (the abort lands mid-stream) did not reach the workflow: the engine runs no call of a
95
+ message the model did not finish. The Harness tells the two cuts apart by whether the call was delivered, read before
96
+ it re-tags an aborted message so that pi-ai replays it: a call in a finished message keeps the text above, and a call
97
+ in an aborted message gets jr2's text in the same form — the Turn was cut while the model was still writing this call,
98
+ it did not reach the workflow, nothing ran, and the model may call it again if the newest "Allowed now" still lists
99
+ it. That text prints once too, before the conversation's next prompt, in the same process or the next. The Harness
100
+ reads whether a call was delivered off its message, not off a record of the POST, and that is the limit of the test: a
101
+ SIGKILL of the Harness after the model finished the message and before the pick's POST reached the Orchestrator leaves
102
+ a call that never reached the workflow, and the rebuilt Harness answers it with the restart's text. Closing that
103
+ window needs a delivery record written before the POST, which is not built. A Menu call in a message that a provider
104
+ error ended mid-stream (stopReason `error`) is neither cut: pi runs no call of such a message (pi-agent-core 1.4.0
105
+ `agent-loop.js` line 148 returns after a message that ended `error` or `aborted`) and drops it from every later
106
+ request (pi-ai 1.4.0 `transform-messages.js` lines 172 to 180), so the Harness does not re-tag it and adds no answer
107
+ for its call: an answer would be a tool result with no call before it, which the providers refuse. The pick did not
108
+ reach the workflow, the Submission settles `failed` as any provider error does, and on the next request the model
109
+ reads no trace of the call and is free to make it again. A poll takes the answer only while its connection lives: the
110
+ killed Harness's last poll, still waiting at the Orchestrator, ends unanswered when its connection closes, so an
111
+ answer that lands after the kill, and before the rebuilt stream reports the restart, is still unread. The same holds
112
+ for a Sandbox's Harness, whose restart the watch sees (ADR-0021). A restart at Rest with nothing unread is the fault
113
+ it always was. Only the Turn's OWN pick counts as unread: a pick of an earlier Turn on the same continued address,
114
+ whose route left the Agent's state, was answered as a notice to this Turn, and a restart at Rest in this Turn is still
115
+ the fault. A route that leaves the Agent's state ends the Turn while it waits, as any route does.
116
+ - **Both placements persist, to different volumes.** The Instance Harness to a PersistentVolumeClaim of its own; a
117
+ Sandbox's Harness to an emptyDir mounted into the Harness container alone (the User Container must not read it), so a
118
+ Harness container that restarts inside a living pod keeps its conversations. A pod that is gone takes the emptyDir
119
+ with it — that Workspace is `Lost` (ADR-0021), and no conversation persistence should say otherwise.
120
+ - **The Instance Harness is a StatefulSet of one, with a per-ordinal volume.** The value is the stable volume; the
121
+ stable name is what N > 1 would pin a conversation to (the admission ledger already records its endpoint). N is a kit
122
+ value, 1 until a measured need; never an HPA, which would kill conversations on a scale-down and land a continuation
123
+ on a replica that never saw it. A rollout at N = 1 has no surge, so admissions are refused while the new pod starts;
124
+ the Orchestrator already absorbs an admission refused at the connection (ADR-0042), so a rollout delays a Turn and
125
+ faults nothing.
126
+ - **A rollout drains Turns.** On SIGTERM the Harness stops admitting, finishes the Turns it holds (the queued ones too),
127
+ then exits; the termination grace is a Turn's worst case. A Turn still generating or running a tool keeps that
128
+ graceful wait. A Turn whose only pending work is an outstanding pick's poll — whatever the pick waits on: a Gate, an
129
+ Effect, a child Machine — is cut at once, or as soon as it comes to that: its answer can wait on a human (a Gate),
130
+ often the one who is stopping the Harness, so no grace period would help. The cut drops the poll unanswered (the
131
+ answer stays unread at the Orchestrator), leaves the Menu call without a result, asks the model nothing more, and
132
+ settles the Submission `failed`, `harness_restarted`, with everything queued behind it. To the Orchestrator it is a
133
+ restart while the Agent's state is busy (the bullet above): the pick stays outstanding, and once the workflow is back
134
+ at Rest jr2 re-admits the Agent on the same conversation with the answer, and the replacement Harness answers the cut
135
+ call with jr2's text. It refuses an admission with a 503 before it reads or queues anything, and closes that
136
+ connection, so the Orchestrator re-sends it on ADR-0042's ladder and the re-send dials the Service anew. Readiness
137
+ stays up (the probe is the Harness's own `/healthz`, which it keeps answering), and the dialed Service is a ClusterIP
138
+ one, which kube-proxy still routes to a terminating pod that serves while no other is ready — so reads keep answering
139
+ and a `wait` on the draining pod still reads its Settlement. A headless name would not: DNS stops naming a terminating
140
+ pod, so the StatefulSet's headless governing Service is a second one, dialed by no one. The Custodian is a native
141
+ sidecar, stopped only after the Harness exits, so the drain keeps its Menu and its model to the end. A deploy loses no
142
+ Turn and, with the volume, no conversation — as long as the drain and the replacement's start fit ADR-0042's window; a
143
+ longer one faults the waiting admission with the Harness's own 503 words.
144
+ - **The Orchestrator states the live set; the Harness frees the rest.** After every restore and every 5 minutes (the
145
+ Lease's cadence) the Orchestrator tells the Instance Harness which conversations its live runs hold — from the
146
+ admission ledgers in memory, never from the store — and the Harness frees memory and files of every other one.
147
+ Level-triggered, idempotent, a missed statement costs nothing, and a crashed Orchestrator's leftovers go at its next
148
+ boot. No idle TTL: a run parked on a Gate for a day keeps its conversation, and nothing else can decide that. A
149
+ Sandbox's Harness is not told; its Workspace's teardown frees everything.
150
+ - **The Orchestrator keeps holding handles only** (ADR-0007): no conversation state crosses to it. Storing sessions
151
+ centrally and re-sending them to a fresh pod was considered and rejected — it is the design Substrate does not have,
152
+ and it puts every Menu-only Turn's history on the single writer's path.
153
+
154
+ ## Considered options
155
+
156
+ - **statelyai/agent (or any in-process decision actor).** Rejected above; the long form: every ADR from 0011 to 0029
157
+ would stop being the single story of how models drive Machines, and the copy would be the lesser one — its picks
158
+ bypass the Menu's guard-narrowing, its runs bypass the admission ledger, its failures bypass Settlement.
159
+ - **Unconditional ambient fallback** (no enclosing `workspace()` → Instance Harness, for every Agent). Rejected: a coder
160
+ invoked outside its `workspace()` by mistake would run bash in the shared pod instead of failing loudly — precisely
161
+ the mistake today's error catches. The fallback is gated on `workspace: "none"`; everything else keeps the loud error,
162
+ now sharper ("agent `x` has `workspace: "write"` — invoke it inside a `workspace()`").
163
+ - **Nearest-wins placement** (a `"none"` Agent invoked inside a `workspace()` runs on that Workspace's Harness).
164
+ Genuinely attractive — the Workspace pod log stays the one coherent story with zero new mechanism — and rejected on
165
+ the continuation amnesia above plus three lesser costs: decisioner turns die with workspace churn, share limits with
166
+ builds, and scatter one Agent's history across pods. The log story it bought is recovered by ADR-0023's echo.
167
+ - **On-demand pod per workspace-less run.** Rejected: pays pod-start latency on the path whose whole point is a fast
168
+ pick.
169
+ - **Hosting the Turn in the Orchestrator process.** Rejected: no pod logs, and it crosses the line the architecture
170
+ draws hardest — the Orchestrator holds handles; models act remotely.
171
+ - **A host-local dev Harness.** Considered as a dev story and dropped when the premise died:
172
+ [ADR-0019](0019-one-converging-command-against-the-current-context.md) removed `jr2 dev` — there is no host dev mode
173
+ to serve. Local development of a decisioning workflow is `jr2 up` against kind, where the Instance Harness converges
174
+ like everything else; an instructions tweak rides the next admission, no image build. The stub Harness keeps its one
175
+ job: a test fixture reached by explicit `endpoint`.
176
+
177
+ ## Consequences
178
+
179
+ - **The Agent's resolution grows one arm**: explicit `endpoint` (tests) → `workspace: "none"` → Instance Harness
180
+ (deterministic Service DNS) → enclosing `workspace()`'s ambient handles → loud error naming the definition's
181
+ `workspace` value.
182
+ - **A `"none"` conversation survives the Instance Harness pod's restart**: the StatefulSet restores the endpoint and the
183
+ volume restores the history. A Turn in flight at the restart settles `failed`; nothing is continued silently — a Turn
184
+ whose state is busy is re-admitted with a prompt that says it was cut.
185
+ - **An instance with no `"none"` definitions deploys nothing new.** The feature is invisible until the first
186
+ `agent({ ..., workspace: "none" })` a Machine carries, which is its entire user-facing surface.
187
+ - **The harness conformance suite (ADR-0027) gains the Menu-only shape**: a Turn whose definition withholds every
188
+ Working tool, settled by pick alone — no infra, same scripted provider.
189
+ - **GLOSSARY.md**: Harness's "inside a Sandbox" loosens to name the second placement; **Instance Harness** becomes a
190
+ glossary term.
@@ -0,0 +1,61 @@
1
+ # The Console unlocks with the Instance token, and the bands do not move
2
+
3
+ [ADR-0014](0014-observation-is-open-run-state-is-not.md) built the three-band HTTP surface on one premise: the
4
+ visualizer page is a browser, and a browser holds no token — so it got a projection instead of a credential, and "give
5
+ the page a token" was explicitly rejected. That page is now growing into the **Console**: it starts runs and answers
6
+ Gates, both of which are writes on the guarded surface. This ADR records how a browser earns those writes without moving
7
+ a single band — and it can stay this small because of a fact ADR-0014 did not lean on: the Instance token is already one
8
+ `kubectl get secret` away for exactly the audience the Console serves. There is no new distribution problem, only a new
9
+ presentation surface.
10
+
11
+ ## Decision
12
+
13
+ - **The human presents the token; the page never ships with one.** The Console's nav carries a password field. Empty,
14
+ the page is precisely ADR-0014's observer: open structure and observation, no control rendered. Filled, the page sends
15
+ `Authorization: Bearer` on the guarded surface — which unlocks both directions at once: the writes
16
+ (`POST /workflows/:name/runs`, `POST /runs/:id/gates/:gate/events`) and the `instanceOnly` reads the control UI needs
17
+ anyway (`GET /runs`, and `GET /runs/:id` — where `GateView` lives). The page as _served_ stays open and inert;
18
+ ADR-0014's rejection stands, because what it rejected was baking the credential into the page.
19
+ - **The token lives in `sessionStorage`** — survives a reload, dies with the tab, never a cookie (no ambient credential,
20
+ no CSRF surface). It is validated on entry with a cheap guarded read so the field shows live/invalid immediately; any
21
+ later 401 drops the UI back to observer mode rather than failing per-widget.
22
+ - **Gate discovery is a frame-triggered guarded read, not an open-band extension.** A Gate exists exactly while its
23
+ invoking state is entered (ADR-0011), and every entry/exit lands a status frame on the open workflow feed (ADR-0022) —
24
+ so on each frame, a token-holding Console re-fetches `GET /runs/:id` for the affected run and folds `gates` into its
25
+ store. Perfectly synchronized, no polling loop, and the observation projection is untouched. A delivery's success is
26
+ likewise confirmed by the next frame emptying the card, never by local bookkeeping.
27
+ - **Page addresses content-negotiate; JSON is the default dialect.** `/` and `/workflows/:name` serve the Console shell
28
+ to `Accept: text/html` and JSON to everything else — so the workflow-detail resource keeps its natural address (the
29
+ run-input schema's home, [ADR-0033](0033-a-machine-declares-the-input-that-starts-it.md)) and no future page has to
30
+ squat on a mangled sibling path. Nothing existing moves: EventSource asks `text/event-stream`, and the Harness's Menu
31
+ and the CLI never ask for HTML. `/viz/*` is deleted (greenfield, no redirects); assets move to `/assets/*`, which also
32
+ retires the "a workflow named `assets` is shadowed" hazard.
33
+
34
+ ## Considered options
35
+
36
+ - **`jr2 ui` — the CLI opens the browser through an authenticated local proxy.** A convenience layer worth building _on
37
+ top of_ the token field later, not a substitute: it has no answer for the deployed jr2 Application behind an ingress,
38
+ where the browser talks to the Service directly and the CLI is not in the path.
39
+ - **A fourth, scoped credential** ("may answer gates, may not cancel or read context"). Still rejected, as in ADR-0014 —
40
+ but the calculus has narrowed: it becomes worth writing only when gate-answering is delegated to people who must _not_
41
+ hold cluster RBAC. That is a real future (an approval inbox for non-operators); it is not this one.
42
+ - **Announce gates on the open observation feed** (even just `{runId, gate}`). Rejected: it widens the projection for a
43
+ convenience the token already solves, and the open band has an unresolved exposure question as it stands (spawn ids —
44
+ the ADR-0014 open call). The projection stays the one place the line is drawn.
45
+ - **A `/ui` path prefix, or moving the API under `/api`.** Rejected for negotiation: the first is ceremony on the
46
+ address a human types, the second churns the Harness's Menu, the Custodian's route allowlist, the CLI, and the
47
+ ADR-0009/0013 route tables for cosmetics.
48
+
49
+ ## Consequences
50
+
51
+ - **Zero new server surface except negotiation.** No new token, no new band, no new route class; `auth.test.ts`'s
52
+ boundary assertions are untouched. The server-side diff is: negotiate two addresses, serve the shell, retire `/viz/*`.
53
+ - **Possession remains cluster RBAC.** Whoever can read the instance Secret could already do everything the Console
54
+ unlocks; the field changes where the token is typed, not who holds it.
55
+ - **The XSS caveat is the real one and is bounded by what the page is:** a token in `sessionStorage` is readable by
56
+ scripts on the page, so the Console must keep serving only its own assets — no CDN, no third-party script, exactly the
57
+ constraint the viz page already lives under.
58
+ - **GLOSSARY.md**: **Console** becomes a glossary term (visualizer, dashboard, UI, viz on its Avoid list).
59
+ - **The Console's UX gets a browser tier** — `@console`-tagged Cucumber scenarios holding a Playwright page, in
60
+ `@jr2/e2e`, excluded from the default profile ([ADR-0010](0010-bdd-acceptance-tests.md) as amended). What the page
61
+ _believes_ stays unit-tested in `console-store.test.ts`; the browser tier asserts what a user does and sees.
@@ -0,0 +1,127 @@
1
+ # A Machine declares the input that starts it
2
+
3
+ `POST /workflows/:name/runs` takes a JSON body and hands it to the machine as xstate `input` — unvalidated, and
4
+ undescribed: the module contract is `export const machine` alone (ADR-0011/0015), so nothing at runtime says what a run
5
+ of a given workflow should be started _with_. The Console's start form
6
+ ([ADR-0032](0032-the-console-unlocks-with-the-instance-token.md)) forced the question, but the gap predates it —
7
+ `jr2 run` has the same blindness, and a typo'd input surfaces as a confusing mid-run failure instead of a refusal at the
8
+ door. Meanwhile every event a machine _accepts_ already carries a zod schema, attached to the machine itself
9
+ ([ADR-0015](0015-authoring-surface-absorbs-the-mechanism.md)'s vocabulary-on-the-machine). The input that starts a
10
+ machine is the one piece of its vocabulary that pattern missed.
11
+
12
+ ## Decision
13
+
14
+ - **The schema rides the machine, like the rest of the vocabulary.** `jr2Setup.createMachine` accepts an `input` (a zod
15
+ object) and attaches it the same way event defs ride today (WeakMap-keyed on `machine.config`, so a `.provide()` clone
16
+ keeps it — ADR-0011). `input` is deliberately xstate's own word for what a machine receives at creation — the
17
+ authoring surface teaches nothing new.
18
+ - **A door that no one reads is refused.** The `setup()` of xstate keeps an `input` in the config of `createMachine`,
19
+ but it does not read it. TypeScript accepts that `input`. Thus a Machine that `setup()` made and that has a zod
20
+ `input` has no door. A run starts with any input, and the Console start form is only a field for JSON. `jr2 up` and
21
+ the publish door use the same walk (`doorRefusals`, ADR-0069). The walk refuses a registered Machine if its config has
22
+ a zod `input` and no door is attached to it. The refusal line gives the fix: make the Machine with `jr2Setup`. If the
23
+ Machine takes no workflow event, leave out `events` (ADR-0015). The walk tests the attached door, not the factory.
24
+ Thus a `workspace()` or `pool()` wrapper with its own door passes. A `.provide()` clone keeps the config of its
25
+ original, so the walk gets the same result for the clone. A Machine with no `input`, or with an `input` that is not
26
+ zod, can use `setup()`. The `ping` Workflow that `jr2 init` writes uses it. The walk checks the registered roots only,
27
+ because the `input` of a nested Machine is never the door (see below). `workspace()` refuses a body with a zod `input`
28
+ when you call it. It reads the config and the attached door, so it also refuses a body that `setup()` made. Considered
29
+ and rejected:
30
+ - Refuse all `setup()` Machines. This adds work to the first Workflow and gives nothing.
31
+ - Refuse at boot. Then an Assembly that is already published stops the Orchestrator. The walk refuses only the next
32
+ publish.
33
+ - Take a zod `input` on a `setup()` Machine as the door at load. The loader can read that `input`. But then there are
34
+ two ways to declare one door. Also, the zod schema stays in `machine.config`, and a schema must not go into the
35
+ fingerprint and serialization paths that read the config. And that Machine does not get the Vocabulary checks of
36
+ `jr2Setup`.
37
+ - **A wrapper declares its own door; it never borrows its child's.** `workspace(body, { input, spec })` and
38
+ `pool(worker, { input, … })` each take an `input` of their own, and neither propagates one upward. The reason is the
39
+ same in both, at different strengths: a wrapper does not feed its child what a caller sent. `pool` is the sharp case —
40
+ a worker is fed per-item, never the run body, so propagating its schema would demand fields the pool never passes and
41
+ strip the pool-level fields `cap`/`itemInput` read. `workspace` is the mild one — its body gets the run input _plus_
42
+ the injected `workspace` handles, so the body's contract is the door PLUS a field no caller can send (the handles do
43
+ not exist until a Sandbox is provisioned and attached). Propagating it would make `createMachine({ input })` mean "my
44
+ input" on a bare machine and "my input minus what my wrapper injects" on a wrapped body — a context-dependent
45
+ contract, and a trap: a body that declared its input honestly, handles included, would 400 every valid run start. The
46
+ vocabulary does not propagate either — event names resolve against the Machine that invoked the actor, so a body keeps
47
+ its own defs and nothing is merged upward (ADR-0011, ADR-0049). The door is one more part a Machine carries and scopes
48
+ to itself. The composition the body needs a name for is exported instead:
49
+ `Workspaced<T, Slot> = WithoutEmptyIndex<T> & { workspace: WorkspaceHandles<Slot> }`, the handles keyed by the Repo
50
+ Slots the body names (ADR-0050, ADR-0051). A door with no fields gets the same name: `z.infer` of `z.object({})` is
51
+ `Record<string, never>`, an index signature that says each key holds `never`, and intersected with the handles it
52
+ makes each `assign` to the body's context a type error that does not name the cause. So `Workspaced` removes a string
53
+ index signature whose value is `never`, and only that: named fields beside it stay (`HostInjectedInput & …` keeps
54
+ `instanceId`), and an input that is a real map is not changed. A separate name for the empty door was the alternative;
55
+ it was rejected because the author then chooses between two names for one statement, and finds the second only after
56
+ the opaque error. A body that _still_ declares a schema is refused by `workspace()` with a message naming the fix:
57
+ nothing would ever serve or validate it, and a contract nobody enforces is worse than none.
58
+ - **The door types the mapping, and constrains the body.** `WorkspaceOptions.spec` reads its `input` from the declared
59
+ schema (`z.infer`), so a workflow states its shape once and the workspace mapping is checked against it — the drift
60
+ this ADR exists to close, closed in the one place that used to re-declare the shape by hand. The same schema checks
61
+ the _body_, in **one direction only**: the body may not demand more than the wrapper will hand it, which is
62
+ `Workspaced<door, slots>`; demanding _less_ is safe, because it is fed a superset. So a body reading a field the door
63
+ never carries is a build error at the `workspace()` call instead of an `undefined` mid-run. The guard counts the raw
64
+ door as provided beside `Workspaced<door, slots>`: for the door `z.object({})` the two differ, and a body that writes
65
+ `z.infer<typeof door>`, alone or with the handles, stays legal. Comparing against `Workspaced` alone was the
66
+ alternative; it was rejected because it refused those bodies, which compiled before `Workspaced` removed the empty
67
+ index signature. One field sits outside that check, named as `HostInjectedInput`: the host adds `instanceId` to the
68
+ input of the machine a run _starts_ with, so a root-placed wrapper hands its body the door, the handles, _and_ that —
69
+ while a nested one does not, and no type can see which. It is not door material either (no caller sends it, `start`
70
+ overwrites it after the parse, nothing serves it), so widening the door would be the wrong fix. The guard takes the
71
+ permissive answer: it counts those keys as _provided_, so a body may declare the field honestly. Counting them as
72
+ provided rather than subtracting them from what the body demands is deliberate — subtraction would drop the keys from
73
+ the comparison and with them two cases the check owns: a body that mistypes `instanceId`, and a body whose input is a
74
+ union, which subtraction compares on its members' shared keys alone. The typed wrapper also checks the seam above it —
75
+ a parent invoking a nested `workspace()` has its input mapper checked against that wrapper's door, and gets the body's
76
+ output typed back. With no schema declared the door is permissive, so there is nothing to infer and the mapper's
77
+ argument is `unknown` — an honest "jr2 does not know", not `any`; a wrapper fed by something other than a caller (a
78
+ pool worker) states what it is fed by annotating the parameter, as `PoolSpec.cap`/`itemInput` do. `spec` takes the
79
+ same permissive answer as the guard: its `input` is the door plus `HostInjectedInput`, so a `spec` gives each run a
80
+ branch from `instanceId` with no cast, and a nested wrapper whose `spec` reads it needs a parent that gives it.
81
+ `Partial<HostInjectedInput>` was the alternative; it was rejected because each root `spec` would then write a fallback
82
+ for a value that is always there. The input of the wrapper stays the door alone, so no parent must send `instanceId`.
83
+ For the door `z.object({})` that input is `{}`, the door without its `Record<string, never>`, so a parent can give
84
+ `instanceId` to a nested `spec` that reads it; with the raw `z.infer` it could not, and the nested Workspace then
85
+ pushed each run to `fix/undefined`. `z.infer<…> & Partial<HostInjectedInput>` was the alternative; it was rejected
86
+ because the index signature makes `instanceId` `never` there too. A per-run Repo Slot's mapper keeps the door alone,
87
+ because a url does not come from a run's id.
88
+ - **The schema is structure, so it is open.** Workflow-detail JSON (`GET /workflows/:name`, the address ADR-0032's
89
+ negotiation kept) serves it as JSON Schema — same band as the Machine doc, and what drives the Console's start form
90
+ before any token is entered. The _submit_ stays `authenticated`; schema open, trigger guarded.
91
+ - **A declared schema is enforced at the door.** `POST /workflows/:name/runs` validates the body and 400s naming the
92
+ accepted shape — parity with what Gates already do for deliveries. No schema declared → accept anything (today's
93
+ behavior), and the Console falls back from a generated form to a raw-JSON textarea.
94
+ - **The door takes an idempotency key, out of band.** A start may carry one in a header, never in the body (a body field
95
+ would be one every door had to reserve); a repeat with the same key answers with the run the first one made. A
96
+ `dispatch` Effect sends its ledger row's id ([ADR-0071](0071-a-run-starts-a-run-through-a-dispatch-effect.md)), so a
97
+ start whose answer was lost is sent once where it may have arrived (ADR-0067) and makes one run. `jr2 run` and the
98
+ Console send none.
99
+
100
+ ## Considered options
101
+
102
+ - **`export const input` beside `export const machine`.** Rejected: it re-opens what ADR-0015 closed — a workflow's
103
+ self-description split across module exports, when the machine object already carries every other piece of the
104
+ workflow's self-description.
105
+ - **`workspace` propagates its body's schema onto the wrapper** (the shape this ADR first took, before `pool` forced the
106
+ question and `workspace` turned out to be the same species). Rejected: the wrapper's pass-through is not the body's
107
+ input — the body also gets the injected `workspace` field — so the propagated schema is right only for a body that
108
+ under-declares its own contract. Two ways to declare one door also cost more than the line of ceremony they save: an
109
+ author reading a wrapped workflow could not tell, without opening the factory, which declaration the runtime serves.
110
+ - **Derive from xstate's `types.input`.** Impossible: TypeScript types are erased; there is nothing at runtime to serve
111
+ or validate with.
112
+ - **Constrain the body by pinning its `TInput` slot** (`body: StateMachine<…, Workspaced<door, slots>, …>`). Rejected as
113
+ unsound, not as style: `StateMachine`'s members include methods, method parameters are bivariant, and a body demanding
114
+ _more_ than the door provides therefore compiles — the one case the constraint exists to catch. The shipped
115
+ formulation intersects the body with a conditional guard whose failure branch is an object type keyed by the sentence
116
+ to read, so the diagnostic names the fix rather than printing a structural diff.
117
+ - **Require a schema on every workflow.** Rejected: it taxes the zero-ceremony hello-world for a guarantee only
118
+ input-taking workflows need. Absence is permissive, and permissive is what the wire already was.
119
+
120
+ ## Consequences
121
+
122
+ - **Both entry points sharpen for free**: the Console generates its form from the same JSON Schema `jr2 run` can later
123
+ use to refuse bad input client-side before the POST.
124
+ - **The open band grows a schema and nothing else** — the same class of thing as the Machine doc it sits beside;
125
+ ADR-0014's projection is untouched.
126
+ - **Validation parity means error parity**: a bad run start and a bad gate delivery now fail the same way, a 400 naming
127
+ what is accepted.
@@ -0,0 +1,57 @@
1
+ # The Console renders with Preact, and the server erases the types
2
+
3
+ The Console ([ADR-0032](0032-the-console-unlocks-with-the-instance-token.md)) outgrew its framework-free form: `main.js`
4
+ reached ~1,500 lines, much of it hand-rolled DOM reconciliation (`memo()`, `paint()`, manual list diffing) — work a view
5
+ library exists to own. Three standing constraints shaped the replacement: the repo is zero-build (Node 24 strips types;
6
+ nothing compiles), the Console serves only its own assets (ADR-0032), and `@jr2/orchestrator` is a library that serves
7
+ its files from disk wherever the process starts. A fourth arrived with this decision: Console source is TypeScript like
8
+ everything else.
9
+
10
+ ## Decision
11
+
12
+ - **Preact, called through `h()` — no JSX.** JSX is codegen, and codegen needs a compiler; `h()` calls are plain
13
+ TypeScript, typechecked against Preact's own types, so the views sit inside `tsc --noEmit` with no toolchain. Preact
14
+ is React's API and mental model in ~4KB, shipped as browser-native ESM the orchestrator can serve verbatim.
15
+ - **Console source is `.ts`; the server erases the types.** The browser requests `/assets/*.ts` and receives the file
16
+ with its types replaced by whitespace (`ts-blank-space`), erased once and cached in memory. Erasure is not
17
+ compilation: positions are preserved, so browser stack traces point at the real source line with no sourcemaps, and
18
+ import specifiers keep their `.ts` extensions — the orchestrator does for the browser exactly what Node does for
19
+ itself. The failure class is gated where it belongs: what `ts-blank-space` cannot erase, `tsc --noEmit` already
20
+ rejected in CI.
21
+ - **Vendor rides the dep edge.** Preact's ESM files are served at `/assets/vendor/*`, resolved through this package's
22
+ own dependency edge and mapped to bare specifiers by an import map in `page.html` — the elkjs precedent, extended. No
23
+ CDN, no third-party origin; ADR-0032's asset clause does not move.
24
+ - **The store stays a pure reducer; the vdom replaces the painter.** `store.js` + `store.d.ts` collapse into one
25
+ `store.ts` read by both consumers (tests through Node's stripping, the browser through the server's). `dispatch` stays
26
+ the one door: fold the frame, render the app from the top, and let vdom diffing do what `memo()`/`paint()` did by
27
+ hand. Signals were rejected: finer-grained updates solve a performance problem SSE-paced frames do not have, at the
28
+ cost of the reducer's tests and the store-first language.
29
+ - **The machine canvas stays an imperative island.** elk's layout is async and pan/zoom is a 60Hz transform; neither
30
+ wants a vdom. A component owns the `<svg>` by ref and the existing rendering code moves house without being rebuilt.
31
+
32
+ ## Considered options
33
+
34
+ - **SolidJS** (the requested starting point). Its fine-grained reactivity lives in its compiler, and the compiler is
35
+ babel-only (`babel-plugin-jsx-dom-expressions`; `esbuild-plugin-solid` wraps babel rather than replacing it). Every
36
+ route to it costs an invariant: babel at runtime widens the production dep chain of the token-guarded package, vite
37
+ middleware puts a dev server in production as the asset pipeline, and a pack-time build hands the repo its only
38
+ toolchain plus the stale-artifact problem class. Solid without its compiler has no reason to exist.
39
+ - **React.** Verified against the `react@19.2.8` / `react-dom@19.2.8` tarballs: still CJS
40
+ (`module.exports = require(...)` behind a `process.env.NODE_ENV` branch) — a browser evaluates neither. Self-serving
41
+ it means a resolve-and-bundle step at boot. Preact offers the same API for a page that ADR-0032 already bars from
42
+ React's real advantage, the third-party component ecosystem.
43
+ - **Runtime vite.** Works mechanically (middleware mode); rejected because vite's dev server is explicitly not for
44
+ production serving, and it relocates compile errors to the worst place — a 500 from a deployed orchestrator instead of
45
+ a red CI step.
46
+ - **`htm` tagged templates.** A no-build JSX-alike, but props inside template strings sit outside the type checker — an
47
+ untyped gap exactly where the Console's bugs live (rail rows, gate cards, schema forms). `h()` keeps the views typed.
48
+
49
+ ## Consequences
50
+
51
+ - The erasable-syntax constraint (no enums, no constructor parameter properties) now applies to Console code, as it
52
+ already does to everything Node runs in this repo.
53
+ - `ts-blank-space` and `preact` become production dependencies of `@jr2/orchestrator`; the package's `typecheck` must
54
+ cover `console/` with DOM types.
55
+ - TSX later is additive — an `esbuild.transform()` at the same serve point — not a rework.
56
+ - The refactor is behavior-preserving: `console-store.test.ts` retargets to `store.ts`, the `@console` browser tier and
57
+ the default e2e profile stay the acceptance bar, and no route moves except how `/assets/*` bytes are produced.
@@ -0,0 +1,92 @@
1
+ # A runaway turn is ended by the Harness, rerolled once, then a fault
2
+
3
+ [ADR-0016](0016-agent-turn-mechanics-are-internal.md) absorbed turn mechanics behind one terminal
4
+ `agent.fault { reason }` and named two fault classes: **infra** (the turn broke — provider retries in the Harness, then
5
+ fault) and **no-signal** (the turn ended without a menu pick — budgeted nudge in `agentRun`, then fault). Both assume
6
+ the turn ends. Production found the third case: a turn that never does. Two live `task-with-review` runs degenerated — a
7
+ reviewer handed an empty diff collapsed into ~100 byte-identical `grep` calls; a coder rewrote the same README
8
+ indefinitely after committing real work. Nothing bounded either: `agentRun` awaits settlement, `nudgeBudget` covers only
9
+ turns that end, and neither jr2's Harness nor the retired flue runtime ever capped steps.
10
+
11
+ The investigation ruled the refactor out as the cause. Replays of both failure shapes against the live endpoint —
12
+ including the exact empty-diff reviewer state — terminated 13/13 with max 2 consecutive identical calls, in both the
13
+ current bare-instructions prompt and the flue-shaped one. The degeneration is stochastic model tail behavior,
14
+ concentrated where a task gives the model nothing real to do. That shapes the design: the guard is liveness insurance
15
+ against a tail event, not a correctness mechanism, and the recovery that works is a fresh roll of the dice.
16
+
17
+ **Runaway** names what jr2 observed — the turn ran past the point where jr2 stops believing it will conclude — on the
18
+ same principle as "no-signal": fault classes are named for the observation, not the model's inner pathology.
19
+ "Degeneration" is the behavior the guard usually catches; the budget trigger also ends honest dithering that never
20
+ collapses into repetition.
21
+
22
+ ## Decision
23
+
24
+ - **Detection lives in the Harness turn loop** (`turn.ts`) — the one seat that watches steps as they happen; the
25
+ Orchestrator only sees the settlement, and a runaway never settles. Two triggers, both jr2-owned defaults with **no
26
+ author surface** (ADR-0016: budgets are defaulted knobs): a **step budget** (initial 256; healthy turns peaked at 53
27
+ on a trivial task, and an honest live bug-hunt burned 161 steps before exhausting its context) as the unconditional
28
+ backstop, and **K consecutive byte-identical tool calls** (initial K=4; healthy max observed 2, the production loop
29
+ ~100) as the early exit. If a surface is ever demanded, note it would be a Dial question (ADR-0018) — how hard to run
30
+ this one Turn — but no workflow has needed one. Both count tool calls alone. A step is a tool call that starts while
31
+ the Turn is live: the Harness counts it at the engine's `tool_start` event
32
+ ([ADR-0027](0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md)), before the engine checks the tool or
33
+ the arguments. So these calls are steps: a call that runs; a call of an unknown tool, or with arguments that do not
34
+ validate, which gets the engine's own error; a pick that the Harness blocks as the second pick of its model answer
35
+ ([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)); each call of a model answer that hit the
36
+ output limit, which the engine answers with an error and does not run; and the tripping call itself, which is counted
37
+ and does not run. A tripping call of a tool the run has, with valid arguments, reads `Operation aborted`. A tripping
38
+ call of an unknown tool, or with invalid arguments, reads the engine's own error for it. A model that repeats any of
39
+ them is a Runaway. Only one kind of call is not a step: a call after a pick that ended the Turn, which the Harness
40
+ blocks. The Turn that it would count for is over, so a trip cannot make an ended Turn a Runaway. A Turn part the
41
+ Harness restates at a step boundary
42
+ ([ADR-0029](0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md)) is not a step and
43
+ neither breaks nor extends an identical-call streak. An outstanding pick
44
+ ([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)) is one call however long the workflow runs
45
+ it — the guard has no clock, so a held Turn is never a Runaway for waiting — and its answer **resets the
46
+ identical-call streak**: the world moved, so the same pick again is a new question. Four `run_tests` in a row, each
47
+ answered by a flaky suite, is a model reading answers, not a loop. A pick whose child replies inside the pick's own
48
+ send resets it the same way. A guard straight back does not: it is answered, but nothing ran and the state is where it
49
+ was, so the same pick four times is a Runaway.
50
+ - **Tripping settles the Submission `failed` with `SettlementError.type: "runaway"`** and a legible message ("repeated
51
+ an identical tool call 4 times", "exceeded 256 steps"). A Runaway is a kind of failure, so it stays `failed`, and the
52
+ typed error is what lets `agentRun` switch on the class without parsing prose. A Turn that its own pick ended is a
53
+ different end, not a failure. So it has an outcome of its own, `ended`. `aborted` is a Turn that something else
54
+ stopped ([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)). A Runaway is neither: the Harness
55
+ ended a Turn that nothing had ended.
56
+ - **`agentRun` retries once, as a fresh conversation, with the identical prompt.** The degenerate context is poisoned —
57
+ a model that just emitted a hundred identical calls conditions on them, so a nudge into the same conversation rerolls
58
+ loaded dice; the fresh session is ADR-0016's lossy handoff applied as recovery. The prompt carries no annotation:
59
+ mechanism must not bleed into the one thing the workflow authored, and the failure being stochastic means the fix is
60
+ the reroll, not different instructions. Budget 1 (a defaulted knob): a runaway retry costs an entire turn, and two
61
+ independent rolls both running away is evidence the task itself is pathological — that belongs with the workflow's
62
+ fault routing, not a third attempt.
63
+ - **A `continue: true` invocation gets no retry — straight to the fault.** The one recovery jr2 knows is the identical
64
+ prompt on a fresh conversation, and a continued Turn's prompt is meaningless fresh. The fault retires that
65
+ conversation: jr2 bumps its epoch, so the next `continue` on the Agent lands on a virgin one (ADR-0057), and the fault
66
+ routing decides what to brief it with.
67
+ - **Exhaustion emits the same single terminal `agent.fault { reason }`.** The workflow surface does not change; routing
68
+ (park at a Gate, re-invoke at a higher Dial) stays workflow policy, and `task-with-review`'s existing `agent.fault`
69
+ arm already handles it. Retry attempts surface as run-feed telemetry like nudge attempts.
70
+ - **The claims are proven in the ADR-0027 conformance suite** — a scripted provider that loops deterministically is the
71
+ only tier that can produce a runaway on demand; it runs in the default `pnpm -r test` gate.
72
+
73
+ ## Considered options
74
+
75
+ - **Windowed duplicate-fraction trigger** (catches A-B-A-B alternation early). Rejected for now: no observed instance,
76
+ the step budget backstops it, and it is the only trigger with a plausible false positive — read → edit → read of the
77
+ same path is an identical signature twice, legitimately.
78
+ - **Retry into the same conversation** (nudge-style). Rejected: the poisoned-context argument above; a nudge explains
79
+ itself to a conversation that must continue, a runaway retry exists because the conversation must not.
80
+ - **Workflow-side detection.** Rejected: workflows speak vocabulary, not step counts (ADR-0015/0016), and the
81
+ Orchestrator cannot see inside an unsettled turn without growing a new observation surface for mechanism.
82
+ - **A per-Agent or per-Turn knob now.** Rejected: greenfield, no evidence any turn needs a bound other than the default;
83
+ adding surface first invites tuning before data exists.
84
+
85
+ ## Consequences
86
+
87
+ - ADR-0016's taxonomy is three classes: infra, no-signal, runaway. Its "either budget exhausting emits the one terminal
88
+ fault" sentence now reads across three budgets.
89
+ - A runaway that survives the reroll parks where `agent.fault` routes — for `task-with-review`, the `humanReview` Gate
90
+ with the runaway reason in `meta`, Sandbox intact for inspection.
91
+ - The bound makes non-termination cost at most `(1 + retry) × step budget` instead of a context window; it does nothing
92
+ for output quality — a no-op task still yields invented churn, which is task authoring, not mechanics.