@jr2/orchestrator 0.3.0 → 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 (354) hide show
  1. package/README.md +20 -2
  2. package/bin/server.ts +7 -4
  3. package/console/canvas.ts +27 -14
  4. package/console/components/app.ts +12 -1
  5. package/console/components/fleet.ts +52 -6
  6. package/console/components/machine-pane.ts +128 -2
  7. package/console/components/nav.ts +1 -1
  8. package/console/components/schema-form.ts +12 -4
  9. package/console/json-field.ts +23 -0
  10. package/console/main.ts +122 -39
  11. package/console/store.ts +321 -21
  12. package/console/style.css +111 -0
  13. package/docs/agents-block.md +5 -0
  14. package/docs/concepts/agents-and-turns.md +442 -0
  15. package/docs/concepts/architecture.md +230 -0
  16. package/docs/concepts/charts.md +143 -0
  17. package/docs/concepts/composition.md +357 -0
  18. package/docs/concepts/converge.md +254 -0
  19. package/docs/concepts/durable-runs.md +189 -0
  20. package/docs/concepts/effects.md +603 -0
  21. package/docs/concepts/gates.md +208 -0
  22. package/docs/concepts/machines-and-workflows.md +289 -0
  23. package/docs/concepts/pools-and-sources.md +230 -0
  24. package/docs/concepts/secrets-and-trust.md +222 -0
  25. package/docs/concepts/the-menu.md +378 -0
  26. package/docs/concepts/workspaces.md +678 -0
  27. package/docs/get-started/a-model-decides.md +563 -0
  28. package/docs/get-started/an-agent-in-a-sandbox.md +381 -0
  29. package/docs/get-started/connect-a-model.md +248 -0
  30. package/docs/get-started/create-an-instance.md +194 -0
  31. package/docs/get-started/have-your-assistant-write-a-machine.md +922 -0
  32. package/docs/get-started/install.md +126 -0
  33. package/docs/guides/add-tools-to-the-sandbox.md +295 -0
  34. package/docs/guides/answer-a-gate.md +304 -0
  35. package/docs/guides/bind-open-parts.md +223 -0
  36. package/docs/guides/cancel-a-run.md +146 -0
  37. package/docs/guides/clean-up.md +206 -0
  38. package/docs/guides/fix-a-failed-converge.md +361 -0
  39. package/docs/guides/give-the-cluster-git-credentials.md +222 -0
  40. package/docs/guides/iterate-on-a-machine-on-kind.md +190 -0
  41. package/docs/guides/let-an-agent-write-a-chart.md +226 -0
  42. package/docs/guides/look-inside-a-sandbox.md +174 -0
  43. package/docs/guides/mirror-the-kit-images.md +176 -0
  44. package/docs/guides/move-from-kind-to-a-shared-cluster.md +302 -0
  45. package/docs/guides/open-the-console.md +91 -0
  46. package/docs/guides/publish-a-machine-as-an-npm-package.md +350 -0
  47. package/docs/guides/push-and-sign-a-branch.md +486 -0
  48. package/docs/guides/run-a-chart-by-name.md +270 -0
  49. package/docs/guides/run-a-command-or-call-a-url.md +544 -0
  50. package/docs/guides/run-one-worker-per-item.md +321 -0
  51. package/docs/guides/set-an-agents-access-and-dials.md +375 -0
  52. package/docs/guides/size-a-workspace.md +204 -0
  53. package/docs/guides/start-a-run-from-a-run.md +237 -0
  54. package/docs/guides/start-a-run.md +207 -0
  55. package/docs/guides/upgrade-the-kit-version.md +240 -0
  56. package/docs/guides/use-a-hosted-provider.md +208 -0
  57. package/docs/guides/use-a-local-model.md +313 -0
  58. package/docs/guides/watch-a-run.md +326 -0
  59. package/docs/guides/write-a-workflow.md +318 -0
  60. package/docs/index.md +44 -0
  61. package/docs/llms.txt +216 -0
  62. package/docs/reference/authoring-surface/agent.md +128 -0
  63. package/docs/reference/authoring-surface/answer.md +140 -0
  64. package/docs/reference/authoring-surface/arg.md +60 -0
  65. package/docs/reference/authoring-surface/basic.md +62 -0
  66. package/docs/reference/authoring-surface/bearer.md +53 -0
  67. package/docs/reference/authoring-surface/chartschema.md +23 -0
  68. package/docs/reference/authoring-surface/chartschemafor.md +48 -0
  69. package/docs/reference/authoring-surface/command.md +191 -0
  70. package/docs/reference/authoring-surface/customize.md +64 -0
  71. package/docs/reference/authoring-surface/defineconfig.md +41 -0
  72. package/docs/reference/authoring-surface/defineevent.md +114 -0
  73. package/docs/reference/authoring-surface/dispatch.md +126 -0
  74. package/docs/reference/authoring-surface/fail.md +83 -0
  75. package/docs/reference/authoring-surface/file.md +122 -0
  76. package/docs/reference/authoring-surface/frompick.md +277 -0
  77. package/docs/reference/authoring-surface/http.md +159 -0
  78. package/docs/reference/authoring-surface/isfailure.md +58 -0
  79. package/docs/reference/authoring-surface/jr2setup.md +237 -0
  80. package/docs/reference/authoring-surface/open.md +74 -0
  81. package/docs/reference/authoring-surface/openbody.md +86 -0
  82. package/docs/reference/authoring-surface/paramsof.md +125 -0
  83. package/docs/reference/authoring-surface/pool.md +186 -0
  84. package/docs/reference/authoring-surface/push.md +112 -0
  85. package/docs/reference/authoring-surface/secret.md +118 -0
  86. package/docs/reference/authoring-surface/sentby.md +83 -0
  87. package/docs/reference/authoring-surface/source.md +51 -0
  88. package/docs/reference/authoring-surface/text.md +62 -0
  89. package/docs/reference/authoring-surface/type-agentdeclaration.md +27 -0
  90. package/docs/reference/authoring-surface/type-agentdefinition.md +80 -0
  91. package/docs/reference/authoring-surface/type-agentlogic.md +15 -0
  92. package/docs/reference/authoring-surface/type-agentslots.md +12 -0
  93. package/docs/reference/authoring-surface/type-answeraction.md +12 -0
  94. package/docs/reference/authoring-surface/type-arg.md +19 -0
  95. package/docs/reference/authoring-surface/type-argnames.md +12 -0
  96. package/docs/reference/authoring-surface/type-binding.md +62 -0
  97. package/docs/reference/authoring-surface/type-bindingof.md +12 -0
  98. package/docs/reference/authoring-surface/type-bodyacceptsdoor.md +15 -0
  99. package/docs/reference/authoring-surface/type-bodyacceptsslots.md +17 -0
  100. package/docs/reference/authoring-surface/type-ceilings.md +41 -0
  101. package/docs/reference/authoring-surface/type-chainlink.md +19 -0
  102. package/docs/reference/authoring-surface/type-chartaction.md +46 -0
  103. package/docs/reference/authoring-surface/type-chartagent.md +28 -0
  104. package/docs/reference/authoring-surface/type-chartevent.md +20 -0
  105. package/docs/reference/authoring-surface/type-chartguard.md +20 -0
  106. package/docs/reference/authoring-surface/type-chartinvoke.md +28 -0
  107. package/docs/reference/authoring-surface/type-chartjson.md +14 -0
  108. package/docs/reference/authoring-surface/type-chartstate.md +46 -0
  109. package/docs/reference/authoring-surface/type-charttext.md +40 -0
  110. package/docs/reference/authoring-surface/type-charttransition.md +22 -0
  111. package/docs/reference/authoring-surface/type-charttransitions.md +14 -0
  112. package/docs/reference/authoring-surface/type-chartvalue.md +15 -0
  113. package/docs/reference/authoring-surface/type-childat.md +12 -0
  114. package/docs/reference/authoring-surface/type-childslots.md +12 -0
  115. package/docs/reference/authoring-surface/type-commandenv.md +15 -0
  116. package/docs/reference/authoring-surface/type-commandinput.md +19 -0
  117. package/docs/reference/authoring-surface/type-commandoptions.md +78 -0
  118. package/docs/reference/authoring-surface/type-commandoutput.md +104 -0
  119. package/docs/reference/authoring-surface/type-customize.md +109 -0
  120. package/docs/reference/authoring-surface/type-dispatchdefinition.md +39 -0
  121. package/docs/reference/authoring-surface/type-dispatchinput.md +14 -0
  122. package/docs/reference/authoring-surface/type-dispatchopenparts.md +14 -0
  123. package/docs/reference/authoring-surface/type-dispatchoutput.md +65 -0
  124. package/docs/reference/authoring-surface/type-duration.md +15 -0
  125. package/docs/reference/authoring-surface/type-effectbinding.md +93 -0
  126. package/docs/reference/authoring-surface/type-effectlogic.md +12 -0
  127. package/docs/reference/authoring-surface/type-envopenparts.md +14 -0
  128. package/docs/reference/authoring-surface/type-eventaudience.md +27 -0
  129. package/docs/reference/authoring-surface/type-eventdef.md +45 -0
  130. package/docs/reference/authoring-surface/type-eventfrom.md +16 -0
  131. package/docs/reference/authoring-surface/type-eventsemantics.md +26 -0
  132. package/docs/reference/authoring-surface/type-execoutcome.md +34 -0
  133. package/docs/reference/authoring-surface/type-failslot.md +14 -0
  134. package/docs/reference/authoring-surface/type-failure.md +19 -0
  135. package/docs/reference/authoring-surface/type-failureinput.md +20 -0
  136. package/docs/reference/authoring-surface/type-filebody.md +41 -0
  137. package/docs/reference/authoring-surface/type-frominput.md +14 -0
  138. package/docs/reference/authoring-surface/type-hostinjectedinput.md +58 -0
  139. package/docs/reference/authoring-surface/type-httpdefinition.md +101 -0
  140. package/docs/reference/authoring-surface/type-httpinput.md +12 -0
  141. package/docs/reference/authoring-surface/type-httpmethod.md +14 -0
  142. package/docs/reference/authoring-surface/type-httpopenparts.md +12 -0
  143. package/docs/reference/authoring-surface/type-httpoutput.md +89 -0
  144. package/docs/reference/authoring-surface/type-httprequestparts.md +33 -0
  145. package/docs/reference/authoring-surface/type-jr2config.md +126 -0
  146. package/docs/reference/authoring-surface/type-lends.md +38 -0
  147. package/docs/reference/authoring-surface/type-logicat.md +12 -0
  148. package/docs/reference/authoring-surface/type-markedif.md +12 -0
  149. package/docs/reference/authoring-surface/type-mergedactors.md +12 -0
  150. package/docs/reference/authoring-surface/type-namedsecret.md +25 -0
  151. package/docs/reference/authoring-surface/type-noslotspec.md +26 -0
  152. package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +32 -0
  153. package/docs/reference/authoring-surface/type-openbodydeclaration.md +40 -0
  154. package/docs/reference/authoring-surface/type-openbodylogic.md +14 -0
  155. package/docs/reference/authoring-surface/type-openbodyoutput.md +42 -0
  156. package/docs/reference/authoring-surface/type-openbodyslot.md +18 -0
  157. package/docs/reference/authoring-surface/type-opencommandinput.md +17 -0
  158. package/docs/reference/authoring-surface/type-openeffect.md +12 -0
  159. package/docs/reference/authoring-surface/type-openeffectslots.md +12 -0
  160. package/docs/reference/authoring-surface/type-openof.md +12 -0
  161. package/docs/reference/authoring-surface/type-opensecret.md +23 -0
  162. package/docs/reference/authoring-surface/type-opensecretmark.md +12 -0
  163. package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +31 -0
  164. package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +32 -0
  165. package/docs/reference/authoring-surface/type-poolmachine.md +28 -0
  166. package/docs/reference/authoring-surface/type-poolspec.md +60 -0
  167. package/docs/reference/authoring-surface/type-pushinput.md +15 -0
  168. package/docs/reference/authoring-surface/type-pushoptions.md +79 -0
  169. package/docs/reference/authoring-surface/type-pushoutput.md +124 -0
  170. package/docs/reference/authoring-surface/type-pushwireoutput.md +12 -0
  171. package/docs/reference/authoring-surface/type-quantity.md +14 -0
  172. package/docs/reference/authoring-surface/type-reached.md +12 -0
  173. package/docs/reference/authoring-surface/type-reposlot.md +70 -0
  174. package/docs/reference/authoring-surface/type-reposlotsof.md +16 -0
  175. package/docs/reference/authoring-surface/type-sandboxoptions.md +77 -0
  176. package/docs/reference/authoring-surface/type-secret.md +25 -0
  177. package/docs/reference/authoring-surface/type-sender.md +19 -0
  178. package/docs/reference/authoring-surface/type-sentbyguards.md +12 -0
  179. package/docs/reference/authoring-surface/type-size.md +85 -0
  180. package/docs/reference/authoring-surface/type-slothandles.md +32 -0
  181. package/docs/reference/authoring-surface/type-sourcenextresult.md +17 -0
  182. package/docs/reference/authoring-surface/type-sourcespec.md +44 -0
  183. package/docs/reference/authoring-surface/type-syncstate.md +12 -0
  184. package/docs/reference/authoring-surface/type-text.md +19 -0
  185. package/docs/reference/authoring-surface/type-textpart.md +24 -0
  186. package/docs/reference/authoring-surface/type-thinkinglevel.md +39 -0
  187. package/docs/reference/authoring-surface/type-toparameterizedobject.md +14 -0
  188. package/docs/reference/authoring-surface/type-toprovidedactor.md +14 -0
  189. package/docs/reference/authoring-surface/type-unknown.md +21 -0
  190. package/docs/reference/authoring-surface/type-usercontainer.md +77 -0
  191. package/docs/reference/authoring-surface/type-withoutemptyindex.md +20 -0
  192. package/docs/reference/authoring-surface/type-workflowevent.md +46 -0
  193. package/docs/reference/authoring-surface/type-workspaceaccess.md +16 -0
  194. package/docs/reference/authoring-surface/type-workspaced.md +94 -0
  195. package/docs/reference/authoring-surface/type-workspacehandles.md +23 -0
  196. package/docs/reference/authoring-surface/type-workspacemachine.md +29 -0
  197. package/docs/reference/authoring-surface/type-workspaceof.md +13 -0
  198. package/docs/reference/authoring-surface/type-workspaceoptions.md +39 -0
  199. package/docs/reference/authoring-surface/type-workspacespec.md +37 -0
  200. package/docs/reference/authoring-surface/workspace.md +259 -0
  201. package/docs/reference/authoring-surface.md +197 -0
  202. package/docs/reference/cli.md +433 -0
  203. package/docs/reference/config.md +756 -0
  204. package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +113 -0
  205. package/docs/reference/decisions/0002-duplex-streaming-actor.md +110 -0
  206. package/docs/reference/decisions/0004-worktree-provisioning-model.md +124 -0
  207. package/docs/reference/decisions/0005-sandbox-pod-composition.md +230 -0
  208. package/docs/reference/decisions/0006-agent-control-surface.md +47 -0
  209. package/docs/reference/decisions/0007-durable-machine-state.md +109 -0
  210. package/docs/reference/decisions/0008-orchestrator-as-deployed-app.md +60 -0
  211. package/docs/reference/decisions/0009-cli-and-instance-interface.md +275 -0
  212. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +160 -0
  213. package/docs/reference/decisions/0011-workflow-defined-events.md +141 -0
  214. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +103 -0
  215. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +157 -0
  216. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +95 -0
  217. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +265 -0
  218. package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +124 -0
  219. package/docs/reference/decisions/0017-pool-over-a-source-port.md +69 -0
  220. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +111 -0
  221. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +275 -0
  222. package/docs/reference/decisions/0020-private-ca-trust-is-a-named-concept.md +60 -0
  223. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +100 -0
  224. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +103 -0
  225. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +176 -0
  226. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +439 -0
  227. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +65 -0
  228. package/docs/reference/decisions/0026-a-turn-that-is-over-is-never-prompted.md +62 -0
  229. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +237 -0
  230. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +89 -0
  231. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +154 -0
  232. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +100 -0
  233. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +190 -0
  234. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +61 -0
  235. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +127 -0
  236. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +57 -0
  237. package/docs/reference/decisions/0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md +92 -0
  238. package/docs/reference/decisions/0036-a-turn-compacts-its-own-context-mid-flight.md +138 -0
  239. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +196 -0
  240. package/docs/reference/decisions/0038-jr2-up-builds-every-image-it-deploys.md +170 -0
  241. package/docs/reference/decisions/0039-image-garbage-collects-by-reachability.md +115 -0
  242. package/docs/reference/decisions/0041-a-build-the-host-already-holds-is-not-spent-again.md +64 -0
  243. package/docs/reference/decisions/0042-ready-is-not-routable-an-admission-retries-its-connection.md +142 -0
  244. package/docs/reference/decisions/0043-the-kit-is-tested-as-installed-a-local-registry-stands-in-for-npm.md +170 -0
  245. package/docs/reference/decisions/0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md +85 -0
  246. package/docs/reference/decisions/0045-the-platform-joins-the-image-address-and-the-cluster-chooses-it.md +82 -0
  247. package/docs/reference/decisions/0046-a-converge-failure-carries-its-diagnosis.md +34 -0
  248. package/docs/reference/decisions/0047-the-git-ssh-key-source-is-the-users-choice.md +47 -0
  249. package/docs/reference/decisions/0048-the-orchestrator-boots-without-its-repos.md +36 -0
  250. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +126 -0
  251. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +73 -0
  252. package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +227 -0
  253. package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +81 -0
  254. package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +111 -0
  255. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +126 -0
  256. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +107 -0
  257. package/docs/reference/decisions/0056-a-global-jr2-hands-off-to-the-instances-own-and-an-instance-has-one-kit-version.md +62 -0
  258. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +152 -0
  259. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +106 -0
  260. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +384 -0
  261. package/docs/reference/decisions/0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md +111 -0
  262. package/docs/reference/decisions/0061-a-memory-kill-ends-the-agents-processes-not-the-harness.md +83 -0
  263. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +212 -0
  264. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +83 -0
  265. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +79 -0
  266. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +65 -0
  267. 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
  268. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +430 -0
  269. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +464 -0
  270. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +180 -0
  271. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +323 -0
  272. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +183 -0
  273. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +144 -0
  274. package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +122 -0
  275. package/docs/reference/decisions/0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md +76 -0
  276. package/docs/reference/decisions.md +83 -0
  277. package/docs/reference/glossary.md +648 -0
  278. package/docs/reference/http-api.md +1348 -0
  279. package/docs/reference/machines.md +190 -0
  280. package/docs/reference/mechanism-events.md +72 -0
  281. package/docs/why.md +206 -0
  282. package/package.json +6 -4
  283. package/src/actor.ts +976 -61
  284. package/src/agent.ts +114 -38
  285. package/src/ambient.ts +4 -1
  286. package/src/assembly-checks.ts +347 -0
  287. package/src/assembly-tar.ts +494 -0
  288. package/src/assembly.ts +721 -0
  289. package/src/cause.ts +234 -0
  290. package/src/chart.ts +2182 -0
  291. package/src/charts.ts +748 -0
  292. package/src/config.ts +520 -162
  293. package/src/content-hash.ts +105 -0
  294. package/src/custodian.lua +6 -5
  295. package/src/custodian.ts +143 -39
  296. package/src/customize.ts +388 -55
  297. package/src/dispatch-effect.ts +945 -0
  298. package/src/door-compat.ts +217 -0
  299. package/src/durability.ts +11 -4
  300. package/src/effect-checks.ts +557 -0
  301. package/src/effect.ts +1202 -0
  302. package/src/effector-wire.ts +674 -0
  303. package/src/fail.ts +209 -0
  304. package/src/fingerprint.ts +4 -3
  305. package/src/gate.ts +8 -0
  306. package/src/git-clone.ts +65 -0
  307. package/src/git-push.ts +336 -0
  308. package/src/harness-client.ts +764 -48
  309. package/src/held-secrets.ts +82 -26
  310. package/src/holds.ts +156 -0
  311. package/src/http-effect.ts +1423 -0
  312. package/src/http.ts +916 -113
  313. package/src/images.ts +47 -21
  314. package/src/index.ts +62 -43
  315. package/src/instance.ts +505 -59
  316. package/src/internal.ts +84 -0
  317. package/src/kube-client.ts +333 -0
  318. package/src/machine-doc.ts +106 -6
  319. package/src/mechanism-routes.ts +168 -0
  320. package/src/model-keys.generated.ts +53 -0
  321. package/src/model-keys.ts +18 -58
  322. package/src/mount-digest.ts +53 -0
  323. package/src/names.ts +66 -8
  324. package/src/notices.ts +141 -0
  325. package/src/open-body.ts +1580 -0
  326. package/src/open-effect.ts +217 -0
  327. package/src/open.ts +59 -6
  328. package/src/parts.ts +709 -41
  329. package/src/party.ts +534 -0
  330. package/src/pick.ts +761 -0
  331. package/src/pool.ts +248 -37
  332. package/src/registration.ts +382 -19
  333. package/src/repo-fetch.ts +60 -85
  334. package/src/repos.ts +65 -75
  335. package/src/rest.ts +133 -0
  336. package/src/run-host.ts +2383 -115
  337. package/src/run-output.ts +38 -0
  338. package/src/sandbox-effect.ts +1126 -0
  339. package/src/sandbox-effector.ts +111 -0
  340. package/src/sandbox-kube.ts +1721 -0
  341. package/src/sandbox-watch.ts +260 -0
  342. package/src/secret-placement.ts +529 -0
  343. package/src/server.ts +284 -84
  344. package/src/setup.ts +819 -117
  345. package/src/size.ts +454 -0
  346. package/src/snapshot-store.ts +115 -20
  347. package/src/stub-harness.ts +49 -16
  348. package/src/sync-client.ts +54 -0
  349. package/src/timers.ts +191 -0
  350. package/src/tokens.ts +35 -4
  351. package/src/vocabulary.ts +135 -15
  352. package/src/wire.ts +238 -10
  353. package/src/workspace.ts +1195 -338
  354. package/src/sandbox-kubectl.ts +0 -1108
@@ -0,0 +1,237 @@
1
+ # The Harness is jr2's own server: flue retires, the wire stays
2
+
3
+ jr2 has been programming around its Harness, not with it. The evidence is the ADR trail itself.
4
+ [ADR-0018](0018-instance-agents-are-definitions-jr2-assembles-the-harness.md)'s entire codegen-at-pod-start mechanism —
5
+ generated shims, a boot-time `flue build`, agent names frozen at that build — exists because flue offers no programmatic
6
+ server assembly; its own Considered options says so. [ADR-0026](0026-a-turn-that-is-over-is-never-prompted.md) was
7
+ forced by a foreign advisory session jr2 never asked for, and records a rejection ("there is nothing on the wire to
8
+ branch on") that is only true because someone else writes the wire.
9
+ [ADR-0023](0023-the-harness-prints-the-conversation.md)'s printer needed a loopback SSE client plus a 40-attempt
10
+ 404-chase against flue's own parking behavior. The `flue-contract` tier ([ADR-0010](0010-bdd-acceptance-tests.md)) holds
11
+ a test asserting a defect we are waiting on someone else to fix: on pinned beta.9, an abort landing mid-stream erases
12
+ the assistant message from every later turn — fixed only on flue's unreleased line. And the Agent tool-restriction gap
13
+ has no seat in beta.9 at all: `SandboxFactory.tools` replaces the whole list, the default tools are private to a bundled
14
+ chunk, and ADR-0018's recorded escape hatch — "eject to a real flue project" — un-decides ADR-0018.
15
+
16
+ Waiting was the standing plan, and flue 2.0 ends it. The 2.0 nightlies do export tool constructors, but as part of a
17
+ rewrite: `defineAgent` is gone (an `AgentFunction` with hooks replaces it), `connectMcpServer` is renamed, the workflow
18
+ exports are absent from the runtime index, and the schema resets. `latest` has sat frozen at `1.0.0-beta.9` for a month
19
+ while all activity lands on an unannounced 2.0 line. That is a framework heading somewhere else, carrying our defect fix
20
+ with it.
21
+
22
+ Meanwhile the thing jr2 actually needs from flue is small, and jr2 already owns its specification. The Orchestrator
23
+ imports `@flue/sdk` in exactly one module (`flue-client.ts`), uses three verbs — send, wait, abort — behind
24
+ `AgentRunPort`, and persists a three-string admission handle. The stub Harness (`stub-harness.ts`) is a hand-written,
25
+ SDK-verified model of the five wire endpoints. And the base flue builds on is public: `@earendil-works/pi-agent-core`
26
+ (MIT, independent of flue's authors) — from which flue imports only the low-level `Agent` loop and reimplements the
27
+ rest. jr2 takes the same slim core: pi-agent-core gives the `Agent` loop, its tool hooks (`beforeToolCall`,
28
+ `finishTurn`, `prepareRequest`), the abort, the steering queue and an in-process event stream. The tool-restriction gap
29
+ does not exist for a caller that owns tool assembly. jr2 owns the rest: the engine part on disk, Compaction
30
+ ([ADR-0036](0036-a-turn-compacts-its-own-context-mid-flight.md)) and the six Working tools, whose text the model reads.
31
+
32
+ ## Decision
33
+
34
+ - **jr2 owns its Harness: `@jr2/harness` at `packages/harness`**, built on `@earendil-works/pi-agent-core` +
35
+ `@earendil-works/pi-ai` (pinned exact; a minor can break, and pi 1.0 removed the session, Compaction and the coding
36
+ tools from pi-agent-core) and Hono, which the repo already uses. The stock image keeps its tag
37
+ (`jr2-harness:<kitversion>`) and every operator contract: numeric uid, binding `:8080` is Ready, `git` in the image —
38
+ plus `ripgrep` for the grep tool.
39
+ - **pi is the engine, not the contract.** The Harness keeps its contract in jr2's own shapes. One seam (`Engine`,
40
+ `engine.ts`) is the interface between the Turn and the engine. The Turn gives the engine one Submission's run: the
41
+ model, the thinking level, the system prompt, the tools, the context and the prompt. It hears the run as events
42
+ (`message_end`, `step_boundary`, `tool_start`, `tool_end`), and it can `steer` and `abort` the run. It counts the
43
+ Runaway watch's steps at `tool_start` (ADR-0035): the engine sends `tool_start` for every call of a model answer, in
44
+ order, before it checks the tool or the arguments, and the event carries the call's name and arguments. A listener can
45
+ abort the run there. Inside the run, the Turn decides through four hooks:
46
+ - `convertToLlm`: the messages that the provider gets.
47
+ - `beforeRequest`: Compaction (ADR-0036).
48
+ - `beforeToolCall`: the blocks of ADR-0024 (a second pick of one model answer, and a call after a pick that ended the
49
+ Turn), and the block of a tripped runaway watch (ADR-0035). A block can also abort the run.
50
+ - `endsRun`: the end of the run at the step boundary, after a trip or when the Turn is over.
51
+
52
+ The engine runs the tool calls of one model answer one at a time, in the order the model wrote them. A call's
53
+ `beforeToolCall` comes after the result of the call before it is complete. This rule is part of the seam, not a choice
54
+ of pi's. pi's own default is `parallel`, which starts each call before the call ahead of it ends. So the pi adapter
55
+ sets `toolExecution: "sequential"`. The reason is a measurement on the real Anthropic API (Haiku 5.5, 2026-10-22), in
56
+ the `@model` claim `continue-after-signed-push`
57
+ ([ADR-0066](0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md)). The model sent
58
+ `bash` with `git add … && git commit …` and the pick `publish` in one model answer. In 3 of 10 trials, `publish`
59
+ pushed before the commit existed. A `write` and a `bash` test run in one model answer had the same race. The rule is
60
+ for all tools, the Working tools and the Menu. The cost is that independent reads of one model answer do not run at
61
+ the same time. Four facts follow:
62
+ - Only the first pick of a model answer runs
63
+ ([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)). The model wrote a later one before it
64
+ read the first one's answer. The seam states two more facts, so that the Turn can find the first pick by itself. The
65
+ engine tells a model answer (`message_end`) before it asks `beforeToolCall` about a call of it. And it asks only
66
+ about a call of a tool the run has, with valid arguments. So the first pick is the first that reached
67
+ `beforeToolCall` in that model answer. A call of an unknown tool, or with invalid arguments, gets the engine's own
68
+ error, and it is never the first pick. A list of the model answer's calls would name such a call, and it would need
69
+ an id that some providers do not send.
70
+ - A call after a pick whose answer ended the Turn does not run: `beforeToolCall` blocks it. `endsRun` then ends the
71
+ run at the step boundary, so no model request follows. The Submission does not settle then: it waits for the
72
+ Orchestrator's sweep, which says how the Turn ended (ADR-0024), at most 10 s.
73
+ - After an abort or a Runaway's trip ([ADR-0035](0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md)), the
74
+ calls before it have run. The engine runs no later call of that model answer and gives it no result. When the run
75
+ ends, the Turn writes jr2's answer for each such call in the engine part: "This call did not run: your turn ended
76
+ before it started." Without it, pi-ai answers "No result provided", which does not say whether the call ran.
77
+ [ADR-0031](0031-menu-only-agents-run-on-the-instance-harness.md) gives the restart's case.
78
+ - Each "did not run" result is an error result: a blocked call, and a call that the run never started. The call did
79
+ not do its work, and the model may make it again. A cut or unsent pick's answer (ADR-0031) is not an error, and
80
+ neither is the answer of a pick that the Harness may have delivered (ADR-0002), because an error reads as a failed
81
+ pick, and a cut pick may have run.
82
+
83
+ The seam's tools, events and hooks are jr2's types. The seam's messages are pi-ai's provider messages, plus jr2's
84
+ Compaction summary. pi-ai is the provider library, not the engine (see below), so these shapes are not the engine's.
85
+ An adapter (`EngineAdapter`) opens the conversation's engine part and makes its engine. `pi-engine.ts` is the one
86
+ adapter, and the one module that imports pi-agent-core. It holds pi's `Agent` loop, its hooks and events, and the
87
+ leading system message that carries the system prompt and the tool declarations (pi 1.x's way).
88
+
89
+ The engine part (ADR-0031) is engine-named: the adapter chooses its directory (`engine/pi`). Its format is jr2's
90
+ transcript (`transcript.ts`), not pi's, because pi 1.x keeps no conversation. The Turn and Compaction (ADR-0036) read
91
+ and write it. So the adapter does not own the format. It owns the name. The name is the guard: only the engine that
92
+ wrote a part is known to read it correctly, because that engine's loop decides what goes into it. So a Harness on
93
+ another engine opens another directory, does not read pi's part, and continues from the record's history.
94
+
95
+ pi-ai is the provider library that every engine sends its requests through, so the Harness uses it directly:
96
+ - the model catalog and the custom provider (`provider.ts`);
97
+ - the message and tool-declaration types;
98
+ - TypeBox `Type` for the Working tools' schemas (pi-ai coerces the arguments of a TypeBox schema differently from
99
+ those of a plain JSON Schema);
100
+ - `uuidv7`, for two ids that go to a provider as a prompt-cache key (pi-ai's `sessionId`): the engine part's id, and
101
+ the id of each Compaction summary request;
102
+ - `contentText`: the summary request joins a message's text by pi-ai's rule, as pi 0.82.1 did;
103
+ - `retryAssistantCall`: the summary call retries by the same rule as each provider request.
104
+
105
+ A second engine is a second adapter. One module chooses the Harness's adapter (`engine-choice.ts`), and the Turn names
106
+ no adapter: a test rig can pass another (`TurnDeps.engineAdapter`). So a second engine changes `engine-choice.ts`, not
107
+ the Turn. The Working tools, the printer, the transcript and Compaction do not change.
108
+
109
+ - **Assembly is runtime construction, not codegen.** `main.ts` constructs the server and listens; each admission carries
110
+ its Agent's definition (ADR-0049), validated loudly and re-read per Submission. `boot.mjs`, the generated shims, the
111
+ boot-time `flue build`, and the readiness lag it caused are deleted, and so — later — was the `JR2_AGENTS_JSON` roster
112
+ this cut read at boot. The ADR-0018 contract — stock image, user-owned plain-data definitions — survives with its
113
+ mechanism simplified.
114
+ - **The wire stays, and becomes normative.** The five endpoints the stub documents (`POST /agents/:name/:id`,
115
+ updates/history views, long-poll, `POST .../abort {cause?, submissionId?} → {aborted}`) and the three-string admission
116
+ `{streamUrl, offset, submissionId}` (jr2 now mints its own opaque values) are jr2's protocol, not flue's. The stub
117
+ Harness, the admission ledger ([ADR-0016](0016-agent-turn-mechanics-are-internal.md)), `AgentRunPort`, and every
118
+ mechanics/`@kind` assertion came through the move from flue byte-identical. Later decisions changed them in place. The
119
+ sweep's cause and the outcome `ended` ([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md))
120
+ changed the stub's settlements, the port's `abort`, and the mechanics and `@kind` assertions of a Turn that its own
121
+ pick ended. Long-poll is the only wait transport; the SDK's `?wait=result` and SSE paths are not part of the contract.
122
+ The admit body is `{message, model?, thinkingLevel?}` — the optional dials
123
+ ([ADR-0018](0018-instance-agents-are-definitions-jr2-assembles-the-harness.md)) are this Submission's override layer
124
+ over the Agent's definition, omitted when unset, and an unresolvable one is a 400 at admission rather than a
125
+ Submission that settles `failed` — a one-line addition precisely because the protocol is jr2's own.
126
+
127
+ - **The re-owned semantics are stated, not inherited:**
128
+ - **Per-instance queue: accept and queue.** A Submission is promoted when it is the first unsettled Submission of its
129
+ conversation; admission order. [ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)'s correction
130
+ — flue queues, and the ordered abort depends on it — is now a design jr2 asserts rather than a behavior it
131
+ discovered.
132
+ - **Abort sweeps, and says its cause.** The active Submission and everything queued at the moment the abort is
133
+ processed settle in admission order. The response answers `{aborted: boolean}` (`false` on an idle instance).
134
+ Arrival order at the Harness is the order; the client-side pending-abort await (ADR-0024) stays as the cross-request
135
+ guarantee. The body is optional, and it is the Orchestrator's judgment of how the Turn ended:
136
+ - `{cause: "pick", submissionId}` says that the Turn's own pick ended it. The active Submission settles `ended` when
137
+ the body names it.
138
+ - Every other body settles it `aborted`: no body, a body that does not parse, a cause the Harness does not know, or
139
+ a `submissionId` of another Submission (or none).
140
+ - The active Submission may still be running. Or its run may have ended because a pick found the Turn over, and it
141
+ waits for this sweep. Either way the sweep settles it. When no sweep comes within 10 s, it settles `aborted` and
142
+ says so.
143
+ - A queued Submission never ran, so it settles `aborted` for every cause.
144
+ - An Orchestrator from before the field still works on the wire. But it gives no cause, so its Turns settle
145
+ `aborted`, every time.
146
+ - **Settlement is `completed | ended | failed | aborted`**, and the Orchestrator is the one judge of how a Turn ended
147
+ (ADR-0024). `completed`: the run returned, and the Turn is still open, so the Agent's Actor asks again (the nudge or
148
+ the wake, ADR-0016). `ended`: the Turn's own pick ended the Turn, and the sweep said `cause: "pick"`. The pick may
149
+ be from an earlier Submission of the same Turn: a nudge or a wake starts a new Submission in the same Turn.
150
+ `failed`: the run threw, a Runaway included
151
+ ([ADR-0035](0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md)). `aborted`: something else ended the Turn,
152
+ or no sweep came in time, or it was over before it started
153
+ ([ADR-0026](0026-a-turn-that-is-over-is-never-prompted.md)). `failed` and `aborted` carry an error, `aborted` the
154
+ type `{type: "submission_aborted"}` — continuity with what ADR-0024/0025 recorded. `completed` and `ended` carry
155
+ none: nothing went wrong. A Settlement also carries its Submission's `usage`: the input, output, cache-read and
156
+ cache-write tokens and the cost, summed over the Submission's model answers. The cost is pi-ai's, from its catalog
157
+ rates, so a `harness.provider` model with no rates costs 0. A Compaction summary's model call is not in it. The
158
+ field is optional. A Submission whose model never answered carries none, and so does one lost with a restarted
159
+ Harness, because its spend was lost with it. The reason is the `@model` tier
160
+ ([ADR-0066](0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md)): it records the
161
+ tokens and the cost of each trial, and the Settlement is the one place outside the pod that knows them. The
162
+ Orchestrator does not read it yet.
163
+ - **An unknown conversation is 404**, on both views. `POST` creates (that is admission), except an admission with
164
+ `kept: true`, which is 404 (with `kept: false` in the body) on an unknown conversation and creates nothing
165
+ ([ADR-0062](0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md)); abort answers `{aborted: false}`. This is
166
+ a deliberate divergence from the stub, which answers every read with an empty stream: the stub is inert by design,
167
+ but a real Harness that answered a lost conversation with silence would park a re-attached `wait` forever. Honesty
168
+ about loss beats an endless poll.
169
+ - **The durability contract is explicit: a conversation lives as long as its Sandbox, and on the Instance Harness as
170
+ long as its run** (ADR-0031). The Harness persists each conversation to a directory as it goes and rebuilds it on
171
+ boot, so a Harness restart inside a living pod keeps it; a Submission in flight at the restart settles `failed`.
172
+ What is durable across a lost pod is Orchestrator-side — the admission ledger and snapshot
173
+ ([ADR-0007](0007-durable-machine-state.md)), and `wait`'s reconnect-from-offset. A conversation that is gone
174
+ surfaces as 404 → fault → workflow policy, the same family as `workspace.lost`. Provider-stream retry moves inside
175
+ the turn: the Harness states 2 retries, and pi-ai runs them (`maxRetries`) — the seat ADR-0016 delegated to flue's
176
+ `durability{}`.
177
+ - **[ADR-0013](0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md)'s enabling fact is reproduced
178
+ as explicit code.** Each Submission reads `$JR2_CUSTODIAN_URL/agents/<iid>/surface` afresh and presents it to pi as
179
+ the Menu, so the menu is per-turn with no push channel, no turn index, no second port. Menu tools keep the
180
+ `mcp__jr2__<name>` naming — shipped instructions and the printer's prefix-stripping depend on it — though no MCP
181
+ carries them ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)). The
182
+ retire-the-previous idiom and ADR-0023's 404-chase existed to work around flue API gaps that are gone.
183
+ - **The Orchestrator side shrinks.** `harness-client.ts` replaces `flue-client.ts`: a fetch client for the three verbs,
184
+ with `wait` as a long-poll loop from the admitted offset (capped backoff, indefinite reconnect — pod death is
185
+ `workspace.lost`'s job) that resolves on `completed` alone and raises a typed `SettlementFault` on
186
+ `failed`/`ended`/`aborted`/404 (an `ended` reaches only an actor that its own pick already stopped, and a stopped
187
+ actor drops it). `actor.ts` did not change in the move from flue: the port, the admission shape, the nudge loop, and
188
+ the abort ordering stayed as they were. ADR-0024's sweep cause later changed it in two places. The port's `abort`
189
+ takes the cause and the Submission it names, and the Actor's `abandon` reads the cause before its registration goes.
190
+
191
+ ## Considered options
192
+
193
+ - **Wait for flue 2.0.** Rejected: unreleased, unscheduled, and a rewrite of the surfaces jr2 generates against
194
+ (`defineAgent`, MCP wiring, routing). The structural problems — foreign advisory sessions, no programmatic assembly —
195
+ are architecture, not backlog. Meanwhile the pinned defect erases assistant messages on real abort timings.
196
+ - **Fork flue at beta.9.** Rejected: ~17K lines of built output plus a CLI codegen owned to keep seven imported symbols,
197
+ diverging from a codebase whose maintainers are mid-rewrite — the defect fix would have to be backported from a line
198
+ that left. And its durability layer, the one genuinely load-bearing-looking piece, is in-memory as jr2 deploys it.
199
+ - **Fresh on pi, new wire.** Rejected: redesigning the protocol throws away the tested contract, the stub, the ledger
200
+ shape, and the mechanics tier for no identified semantic gain. The wire was never the problem; the runtime behind it
201
+ was.
202
+ - **Fresh on pi, wire kept** — chosen.
203
+ - **Run in order only a model answer that holds a Menu pick** (pi's per-tool `executionMode: "sequential"` on each Menu
204
+ tool), and the other model answers in parallel. Rejected. It is narrower, and independent reads keep their speed. But
205
+ it leaves the race between Working tools: a `write` and the `bash` test run that reads its file. Also, the seam would
206
+ have no rule of its own: the order would be a fact of which tools a model answer holds.
207
+ - **Give `beforeToolCall` the names of the calls ahead of the call in its model answer** (`earlier`), for the first-pick
208
+ rule. Rejected. The list names a call that the engine did not prepare (an unknown tool, invalid arguments), so the
209
+ rule blocked a valid pick behind it. And the position of a call in the list needs the call's id, which a provider can
210
+ leave empty.
211
+
212
+ ## Consequences
213
+
214
+ - **Deleted**: `packages/orchestrator/harness/` (boot.mjs, the flue project skeleton, the boot build and its readiness
215
+ lag), the printer's loopback client and 404-chase, ADR-0026's foreign-advisory rationale (the decision stands on its
216
+ other leg: the turn-start race), and the "eject to a real flue project" escape hatch — which means the tool contract
217
+ must now be answered, not re-deferred
218
+ ([ADR-0028](0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md)).
219
+ - **Kept verbatim**: the wire, the stub as the mechanics fixture, `AgentRunPort` and the admission ledger, ADR-0024's
220
+ rule, receipt, and ordering, and the container split — restated in jr2 terms: the pod's credentials live in a separate
221
+ container (the Custodian, ADR-0059) _because working tools execute in the Harness container_, so the
222
+ credential-isolation premise survives while `local()` leaves the vocabulary.
223
+ - **The `flue-contract` tier dissolves into the conformance suite.** Its rig — scripted OpenAI-compatible provider, the
224
+ Menu over a real socket to a killable fake Orchestrator, real server — moves into `packages/harness/test/` and runs in
225
+ the default `test` gate: the opt-in-ness existed only because the tier owned a foreign pin and build. Its claims
226
+ become jr2 requirements, with the pinned-defect assertion inverted: an abort mid-stream must **not** erase the
227
+ assistant message. This suite is the first canary for pi bumps; `@kind` (ADR-0038) drives the same turn loop in a real
228
+ pod against a scripted model, and `@model`
229
+ ([ADR-0066](0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md)) against a real one —
230
+ run all three before bumping the pin.
231
+ - **The pinning risk moves; it does not vanish.** pi releases often and a minor can break. The pin is exact, a bump is a
232
+ deliberate kit change gated by the conformance suite, and the breaking surface is now the `Agent` loop's hooks and
233
+ pi-ai's provider adapters — not a code generator, a CLI build, and a process-global registry. What pi removed jr2 now
234
+ owns, so a bump cannot move it: the Working tools keep their text, and Compaction its algorithm.
235
+ - **Migration is phased behind the wire.** Because both ends speak the same protocol, the image swap and the
236
+ Orchestrator client swap are independent commits, each leaving every tier green; an old Orchestrator drives the new
237
+ image and vice versa. `@flue/*` leaves the repo when the last pin (the retired contract tier) is deleted.
@@ -0,0 +1,89 @@
1
+ # What an Agent may do to the Workspace is part of its definition
2
+
3
+ jr2 is exact about what an Agent may **say** and silent about what it may **do**. The control plane is a menu the
4
+ Machine sets per turn — derived from the invoking state's transitions
5
+ ([ADR-0015](0015-authoring-surface-absorbs-the-mechanism.md)), served from a separate container
6
+ ([ADR-0013](0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md)), unforgettable by construction
7
+ ([ADR-0016](0016-agent-turn-mechanics-are-internal.md)). The data plane is one identical working toolset with full write
8
+ access to the shared Workspace, for every Agent, in every state.
9
+
10
+ The gap is not hypothetical. In a live `task-with-review` run (2026-07-27), the coder found nothing to do and committed
11
+ nothing; the reviewer then edited `README.md`, set a `user.email`, committed, and approved its own commit — against
12
+ instructions that say "Do not modify the code yourself".
13
+ [ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md) already established what that means: the same
14
+ prompt, model, and state produce compliance on one run and not the next, "which is what rules out fixing this with
15
+ words." The prose ban is a persona, not a boundary. ADR-0024 also named the stakes — an Agent with write access is a
16
+ concurrent writer in a Workspace the Machine believes is single-threaded — and
17
+ [ADR-0012](0012-workspace-wrapper-machine.md) puts coder and reviewer in that one Workspace on purpose.
18
+
19
+ Under the retired flue runtime this had no seat — its tool list was take-it-or-replace-it, and the only recorded out was
20
+ ejecting to a foreign harness project. [ADR-0027](0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md)
21
+ retires that constraint: jr2 assembles the working tools per Submission, so what an Agent may do is finally something a
22
+ definition can state.
23
+
24
+ ## Decision
25
+
26
+ - **`AgentDefinition` gains `workspace?: "write" | "read" | "none"`, default `"write"`.** One word that describes the
27
+ persona — a reviewer reads, a decisioner needs no Workspace at all — not a tool inventory. It is deliberately **not**
28
+ named `tools`: that word already means the control-plane menu at the invoke seam (`actor.ts`'s escape hatch) and would
29
+ name the Harness's menu rather than the Agent. And it is named for the thing accessed: the field was first shipped as
30
+ `access`, and `"none"` exposed the dropped word as load-bearing — access _to what?_ The object was always the
31
+ Workspace (it is this ADR's title). A string field, JSON-serializable, riding the existing ConfigMap channel unchanged
32
+ (ADR-0018's constraint holds).
33
+ - **`"none"` is the Menu-only Agent — and the value also names the Harness that hosts the Agent.** `"none"` withholds
34
+ the entire Working toolset: the Agent converses and picks from its Menu, nothing else — the decisioning persona (read
35
+ inputs, pick the next event). Because such an Agent touches no Workspace, it does not run in one: `"write"`/`"read"`
36
+ place a Turn on the enclosing Workspace's Harness; `"none"` places it on the
37
+ [Instance Harness](0031-menu-only-agents-run-on-the-instance-harness.md), always — even when a `workspace()` encloses
38
+ the invocation. Placement is definitional, which is this ADR's thesis extended one step: what an Agent may do to the
39
+ Workspace decides where it runs. (`cwd` is moot for `"none"` — only Working tools consume it.)
40
+ - **The tool layer enforces the honest path.** `access: "read"` withholds `write` and `edit` from the assembled working
41
+ tools (the tool set that the Harness gives the loop for each Submission). `bash` stays — the reviewer's own
42
+ instructions require running the tests — and the ADR says plainly what that means: a shell can write, so the tool
43
+ layer states intent and stops the honest path. It is known leaky, and it is not the guarantee.
44
+ - **The worktree layer contains the blast radius: a reviewer works in a detached review worktree.** The attach step —
45
+ already jr2-owned (`sandbox-kubectl.ts`'s `attachScript`) — adds a worktree at the sha under review with a detached
46
+ HEAD, sibling to the branch worktree per the Project layout. A write there cannot move the branch or dirty the coder's
47
+ worktree; a rogue commit lands on a detached HEAD and evaporates with the checkout. This is not a permission boundary
48
+ — it is containment, the property ADR-0024 bought for turns, bought for the filesystem.
49
+ - **Two layers, one structure: the tool list is the hint, the worktree is the guarantee** — the same shape as ADR-0024's
50
+ receipt (hint) and abort (guarantee). Neither layer alone: tools without the worktree leak through bash; the worktree
51
+ without tools invites the model to fight its checkout instead of declining politely.
52
+ - **The reviewing state requests the review worktree per round.** The sha under review moves between rounds, so the
53
+ detached worktree is attached (idempotently, by sha) on entry to the reviewing state, and its path frames the
54
+ reviewer's turn — cwd and prompt — instead of the branch worktree. The exact Workspace-port verb is settled at
55
+ implementation; what is decided here is the seat: worktree mechanics stay in the attach step and the Workspace
56
+ wrapper, never in Agent instructions.
57
+
58
+ ## Considered options
59
+
60
+ - **Prose alone** (today). Rejected by the incident; the coin flip is already on record.
61
+ - **A `SessionEnv` wrapper that throws on denied operations.** The right stopgap against the retired runtime's fixed
62
+ toolset — ~20 lines, no drift — and moot the day jr2 owns tool assembly. Building and testing a mechanism the
63
+ replacement deletes was the argument for folding this decision into ADR-0027.
64
+ - **Wait for flue 2.0's tool exports.** Rejected with ADR-0027.
65
+ - **A read-only filesystem for the reviewer.** Rejected: the reviewer must run tests and builds, which write caches,
66
+ `node_modules`, temp files. Read-only breaks the persona's own job description.
67
+ - **A separate reviewer Sandbox.** Rejected: it pays a pod per review round and breaks ADR-0012's premise — coder and
68
+ reviewer share one Workspace so the review sees exactly the bytes the coder produced, on the shared clone's economics.
69
+ - **Naming the field `tools: ["read", "grep", ...]`.** Rejected: it names the Harness's menu, breaks when a tool is
70
+ renamed, and says nothing about the Agent. `workspace` describes the persona's relationship to the thing it works in;
71
+ the tool list is derived mechanism.
72
+ - **Keeping the name `access`.** Rejected when `"none"` arrived: without the object in the name, `access: "none"` reads
73
+ as "no access to anything," which is false — the Agent has its full Menu. `workspace: "none"` states exactly what is
74
+ absent, and doubles as the placement statement above.
75
+
76
+ ## Consequences
77
+
78
+ - **The reviewer definition becomes `workspace: "read"`**, and the prose ban stays in its instructions as intent — the
79
+ half of the contract a model reads — no longer as the mechanism. A decisioner becomes `workspace: "none"`.
80
+ - **The definition stays plain data.** No code enters the ConfigMap channel; composition by re-export/spread (ADR-0018)
81
+ is preserved. Custom tool _implementations_ remain out of the contract — this decision adds a restriction vocabulary,
82
+ not an extension one. Growing extension tools is a separate, later decision.
83
+ - **GLOSSARY.md arbitrates the three senses of "tools"**: the Menu (control plane — what an Agent may say) and Working
84
+ tools (data plane — what it may do) become glossary entries; unqualified "tools" joins the Avoid lists.
85
+ - **The `@kind` tier gains the containment scenario**: from the review worktree, a write probe followed by a commit must
86
+ leave the branch ref and the coder's worktree untouched.
87
+ - **The zero-support guard stays available to workflows**: a workflow can still have both Agents report the sha and gate
88
+ on `event.head === context.head`. Self-reported and unenforced — but it catches the exact incident above, and it needs
89
+ nothing from this ADR.
@@ -0,0 +1,154 @@
1
+ # A Menu is fixed for a conversation, and a Turn is told its Allowed picks
2
+
3
+ [ADR-0015](0015-authoring-surface-absorbs-the-mechanism.md) derives what an Agent may pick from the invoking state's
4
+ transitions. Its config walk reads transition **keys** and nothing else, so a guard is invisible to it. Two failures
5
+ follow from where that answer is put.
6
+
7
+ **A pick that moves nothing reads as progress.** An event whose every transition is guarded false validates, reaches the
8
+ invoking state, and moves nothing. The receipt said _"the workflow is still in the state that asked for this turn"_ —
9
+ the same sentence a successful in-state move produces.
10
+ [ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md) exists because a receipt that says nothing
11
+ actionable gets answered by calling again.
12
+
13
+ **A tools block that changes bills the whole conversation again.** The first fix for the failure above narrowed the
14
+ tools block to what the guards would accept, per Submission. But provider prompt caches are prefix caches in the order
15
+ tools → system → messages: Anthropic invalidates every level when a tool definition changes, and vLLM and
16
+ OpenAI-compatible chat templates render the tools into the head of the prompt. Measured on the home-lab vLLM Instance
17
+ (2026-09-28, a 24.8k-token history): the next Turn with the same tools hit 24288 cached tokens; with one pick added, 0;
18
+ with the same picks reordered, 0. A continued conversation (`continue: true`, ADR-0057) changes state by design — a
19
+ coder in `implement` may `finish`, the same coder in `fix` may `finish` or `dispute` — so every such step prefilled the
20
+ whole history again. It also left the history calling tools the request no longer defined.
21
+
22
+ ## Decision
23
+
24
+ - **The Menu is the whole of what an Agent can ever be offered in its Machine, and it is the tools block.** It is the
25
+ union of the derived sets of every state that invokes an Agent of that name — the same scope as its continued
26
+ conversation's id (ADR-0057). `jr2Setup.createMachine` derives it in ADR-0015's walk, in a fixed order, so it is
27
+ byte-stable for every Turn of the conversation and for every fresh conversation of that Agent. The descriptions are
28
+ the event defs' own and do not vary by state, so a def's description must hold at every writing of it the Agent can
29
+ reach; the load warns where its writings on one path differ (ADR-0015).
30
+ - **What one Turn may pick is its Allowed picks**: the invoking state's derived set (its own, its ancestors' and its
31
+ modes' picks — ADR-0015), narrowed to what the live Machine would currently accept **from this Agent**: xstate's
32
+ selection for an event that carries the Agent's sender, a guard that reads the payload counted as passing, must move
33
+ something, and every writing it selects must be the Agent's (ADR-0015). Read off the live snapshot, they follow the
34
+ active mode as well as the guards; the Menu stays byte-identical in every mode. They are EMPTY while the Agent's state
35
+ is not at Rest — something it started runs below it, or a timer holds — because every pick is refused then
36
+ ([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)); an outstanding pick's answer carries them
37
+ read after the route. The surface serves both (`GET /agents/:iid/surface` → `menu` and `allowed`); the Harness
38
+ presents `menu` as the tools block and states `allowed` in the Briefing's Turn part, after the Frame's prompt
39
+ ([ADR-0062](0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md)). It rides the newest message, so it never
40
+ touches the cached prefix. The Briefing's standing part says how to read every such list: a pick is a call that the
41
+ newest "Allowed now" lists, and each pick's answer says if the Turn is over.
42
+ - **The Orchestrator enforces, at delivery, and only there.** A pick while the Agent's state is not at Rest is refused
43
+ before anything else, `finish` included (ADR-0024). A pick off the Menu is refused. A pick on the Menu is not refused
44
+ before delivery: one inside the invoking state's derived set that the Machine does not take — a pick of another mode
45
+ included — is delivered and answered `moved: false`; one that only another state of this Agent handles is answered
46
+ `moved: false` whatever its arguments, and never reaches the Machine, because only a state the Agent does not own (a
47
+ nested Agent's, a Gate's) could act on it there (ADR-0015). A pick whose selection, with its real payload, holds
48
+ another party's writing — a mode and a sibling Gate's or nested Agent's state that write the same name, say — is
49
+ answered `moved: false` the same way and does not reach the Machine (ADR-0015). Every answer carries `allowed`, read
50
+ **after** the delivery, and the Harness renders it ("Allowed now: `finish`"). A `moved: false` pick outside `allowed`
51
+ gets the **correction receipt** — "`<pick>` is not allowed in the workflow's current state, whatever its arguments.
52
+ Your turn is not over. Do not repeat this call — pick one of the allowed tools." — because the state refused it and
53
+ new arguments cannot help; one inside `allowed` was refused by a guard that read its payload, and its receipt says to
54
+ change the arguments. The Harness does not pre-check: its list is as old as what it last read, and an in-state move
55
+ (`moved: true, turnComplete: false`) can change the guards and the mode mid-Turn.
56
+ - **The Turn part is restated at a step boundary when the Allowed picks changed under the Agent.** A Gate's answer, an
57
+ outside event or a child's settled invoke the Agent did not pick can move the Machine while the Agent works with its
58
+ Working tools and picks nothing. So the Harness keeps the last Allowed-picks list the model read — the Turn part, then
59
+ every receipt and refusal — and after each model answer's tool results reads the surface once and compares the two as
60
+ sets. If they differ, it appends one message before the next model request: "The workflow's state changed since you
61
+ last read the Allowed picks. Allowed now: …", in a `<jr2-turn>` block. The Agent's own pick is never restated, because
62
+ its receipt already told the model. The read is one bounded attempt and a failure is silence; the next boundary asks
63
+ again. Nothing is read or said after the Turn ended or after an abort. The Harness knows the Turn ended from a pick's
64
+ answer (`turnComplete`, or a 404), not only from a read: under `continue: true` the next state re-registers the same
65
+ address before the abort lands, so a read there would answer for the next Turn. An empty list is not restated to a
66
+ model that stopped; the Orchestrator's wake asks it when a pick opens (ADR-0016's ask-again rule). The comparison is
67
+ engine-neutral (`between-steps.ts`); only the placement is the engine's — pi's between-steps input (`steer`). An
68
+ engine with no such input falls back to the next receipt. No route is added and no cap is set; the Orchestrator
69
+ already serves the truth on `GET /agents/:iid/surface`.
70
+ - **Presentation stays in the Harness's neutral layer.** `menu.ts` renders the Allowed-picks text and every receipt; a
71
+ presenter (`menu-tools.ts` for pi) only maps Menu items to its tool API and places the text it is handed. A second
72
+ harness — one that takes MCP tools fixed per session and the prompt as text — needs a presenter, not a second Menu.
73
+
74
+ **The guard question is asked twice, because the two askings can see different things.**
75
+
76
+ - The Allowed picks: payload-blind, asked as the Agent's sender (`takesNow`, ADR-0015) — or `mayMove(invoker, name)`
77
+ where no seat can be read (a plain `setup()` actor tree). The pick has not happened, so there are no arguments to
78
+ judge.
79
+ - Delivery: `wouldMove(invoker, event)` and xstate's own selection on the real event. The validated payload and the
80
+ sender are in hand, so the answer is exact.
81
+
82
+ **A guard that reads the pick is never hidden.** A guard like `({ event }) => event.verdict === "approved"` answers
83
+ `false` on a payload it was not given — by plain comparison against `undefined`, not by throwing. So the payload-blind
84
+ reading passes the event behind a Proxy that records any read outside `type` and `sender`, per guard, and trusts a
85
+ `false` only when the guard never looked. A guard that looked is allowed, and settled at delivery. This rests on xstate
86
+ handing a guard the object passed to `can()`, unspread — pinned by a test asserting a payload-reading guard stays
87
+ allowed. A future xstate that copies the event first would degrade this to allowing more, never less.
88
+
89
+ **Both readings fail open.** No invoker, or a guard that throws, reads as allowed. A wrong `true` costs one refused
90
+ pick; a wrong `false` tells an Agent its work is not wanted when the workflow would have taken it, and nothing recovers
91
+ from that.
92
+
93
+ **Absent `moved` is not `false`, and absent `allowed` is not empty.** The Harness ships as a stock image and the
94
+ Orchestrator (ADR-0027), so the two can skew. A surface without `menu` is read as today's narrowed list, presented as
95
+ tools.
96
+
97
+ **A Gate's accepted set stays whole; `acceptsNow` sits beside it.** `GET /runs/:id` lists each Gate's `accepts` — every
98
+ name it may ever send, with its schema, which drives a form — and `acceptsNow`, the same payload-blind question the
99
+ Allowed picks ask, asked as the Gate's sender (ADR-0011). Neither is filtered to empty into a fault: a Gate whose
100
+ `acceptsNow` is empty waits. A send the run would not take is refused 409 with `acceptsNow` (ADR-0013); a human reads
101
+ the Gate's `meta` and the refusal and can tell why, which is precisely what an Agent needs the Allowed picks for.
102
+
103
+ ## Why the text comes after the prompt, and why refusal is not optional
104
+
105
+ Measured on the home-lab vLLM model (Qwen3.6-35B-A3B, 10 trials per cell, a prompt arguing for a pick the Turn did not
106
+ allow): with the whole Menu as tools and no statement, 0/10 first picks were allowed; with the Allowed picks stated
107
+ **ahead** of the prompt, 0/10 (thinking off) and 5/10 (on), whatever the wording; stated **after** it, 10/10 both. On a
108
+ weaker pull the statement alone sufficed. After one refusal naming the Allowed picks, 57 of 58 wrong picks recovered on
109
+ the next call. So the statement carries the everyday case, and the refusal is the net under it. Narrowing the tools
110
+ block was not perfect either: once the model invented a tool it had never been given.
111
+
112
+ ## Considered options
113
+
114
+ - **Narrow the tools block per Turn** (this ADR's first form). Rejected: it breaks the prefix cache on every Menu
115
+ change, and the history then calls tools the request no longer defines — which models are observed to call anyway.
116
+ - **Fix the tools per invoking state, drop guard narrowing.** Rejected: fixes the within-state case, not ADR-0057's
117
+ cross-state `continue`.
118
+ - **The Machine's whole agent-audience vocabulary as the Menu.** Rejected: it offers picks only another Agent's states
119
+ handle.
120
+ - **Put the Menu tools last.** Rejected: measured, a reordered tools block misses as completely as a changed one.
121
+ - **Mask at decode time** (OpenAI `allowed_tools`, logit masking). Deferred: it is provider-specific — Anthropic has no
122
+ subset form, and changing `tool_choice` there drops the messages cache; vLLM's Chat Completions refuses it. The design
123
+ must hold without it; where it is cache-safe, it can be added in the presenter.
124
+ - **Refuse the delivery outright when `moved` is false.** Rejected for a guard that says no: `deliver` is the single
125
+ validation path behind both dialects, and the receipt already carries the distinction. A pick whose selection holds
126
+ another party's writing is a different class — delivering it would run that party's writing — so it is answered like a
127
+ pick off the invoking state's derived set, and never delivered.
128
+ - **Filter mid-Turn and push `list_changed`.** Rejected: nothing pushes to the Harness (ADR-0013), and a list that moves
129
+ under an Agent invites the retry loop from the other direction.
130
+ - **The Orchestrator pushes the restated Turn part over a new route.** Rejected: pi appends only at a step boundary, so
131
+ a push arrives no earlier than the Harness's read, and it needs a route that writes text into a conversation, change
132
+ detection over every run, and a copy of what the model read that an Orchestrator restart loses.
133
+ - **Parse a JSON-text argument where the pick's schema declares an object** (pi's `prepareArguments`, before the engine
134
+ validates). Rejected, measured: on the home-lab instrument writing a Chart
135
+ ([ADR-0070](0070-a-machine-lends-its-parts-to-a-chart.md)) about one call in three carried the object as a string, and
136
+ 0 of 89 such strings were JSON — each had its braces miscounted around a `{{...}}` reference — so the parse would have
137
+ fired on none. A leniency that no run can show working is not kept on a hypothesis; a model that stringifies cleanly
138
+ will show it in its own trials, and the ten lines come back with that number.
139
+ - **The Allowed picks on every Working-tool result.** Rejected: a statement repeated where nothing changed is one the
140
+ model learns to skip.
141
+
142
+ ## Consequences
143
+
144
+ - **One state can allow different picks as its context changes or its active mode changes; the Menu does not change.**
145
+ The conformance tier asserts it: two Submissions with different Allowed picks send a byte-identical tools block
146
+ (ADR-0027).
147
+ - **A restatement is appended, never rewritten.** Conformance asserts that each request of a Submission is a byte prefix
148
+ of the next, with the tools and the system prompt unchanged, and that the restatement sits right after the model
149
+ answer's last tool result.
150
+ - **A workflow expresses "not available yet" with a guard alone,** and the Agent is told so rather than finding a tool
151
+ that silently does nothing.
152
+ - **`on: { X: {} }` — targetless and actionless — is never allowed,** matching xstate's `can()`.
153
+ - **The receipt declarations stay hand-synchronized** across `run-host.ts` and the Harness's `menu.ts`, now carrying
154
+ `allowed` too — the cost of the packages not importing each other.
@@ -0,0 +1,100 @@
1
+ # A snapshot names the Machine it was written under
2
+
3
+ [ADR-0007](0007-durable-machine-state.md) persists a run's snapshot so a restarted Orchestrator resumes it.
4
+ [ADR-0008](0008-orchestrator-as-deployed-app.md)/[ADR-0019](0019-one-converging-command-against-the-current-context.md)
5
+ publish the Instance's Assembly to an Orchestrator whose volume outlives every publish
6
+ ([ADR-0069](0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md)). And a persisted run is
7
+ matched to its workflow by **filename stem**: by name inside the Assembly its snapshot names, and, for a run that no
8
+ Assembly started (an in-process test host), by name in the registry.
9
+
10
+ Those three facts compose into a hole. Park a run at a Gate, edit `workflows/task.ts`, `jr2 up`. The name still
11
+ resolves. The def found under it is a different Machine. Nothing checks, and — this is the part that decides the shape
12
+ of the fix — **xstate does not validate a restored state value.** A snapshot whose `value` names a state the new chart
13
+ does not have does not throw: `createActor` accepts it and the run starts with `value: undefined`, carrying on from
14
+ nowhere. There is no downstream check to fall back on. Either restore refuses, or nothing does.
15
+
16
+ ## Decision
17
+
18
+ **Every save stamps the shape of the Machine that wrote it; every restore compares before interpreting.** `RunBlob`
19
+ gains `machine`, a 12-hex digest — the same width `jr2 up`'s content hash uses, and for the same reason: it is read by
20
+ people in error messages more often than by code. A mismatch is refused before `reconcile`, because proving a Sandbox is
21
+ alive for a run that cannot be read is wasted work.
22
+
23
+ **The digest covers restorability, not behavior.** It is built from `machine-doc.ts`'s traversal — already deterministic
24
+ (states sorted by `order`), JSON-pure, provider-independent, and descending into invoked and spawned child machines —
25
+ projected down to the facts a snapshot has to agree with:
26
+
27
+ - state ids, keys, types, nesting, and which child is `initial`;
28
+ - each state's invokes by `id` **and** `src` (the id is the key in the persisted `children` map, so re-attach depends on
29
+ it — the most easily missed of the three, and a reason a `src`-only hash would not do);
30
+ - every transition as `source|event|targets`, **sorted**, so reordering `on:` keys is not drift.
31
+
32
+ Deliberately excluded: guard bodies, actions and assigns, prompts, descriptions. Editing a guard changes what a run does
33
+ _next_; it does not make the snapshot unreadable. The alternative — any edit to a workflow strands its parked runs — is
34
+ defensible in the abstract and unusable in practice: it makes `jr2 dev`'s reload a migration event and prompt-tuning a
35
+ stop-the-world operation. **Drift means "I can no longer read what I saved," not "something changed."**
36
+
37
+ **Absent means drift.** A blob written before the stamp existed cannot be vouched for, and the point is to never
38
+ interpret one that cannot be. Greenfield, so a stale local `.jr2/state.db` is deleted, not migrated.
39
+
40
+ **A drifted run is refused, kept, and readable.** Not `markLost`: that nulls the snapshot, and `read()` returns
41
+ undefined for a null blob, so `jr2 status <id>` would answer `no run "<id>"` — a refusal indistinguishable from a run
42
+ that never existed, on precisely the run a human most needs to look at. `drifted` is its own status, the snapshot is
43
+ untouched, and `RunStatus` gains `reason`, which nothing on any HTTP route surfaced before. The refusal names the
44
+ workflow and both digests.
45
+
46
+ **A throw during restore is a different claim from drift, and is not recorded as one.** Drift is durable — the Machine
47
+ will still have changed on the next boot. A `reconcile` failure is a kubectl blip, and condemning a run for it would
48
+ make a transient fault permanent (or, via `markLost`, throw the snapshot away). Those rows are left `live` for the next
49
+ boot to retry, reported through `onRestoreError`, and counted separately as `failed`.
50
+
51
+ **One unresumable run is one run.** The restore loop is sequential, and a throw used to reject `restore()`, which
52
+ rejects `startInstance`, which crash-loops the pod — taking every _later_ run in the store down with it, unrestored.
53
+ Each run is now handled independently.
54
+
55
+ **The boot announces what it did not resume.** `restore()` returns `{ reattached, lost, drifted, failed }`, and the
56
+ entrypoint puts the last three on its announce line. A boot with no current Assembly lists its runs as `waiting`
57
+ instead, and the first publish, which restores them, announces one line `{ assembly, lost?, drifted?, failed? }` when
58
+ any was not resumed ([ADR-0069](0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md)).
59
+ Resumed runs are routine and stay quiet. Without this, `drifted` is reachable only by asking after a run id nobody knows
60
+ to ask about.
61
+
62
+ ## Considered options
63
+
64
+ - **Hash the workflow module source, or the whole config including guards and actions.** Rejected — see above. Maximal
65
+ safety at the cost of making every workflow edit a migration.
66
+ - **Reuse `JR2_CONTENT_HASH`**, which `jr2 up` already computes and injects. Rejected: it is a digest of the whole
67
+ staged bundle, so it moves on a lockfile bump, a README edit, or a kit upgrade — constant false drift — and it is
68
+ absent under `jr2 dev` entirely.
69
+ - **Topology plus named guards** (`setup()` keys and named functions). Rejected: inline arrows collapse to `"inline"` in
70
+ the doc and would stay invisible, so the coverage is uneven in a way no one could predict from reading a workflow.
71
+ - **A `machine_snapshots` schema column.** Rejected: `init()` is a single `CREATE TABLE IF NOT EXISTS` with no migration
72
+ mechanism, so a new column would silently not appear on an existing PVC. The blob is versioned by being JSON.
73
+ - **Warn and resume anyway.** Rejected: there is no safe interpretation to fall back on. xstate resumes a missing state
74
+ as `undefined` rather than failing, so a warning would be the only signal, on a run already running wrong.
75
+ - **Fold the workflow name into the digest.** Rejected: restore resolves the def by name first, inside the run's own
76
+ Assembly, so a rename never reaches the comparison — it would only make a rename look like a shape change on a run
77
+ that no longer resolves at all.
78
+
79
+ ## Consequences
80
+
81
+ - **Editing a workflow strands nothing.** A run holds the Assembly it started under, through restarts too
82
+ ([ADR-0069](0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md)), so a publish never
83
+ changes the Machine a live run resolves to. Drift is reached only when a held Assembly cannot be read any more: it
84
+ does not load under a bumped Kit version, or the volume lost it. The stamp still stands as the last check, because the
85
+ Assembly hash says which tree was loaded and the stamp says the Machine in it still has the shape it had.
86
+ - **There is no migration path.** A drifted run can be read and cancelled; it cannot be adapted onto a new Machine. That
87
+ is deliberate for now — a migration story needs a way to express what a state maps to, which nothing here does.
88
+ - **A run with an Open body keeps its wrapper's stamp.** A Chart
89
+ ([ADR-0070](0070-a-machine-lends-its-parts-to-a-chart.md)) is the run's data, not its Machine's shape: the Chart's
90
+ text and its hash ride the body's own snapshot, restore admits the text again and compares the hash, and a mismatch or
91
+ a refusal is drift naming the body slot and the reason, after the stamp check and in the same words.
92
+ - **A drifted run is not in `jr2 runs`**, which lists only the live registry. It is reachable through the announce line
93
+ and `jr2 status <id>`. If stranded runs become common enough to browse, the listing is where that belongs.
94
+ - **The digest is only as complete as the walk.** A state running `enqueueActions` can spawn children no static analysis
95
+ recovers (`machine-doc.ts`'s `opaqueStates`); the opacity is folded into the hash, but what it hides is not. Drift
96
+ inside such a subtree can go unnoticed.
97
+ - **`fingerprintOf` is memoized per machine object** — `persist` runs on every snapshot microtask, and a dev reload's
98
+ fresh machine object correctly gets a fresh entry. That is not `vocabularyOf`'s key, which is `machine.config` so a
99
+ `.provide()` clone keeps the defs it was built with (ADR-0011); a shape has nothing to keep, being derived from the
100
+ config alone, so a clone recomputes the identical digest.