@jr2/orchestrator 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (361) 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 +478 -0
  10. package/docs/concepts/architecture.md +243 -0
  11. package/docs/concepts/charts.md +146 -0
  12. package/docs/concepts/composition.md +357 -0
  13. package/docs/concepts/converge.md +257 -0
  14. package/docs/concepts/durable-runs.md +189 -0
  15. package/docs/concepts/effects.md +658 -0
  16. package/docs/concepts/gates.md +208 -0
  17. package/docs/concepts/machines-and-workflows.md +298 -0
  18. package/docs/concepts/pools-and-sources.md +230 -0
  19. package/docs/concepts/secrets-and-trust.md +306 -0
  20. package/docs/concepts/the-menu.md +381 -0
  21. package/docs/concepts/workspaces.md +756 -0
  22. package/docs/get-started/a-model-decides.md +563 -0
  23. package/docs/get-started/an-agent-in-a-sandbox.md +383 -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 +993 -0
  27. package/docs/get-started/install.md +130 -0
  28. package/docs/guides/add-tools-to-the-sandbox.md +308 -0
  29. package/docs/guides/answer-a-gate.md +304 -0
  30. package/docs/guides/bind-open-parts.md +224 -0
  31. package/docs/guides/cancel-a-run.md +146 -0
  32. package/docs/guides/clean-up.md +206 -0
  33. package/docs/guides/cut-a-worktree.md +560 -0
  34. package/docs/guides/fix-a-failed-converge.md +361 -0
  35. package/docs/guides/give-the-cluster-git-credentials.md +222 -0
  36. package/docs/guides/iterate-on-a-machine-on-kind.md +190 -0
  37. package/docs/guides/let-an-agent-write-a-chart.md +230 -0
  38. package/docs/guides/look-inside-a-sandbox.md +177 -0
  39. package/docs/guides/mirror-the-kit-images.md +176 -0
  40. package/docs/guides/move-from-kind-to-a-shared-cluster.md +318 -0
  41. package/docs/guides/open-the-console.md +91 -0
  42. package/docs/guides/publish-a-machine-as-an-npm-package.md +350 -0
  43. package/docs/guides/push-and-sign-a-branch.md +532 -0
  44. package/docs/guides/run-a-chart-by-name.md +270 -0
  45. package/docs/guides/run-a-command-or-call-a-url.md +587 -0
  46. package/docs/guides/run-one-worker-per-item.md +322 -0
  47. package/docs/guides/set-an-agents-access-and-dials.md +420 -0
  48. package/docs/guides/size-a-workspace.md +217 -0
  49. package/docs/guides/start-a-run-from-a-run.md +237 -0
  50. package/docs/guides/start-a-run.md +207 -0
  51. package/docs/guides/upgrade-the-kit-version.md +240 -0
  52. package/docs/guides/use-a-hosted-provider.md +214 -0
  53. package/docs/guides/use-a-local-model.md +313 -0
  54. package/docs/guides/watch-a-run.md +349 -0
  55. package/docs/guides/write-a-workflow.md +318 -0
  56. package/docs/index.md +44 -0
  57. package/docs/llms.txt +237 -0
  58. package/docs/reference/authoring-surface/agent.md +151 -0
  59. package/docs/reference/authoring-surface/answer.md +156 -0
  60. package/docs/reference/authoring-surface/arg.md +82 -0
  61. package/docs/reference/authoring-surface/basic.md +62 -0
  62. package/docs/reference/authoring-surface/bearer.md +53 -0
  63. package/docs/reference/authoring-surface/branchname.md +15 -0
  64. package/docs/reference/authoring-surface/chartschema.md +23 -0
  65. package/docs/reference/authoring-surface/chartschemafor.md +48 -0
  66. package/docs/reference/authoring-surface/command.md +206 -0
  67. package/docs/reference/authoring-surface/customize.md +64 -0
  68. package/docs/reference/authoring-surface/defineconfig.md +41 -0
  69. package/docs/reference/authoring-surface/defineevent.md +114 -0
  70. package/docs/reference/authoring-surface/dispatch.md +126 -0
  71. package/docs/reference/authoring-surface/fail.md +96 -0
  72. package/docs/reference/authoring-surface/file.md +142 -0
  73. package/docs/reference/authoring-surface/frompick.md +277 -0
  74. package/docs/reference/authoring-surface/http.md +259 -0
  75. package/docs/reference/authoring-surface/isfailure.md +58 -0
  76. package/docs/reference/authoring-surface/isready.md +14 -0
  77. package/docs/reference/authoring-surface/jr2setup.md +237 -0
  78. package/docs/reference/authoring-surface/open.md +105 -0
  79. package/docs/reference/authoring-surface/openbody.md +86 -0
  80. package/docs/reference/authoring-surface/paramsof.md +143 -0
  81. package/docs/reference/authoring-surface/pool.md +186 -0
  82. package/docs/reference/authoring-surface/push.md +130 -0
  83. package/docs/reference/authoring-surface/readyof.md +21 -0
  84. package/docs/reference/authoring-surface/secret.md +139 -0
  85. package/docs/reference/authoring-surface/sentby.md +83 -0
  86. package/docs/reference/authoring-surface/source.md +51 -0
  87. package/docs/reference/authoring-surface/text.md +62 -0
  88. package/docs/reference/authoring-surface/type-agentdeclaration.md +27 -0
  89. package/docs/reference/authoring-surface/type-agentdefinition.md +80 -0
  90. package/docs/reference/authoring-surface/type-agentlogic.md +15 -0
  91. package/docs/reference/authoring-surface/type-agentslots.md +15 -0
  92. package/docs/reference/authoring-surface/type-answeraction.md +12 -0
  93. package/docs/reference/authoring-surface/type-anyframepath.md +14 -0
  94. package/docs/reference/authoring-surface/type-anyworktreepath.md +15 -0
  95. package/docs/reference/authoring-surface/type-arg.md +19 -0
  96. package/docs/reference/authoring-surface/type-argnames.md +12 -0
  97. package/docs/reference/authoring-surface/type-bindactors.md +14 -0
  98. package/docs/reference/authoring-surface/type-binding.md +61 -0
  99. package/docs/reference/authoring-surface/type-bindingof.md +12 -0
  100. package/docs/reference/authoring-surface/type-bodyacceptsdoor.md +15 -0
  101. package/docs/reference/authoring-surface/type-bodyacceptsslots.md +17 -0
  102. package/docs/reference/authoring-surface/type-ceilings.md +41 -0
  103. package/docs/reference/authoring-surface/type-chainlink.md +19 -0
  104. package/docs/reference/authoring-surface/type-chartaction.md +46 -0
  105. package/docs/reference/authoring-surface/type-chartagent.md +28 -0
  106. package/docs/reference/authoring-surface/type-chartevent.md +20 -0
  107. package/docs/reference/authoring-surface/type-chartguard.md +20 -0
  108. package/docs/reference/authoring-surface/type-chartinvoke.md +28 -0
  109. package/docs/reference/authoring-surface/type-chartjson.md +14 -0
  110. package/docs/reference/authoring-surface/type-chartstate.md +46 -0
  111. package/docs/reference/authoring-surface/type-charttext.md +40 -0
  112. package/docs/reference/authoring-surface/type-charttransition.md +22 -0
  113. package/docs/reference/authoring-surface/type-charttransitions.md +14 -0
  114. package/docs/reference/authoring-surface/type-chartvalue.md +15 -0
  115. package/docs/reference/authoring-surface/type-childat.md +12 -0
  116. package/docs/reference/authoring-surface/type-childslots.md +12 -0
  117. package/docs/reference/authoring-surface/type-commandenv.md +15 -0
  118. package/docs/reference/authoring-surface/type-commandinput.md +20 -0
  119. package/docs/reference/authoring-surface/type-commandoptions.md +56 -0
  120. package/docs/reference/authoring-surface/type-commandoutput.md +122 -0
  121. package/docs/reference/authoring-surface/type-customize.md +111 -0
  122. package/docs/reference/authoring-surface/type-dispatchdefinition.md +39 -0
  123. package/docs/reference/authoring-surface/type-dispatchinput.md +14 -0
  124. package/docs/reference/authoring-surface/type-dispatchopenparts.md +14 -0
  125. package/docs/reference/authoring-surface/type-dispatchoutput.md +72 -0
  126. package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
  127. package/docs/reference/authoring-surface/type-duration.md +15 -0
  128. package/docs/reference/authoring-surface/type-effectbinding.md +93 -0
  129. package/docs/reference/authoring-surface/type-effectlogic.md +12 -0
  130. package/docs/reference/authoring-surface/type-envopenparts.md +14 -0
  131. package/docs/reference/authoring-surface/type-eventaudience.md +27 -0
  132. package/docs/reference/authoring-surface/type-eventdef.md +45 -0
  133. package/docs/reference/authoring-surface/type-eventfrom.md +16 -0
  134. package/docs/reference/authoring-surface/type-eventsemantics.md +26 -0
  135. package/docs/reference/authoring-surface/type-execoutcome.md +34 -0
  136. package/docs/reference/authoring-surface/type-failslot.md +14 -0
  137. package/docs/reference/authoring-surface/type-failure.md +19 -0
  138. package/docs/reference/authoring-surface/type-failureinput.md +20 -0
  139. package/docs/reference/authoring-surface/type-filebody.md +37 -0
  140. package/docs/reference/authoring-surface/type-framepath.md +14 -0
  141. package/docs/reference/authoring-surface/type-frominput.md +14 -0
  142. package/docs/reference/authoring-surface/type-hostinjectedinput.md +56 -0
  143. package/docs/reference/authoring-surface/type-httpdefinition.md +104 -0
  144. package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
  145. package/docs/reference/authoring-surface/type-httpinput.md +12 -0
  146. package/docs/reference/authoring-surface/type-httpmethod.md +14 -0
  147. package/docs/reference/authoring-surface/type-httpopenparts.md +12 -0
  148. package/docs/reference/authoring-surface/type-httpoutput.md +95 -0
  149. package/docs/reference/authoring-surface/type-httprequestparts.md +33 -0
  150. package/docs/reference/authoring-surface/type-jr2config.md +126 -0
  151. package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
  152. package/docs/reference/authoring-surface/type-lends.md +38 -0
  153. package/docs/reference/authoring-surface/type-logicat.md +12 -0
  154. package/docs/reference/authoring-surface/type-markedif.md +12 -0
  155. package/docs/reference/authoring-surface/type-mergedactors.md +12 -0
  156. package/docs/reference/authoring-surface/type-namedsecret.md +25 -0
  157. package/docs/reference/authoring-surface/type-namestart.md +77 -0
  158. package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +27 -0
  159. package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
  160. package/docs/reference/authoring-surface/type-openbodydeclaration.md +40 -0
  161. package/docs/reference/authoring-surface/type-openbodylogic.md +14 -0
  162. package/docs/reference/authoring-surface/type-openbodyoutput.md +42 -0
  163. package/docs/reference/authoring-surface/type-openbodyslot.md +18 -0
  164. package/docs/reference/authoring-surface/type-opencommandinput.md +22 -0
  165. package/docs/reference/authoring-surface/type-openeffect.md +12 -0
  166. package/docs/reference/authoring-surface/type-openeffectslots.md +12 -0
  167. package/docs/reference/authoring-surface/type-openof.md +12 -0
  168. package/docs/reference/authoring-surface/type-opensecret.md +23 -0
  169. package/docs/reference/authoring-surface/type-opensecretmark.md +12 -0
  170. package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +26 -0
  171. package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +27 -0
  172. package/docs/reference/authoring-surface/type-poolmachine.md +28 -0
  173. package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
  174. package/docs/reference/authoring-surface/type-poolspec.md +60 -0
  175. package/docs/reference/authoring-surface/type-pushinput.md +34 -0
  176. package/docs/reference/authoring-surface/type-pushoptions.md +79 -0
  177. package/docs/reference/authoring-surface/type-pushoutput.md +149 -0
  178. package/docs/reference/authoring-surface/type-pushwireoutput.md +12 -0
  179. package/docs/reference/authoring-surface/type-quantity.md +14 -0
  180. package/docs/reference/authoring-surface/type-reached.md +12 -0
  181. package/docs/reference/authoring-surface/type-reposlot.md +70 -0
  182. package/docs/reference/authoring-surface/type-reposlotsof.md +16 -0
  183. package/docs/reference/authoring-surface/type-sandboxoptions.md +84 -0
  184. package/docs/reference/authoring-surface/type-secret.md +25 -0
  185. package/docs/reference/authoring-surface/type-sender.md +19 -0
  186. package/docs/reference/authoring-surface/type-sentbyguards.md +12 -0
  187. package/docs/reference/authoring-surface/type-size.md +85 -0
  188. package/docs/reference/authoring-surface/type-slothandles.md +24 -0
  189. package/docs/reference/authoring-surface/type-sourcenextresult.md +17 -0
  190. package/docs/reference/authoring-surface/type-sourcespec.md +44 -0
  191. package/docs/reference/authoring-surface/type-syncstate.md +12 -0
  192. package/docs/reference/authoring-surface/type-text.md +19 -0
  193. package/docs/reference/authoring-surface/type-textpart.md +24 -0
  194. package/docs/reference/authoring-surface/type-thinkinglevel.md +39 -0
  195. package/docs/reference/authoring-surface/type-toparameterizedobject.md +14 -0
  196. package/docs/reference/authoring-surface/type-toprovidedactor.md +14 -0
  197. package/docs/reference/authoring-surface/type-unknown.md +21 -0
  198. package/docs/reference/authoring-surface/type-usercontainer.md +77 -0
  199. package/docs/reference/authoring-surface/type-withoutemptyindex.md +20 -0
  200. package/docs/reference/authoring-surface/type-workflowevent.md +46 -0
  201. package/docs/reference/authoring-surface/type-workspaceaccess.md +16 -0
  202. package/docs/reference/authoring-surface/type-workspaced.md +115 -0
  203. package/docs/reference/authoring-surface/type-workspacehandles.md +22 -0
  204. package/docs/reference/authoring-surface/type-workspacemachine.md +29 -0
  205. package/docs/reference/authoring-surface/type-workspaceof.md +13 -0
  206. package/docs/reference/authoring-surface/type-workspaceoptions.md +27 -0
  207. package/docs/reference/authoring-surface/type-worktreefound.md +16 -0
  208. package/docs/reference/authoring-surface/type-worktreeinput.md +56 -0
  209. package/docs/reference/authoring-surface/type-worktreekey.md +14 -0
  210. package/docs/reference/authoring-surface/type-worktreeoptions.md +29 -0
  211. package/docs/reference/authoring-surface/type-worktreeoutput.md +45 -0
  212. package/docs/reference/authoring-surface/type-worktreepath.md +16 -0
  213. package/docs/reference/authoring-surface/type-worktreeready.md +37 -0
  214. package/docs/reference/authoring-surface/type-worktreerefused.md +21 -0
  215. package/docs/reference/authoring-surface/workspace.md +287 -0
  216. package/docs/reference/authoring-surface/worktree.md +71 -0
  217. package/docs/reference/authoring-surface.md +217 -0
  218. package/docs/reference/cli.md +433 -0
  219. package/docs/reference/config.md +765 -0
  220. package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +114 -0
  221. package/docs/reference/decisions/0002-duplex-streaming-actor.md +117 -0
  222. package/docs/reference/decisions/0004-worktree-provisioning-model.md +130 -0
  223. package/docs/reference/decisions/0005-sandbox-pod-composition.md +240 -0
  224. package/docs/reference/decisions/0006-agent-control-surface.md +50 -0
  225. package/docs/reference/decisions/0007-durable-machine-state.md +109 -0
  226. package/docs/reference/decisions/0008-orchestrator-as-deployed-app.md +60 -0
  227. package/docs/reference/decisions/0009-cli-and-instance-interface.md +275 -0
  228. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +160 -0
  229. package/docs/reference/decisions/0011-workflow-defined-events.md +141 -0
  230. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +103 -0
  231. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +175 -0
  232. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +95 -0
  233. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +271 -0
  234. package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +126 -0
  235. package/docs/reference/decisions/0017-pool-over-a-source-port.md +71 -0
  236. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +111 -0
  237. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +287 -0
  238. package/docs/reference/decisions/0020-private-ca-trust-is-a-named-concept.md +60 -0
  239. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +101 -0
  240. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +103 -0
  241. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +175 -0
  242. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +436 -0
  243. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +65 -0
  244. package/docs/reference/decisions/0026-a-turn-that-is-over-is-never-prompted.md +62 -0
  245. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +245 -0
  246. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +90 -0
  247. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +154 -0
  248. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +100 -0
  249. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +193 -0
  250. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +61 -0
  251. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +126 -0
  252. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +57 -0
  253. package/docs/reference/decisions/0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md +92 -0
  254. package/docs/reference/decisions/0036-a-turn-compacts-its-own-context-mid-flight.md +138 -0
  255. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +196 -0
  256. package/docs/reference/decisions/0038-jr2-up-builds-every-image-it-deploys.md +170 -0
  257. package/docs/reference/decisions/0039-image-garbage-collects-by-reachability.md +115 -0
  258. package/docs/reference/decisions/0041-a-build-the-host-already-holds-is-not-spent-again.md +64 -0
  259. package/docs/reference/decisions/0042-ready-is-not-routable-an-admission-retries-its-connection.md +142 -0
  260. package/docs/reference/decisions/0043-the-kit-is-tested-as-installed-a-local-registry-stands-in-for-npm.md +170 -0
  261. package/docs/reference/decisions/0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md +85 -0
  262. package/docs/reference/decisions/0045-the-platform-joins-the-image-address-and-the-cluster-chooses-it.md +82 -0
  263. package/docs/reference/decisions/0046-a-converge-failure-carries-its-diagnosis.md +34 -0
  264. package/docs/reference/decisions/0047-the-git-ssh-key-source-is-the-users-choice.md +47 -0
  265. package/docs/reference/decisions/0048-the-orchestrator-boots-without-its-repos.md +36 -0
  266. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +126 -0
  267. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +73 -0
  268. package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +238 -0
  269. package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +86 -0
  270. package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +123 -0
  271. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +126 -0
  272. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +107 -0
  273. package/docs/reference/decisions/0056-a-global-jr2-hands-off-to-the-instances-own-and-an-instance-has-one-kit-version.md +62 -0
  274. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +152 -0
  275. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +108 -0
  276. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +427 -0
  277. package/docs/reference/decisions/0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md +111 -0
  278. package/docs/reference/decisions/0061-a-memory-kill-ends-the-agents-processes-not-the-harness.md +83 -0
  279. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +239 -0
  280. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +82 -0
  281. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +79 -0
  282. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +65 -0
  283. package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +552 -0
  284. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +458 -0
  285. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +475 -0
  286. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +180 -0
  287. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +334 -0
  288. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +183 -0
  289. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +144 -0
  290. package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +122 -0
  291. package/docs/reference/decisions/0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md +76 -0
  292. package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +295 -0
  293. package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +320 -0
  294. package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
  295. package/docs/reference/decisions.md +86 -0
  296. package/docs/reference/glossary.md +687 -0
  297. package/docs/reference/http-api.md +1386 -0
  298. package/docs/reference/machines.md +189 -0
  299. package/docs/reference/mechanism-events.md +72 -0
  300. package/docs/why.md +207 -0
  301. package/package.json +5 -3
  302. package/src/actor.ts +694 -106
  303. package/src/agent.ts +114 -38
  304. package/src/ambient.ts +3 -4
  305. package/src/assembly-checks.ts +189 -7
  306. package/src/cause.ts +234 -0
  307. package/src/chart.ts +345 -46
  308. package/src/charts.ts +43 -8
  309. package/src/config.ts +440 -178
  310. package/src/custodian.lua +17 -5
  311. package/src/custodian.ts +325 -99
  312. package/src/customize.ts +199 -49
  313. package/src/dispatch-effect.ts +945 -0
  314. package/src/door-compat.ts +217 -0
  315. package/src/durability.ts +11 -4
  316. package/src/effect-checks.ts +50 -3
  317. package/src/effect.ts +641 -128
  318. package/src/effector-wire.ts +165 -13
  319. package/src/fail.ts +222 -0
  320. package/src/harness-client.ts +587 -50
  321. package/src/harness-env.ts +33 -0
  322. package/src/held-secrets.ts +125 -16
  323. package/src/holds.ts +3 -0
  324. package/src/http-effect.ts +556 -58
  325. package/src/http.ts +480 -30
  326. package/src/index.ts +74 -82
  327. package/src/instance.ts +68 -8
  328. package/src/internal.ts +85 -0
  329. package/src/kube-client.ts +6 -3
  330. package/src/machine-doc.ts +24 -7
  331. package/src/mechanism-routes.ts +168 -0
  332. package/src/model-keys.generated.ts +53 -0
  333. package/src/model-keys.ts +42 -58
  334. package/src/names.ts +19 -6
  335. package/src/notices.ts +483 -41
  336. package/src/open-body.ts +242 -40
  337. package/src/open-effect.ts +81 -8
  338. package/src/open.ts +90 -6
  339. package/src/parts.ts +213 -30
  340. package/src/party.ts +101 -2
  341. package/src/pick.ts +497 -11
  342. package/src/pool.ts +282 -50
  343. package/src/registration.ts +133 -12
  344. package/src/repo-identity.ts +7 -3
  345. package/src/repos.ts +22 -3
  346. package/src/run-host.ts +665 -45
  347. package/src/run-output.ts +38 -0
  348. package/src/sandbox-effect.ts +457 -71
  349. package/src/sandbox-kube.ts +66 -17
  350. package/src/secret-placement.ts +59 -17
  351. package/src/server.ts +3 -0
  352. package/src/setup.ts +325 -62
  353. package/src/size.ts +121 -9
  354. package/src/snapshot-store.ts +54 -10
  355. package/src/stub-harness.ts +44 -16
  356. package/src/timers.ts +14 -1
  357. package/src/tokens.ts +107 -32
  358. package/src/vocabulary.ts +88 -14
  359. package/src/wire.ts +100 -26
  360. package/src/workspace.ts +771 -238
  361. package/src/worktree.ts +249 -0
@@ -0,0 +1,111 @@
1
+ # The instance authors Agent definitions; jr2 assembles the Harness
2
+
3
+ Building the first real Harness (the jr-parity exercise, 2026-07-18) landed a **full harness-framework project** in the
4
+ instance — package.json, runtime config, Dockerfile, per-agent source (under flue, the embedded framework since retired
5
+ — [ADR-0027](0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md)). That put jr2 mechanism in the user's
6
+ hands: the [ADR-0013](0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md) leash (read the Menu per
7
+ Submission — forget it and the Agent is silently mute, the same footgun class ADR-0016 removed from workflows), the
8
+ ADR-0004/0012 sandbox geography (working tools execute in the Harness container against `/work`), the dependency pin
9
+ that must match the Orchestrator's wire, and the image contracts the operator imposes (numeric uid, `:8080` = Ready, git
10
+ present). The only content a user genuinely owns is the persona: model + instructions (+ someday tools/skills).
11
+ GLOSSARY.md already promised the split — an Agent is a definition jr2 maps into the Harness, and the Instance folder
12
+ holds only what a Machine cannot carry (ADR-0049/0050: the definition is one of the things it can).
13
+
14
+ ## Decision
15
+
16
+ - **An Agent is a plain-data definition carried by a Machine as an actor slot** —
17
+ `actors: { <name>: agent({ model, instructions, … }) }`, slot key = Agent name, invoked as `src: "<name>"` (ADR-0049).
18
+ The definition may be written inline or imported from any module. Serializable persona data only; **the instance never
19
+ imports the Harness runtime**. `model` is **required**: every Agent is independently valid, and definitions are the
20
+ only models `jr2 up` can preflight — it collects them by walking the registered Machines. (The first cut
21
+ filename-discovered `agents/<name>.ts`, mirroring `workflows/`, and a second put them in `jr2.config.ts`; both were
22
+ Instance rosters, and a roster cannot hold two Machines' `coder`s.)
23
+ - **The Harness image is stock and published** — `jr2-harness:<kitversion>`, one release train with the kit (like the
24
+ operator image, ADR-0019). An instance builds **no** Harness image. The image is `@jr2/harness`, jr2's own server
25
+ (ADR-0027), honoring the operator's image contracts.
26
+ - **The definition rides the Turn, never the image.** The admission body carries the slot's definition (ADR-0049); the
27
+ server validates it loudly and re-reads model/instructions per Submission, with the Turn's Frame (`prompt`, `cwd`) and
28
+ Dials beside it on the admit body (ADR-0057). No image is built anywhere for a definition edit. (A ConfigMap roster
29
+ read at boot — `JR2_AGENTS_JSON` — was the mechanism between the codegen-at-boot cut and this one; it retired because
30
+ a Machine edit already publishes the Assembly again, so the roster bought nothing a Machine-carried definition loses.)
31
+ - **The kit's server assembly carries the mechanism**: the Menu leash, rooting the Working tools at the Frame's cwd,
32
+ tool assembly — existing only in kit code, unforgettable by construction (symmetric with ADR-0016 making
33
+ endpoint/sandbox threading unrepresentable in workflows).
34
+ - **`harness` is the agent-runtime section of `jr2.config.ts`** — it declares what the instance can **reach**, never
35
+ which model to use: a custom provider (`{ api, baseUrl }` plus token limits — `contextWindow`/`maxTokens`,
36
+ provider-level and per-model, since a custom id has no catalog entry and unset limits resolve to 0, starving
37
+ auto-compaction — e.g. an OpenAI-compatible vLLM endpoint), the gateways pi's catalog providers send their calls to
38
+ (`catalog`, below), and any other env Agents need; the credentials Agents use are the Held secrets of the top-level
39
+ `secrets` list (below). It moved out of `sandbox` deliberately: `sandbox` is pod-transport (it still _carries_ this
40
+ env to the Harness container), but model concerns are Harness semantics and users configure them here. `jr2 up`
41
+ preflights every model the definitions name, from inside the cluster (ADR-0019). A model of pi's catalog whose
42
+ provider no pod holds a key for fails the preflight, unless a credential in `harness.env` (an AWS role for Bedrock,
43
+ say) reaches it. An instance-wide default model was tried and removed: it was reached through `.env` (`JR2_MODEL`),
44
+ which put a design decision in the file reserved for deployment-varying values (ADR-0019) — and split one fact in two,
45
+ since the endpoint's per-model token limits were already committed in `jr2.config.ts` keyed by the very model id
46
+ `.env` was choosing. The honest split: **`harness` declares reach; the definition makes the choice.**
47
+ - **Credentials are held, not handed over**
48
+ ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)). The Agent executes code in the
49
+ Harness container, so a key there is a key it can take away. The top-level `secrets` list declares each credential
50
+ ([ADR-0068](0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md)). A model key is a Held secret in the
51
+ pods whose Agents name a model of a provider that reads it; `jr2 up` gives its value to the Custodian of those pods —
52
+ and, where an Effect also names it, to that Effect's Effector — and the Harness container's env holds the key's
53
+ Stand-in. `harness.provider.apiKey` is one implicitly: the held secret `JR2_PROVIDER_API_KEY`, bound to `baseUrl`'s
54
+ host, so it leaves the `jr2-harness-env` Secret and `baseUrl` must be `https://` when a key is set. A known model key
55
+ in `harness.env`, or in an `envFrom` Secret, is refused.
56
+ - **A catalog provider can be moved to a gateway, and stays a catalog provider.** `harness.catalog.<id>.baseUrl`
57
+ replaces the base URL of every model of one pi catalog provider — a LiteLLM gateway in front of Bedrock, for
58
+ `anthropic/…`. The catalog keeps its limits, cost and thinking levels; only the endpoint moves. It is reach, not a
59
+ model choice: it says nothing about which model an Agent names.
60
+
61
+ ## Dials: a workflow may set two of a definition's fields for one Turn
62
+
63
+ One definition value may be carried by several Machines, so one persona legitimately runs at different settings in
64
+ different workflows — a reviewer on a one-line diff and the same reviewer on an architecture change want identical
65
+ instructions and different effort. Spread-composition (below) answers this with persona×tier files where "heavy" is a
66
+ knob wearing a persona's filename, so `agentRun` takes two optional **dials**, `model` and `thinkingLevel`, layered over
67
+ the definition per Submission. A model can lack a level (Claude Sonnet 5.5 has no `off`). The Harness does not refuse it
68
+ at admission: the Turn runs, and the printed conversation names the level that the model ran at
69
+ ([ADR-0023](0023-the-harness-prints-the-conversation.md)).
70
+
71
+ The line that keeps this from becoming "re-specify the definition at the call site": **identity vs. dial.** Identity —
72
+ `instructions`, `access` — is definition-only: a call site that rewrote it would make the Agent's name a lie, and
73
+ `access` carries [ADR-0028](0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md)'s containment claim
74
+ that a read-only reviewer _cannot_ write, which per-invocation escalation would void. **ADR-0028 is therefore untouched
75
+ by the dials.** Dials say only how hard to run: it is still the coder, it is the coder running hot. What the Turn is
76
+ about and where it works — `prompt`, `cwd` — is neither identity nor a dial but the Turn's **Frame**
77
+ ([ADR-0057](0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md)): per Turn by nature, since a worktree path
78
+ exists only once a run does, and no more a property of the persona than the prompt is.
79
+
80
+ Where a model is checked: two seats, each where the knowledge is. At **converge**, `jr2 up` walks the registered
81
+ Machines for the definitions they carry (ADR-0049) and checks a configured provider once per distinct model they name.
82
+ At **admission**, the Harness resolves the definition it was handed against its registry and 400s the invoke as its
83
+ state is entered, rather than settling the Submission `failed` mid-run. The pod-boot seat retired with the roster it
84
+ read: the Harness no longer knows an Agent before a Turn brings it one. A dial has only ever had the admission seat — a
85
+ call-site model cannot be checked earlier, because an invoke's `input` is a function and is not statically recoverable.
86
+ Definitions keep converge-time safety; overrides pay the later check for the flexibility.
87
+
88
+ ## Considered options
89
+
90
+ - **Thin wrapper** (a jr2 identity imported inside a user-owned harness project). Rejected: shrinks the per-agent file
91
+ but leaves the dependency pin, the runtime config, and the Dockerfile contracts in user space — the parts that break
92
+ silently when they drift from the Orchestrator's wire and the operator's pod.
93
+ - **Instance owns the harness project** (the first cut). Rejected: violates the documented Instance/Agent split, and
94
+ every new agent re-copies the leash.
95
+ - **Per-instance Harness image built by a `jr2 harness build`** (the second cut: jr2 generates the project under
96
+ `.jr2/harness/`, the user docker-builds it). Rejected once definitions were plain data: the image content was 100%
97
+ kit-owned mechanism plus a JSON's worth of persona — so the persona moves at runtime and the image joins the published
98
+ release train. This also deleted a build + `kind load` step from every agent edit.
99
+
100
+ ## Consequences
101
+
102
+ - The definition contract is data-only: what an Agent may do to the Workspace is part of it (`access`, ADR-0028); custom
103
+ tools/skills/subagents have no seat yet. When someone needs them, the contract grows deliberately — there is no eject
104
+ hatch to a foreign harness project (ADR-0027 deleted it).
105
+ - Because definitions are plain data, stock ones compose with no API: a Machine carries `agent(coder)`, and a consumer
106
+ retunes it with `customize(machine, { agents: { coder: { model: "…" } } })` (ADR-0049, ADR-0009).
107
+ - An invalid definition fails at admission, loudly, naming the slot: a model no registry resolves, a malformed field. A
108
+ Machine with no Agent slots is valid — a `workspace()` body parking a Sandbox (ADR-0012) invokes none. Readiness is
109
+ binding `:8080`, with no boot build ahead of it (ADR-0027).
110
+ - The kit owns version compatibility: the Harness runtime and its exact pins are kit concerns (ADR-0027) — bumping them
111
+ is a kit change, never an instance chore.
@@ -0,0 +1,287 @@
1
+ # The operational surface is one converging command against the current kube context
2
+
3
+ Bringing the first real workflow to a cluster (2026-07-18) took four command surfaces (`jr2`, `just`, `kind`,
4
+ `kubectl`), three long-running shells, and a runbook of ordered steps whose footguns the CLI absorbed none of: kind
5
+ cluster names were host-global and instance-bound, images had to be re-`kind load`-ed after every cluster recreate, a
6
+ missing `.jr2/cluster.json` surfaced as a provision-time routing error, and a missing Secret parked pods in
7
+ `CreateContainerConfigError`. Root cause: the **local/remote split**. `jr2 dev` ran the Orchestrator on the host while
8
+ Sandboxes ran in the cluster, which forced two host↔cluster seams (the pod→host callback route and the `repos/` hostPath
9
+ baked at cluster creation) — and every footgun above is one of those seams leaking. If a runbook is needed to use `jr2`,
10
+ the interface has failed.
11
+
12
+ ## Decision
13
+
14
+ **The Orchestrator always runs in-cluster.** There is no host dev mode. `jr2` targets whatever cluster the current
15
+ `kubectl` context points at (the argo/cilium model) — which may be a local kind cluster, but nothing is kind-special: no
16
+ baked mounts, no recorded pod→host route, no `.jr2/dev.json` / `.jr2/cluster.json`. `jr2 dev`, `jr2 cluster up|down`,
17
+ and `jr2 harness build` are removed.
18
+
19
+ **`jr2 up` is the one command.** It idempotently converges the target namespace to match the instance — every layer,
20
+ loudly narrated, safe to re-run:
21
+
22
+ - **Operator** (per-cluster, shared): CRD + Deployment applied from manifests shipped _inside the installed npm
23
+ package_. The image ref follows [ADR-0038](0038-jr2-up-builds-every-image-it-deploys.md): in a kit checkout, `up`
24
+ builds the operator image and content-addresses it; installed from npm, the ref is the published `<kitversion>` tag at
25
+ the canonical home, re-homed by `kitRegistry`
26
+ ([ADR-0044](0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md)). `up` upgrades an older deployed
27
+ operator and **never downgrades** — if another instance upgraded it past this kit's pin, warn and leave it (CRD
28
+ versioning is the skew contract). `operator.manage: false` skips the layer to run the controller loop in the
29
+ foreground (kit development).
30
+ - **Orchestrator** (per-instance): a Kit image at the Kit version, resolved like the operator's, plus the instance's
31
+ **Assembly** — its `workflows/`, config and deps, with the kit's packages stripped — staged, sealed, and
32
+ **content-addressed by the staged tree**, then published to the running Orchestrator over its API
33
+ ([ADR-0069](0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md)). A kit upgrade rolls the
34
+ image; a Machine edit publishes an Assembly and rolls nothing. Hashing the instance _folder_ instead — where deps
35
+ appear only as version ranges — is what let `up` skip work it needed. `--force` publishes against an unchanged hash.
36
+ - **Harness**: no per-instance Harness image (ADR-0018). The Harness is a Kit image, resolved like the operator's
37
+ (checkout-built or published, ADR-0038/0044), and the Custodian beside it in every Harness pod runs a Pinned image
38
+ ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)); the Harness reaches each Sandbox as
39
+ an `/opt/jr2` volume injected at pod time, over a Sandbox Image the instance builds or brings
40
+ ([ADR-0037](0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md)). Each Turn carries its Agent's
41
+ definition to the Harness in the admission (ADR-0049); `up` delivers no roster.
42
+ - **Secrets**: values declared in config (which may read `process.env`, populated from the uncommitted `.env`) are
43
+ materialized into an instance-owned Secret, by server-side apply, so that no value is in a
44
+ `last-applied-configuration` annotation
45
+ ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)). Referenced-but-unmanaged Secrets
46
+ (`envFrom` refs, git credentials) are **preflighted**: `up` fails naming the missing Secret with the exact creation
47
+ hint — converting the `CreateContainerConfigError` hang into an immediate error. Sealed Secrets/External Secrets ride
48
+ this seam untouched: jr2's contract is "a Secret named X exists", however it got there. `up` also refuses a held
49
+ secret's name that is a key of an `envFrom` Secret, and names the Secret, the key and the held secret (ADR-0059, R14).
50
+ A credential the Harness uses is a held secret
51
+ ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)): `up` writes a literal value into
52
+ `jr2-held-secrets-<pod>` for each pod that names it, which only that pod's Custodian mounts, and checks that a
53
+ referenced Secret and key exist. Like the signing key, the held-secret CA (`jr2-held-ca`) is kept across converges —
54
+ minted on first need, never rotated by `up` — because a live pod trusts it.
55
+ - **Repos**: `up`'s walk of the registered Machines collects every bound Repo Slot (ADR-0049, ADR-0051) — to refuse an
56
+ open slot nobody bound, and to see which urls need a key. The Orchestrator creates one `Repo` resource per url at
57
+ boot, and the operator's cache agent clones each onto the nodes that need it (ADR-0004). There is no `repos` config
58
+ list and no host `repos/` directory. Private repos: `git.credentials` matches each url to a token env var or an ssh
59
+ Secret by longest prefix (ADR-0051); the scaffold's wildcard entry makes `JR2_GIT_TOKEN` from `.env` the default path
60
+ for https; for ssh URLs whose entry names a Secret that does not exist, `up` asks where the key comes from — a fresh
61
+ in-cluster deploy keypair (the recommended default, public key printed to register), a local key, or one pasted on
62
+ stdin ([ADR-0047](0047-the-git-ssh-key-source-is-the-users-choice.md)) — declining bails. A personal key never enters
63
+ a cluster silently: only by that explicit, warned choice.
64
+ - **Landlock**: `up` refuses to converge while a Sandbox node lacks Landlock. It reads the pods of the Repo cache
65
+ agent's DaemonSet, which probe Landlock on each Sandbox node, within the rollout wait it already makes for the cache.
66
+ It is an ordinary converge failure: it names each node whose agent reports none, the probe's reason, and the fix (keep
67
+ the node out with `sandbox.nodeSelector` or a taint, ADR-0052, or enable Landlock); a run after the fix converges
68
+ ([ADR-0077](0077-jr2-requires-landlock-on-every-sandbox-node.md)).
69
+ - **Provider preflight**: `up` checks each provider that a carried model names. It checks from inside the cluster, with
70
+ one trivial tool-call completion. An endpoint that the pods cannot reach, a wrong or missing key, or a vLLM without
71
+ `--enable-auto-tool-choice` then fails at converge time, not as `agent.fault` in a run. Reachability is the user's
72
+ concern (`localhost` never works from a pod; a LAN address does on kind). The check must take the path that the
73
+ Harness takes. If it does not, a pass means nothing.
74
+ - **One path for every provider.** `up` checks the custom provider (`harness.provider`, ADR-0018) and a provider of
75
+ pi's catalog (for example `anthropic/…`) in the same way. A key of either is a held secret, and only a Custodian can
76
+ use it ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)). So no key enters a pod
77
+ spec, an argv or a script. The check calls the custom provider as a Turn calls it: it does not force the tool,
78
+ because a vLLM without a tool parser answers a forced tool and then fails every Turn. When the custom provider
79
+ answers twice with no tool call, the check fails. `up` refuses a `harness.provider.api` that the Harness does not
80
+ speak before it builds or applies anything: the list is the Harness's own (`CUSTOM_APIS`, held equal to
81
+ `CUSTOM_PROVIDER_APIS` in `@jr2/orchestrator` by a test). When the check container cannot build its registry from
82
+ the config, each model's result says why, in words.
83
+ - **The check pod.** It is a Harness pod with no Harness in it. It has a Custodian and the stock Harness image's
84
+ `provider-preflight.ts`. The function that composes the Custodian of every Harness pod composes this Custodian too.
85
+ Its token is a value that the Orchestrator never signed. Its Stand-ins are secret and its own: `jr2 up` mints them
86
+ with the signing key, as it does for the Instance Harness
87
+ ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)). The check pod runs no Workspace
88
+ code, so it may hold them in env. `provider-preflight.ts` gets the env of the Harness container, with the Stand-ins
89
+ in it: the harness config, `harness.env`, `harness.envFrom`, the Stand-ins, the proxy and the trust. pi resolves
90
+ each model and uses its api, as in a Turn. The `harness.catalog` gateway applies. The key never enters the check
91
+ container, its argv or its log.
92
+ - **Its own objects.** The check pod reads no object of the Instance. `up` writes copies from the values on the host:
93
+ - a ConfigMap with the Custodian's files, the program and the harness config;
94
+ - a ConfigMap with the trust bundles;
95
+ - a Secret with the token, the check pod's Stand-ins and the literal values of the held secrets;
96
+ - a Secret with the leaves;
97
+ - a Secret with the literal `harness.env` values.
98
+
99
+ A held secret from `valueFrom` stays a reference to the user's Secret. A literal value is thus in a second Secret
100
+ while the check pod lives. `up` deletes that Secret after the check.
101
+
102
+ - **What it checks.** It checks every carried model that a Harness pod runs. A typo, a retired model and a model that
103
+ the key cannot use are each per model. One check pod stands for one class of Harness pod. A class is a placement and
104
+ a held set:
105
+ - The placement: a Workspace's model runs on the Sandbox nodes (`config.sandbox`, ADR-0052). A `"none"` Agent's
106
+ model runs where the Instance Harness runs.
107
+ - The held set: a pod that holds a secret sends its HTTPS through its Custodian, and a tunnel carries a host that no
108
+ secret is bound to. A pod that holds no secret sends its HTTPS direct. So the check pod holds the held set of the
109
+ first pod of its class, and the keys that the other pods of the class hold. A class that holds nothing gets a
110
+ check pod that holds nothing.
111
+
112
+ When no pod of the class holds a key of the provider, the check pod holds none. Then pi refuses the call
113
+ (`not configured`), and the converge fails. A signing credential in `harness.env` reaches the check pod, as it
114
+ reaches a Turn. An id that pi does not know (a typo) fails, because pi resolves no model for it. An id that pi
115
+ knows, and for which jr2 knows no key name, is checked with no held key, as a Turn runs it. The check pod has the
116
+ priority class of the Sandbox tier, as the Harness pods have (ADR-0060).
117
+
118
+ - **The request.** The check's request shares these with a Turn's: the model, its api, its endpoint and key, the
119
+ completion limit (what pi-ai sends for a Turn), the retries, no forced tool, and the level of a definition with no
120
+ `thinkingLevel` (`off`: no `reasoning` is sent). It does not share these: it has no system prompt, one tool, no
121
+ session id, and a one-line prompt (one text part, as a Turn's user message has parts). These are limits of the
122
+ check:
123
+ - An endpoint whose context is too small for a Turn's prompt can pass the check.
124
+ - A definition with a `thinkingLevel` is checked at the default level, because `up` knows the models of the
125
+ definitions and not their levels.
126
+ - **The call.** The first call is the request that a Turn sends. So a pass always means that the Turn's request works.
127
+ Its answer decides:
128
+ - A tool call: the check passes.
129
+ - An error: the check fails, or it is busy (below). A forced tool could pass where a Turn fails: a vLLM behind a
130
+ `harness.catalog` gateway, with no `--enable-auto-tool-choice`, answers a named tool by guided decoding and
131
+ refuses the Turn's request.
132
+ - Text from the custom provider: a Turn there gets a nudge after text, so the check sends the same call once more. A
133
+ tool call then passes, an error decides as above, and text again fails, because that is how a vLLM with no tool
134
+ parser answers.
135
+ - Text from any other model: a warning. When the model is of pi's catalog, has `off`, is not moved to a gateway, and
136
+ its api has a form for a forced tool, a second call forces the tool. It does not change the verdict, only the
137
+ words of the warning: with a tool call, the model answered in text when the tool was not forced; with text or an
138
+ error, the model gave no tool call either way. Anthropic refuses a forced tool while the model thinks, so a model
139
+ with no `off` gets no second call.
140
+ - **Busy.** A provider that is busy after the retries of a Turn ([ADR-0064](0064-a-workspace-waits-for-capacity.md))
141
+ is a warning: it answered, and it took the key. Busy is a 429, a 529, an overload, a rate limit or
142
+ `ResourceExhausted`. A 503 alone is a failure, because a gateway with no backend and a server that still loads its
143
+ model answer 503, and a Turn there fails. The check waits at most 20 s for one `retry-after`. When a provider asks
144
+ for more, the call stops with its 429, and that is busy.
145
+ - **Time.** A call waits 120 s for its first event (text, thinking, a tool call or the end), and then 60 s for each
146
+ gap between two events. An Ollama endpoint loads the model on the first request, and a llama.cpp on a CPU reads the
147
+ prompt before it writes: both are slow to start, and then they stream. An endpoint that stops for a minute in the
148
+ middle of an answer is one that a Turn cannot use either. Each model has 360 s for all its calls: two calls, each
149
+ with 120 s to its first event and 60 s of the gaps that follow. Each model has its own 360 s, so one slow model does
150
+ not use the time of the others. Two literals set it, `PREFLIGHT_MODEL_BUDGET_SECONDS` in `preflight-line.ts` for the
151
+ check and `CHECK_MODEL_SECONDS` in `up.ts` for the pod, and a test holds them equal. A call or a model that runs out
152
+ fails. For the custom provider and a gateway, the error adds that a model that loads on first use can take longer.
153
+ - **Busy, when a call runs out.** The rule: the last status that any try of the call got was a 429 or a 529, and then
154
+ the call ran out. A later 200 or 503 clears it. pi-ai sends each retry as a new request. The check sees each try
155
+ through a `fetch` wrapper of its own. It sees Bedrock's tries through `onResponse`, because Bedrock's retries are
156
+ the AWS SDK's own (pi-ai's `maxRetries` does not reach them). Google's and Vertex's adapters refuse a `fetch` that
157
+ is not `globalThis.fetch`, so the check passes them none and does not replace the global one. For them only the
158
+ final error counts: a 429 followed by a retry that never answers fails the check, where for the other apis it is
159
+ busy. A second call that runs out does not change the verdict of the first.
160
+ - **The order.** The checks run before `up` writes an object that the Instance or the operator reads. Only these come
161
+ before them, because the check pod needs them: the namespace and its label, the PriorityClasses, and the images that
162
+ `up` builds and delivers. The operator (its CRDs, RBAC and Deployment, its label and its rollout), a deploy key and
163
+ the wait for its registration (ADR-0047), the trust bundles, the held objects and the instance List come after every
164
+ check passes.
165
+ - **When a check fails.** The converge stops. `up` applied only the items above and the check pods with their own
166
+ objects, and it deleted the check pods and their objects. It also deleted the leftovers of earlier runs (below). The
167
+ operator, the Orchestrator, its Assembly and every pod of the Instance continue to run as before. Nothing else is
168
+ deleted, and no Assembly is published.
169
+ - **The pods.** `up` runs the check pods in series, one at a time, and reports every failure in one error. When the
170
+ cluster refuses a check pod (a quota, a Pod Security level, a webhook), that check fails, and the other checks still
171
+ run. In a new namespace, `up` waits up to 10 s for the `default` ServiceAccount before it calls a refusal a refusal.
172
+ The check container writes a start line and a result line for each model. `up` reads its log while it runs, and
173
+ writes each result as it comes. When the pod stops before the end, the error names the model that the check was on,
174
+ and each model that it never reached. The name of each pod is unique to the run
175
+ (`jr2-provider-preflight-<run>-<n>`), so two runs in one namespace do not touch each other's pods. `up` deletes the
176
+ objects of each check after it. `up` waits up to 300 s for the check container to start (two cold image pulls), and
177
+ it writes each new reason that the pod gives for the wait. Then it waits 360 s for each model (the model's own
178
+ budget in the check), and 30 s more for the Node.js process, which runs under tini as the Harness does, to start. It
179
+ stops at once in these cases:
180
+ - A container cannot start: a crash loop, a missing Secret or key in the env (`CreateContainerConfigError`), or a
181
+ pull that the registry refuses (not found, manifest unknown, unauthorized, denied). A slow or limited pull, and a
182
+ `CreateContainerError`, can pass by themselves, so the wait goes on.
183
+ - A volume cannot mount: the Secret or the key of a held secret's `valueFrom` is missing. The kubelet says so in a
184
+ `FailedMount` event. It counts when the kubelet says it again (its count or its last time goes up), because the
185
+ first can be a race with the objects of the same apply.
186
+ - The Custodian restarts. The error then quotes the Custodian's refusal line, or the reason of its last end
187
+ (`OOMKilled`).
188
+ - **Leftovers.** `up` has no signal handler. If `up` stops (Ctrl-C), the kubelet stops a running pod at its
189
+ `activeDeadlineSeconds`. A pod that no node took has no start time, and that deadline never applies to it. So a
190
+ later `up` of the same Instance deletes these objects:
191
+ - each check pod that ended more than 10 minutes before: in that time, the run that made the pod can still read its
192
+ log;
193
+ - each check pod, in any phase, that is older than its `activeDeadlineSeconds` and 10 minutes;
194
+ - the objects of those pods, and each object of a check that is more than one hour old and whose pod is gone.
195
+
196
+ It never deletes the objects of another Instance.
197
+
198
+ - **Skipping it.** `jr2 up --skip-provider-preflight` runs no check pod. A check can fail when the model works, for
199
+ example when a model loads for longer than the check waits. That fault must not stop every converge until jr2 is
200
+ fixed. The skip is never silent: `up` writes one warning line, which names each model that it did not check. `up`
201
+ still deletes the leftovers of earlier runs, because their Secrets can hold a key. The flag has no config key,
202
+ because no flag of `up` has one (CI gives `--yes` as a flag). A model that does not work then fails in a run, as
203
+ `agent.fault`.
204
+ - **A limit.** The check pod has labels of its own. It does not get the labels of a Harness pod: the Instance Harness
205
+ Service selects its pods by label, and would send Turns to the check pod. So a NetworkPolicy that selects Harness
206
+ pods by label does not select the check pod. When such a policy closes the egress of the Harness pods, the check can
207
+ pass while a Turn fails. jr2's own egress policy, which closes the cloud metadata addresses
208
+ ([ADR-0076](0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md)), gives the check pod one
209
+ of its own, so it does not have this limit.
210
+
211
+ **`jr2 down`** removes the instance from the cluster. It asks for confirmation unless `--yes` is given (for CI). `--all`
212
+ takes the operator too.
213
+
214
+ ## Addressing: cluster is address, namespace is identity, the cluster is the record
215
+
216
+ - **Cluster = address**: always the current kube context (`--context` to override), never recorded in the repo. The
217
+ instance repo must stay deployable anywhere.
218
+ - **Namespace = identity**: set in `jr2.config.ts` (default: the instance `name`), `-n` overrides. The same namespace on
219
+ kind and prod is what makes switching painless.
220
+ - **Convergence is claimed only of observed state.** Every layer `up` rolls out is checked against the pod actually
221
+ serving — the running image must be the intended one before `up` says `converged`. The staleness labels `up` stamps
222
+ decide whether to spend a build; they are never evidence that the cluster is correct, because a run that reports
223
+ success off its own bookkeeping can deploy nothing and still congratulate itself. Tag equality is the whole check, and
224
+ is only sound because the tag is a content address: on the `kind load` path a pod's `imageID` is containerd's manifest
225
+ digest under a rewritten repo name, comparable to nothing the host holds.
226
+ - **No local target state.** Whether this cluster hosts the instance is derived from the cluster: `up` finds the
227
+ instance's labeled objects → converge silently (it's home); finds nothing → confirm first-time setup interactively
228
+ (`--yes` for CI); finds objects labeled as a _different_ instance → refuse. Run-verbs against a cluster with no
229
+ instance error with "not deployed here — right context?". This is the guardrail against silently pointing at the wrong
230
+ cluster, without a state file that can itself go stale.
231
+ - **CLI transport**: run-verbs port-forward the Orchestrator Service via the kube API for the duration of the command
232
+ and read the Instance token from its Secret — kube RBAC is the real gate. `--url`/`JR2_URL` (+ token env) is the
233
+ escape hatch for ingress-exposed instances and callers without cluster creds.
234
+ - **A dead ADDRESS and an absent IDENTITY are two faults, and resolution names which.** They share a verb and a context
235
+ but not a fix: one is the network, the other is `jr2 up`. A cluster that answered and holds no instance Secret is "not
236
+ deployed here — right context?"; a cluster that could not answer at all is "cannot reach the cluster — context X: …",
237
+ carrying kubectl's own reason. Merging them — which is the easy shape, since both end in no token — sends a user whose
238
+ VPN is down to go and check a context that was right all along. They are told apart by EXIT CODE, not by matching on
239
+ kubectl's English: the Secret read passes `--ignore-not-found`, so an absent Secret exits 0 with empty output and
240
+ everything else (no route, no credentials, RBAC refusing the read) exits non-zero and is reported.
241
+ - **Resolution is bounded; converge is not.** A run-verb waits **10s** to find out whether the cluster is there — the
242
+ same bound the Harness client puts on its own dial, so the two seats that cross a network agree on what "too long"
243
+ means — and then fails with the reason above. Unbounded, it does not fail at all: a cluster whose API server accepts
244
+ the connection and says nothing (a dropped SYN, a sleeping VPN) makes `jr2 runs` hang with no ceiling, which is the
245
+ one outcome a CLI must never have. The bound is spent twice over, because `--request-timeout` bounds a single server
246
+ request while kubectl retries API discovery behind it — measured, a 5s flag bought a 25s command — so the flag makes
247
+ each attempt give up promptly and a process deadline is what bounds the command. It covers the resolution path only:
248
+ `jr2 up` legitimately waits on rollouts for minutes and says so with its own `--timeout`.
249
+
250
+ ## Sharing is npm; the instance repo is a deployment assembly
251
+
252
+ Nothing in `jr2.config.ts` is useful to others _by design_ — it is the boundary where shared code meets your repos and
253
+ your cluster. Reusable work is published as npm packages, and a package exports a **Machine**, nothing beside it: its
254
+ Agents are its own actor slots and its Sandbox Image is a `workspace()` option, so a workflow file imports the Machine
255
+ and the parts ride along (ADR-0049). Nobody clones an instance repo to reuse it — `jr2 init` + a dependency is the path.
256
+ `up` typechecks the instance before it builds anything and refuses on errors (ADR-0050): a wrong Agent slot or a
257
+ `customize()` of a part the Machine does not carry is a compile error at the door, not a mid-run failure.
258
+ `jr2.config.ts` stays committed (the instance repo is the GitOps unit, ADR-0008); everything deployment-varying resolves
259
+ from env.
260
+
261
+ ## Considered options
262
+
263
+ - **Keep host dev mode as an accelerator** (hot reload, in-process debugging) beside a context-targeted everything else.
264
+ Rejected: the two-worlds split _is_ the footgun factory, and "same folder runs locally and deployed" (ADR-0008) is
265
+ only actually delivered when there is one world. Inner-loop cost is absorbed by staleness-checked builds; a `--watch`
266
+ rebuild-redeploy loop can come later.
267
+ - **Split setup from run** (`jr2 install` + `jr2 up`). Rejected: a two-step runbook reintroduces "you forgot step 1",
268
+ which `up` would have to detect anyway — at which point it may as well fix it.
269
+ - **A committed or locally-cached deployment target.** Rejected twice: committed addresses break repo reuse; a local
270
+ cache (`.jr2/target.json`) is derived state that can lie. The cluster itself is the record.
271
+ - **`jr2` creating clusters** (`jr2 cluster up`, or an interactive offer). Rejected: with no baked mounts, any vanilla
272
+ cluster works; creating one is infrastructure provisioning, like installing docker. The no-context error message says
273
+ `kind create cluster` and that is the whole story.
274
+
275
+ ## Consequences
276
+
277
+ - The everyday story is two commands ever: `jr2 init`, `jr2 up`. The full CLI: `init`, `up`, `down`, `gc` (ADR-0039),
278
+ `run`, `runs`, `status`, `logs`, `send`, `kit push` (ADR-0044), plus the workspace verbs `ls`, `ssh`, `rm` (ADR-0009;
279
+ decided, not yet in the binary).
280
+ - **Simplification must not surprise**: `up` prompts exactly when meeting a cluster that isn't yet home, and run-verbs
281
+ print the context they're talking to — the ambient-context magic stays visible.
282
+ - The snapshot store (sqlite) moves onto a PVC in the instance's namespace; `.jr2/` shrinks to scratch.
283
+ - `jr2 build` (build + push + render manifests, no apply — the pure-GitOps CI verb) is planned but deferred; `up` in CI
284
+ covers the interim.
285
+ - The e2e tier (ADR-0010) loses `jr2 dev` as its per-scenario fixture; the fixture boots the Orchestrator's server
286
+ entrypoint as a host process instead — the same real server the cluster runs, no cluster, no user-facing verb.
287
+ - The justfile returns to kit-repo development only; no instance operation appears in it.
@@ -0,0 +1,60 @@
1
+ # Private-CA trust is a named concept, not volume plumbing
2
+
3
+ Running the jr-parity exercise against a real vLLM (2026-07-19) hit an endpoint that is HTTPS-only behind an ingress
4
+ whose cert chains to a private CA. The host trusts that CA; pods do not — so every jr2-owned egress point fails the same
5
+ way: the Harness's model calls, and `jr2 up`'s own provider preflight (which checks from a one-shot in-cluster pod, the
6
+ check pod, ADR-0019). The tempting fix was generic `volumes`/`volumeMounts` passthrough on `sandbox` — the CRD already
7
+ carries both, and `harness.env`/`envFrom` set the precedent of corev1 shapes passed through verbatim. But trusting a CA
8
+ is not pod-shaped config: it is an _intent_ ("this instance's egress must trust this CA") that jr2 alone can thread
9
+ through every place it owns an outbound TLS connection — including the check pod, which no instance-authored volume can
10
+ ever reach. A passthrough would also make the one-intent story a three-part incantation (hand-managed ConfigMap +
11
+ mount + `NODE_EXTRA_CA_CERTS` via `harness.env`), the runbook shape ADR-0019 exists to kill.
12
+
13
+ ## Decision
14
+
15
+ - **`harness.caBundle` is the surface**: a path to a PEM bundle, **relative to the instance folder**, committed (CA
16
+ certs are public data — never a Secret). One file may carry multiple certs; that is what a PEM bundle is.
17
+ - **Only `jr2 up` reads the file, host-side.** `jr2.config.ts` is evaluated in two worlds — by the CLI on the host and
18
+ by the deployed orchestrator in-cluster — so the config carries a _path_, not file contents: the in-cluster evaluation
19
+ checks presence and never touches the filesystem. `up` fails loudly on an unreadable path.
20
+ - **`up` materializes the `jr2-ca` ConfigMap** beside the harness ConfigMap; both Harness placements mount it at
21
+ `/etc/jr2/ca` on the **Harness container** and set `NODE_EXTRA_CA_CERTS=/etc/jr2/ca/ca.crt`. CR-level `volumeMounts`
22
+ land on the primary container by the operator's contract, so the asymmetry costs no operator change: the User
23
+ Container (user-owned image — same rule as `harness.env`, ADR-0005/0013) never inherits the trust path.
24
+ - **While a held secret exists, the bundle also reaches the Custodian and the Harness's other tools**
25
+ ([ADR-0059](0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md)). `jr2 up` adds three keys to
26
+ `jr2-ca`: `extra.crt` (`caBundle` + the Instance's held-secret CA), which `NODE_EXTRA_CA_CERTS` names instead;
27
+ `bundle.crt` (the roots + both), which `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE` and `GIT_SSL_CAINFO` name, because those
28
+ replace a tool's store; and `upstream.crt` (the roots + `caBundle`, never the held-secret CA), which the Custodian
29
+ verifies a bound upstream against — a corporate gateway whose certificate chains to the private CA included. "Roots"
30
+ are the Mozilla roots of the Node that runs `jr2 up`. The ConfigMap is then applied server-side: two copies of the
31
+ roots are more than client-side apply's annotation holds. The Custodian's control listener is plain HTTP to the
32
+ Orchestrator's Service and trusts nothing.
33
+ - **The provider preflight trusts the same bundle the same way** (`NODE_EXTRA_CA_CERTS` on the check pod, from its own
34
+ copy of `jr2-ca`). A preflight that fails where the Harness would succeed — or passes where it would fail — is a
35
+ broken promise; sharing the mechanism keeps the two verdicts identical. (Node 24's `fetch` honoring
36
+ `NODE_EXTRA_CA_CERTS` was verified live against the real endpoint before this landed.)
37
+
38
+ ## Considered options
39
+
40
+ - **Generic `sandbox.volumes`/`volumeMounts` passthrough.** More powerful and precedent-consistent, but it cannot reach
41
+ the check pod, splits one intent across three keys, and adds a forever-surface for a need that has appeared exactly
42
+ once. Deferred until a second real need for arbitrary volumes shows up — the CRD support is already there when it
43
+ does.
44
+ - **`NODE_TLS_REJECT_UNAUTHORIZED=0` via `harness.env`.** Zero kit change, but it disables verification for _every_ TLS
45
+ connection the Harness makes (git, MCP, everything), and the preflight still fails — `up` would refuse to converge an
46
+ instance whose pods would work.
47
+ - **Baking the CA into the Harness image.** Contradicts ADR-0018's stock published image — a per-instance CA means a
48
+ per-instance image, the exact build ADR-0018 deleted.
49
+
50
+ ## Consequences
51
+
52
+ - Rotating or adding a CA is a `ca.crt` edit + `jr2 up` + pod restart — the same ConfigMap-update path as Agent
53
+ definition edits (ADR-0018).
54
+ - `NODE_EXTRA_CA_CERTS` _adds to_ Node's trust store, so public endpoints keep working. With no held secret it reaches
55
+ Node processes alone — an agent shelling out to `curl`/`git` against the private CA needs the system store, which is
56
+ the Sandbox Image's business. With a held secret, `bundle.crt` reaches curl, git and Python too; the cost is that it
57
+ REPLACES their store, so a private CA the Sandbox Image trusts on its own, and `caBundle` does not name, stops being
58
+ trusted by them in that pod (ADR-0059).
59
+ - The orchestrator pod itself (boot-reconcile `git clone`) does not consume the bundle yet; repos behind a private CA
60
+ would extend the same ConfigMap to the orchestrator Deployment — a deliberate later step, same concept.
@@ -0,0 +1,101 @@
1
+ # Workspace continuity is watched; the lease asserts
2
+
3
+ Two Sandboxes were deleted by hand under a live Orchestrator (2026-07-19); `jr2 runs` and the visualizer both kept
4
+ reporting the runs as active, parked on their gates, indefinitely. The gap was structural rather than a bug: the
5
+ Orchestrator's two conversations with the cluster ran in opposite directions and on opposite triggers. The keepalive
6
+ lease (ADR-0001) was **level-triggered and write-only** — a `setInterval` per owned Sandbox, in a process-global map,
7
+ stamping `jr2.dev/keepalive` forever. The restore-reconcile probe (ADR-0012) was **edge-triggered and read-only** — one
8
+ `exists()` per entry into `running`. So the Orchestrator asserted liveness continuously and learned the truth almost
9
+ never, and the state most likely to outlive its Sandbox — a body parked on a Gate for hours — is precisely the one that
10
+ never re-enters `running` to ask again.
11
+
12
+ Worse, `exists()` asked the wrong question. A run does not depend on a Sandbox CR existing; it depends on **the pod it
13
+ attached to still being that pod**. `work` is an `emptyDir`, so an eviction or a node loss takes the clones, the
14
+ worktrees, and every unpushed commit — while the operator recreates the Pod under the same name, leaving the CR present,
15
+ the Service DNS resolving, and every path in `handles` still valid. Presence answers "yes" to a workspace whose contents
16
+ are gone. That divergence was undetectable at any point in the run's life, restore included — the exact "resume into an
17
+ inconsistent world" ADR-0012 exists to prevent.
18
+
19
+ The chaos tests (2026-09-27) found the other ways to lose a workspace. A node-pressure eviction leaves the pod object in
20
+ place, `Failed`, with the same UID — no deletion, no replacement, so neither presence nor identity changed, and the run
21
+ hung. A Harness that crash-looped under a live pod only ever refused connections, so a Turn's `wait` reconnected without
22
+ end. And a restarted Harness answered a `continue` with no memory of the conversation and no fault.
23
+
24
+ ## Decision
25
+
26
+ - **The unit of reconciliation is _continuity_, not existence**: is the workspace I am keeping alive still the one the
27
+ body attached to? Every way to lose it collapses into the one `workspace.lost` event, and the body's policy decides,
28
+ unchanged.
29
+ - **A Sandbox has one pod for its life.** The operator creates the pod once. When that pod is gone (deleted, its node
30
+ lost) or terminal (`Failed` after a node-pressure eviction, `Succeeded` or `Failed` after a node shutdown), the
31
+ Sandbox goes to the terminal phase **`Lost`**, with the pod's own reason and message (for a deleted pod, the last it
32
+ saw). It never returns to `Pending` and is never given a second pod: `work` is an `emptyDir`, so a new pod could
33
+ continue nothing, and it would hold a whole Size for nobody while hiding the loss. The CR stays, so `jr2 status` can
34
+ say why, until teardown or the idle GC. A Sandbox delete waits for its pod, because a retry can take the name of a pod
35
+ that is still going away.
36
+ - **The operator judges identity, because it knows which pod it created.** It still publishes `status.podUID`; the
37
+ Orchestrator reads `Lost`, not a UID comparison.
38
+ - **The watch answers; the lease asserts.** The Orchestrator watches its Sandboxes
39
+ ([ADR-0063](0063-the-orchestrator-watches-the-cluster.md)). A Sandbox gone or `Lost` is `workspace.lost` within
40
+ seconds, carrying the reason — or a provision fault, if the Workspace was still `placing` or `provisioning`
41
+ ([ADR-0064](0064-a-workspace-waits-for-capacity.md)). The lease renewal is a write only — one merge patch every 5
42
+ minutes, ±20% jitter — the assertion that keeps the operator from reaping (ADR-0001). A dropped watch is unknown,
43
+ never loss: fabricating loss would settle live runs holding real work the first time the API server hiccuped, so the
44
+ loop re-lists and reconciles instead.
45
+ - **A Harness restart ends its Turns, not its Workspace and not its conversations.** When the watch sees the Harness
46
+ container's restart count pass its value at a Turn's admission, every Turn waiting on that Harness fails at once:
47
+ `memory limit` if the last end was `OOMKilled` (ADR-0061), otherwise
48
+ `Turn lost (Harness restarted: <reason>, exit <code>)` — unless the Turn's state is busy or its pick's answer is
49
+ unread, which cuts the Turn and faults nothing (ADR-0031). The pod, `/work` and the conversations' directory survive,
50
+ so the Workspace is not lost and the Agent's next Turn continues its conversation (ADR-0031); a conversation the
51
+ rebuilt Harness cannot read gets a `conversation-new` notice (ADR-0062), and a Harness that never comes back faults
52
+ that admission inside its bounded window, with the kubelet's reason. `wait` keeps reconnecting without a deadline —
53
+ every real end now has a watch signal, so a timer would only be a guess at how long a slow Turn may take.
54
+ - **The lease is an invoked actor**, one per workspace, owning both halves: it renews, and it subscribes to the watch
55
+ for its Sandbox. Renewal covers the Sandbox from its write to its teardown, `placing` included, so a Sandbox that
56
+ waits for capacity is never reaped as abandoned (ADR-0064); Continuity is judged from `running`, once there is an
57
+ attached pod. Its lifetime is the state's lifetime, which xstate already manages: it re-invokes on snapshot restore,
58
+ so restore-reconcile stops being a special case and becomes the first tick of the normal loop; and it stops on every
59
+ exit — body final, run stopped, run faulted.
60
+
61
+ That last point deletes machinery rather than adding it. Three mechanisms with three different owners and lifetimes —
62
+ the process-global heartbeat map, the one-shot probe, and `release(runId)` (which label-queried the cluster to stop
63
+ timers the in-process map had lost track of, wired into `RunHost`'s error channel) — collapse into one actor. A faulted
64
+ run now stops its lease because it stops its actors; the abandoned pod stays inspectable and ages out of the operator's
65
+ idle timeout on its own. A cancelled run's Workspaces are destroyed by the cancel unless it says `keep` (ADR-0025). The
66
+ behavior `release()` was written to produce is emergent.
67
+
68
+ ## Considered options
69
+
70
+ - **The renewal answers back** (the first form of this decision): `kubectl annotate --overwrite -o json` returned the
71
+ patched object, so asserting and learning cost one round trip. Replaced: detection then ran only at the lease interval
72
+ (an eviction or drain surfaced 4.6–4.7 minutes late in the chaos tests), and it rode a `kubectl` process the
73
+ Orchestrator no longer starts (ADR-0063).
74
+ - **Keep the probe, just run it on a timer.** Simplest diff: the probe is already a `fromCallback` with teardown, so it
75
+ is a `.then` → `setInterval`. Rejected because it leaves the heartbeat map, `release()`, and the write/read split all
76
+ standing, and doubles the API traffic against the same object — the debt, untouched, plus a timer.
77
+ - **The operator recreates a lost pod, and the Orchestrator compares `podUID`** (this ADR's first form). Rejected: a pod
78
+ that ends in place keeps its UID, so eviction was missed; and the recreated pod was empty, held a Size, and served
79
+ nothing.
80
+ - **A deadline on `wait`.** Rejected: any number either ends a slow Turn or leaves a dead Harness hanging for that long;
81
+ the Harness's restart count is the exact signal.
82
+ - **Treat a Harness restart as a lost Workspace.** Rejected: `/work` survives, and a human or the next Turn can use it.
83
+ - **A deletion tombstone** so a client can tell "reaped" from "never existed". Unnecessary once continuity is the
84
+ question: both answers are `{present: false}`, and the body's policy is the same either way.
85
+
86
+ ## Consequences
87
+
88
+ - Detection latency for a lost workspace is the watch's: milliseconds (ADR-0063). The lease interval (default 5m, well
89
+ inside the 30m idle timeout) now bounds only how long an orphan waits to be reaped.
90
+ - A run that faults after its Sandbox was placed stops its lease; the operator reaps the CR one `idleTimeout` after the
91
+ last renewal. A Sandbox that was never placed is deleted at once (ADR-0064).
92
+ - A Menu-only Agent's Turn on the Instance Harness is outside the watch: its StatefulSet recreates the pod, the new
93
+ process rebuilds the conversation from its volume, and the cut Submission settles `failed` with the typed
94
+ `harness_restarted` error ([ADR-0031](0031-menu-only-agents-run-on-the-instance-harness.md)) — the Agent actor reads
95
+ that as "Turn lost", the same fault the watch gives a Sandbox. An Instance Harness that crash-loops without end still
96
+ leaves such a Turn waiting.
97
+ - The lease is per-workspace, not per-run: a workflow with concurrent workspaces gets one actor each, and each is lost
98
+ independently. This falls out of invoking it beside the body rather than owning it at the host.
99
+ - `SandboxPort` has no optional method: `renew` is a write, `continuity` is a subscription to the watch, and
100
+ `memoryFault` (ADR-0061) answers undefined from a backend that cannot see the container. The `jr2.dev/run` label
101
+ survives for `jr2 ls` and as the watch's selector, no longer load-bearing for lease bookkeeping.