@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,95 @@
1
+ # Observation is open; run state and control need the Instance token
2
+
3
+ [ADR-0013](0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md) minted two tokens and scoped them
4
+ for **delivery** — a Sandbox token may deliver to its own agent registrations, an Instance token may deliver to gates.
5
+ It said nothing about who may **read** a run or **cancel** one, and the code took the obvious shortcut: `authenticated`,
6
+ meaning any token we minted. That is a hole. It also left the visualizer with no way in at all, which is the same
7
+ question from the other side.
8
+
9
+ Building `jr2 visualize` (the Machine renderer and its live run highlighting) forced both halves at once: the page is a
10
+ **browser**, and a browser holds no token. So this ADR splits the HTTP surface into **three bands**, and adds an
11
+ **observation projection** — the page gets a view instead of a credential.
12
+
13
+ ## Decision
14
+
15
+ **Three bands, and the middleware is what says which.**
16
+
17
+ ```
18
+ open /workflows, /workflows/:name/machine, /viz/* structure
19
+ /workflows/:name/runs, …/runs/:runId/events OBSERVATION (new)
20
+ authenticated /agents/:iid/* the Agent's surface [Sandbox token]
21
+ instanceOnly /runs, /runs/:id, /runs/:id/events (GET + POST) run state + control [Instance token]
22
+ POST /workflows/:name/runs starting a run (ADR-0058)
23
+ /runs/:id/gates/:gate/events gates (ADR-0011/0013)
24
+ ```
25
+
26
+ - **Run state and run control are the Instance token's alone.** `authenticated` accepts _any_ token we minted, so a
27
+ Sandbox token — the one credential that sits in a pod an Agent shares a network namespace with — could read **every**
28
+ run's context (other features' branches, ticket bodies, review verdicts, Harness endpoints) and **CANCEL any run**.
29
+ Neither is on the Agent's surface, no more than a gate is: ADR-0013 scopes that token to delivering into registrations
30
+ recorded against its _own_ Sandbox. This closes the same class of hole ADR-0013 closed for gates, on the routes it did
31
+ not name. The band is named for the credential it requires, not for a role — "operator" in this repo is the Kubernetes
32
+ operator (ADR-0001).
33
+
34
+ - **The observer gets a PROJECTION, not a credential.** `RunObservation` is
35
+ `{ runId, workflow, status, value, children }` — identity, and where in the Machine the run is. What is absent is the
36
+ point: `context` is the workflow's working data, and `instanceId`/`fault` name live infrastructure and leak error
37
+ text. A state `value` (and the `children` tree of state values below it) is a tree of state **keys** — it is
38
+ structure, and structure is already open at `/workflows/:name/machine`. So an observer learns nothing here it could
39
+ not read from the Machine doc, except **which states are lit**. `observe()` is the one place that line is drawn, and
40
+ `RunChild` has no context field at any depth — the redaction surface cannot grow by forgetting.
41
+
42
+ - **Observation is scoped to one workflow**, because that is what an observer already knows (it is in the page's path).
43
+ There is no listing of everything an Orchestrator happens to be running, and the feed serves **live runs only** —
44
+ reading a settled run's terminal status through the store stays on `/runs/:id`, which is guarded.
45
+
46
+ - **An `emit`'s payload does not cross.** The observation feed carries an emit's `type` alone: an author's emit payload
47
+ is author data, the same class of thing as context.
48
+
49
+ ## Considered options
50
+
51
+ - **Give the page a token.** It would have to be the **Instance** token — gates, cancel, every run's context — handed to
52
+ whatever can load a URL. Putting it in `jr2.config.ts` (checked in) is worse still, and the token is minted per boot,
53
+ so the page would not have it anyway. Rejected: the visualizer needs to _see_ a run, and seeing is not a subset of the
54
+ credential that steers it. That mismatch is the whole reason a projection exists.
55
+ - **Keep `/runs*` on `authenticated` and filter in the handler.** Rejected: the guard would then depend on every future
56
+ handler remembering to redact. The band is a middleware precisely so the default is refusal.
57
+ - **A fourth "observer" token.** Rejected: it buys nothing today (the observation surface carries no secret) and costs a
58
+ minting, distribution, and revocation story. If observation ever needs to be _private_ — a deployed jr2 Application on
59
+ a shared network — that is the ADR to write, and it is the same open question ADR-0013 left about authn for non-kube
60
+ callers.
61
+
62
+ ## Consequences
63
+
64
+ - **`auth.test.ts` changed its mind.** Its prose already said "Observation and control are the operator's, not the
65
+ Agent's" directly above assertions that a Sandbox token gets **200** on a run read and **200** on CANCEL. Those are
66
+ now **403**. The test pinned the hole.
67
+ - **The API is a cluster Service** (ADR-0019), so "open" means open to whoever can reach it — in-cluster peers, a
68
+ port-forward, or an ingress — deliberately, since what is open is structure plus lit states. Anything that would
69
+ embarrass a user if seen belongs behind `instanceOnly`, and that is where it is.
70
+ - **The CLI is unaffected.** It holds the Instance token from the in-cluster Secret (ADR-0019) and reads the guarded
71
+ routes, as before.
72
+ - **Open, inherited from ADR-0013:** what a deployed jr2 Application uses for human callers. Whatever it is, the band
73
+ split survives it — only the credential in the `instanceOnly` band changes.
74
+
75
+ - **One scoped exception, and it does not live on this surface at all (ADR-0023).** The Harness prints each Agent's
76
+ conversation — prompts, assistant text, thinking, tool calls, and the answer jr2 gave each Menu pick — to container
77
+ stdout, which leaves the pod for the cluster's log plane and lands in front of a reader this document never gets to
78
+ check. That is a real exception to the rule above, and it is stated rather than reasoned away. Two things bound it. It
79
+ is **not a new band**: the HTTP surface is untouched, nothing here is served, and a reader gets it from
80
+ `kubectl logs`, holding cluster log access instead of a jr2 credential. And **a Working tool's results are excluded**
81
+ — the file contents, command output, API responses it returns never print, so the mechanical leak (an Agent `cat`s a
82
+ config while debugging and its secrets ride a log shipper into 30-day retention) is off the table, leaving what an
83
+ Agent reasons aloud and what jr2 answered its picks. What an Effector returns in that answer is scrubbed before it
84
+ signs it as ADR-0068 claims: no secret the Effector holds, in the forms it sent or in common encodings. The scrub does
85
+ not try to catch a transformed secret (a hash, a truncation, an encoding it does not list), a partial echo, or a side
86
+ channel, and a credentialed command's child reads the secret its command names, so it can print that secret in such a
87
+ form; then it prints there as the model read it. The answer also carries an Effect's output as the model read it: a
88
+ `command` Effect's output prints there up to the bound, and a Held value an upstream echoed into that output prints
89
+ with it (ADR-0023, ADR-0059). What remains, deliberately, is that on a shared cluster the population who can read an
90
+ Agent's reasoning, and what its picks answered, is whoever can read pod logs. If that becomes unacceptable, the fix is
91
+ the read-through ADR-0023 declined to build — `instanceOnly`, on this surface, where the band already works.
92
+
93
+ Evidence: `packages/orchestrator/src/http.ts` (the three middlewares), `run-host.ts` (`observe` / `RunObservation` /
94
+ `RunChild`), `test/auth.test.ts` (the boundary), `test/http-viz.test.ts` (a planted secret never reaches the feed, at
95
+ any depth of the child tree).
@@ -0,0 +1,265 @@
1
+ # Workflows are authored through `jr2Setup`; vocabulary and agent menus derive from the machine
2
+
3
+ The jr-parity exercise produced a working model but a bad consumer API: mechanism plumbing (manifests, tool lists,
4
+ event-union bookkeeping) dominated the workflow. The redesign grill (2026-07-13/14, full record in
5
+ [docs/design/workflow-api/](https://github.com/snapwich/jr2/blob/71ebe36/docs/design/workflow-api/proposal.md)) settled
6
+ the split: **the consumer writes pure workflow — states, transitions, prompts, policy — and jr2 absorbs everything else
7
+ through convention.** This ADR covers the Authoring surface; [ADR-0016](0016-agent-turn-mechanics-are-internal.md)
8
+ covers the agent turn; [ADR-0017](0017-pool-over-a-source-port.md) covers the run loop.
9
+
10
+ ## The root of `@jr2/orchestrator` is the Authoring surface
11
+
12
+ A workflow authors against one package: `jr2Setup`, `defineEvent`, `agent`, `workspace`, the Effects, `customize` and
13
+ `defineConfig` all import from `@jr2/orchestrator`. Its root exports the Authoring surface (GLOSSARY.md) and nothing
14
+ else.
15
+
16
+ - **The rule.** A name is on the surface when an author writes it in a workflow module or in `jr2.config.ts`. No other
17
+ name is. A name that an author only gets by inference stays off the surface: the event union that `jr2Setup` derives,
18
+ a Gate's input, an Agent's Frame. The `gate` actor is off the surface too. `jr2Setup` registers it, and a state
19
+ invokes it as `src: "gate"`, so no author writes the value. A type is on the surface when an author must write its
20
+ name, for example `Workspaced`, `HostInjectedInput`, `AgentDefinition`, and the types of the parts that `customize()`
21
+ takes. `JR2Config` is on the surface too: the `jr2.config.ts` Reference documents its fields, and an author names it
22
+ to type a config that is built outside `defineConfig()`.
23
+ - **Named exports only.** `src/index.ts` names each export (`export { a } from …`, `export type { T } from …`), in
24
+ groups by topic, and has no `export *`. Thus an Assistant that reads the file sees the full surface, and each change
25
+ to the surface is a line in a diff.
26
+ - **A test pins it.** `test/authoring-surface.test.ts` makes sure that `index.ts` has no `export *`, that the root's
27
+ runtime values are equal to a sorted list in the test, and that the type names in the `export` lists of `index.ts` are
28
+ equal to a second sorted list. A name that is added or removed fails the test until the list changes. The same test
29
+ makes sure that `@jr2/machines` and each Instance-shaped folder import the root only.
30
+ - **The kit's own packages import `@jr2/orchestrator/internal`.** It holds the surface and all that the kit shares: the
31
+ run host, the HTTP API, the Harness wire, the Custodian, the Effector wire, the cluster names. It is not for authors,
32
+ and it has no stability promise: any name in it can change in any release. A Machine that the kit ships
33
+ (`@jr2/machines`) and each Instance-shaped folder in the repo import the root only. The `exports` map has `.`,
34
+ `./internal` and `./tsconfig.instance.json`, and nothing else. Thus an Instance that imports a deep path, such as
35
+ `@jr2/orchestrator/src/run-host.ts`, does not resolve it, in `tsc` or in Node.
36
+
37
+ ## `jr2Setup`: an xstate `setup()` analog, not a DSL
38
+
39
+ `jr2Setup({ types, events: [defs], actors, actions, guards })` is statically imported and returns xstate's **public
40
+ `SetupReturn`** — `.createMachine()` yields a plain `StateMachine`, Stately-inspectable and `.provide()`-testable, with
41
+ no jr2 runtime needed to construct it (proven by compiled experiment against xstate 5.32.2; consumer surface has zero
42
+ casts). It:
43
+
44
+ - injects the mechanism events (`agent.fault`, `workspace.lost`) into the event union and derives the workflow event
45
+ types from the zod defs — hand-written `EventFrom` unions retire;
46
+ - pre-registers the jr2 actors (`agentRun`, `gate`) with typed inputs;
47
+ - takes the defs **as values**, which lets it validate at `createMachine` time that every event key appearing anywhere
48
+ in the machine maps to a def — closing xstate's nested-`on` typo hole (unknown keys in nested states typecheck
49
+ silently upstream) with a load-time failure;
50
+ - attaches the vocabulary to the machine (`vocabularyOf(machine)`, a WeakMap keyed on `machine.config` — the object
51
+ xstate's `.provide()` passes through, so a clone keeps it, and the returned machine stays bit-identical). **The
52
+ `export const events` manifest retires**, and the workflow module contract shrinks to `export const machine` (revising
53
+ [ADR-0011](0011-workflow-defined-events.md)'s named-exports contract): the defs ride the Machine, and the actors that
54
+ need them resolve names off the Machine that INVOKED them (ADR-0011, ADR-0049) — the host copies nothing at
55
+ registration, and the only other reader is the Machine doc the Console renders. ADR-0011's anti-global-registry
56
+ argument is preserved — attribution flows through the machine, per-Machine by construction.
57
+
58
+ `events` is optional, and so is the whole argument: a Machine with no Vocabulary leaves `events` out, or calls
59
+ `jr2Setup()`, and its event type is the Mechanism events alone. Such a Machine still uses `jr2Setup` when it declares a
60
+ door ([ADR-0033](0033-a-machine-declares-the-input-that-starts-it.md)), because xstate's `setup()` does not read the
61
+ `input` of `createMachine`. An empty Vocabulary is the default, not `readonly EventDef[]`, so the type of `event` does
62
+ not widen to every event name. A required `events` was considered and rejected: `events: []` tells the reader nothing,
63
+ and the compiler gave a missing `events` as a large structural diff of `SetupReturn`, not as the name of the missing
64
+ key.
65
+
66
+ ## Agent menus and gate accepts derive from the machine
67
+
68
+ A state that invokes an Agent slot derives the workflow events its Turn may pick: its own transitions, its ancestors'
69
+ (bubbled, per statechart semantics), and the transitions written on the states below it — its **modes**. xstate hands an
70
+ event to the deepest active state that handles it, and a false guard there falls through to the parent, so a pick
71
+ written on a child is one the Turn can take while that child is active. The descent stops at a child that invokes
72
+ another Agent slot (a nested Turn: its picks are its own) or a `gate` (its transitions are the Gate's accepted set), and
73
+ leaves out everything below that child; a child that runs anything else stays in. A state that invokes `gate` gets its
74
+ accepted set from its own and its ancestors' transitions and those written on its plain children, by the same descent:
75
+ position gives a Gate the writings below it that no other party answers for, as it gives an Agent its modes (the Pool's
76
+ wake Gate, on its root, takes the writing on `parked`). The consumer names neither; MCP appears nowhere in workflow
77
+ code. Mechanics: the **vocabulary** derivation is static, in `jr2Setup.createMachine` — a config walk wraps each
78
+ invoke's `input` to append the derived names, so names still ride serializable input and the ADR-0007 restore path and
79
+ invoke-time validation are unchanged. The same walk unions the derived sets of every state that invokes an Agent of one
80
+ name into that Agent's **Menu** — its tools, fixed for a conversation so the provider's prompt cache survives a change
81
+ of state. The walk reads transition _keys_ and so cannot see guards, which is why what a Turn may pick is not static on
82
+ top of it: `agentSurface` asks the invoking machine's guards, and the Turn is told its **Allowed picks** — the derived
83
+ set, less every event that the live snapshot, in its active mode, would not take
84
+ ([ADR-0029](0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md) owns the rationale).
85
+ Authoring is untouched by the split: a workflow names no tools in either leg. The same walk feeds `jr2 visualize` ("this
86
+ state's agent can call X, Y"). Dotted names (`agent.*`, `workspace.lost`, `xstate.*`, `after`) are mechanically
87
+ excluded.
88
+
89
+ **A transition belongs to the party that its state, or the nearest state above it, invokes.** The party is the Agent
90
+ slot or the `gate` that state invokes, or both when one state invokes both; root transitions, with no such state above,
91
+ belong to both. That is who may send an event whose def leaves `audience` unset
92
+ (`defineEvent({ audience?: "agent" | "external" | "any" })`, no default):
93
+
94
+ | Transition written on | Unset audience: who may send |
95
+ | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
96
+ | A state that invokes an Agent, or a mode below it | That Agent |
97
+ | A state that invokes a Gate | The Gate's caller |
98
+ | A state that invokes both | Both |
99
+ | A state that invokes anything else (a child Machine, a wrapper body, any other actor), or nothing | The party of the nearest invoking state above; none above means both |
100
+
101
+ So child or sibling says only "same Turn or not"
102
+ ([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)): an in-Turn Gate's caller sends the Gate's
103
+ events and not the Agent's picks, and a nested Agent does not get the outer Agent's picks. **Write an audience only to
104
+ give a pick to the other party, or to hold a root pick back.** An explicit `audience` overrides by kind over the whole
105
+ reach: `agent` reaches every Agent whose reach holds a writing of the event, `external` every Gate's, `any` both — so
106
+ `submit: "any"` lets an in-Turn Gate's caller send the Agent's pick, and `report_blocked: "agent"` keeps a root pick
107
+ from the Gates. A def means one thing: its description must be true at every writing its party can reach, and two
108
+ meanings are two defs. The reach itself is unchanged by the rule — own and ancestors, plus the states below with no
109
+ party-invoking state between (an Agent's modes, a Gate's plain children) — and the rule only filters inside it, so no
110
+ case widens who can send a pick; a test scans every shipped Machine and fixture for it, both the derived names and the
111
+ writings each party may take at run time. The walk computes every party's seat — per state, who may send what — and the
112
+ Machine's doc carries it; `jr2 up` prints it for every registered Machine and every Machine it composes, and the Console
113
+ shows the same lines, folded beside the workflow's diagram (structure, so tokenless too). An explicit `accepts:` on a
114
+ Gate's input remains an escape hatch.
115
+
116
+ **`sentBy(id)`: a writing that declines other senders.** Every event a party delivers carries its sender (ADR-0011):
117
+ `sender: { party, id }`, the Agent's slot or the Gate's invoke id. `jr2Setup` registers one guard, `jr2.sentBy`, and the
118
+ author writes it on a writing as `guard: sentBy("coder")` — a named guard with params, alone or inside `and([...])`. It
119
+ is true for an event whose `sender.id` is `id` and for an event with no sender (the Machine's own: `raise`, `sendTo`
120
+ itself, a child's `sendParent`), and false otherwise, so xstate's own selection moves outward to the next writing of the
121
+ name. It can only make a writing decline; it never gives a writing to a party — position does. `id` is an Agent slot or
122
+ a Gate's invoke id of this Machine, and a `sentBy` that names neither is refused at load, like an unknown event key.
123
+ Under `or()` or `not()`, or in an author's own guard that reads `event.sender`, it works at run time but the walk cannot
124
+ read it. A `.provide()` test sends as a party by writing the field: `{ type, sender: { party, id } }`.
125
+
126
+ **A delivery never runs another party's writing.** Audience says who may send a name; xstate selects which writing runs,
127
+ deepest active first, whoever sent it. So at delivery the real event — its payload and its sender — goes through
128
+ xstate's own selection (`getTransitionData`) before the Machine sees it, and if a writing selected is one the sender may
129
+ not take by the position rule above, the event is refused: an Agent's pick is answered `moved: false` and not delivered
130
+ (ADR-0029), a Gate's caller gets 409 with what the Gate takes now (ADR-0013). The lists ask the same question the same
131
+ way, payload-blind: an Agent's Allowed picks and a Gate's `acceptsNow` are the names whose selection, for an event that
132
+ carries the sender and with every guard that reads the payload counted as passing, moves something and holds only
133
+ writings the sender may take. A guard that reads the payload is unknown there and passes the party question; it is never
134
+ the fail-open of a party check skipped, because delivery asks again with the real payload. The payload-blind selection
135
+ is a copy of xstate 5.32.2's (`transitionNode`, `StateNode.next`, `evaluateGuard`, the members of `and`/`or`/`not`),
136
+ pinned against `getTransitionData` in the default test run — the canary for an xstate bump. Without a `sentBy`, nothing
137
+ misroutes: the shallower party is refused while the deeper writing is active, and the walk warns (below). When the
138
+ deeper party runs BELOW an Agent's state — a Gate, a nested Agent — the Agent's state is not at Rest, so the Agent's
139
+ picks are refused before the selection is asked
140
+ ([ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md)); the selection decides the Gate's caller's
141
+ sends there, and every case with the Agent's state at Rest.
142
+
143
+ **Load checks on modes and audience.** `createMachine` refuses a registered event that no state handles: it is in no
144
+ derived set, so it is on no Menu and no Gate. It refuses a parallel region below a state that invokes an Agent — the
145
+ Agent's state itself being parallel included, since its regions are below it: the shape is reserved for picks the Agent
146
+ does not wait for, which jr2 does not build, and background work goes above the Agent's state. It warns — the Machine
147
+ stays legal; the doc carries the warning, `jr2 up` prints it and the Console shows it with the seats — about a mode that
148
+ nothing enters (not initial, and no state the Machine can enter has a transition into it or a state inside it — so
149
+ neither the mode's own `initial` nor its transition into itself counts: its picks are on the Menu and never allowed; a
150
+ transition into a history state enters that history state's default target, shallow or deep, or its parent's initial
151
+ state when it names none, and a default target the walk cannot resolve is its own warning); about a Gate above an
152
+ Agent's state that takes an unset-audience writing the Agent reaches (the one narrowing of the audience rule an author
153
+ may not expect); about two parties' writings of one name that can be active together; about one party's writings of one
154
+ name that mean two things; and about two shapes below an Agent's state that keep it from Rest, so every pick of the
155
+ Agent waits (ADR-0024): a Gate whose answer has no target (the Gate stays open after it), and a long-lived invoke — one
156
+ that runs until its state exits, not a promise, an Effect, a Gate, an Agent or a child Machine. A child with no `onDone`
157
+ that strands the Agent in it — a one-way mode — is legal and silent on purpose, an Effect child included.
158
+
159
+ **Load check on the output of a run.** xstate v5 gives a run one output: the `output` at the root of the Machine. The
160
+ `output` of a final state directly below the root goes to that root `output` as `event.output`, and to nothing else.
161
+ Thus a Machine with an `output` on such a final state and no root `output` ends with no output, and `types.output` does
162
+ not stop it. It was the most frequent error in the Machines that Assistants wrote, and an Assistant then told its user a
163
+ result that the run did not have. Thus the load walk refuses that shape: `createMachine` and the parts walk at `jr2 up`
164
+ and at the boot, so a Machine built with xstate's own `setup()`, and a child Machine, are refused too. The refusal names
165
+ each such final state and the two fixes: an `output` at the root from `context`, or
166
+ `output: ({ event }) => event.output` at the root, which passes on the output of the final state. A final state inside a
167
+ compound state is legal: its `output` is the `event.output` of the `onDone` of that state. A Machine with no `output`
168
+ anywhere is legal: its result is in its `context`, and it tells nothing false about an output. Considered and rejected:
169
+ a warning (a run with no output is never what the author meant, and a warning does not stop the false result); and a
170
+ root `output` that jr2 adds when there is none (it would make jr2 Machines behave differently from xstate Machines, and
171
+ a Machine that a different xstate host runs would lose its output again).
172
+
173
+ **Load checks on Effects** (ADR-0067). `createMachine` refuses a state below an Agent's state that invokes something and
174
+ that nothing enters: what it invokes never runs (a mode nothing enters is only the warning above). Both checks read one
175
+ reachability walk, history defaults included; where a history default the walk cannot resolve may enter such a state,
176
+ the walk does not know, and it warns instead of refusing. Both checks look below a state that invokes an Effect or a
177
+ child Machine, and stop at one that invokes another Agent or a Gate. Where a pick enters a state that invokes an Effect,
178
+ every param of the pick must be read by something — the check is per state, not per invoke — by the input of any invoke
179
+ of that state, an Effect's or not, so a second invoke beside the state's work counts (`fromPick` reads each param that
180
+ is a key of the Effect's input, a command's argv slots, and each one its `fill` reads; on an invoke that is no Effect it
181
+ reads every param, and on a command whose argv is Open it reads every param too, since the argv's slots are the
182
+ composer's — `customize()` runs this check again when it binds the argv and refuses there a param the bound argv does
183
+ not read, and gives the bound Machine the warnings the walk gives for that argv
184
+ ([ADR-0054](0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md)); a plain mapper is probed with
185
+ a recording event), by the entering transition's guard (inline, named, or inside `and`/`or`/`not`), or by an `assign` on
186
+ the entering transition or on the entry of a state it enters — else the Agent supplies it and nothing uses it: refused.
187
+ A pick enters what xstate enters: its target, the target's ancestors below the transition's domain, and their initial
188
+ descendants — and what an eventless `always` on a state it entered enters next, in the same macrostep with the same
189
+ event, whose guard and actions count as the entering transition's. So a pick that targets a compound state enters the
190
+ child that invokes, a pick that targets a child enters the ancestor that invokes, a pick that targets a state whose
191
+ `always` moves on enters where it moves, and a transition inside the invoking state does not enter it. A pick is a
192
+ writing an Agent may send (the position rule above); a Gate's event is a human's, and its params are not checked. The
193
+ walk does not carry a pick's params across a timer: where an `after` on a state the pick entered moves on to a state
194
+ that invokes an Effect, the Effect's input and every guard and action from the timer on read the timer's event, so only
195
+ a guard or `assign` before the timer reads them — a param that must cross is stored with `assign` on the pick's
196
+ transition, and one nothing stores is refused. A pick that targets a history state enters what was last active, which
197
+ the walk cannot know: where that may be a state that invokes an Effect, the pick's params there are unchecked, and it
198
+ warns. A guard and an assign are pure by contract, so the probe runs them; it runs no other action. TypeScript does not
199
+ catch a `fromPick` param that is no key of the input: it accepts an extra key on a spread. It refuses a `fill` that sets
200
+ a key that is also a param (the Machine's value would drop the Agent's), and a `fromPick` of a pick other than the event
201
+ that enters the state. Code the probe cannot or does not run may read a param, so it gives a warning, never a refusal: a
202
+ mapper, guard or assign that throws in the probe, and — where a param is left unread — any other action on the entering
203
+ transition or on the entry of a state it enters, named in the warning. Its params are unchecked. It warns where an
204
+ Agent's state answers on a mode's entry (`answer`) and an Effect below it has no `onDone`: the Effect settles in place,
205
+ that mode is not entered again, and the Agent reads the Effect's output, not the answer. A pick that enters no Effect,
206
+ directly or across a timer, is not checked for its params.
207
+
208
+ - **Two parties' writings of one name that can be active together** — one above the other on one path, or in parallel
209
+ regions — where the deeper writing (each other region's, for parallel regions) does not decline the other sender with
210
+ a `sentBy` the walk can read. xstate would select it for the other party's event, so delivery refuses that event while
211
+ both are active. The warning names both writings and shows the corrected line, with the guard on the DEEPER writing,
212
+ because xstate selects deepest first and only a declining deeper writing lets selection move outward:
213
+
214
+ ```
215
+ "approve" on "s2.release" (Gate "review-1") and on "s2.release.work.draft" (Agent "coder") can be active together;
216
+ Gate "review-1"'s "approve" would run Agent "coder"'s writing, so jr2 refuses it while both are active. Make the
217
+ deeper writing decline other senders:
218
+ draft: { on: { approve: { guard: sentBy("coder"), target: "#s2.doneB" } } }
219
+ ```
220
+
221
+ A root writing is both parties', so the root-writing shape — a root `cancel`, a Gate below it, the Agent's `cancel` in
222
+ a mode — is the same case: the line goes on the mode's writing, and the Gate's caller's `cancel` then runs the root
223
+ writing. The Agent's writing above a party seated BELOW the Agent's state — a Gate, a nested Agent — is not the case:
224
+ while that party runs, the Agent's state is not at Rest, so every pick of the Agent is refused whatever its guard, and
225
+ a `sentBy` on the deeper writing would change nothing; the walk does not warn there. Siblings (a mode and a Gate's or
226
+ another Agent's sibling state) are not the case: neither writing is ever selected for the other, and delivery answers
227
+ a send there like any pick the state does not take. A Gate whose invoke id the walk derived is named so in the line;
228
+ giving its invoke an `id` names it. A `sentBy` under `or()` or `not()` leaves the warning in place.
229
+
230
+ - **One def, one meaning.** One party's writings of one name on one path with different targets: its sender reads one
231
+ description for both, and only the deepest active writing runs. The warning says to make the def's description true at
232
+ each writing or to give them distinct names. xstate's own transition `description` rides the Machine doc beside each
233
+ writing.
234
+
235
+ Known limits, not built: two states on one path that invoke one Agent slot share one sender id, so `sentBy` cannot tell
236
+ their Turns apart; and a child Machine that forwards a party's event to its parent (`sendParent`) hands it the child's
237
+ sender, whose id names a slot or Gate of the child — if the parent has one of that name, `sentBy` matches it. Only
238
+ author code forwards an event.
239
+
240
+ Considered and rejected for modes: a Menu per mode (the tools block would change inside a conversation and bill its
241
+ history again — ADR-0029), and refusing picks written below an Agent's state at build (that shape is a mode).
242
+
243
+ Considered and rejected for the audience: a default of `any` with a convention to tag the sensitive events (an in-Turn
244
+ Gate's caller could then send every pick of the Agent above it unless each was tagged `agent`, and a nested Agent got
245
+ the outer Agent's picks); a fixed default of `agent` (every Gate event then needs a tag, and the nested leak stays); and
246
+ a knob on the Gate (`accepts: "own"`), which fixes the Gate side only. The rule above reads the transition's position,
247
+ not a mark written in it, so it is not the per-transition demarcation rejected below.
248
+
249
+ Considered and rejected: per-transition demarcation (xstate transitions have no typed metadata slot for it without
250
+ inventing jr2-only config inside the transition table — a DSL by the back door) and purely structural
251
+ own-transitions-only derivation (breaks ADR-0011's blessed handle-`report_blocked`-once-at-an-ancestor idiom). Position
252
+ stays the default and the owner of every writing. `sentBy` is not demarcation: it is an ordinary xstate guard, written
253
+ only where a writing must decline other senders, and it cannot give a writing to anyone.
254
+
255
+ ## Why there is no injection model (the retired ADR-0003)
256
+
257
+ jr2's first composability story — "workflows are templates of injected providers": Machines reference their moving parts
258
+ as named xstate slots, filled at deployment via `provide({ actors, actions, guards })`, with noop defaults — was tried
259
+ and retired (its ADR, 0003, is deleted; this section is its record). It never composed: xstate's `provide()` fills only
260
+ the machine it is called on and **cannot reach actors inside child machines**, so host-side injection cannot compose
261
+ past one level — and the host has injected nothing since ADR-0011's static-import doctrine ("actor logic is code; live
262
+ things are built per-invocation from serializable input"). Workflows author machines against statically-imported
263
+ mechanisms. What survives of the idea: the kernel "everything pluggable is an xstate actor", and `.provide()` as the
264
+ unit-test seam. The mock-vs-real swap it wanted lives at the wire instead (ADR-0011's stub Harness). GLOSSARY.md's
265
+ **Template** and **Provider** entries retired with it.
@@ -0,0 +1,124 @@
1
+ # Agent-turn mechanics are jr2-internal: ambient coordinates, host-side offsets, absorbed retries, fresh sessions
2
+
3
+ Second of the workflow-API redesign trio ([ADR-0015](0015-authoring-surface-absorbs-the-mechanism.md) has the context;
4
+ full record in
5
+ [docs/design/workflow-api/](https://github.com/snapwich/jr2/blob/71ebe36/docs/design/workflow-api/proposal.md)).
6
+ `agentRun`'s consumer input shrinks to workflow vocabulary — `{ agent, prompt }` plus opt-ins below — because everything
7
+ else it used to take was jr2 handing the consumer values jr2 already had. Each absorption follows, with what it deletes
8
+ from a jr-shaped workflow.
9
+
10
+ The opt-ins include two optional dials, `model` and `thinkingLevel`
11
+ ([ADR-0018](0018-instance-agents-are-definitions-jr2-assembles-the-harness.md) draws the identity-vs-dial line and
12
+ states where each is validated). The **absorption principle is what admits them**: what this ADR absorbed was values jr2
13
+ already held and was handing back to the consumer to hand in again — mechanism wearing a consumer's clothes. A dial is
14
+ the opposite case: a genuine per-call choice that jr2 cannot derive, because Agents are instance-scoped and only the
15
+ invoking workflow knows how hard this turn is. Both are serializable strings, so they ride the persisted child input; a
16
+ restore re-attaches to a Submission whose model is already fixed server-side and never re-resolves them.
17
+
18
+ ## Ambient endpoint and sandbox (deletes the `turn()` helper and all handle threading)
19
+
20
+ `workspace()`'s `running` state co-invokes a registrar actor that records its handles in a `WeakMap<ActorRef, Handles>`;
21
+ `agentRun` walks `self._parent` to the nearest registered ancestor. Invoked-actor registration (not an entry action) is
22
+ what makes this restore-safe: invoked actors restart on snapshot restore, entry actions do not. Not under a workspace →
23
+ fail loudly, unless input carries an explicit `endpoint` (the dev-stub, workspace-less path). Body-facing workspace
24
+ handles are `{ repos, branch }` ([ADR-0012](0012-workspace-wrapper-machine.md)).
25
+
26
+ **[ADR-0013](0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md)'s token scoping survives and
27
+ strengthens**: the registration records the Sandbox resolved from the enclosing wrapper — the same deterministic
28
+ `workspaceName()` the token was minted for — so the consumer can no longer forget to pass it (the baba71f incident:
29
+ `sandbox` omitted, every tool call 403'd, fail-closed but silent). The parent chain resolves the _enclosing_ wrapper
30
+ structurally, never a sibling's; cross-feature event injection stays impossible.
31
+
32
+ ## Offsets live in a host ledger (deletes `context.offsets` and the `agent.offset` event)
33
+
34
+ `agentRun` reports `iid → offset` through the run binding into a ledger persisted beside the snapshot in the same
35
+ `RunBlob` save. Iids are globally unique, so restore's recursive nested-context offset folding dies with the context
36
+ leg; restore still rewrites the child's persisted input ([ADR-0007](0007-durable-machine-state.md)'s invariant stands
37
+ verbatim — only the offset's location moved). The Harness wire does the heavy lifting
38
+ ([ADR-0027](0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md)): `wait` reconnects from the admitted
39
+ offset, and `abort` is the deliberate terminal cancel ([ADR-0002](0002-duplex-streaming-actor.md)).
40
+
41
+ The ledger's key is the iid, and an iid is not a Turn. Two Turns that continue one conversation share it, and nothing
42
+ removes a Turn's entry when the Turn ends. So the input mapper also mints a **Turn ID** (`turnId`), one per invocation,
43
+ and it persists with the child's input. The actor stamps it on every admission that it ledgers. The restore re-attaches
44
+ a child only to an admission with the child's own Turn ID. An admission with another Turn ID is an earlier Turn's: the
45
+ child's admission was not ledgered, so the child keeps its prompt and is admitted again. If its admit reached the
46
+ Harness before the crash, the prompt is then sent twice. This is ADR-0007's known durability edge, an at-most-once
47
+ re-POST, and it now applies to a continued Turn as it does to a first one. Before the Turn ID, a crash in that window
48
+ restored the Turn onto the earlier Turn's Submission. The Turn then followed a stream that was not its own, and its
49
+ prompt was never sent. A plain `setup()` Machine that passes finalized input mints no Turn ID, and the key alone
50
+ decides.
51
+
52
+ ## Retries and nudges are absorbed; the workflow sees one terminal `agent.fault` (deletes the retry bookkeeping)
53
+
54
+ Two fault classes, deliberately not one knob ([ADR-0035](0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md)
55
+ later adds the third: runaway):
56
+
57
+ - **Infra faults** (stream drop, pod restart, provider error): provider-stream retry lives inside the turn in the
58
+ Harness, and `wait` reconnects from the offset ledger without a deadline; a restarted Harness or a lost Workspace ends
59
+ the Turn through the Sandbox watch (ADR-0021), and a Harness that answers 404 is a lost conversation (ADR-0027). The
60
+ one exception is a restarted Harness while the Agent's state is busy, or with a pick's answer unread: that cuts the
61
+ Turn and faults nothing — the actor waits for Rest and **re-admits** on the same conversation, with the answer as a
62
+ notice (ADR-0024, ADR-0031). A re-admission is mechanism, like a nudge: telemetry on the feed, no marker, and it
63
+ spends no budget, because the model did nothing wrong.
64
+ - **No-signal** (the agent stops while its Turn is still open — jr's dominant failure mode): the wire treats that as a
65
+ normal completed Submission, so recovery is jr2's. **The ask-again rule**: while the Turn is open, a model that
66
+ stopped is asked again once its Allowed picks are not empty, and never with an empty list. A delivered pick does not
67
+ end the question — a pick with no target, or one into a mode, keeps the Turn open (ADR-0024) — only the Turn ending
68
+ does. One rule, asked at three moments:
69
+ - **The step boundary.** Inside the Submission, the Harness restates the Turn part when the Allowed picks changed
70
+ since the model last read them (ADR-0029); that message keeps the engine's loop going and costs no Submission. It
71
+ never restates an empty list to a model that stopped.
72
+ - **The nudge.** The Submission settled with picks left: the Agent's Actor admits a **nudge** on the same conversation
73
+ — "Your previous turn ended, and the workflow is still waiting for your pick." — absorbing ADR-0006's "forced final
74
+ pick is a Machine-level re-prompt" into the actor.
75
+ - **The wake.** The Submission settled with none left: the model stopped correctly, and the Turn waits on a Gate, a
76
+ child's invoke or a timer — the Allowed picks are empty while the Agent's state is not at Rest (ADR-0024). The actor
77
+ watches the invoking Machine and admits the same nudge the first time its Allowed picks are not empty, read after
78
+ the mailbox drains like Rest itself, so a state that sends to itself on its way to more work wakes nobody. The watch
79
+ ends with the state, so a Turn that ended is never woken, and a Turn that was never meant to pick waits until its
80
+ state exits.
81
+
82
+ The nudge and the wake are one Submission kind and spend one budget: a Machine that opens and closes picks under a
83
+ stopped model cannot ask it without bound. All three read one Allowed list, the one the surface serves (ADR-0029).
84
+ Their admission is for the conversation the Turn holds, so it says `kept` (ADR-0062). A Harness that lost the
85
+ conversation while the Turn waited refuses it and seats none, and the Turn is admitted again on a new conversation
86
+ with its task, or faults when it has no task to send. That ask was spent already, so the budget counts it once.
87
+
88
+ Budgets are defaulted knobs, not Machine context. Exhaustion emits the single terminal `agent.fault { reason }`; where
89
+ it routes (escalation, a triage/investigator state, a Failure) is workflow policy, but it must route in a Machine: the
90
+ walk refuses a state that invokes an Agent with no `agent.fault` route on itself or an ancestor, and an `agent.fault`
91
+ that no transition takes faults the run
92
+ ([ADR-0074](0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md)). In a Chart the route is
93
+ optional: the Open body sends an unrouted fault to the Machine that invoked it, whose route takes it
94
+ ([ADR-0070](0070-a-machine-lends-its-parts-to-a-chart.md)). Attempts surface as run-feed/visualize telemetry projected
95
+ to `{ child, attempt }` — no iids on the open observation feed ([ADR-0014](0014-observation-is-open-run-state-is-not.md)
96
+ unchanged).
97
+
98
+ ## Sessions are fresh by default; continuation is opt-in
99
+
100
+ jr's most deliberate design decision is the **lossy handoff** — every relaunch is a fresh session; revision coders read
101
+ the notes and the code, never the prior agent's conversation. That is the default: each `agentRun` invocation is a new
102
+ conversation. `continue: true` opts into continuation
103
+ ([ADR-0057](0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md)): the Turn lands on this Agent's one
104
+ conversation in this Machine instance, iid `<run>/<machine actor path>/<agent>` plus the epoch jr2 bumps on a terminal
105
+ fault; the per-invocation prompt lands as the next user turn; the tool menu still re-derives from the invoking state
106
+ (one conversation travels across the states of its Machine). Mid-turn restore re-attaches the in-flight turn in both
107
+ modes — `continue` governs only what a _new invocation_ means. The author names nothing: in a state machine the
108
+ conversation's identity is already on the page. (This corrects the jr-parity exercise's "same iid = jr's resume
109
+ machinery, free" — an inversion of jr's actual semantics.)
110
+
111
+ Two live invocations of one continued iid — parallel states — are refused at the registration table: one live surface
112
+ per address. A Submission that arrives behind a live one on the Harness queues: prompts for one instance enter one
113
+ per-instance queue in admission order, and a Submission is promoted only when it is the first unsettled one for its
114
+ conversation (ADR-0027 states this as jr2's own semantics). A queued prompt waits behind work that may never finish,
115
+ which is why [ADR-0024](0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md) both ends the turn and orders the
116
+ abort ahead of the next `send`.
117
+
118
+ The minting doctrine — addresses are computed by jr2, never by the workflow — covers gate ids too: a gate with no
119
+ authored id derives one from its actor path below the run root, through the same walk `mintIid` uses (`actorPath` in
120
+ registration.ts; ADR-0011). The two mint at different seats for a reason: iids must be minted in the input mapper
121
+ because minting has a random component (the fresh-session suffix) and a ledger read (the continued conversation's epoch)
122
+ — the input is what persists — while a path-derived gate id is deterministic from machine structure, so it is recomputed
123
+ at every actor (re)start and is restore-stable by construction. The mapper rule is about non-determinism, not a blanket
124
+ rule about where ids are born.
@@ -0,0 +1,69 @@
1
+ # The run loop is a jr2 `pool` over a generalized source port
2
+
3
+ Third of the workflow-API redesign trio ([ADR-0015](0015-authoring-surface-absorbs-the-mechanism.md),
4
+ [ADR-0016](0016-agent-turn-mechanics-are-internal.md); full record in
5
+ [docs/design/workflow-api/](https://github.com/snapwich/jr2/blob/71ebe36/docs/design/workflow-api/proposal.md)). The top
6
+ of every jr-shaped workflow — claim ready items, run one child per item under a cap, wake on push or poll, settle when
7
+ done — was ~90 lines of hand-rolled bookkeeping in the jr-parity exercise (`active[]`, `spawnChild`/`stopChild`,
8
+ `xstate.done.actor.*` casts, the discover/saturated/settling/idle choreography, and a comment-enforced "keep
9
+ `spawnChild` top-level or visualize goes blind" footgun). No xstate-ecosystem primitive exists for this (verified: the
10
+ canonical v5 pattern _is_ the hand-rolled loop), so jr2 ships one.
11
+
12
+ ## `pool(worker, spec)` — a machine factory, like `workspace()`
13
+
14
+ `pool(worker, { source, itemId, cap, itemInput, onDrained, failures })` returns a plain machine that owns
15
+ spawn-under-cap, stable child identity, typed completion collection (`DoneActorEvent<Output, string>` + the
16
+ `xstate.done.actor.*` wildcard — zero casts), `stopChild` bookkeeping, the wake gate, and the re-query timer. The one
17
+ top-level `spawnChild` lives in jr2's pool code, once — the visualize footgun is dissolved by construction, and
18
+ everything durable is a registered string src (xstate cannot persist inline-src children, which independently forces
19
+ this shape). The pool composes: it returns a plain machine and can itself be nested as a child. The typed idioms
20
+ (wildcard done events, `assertEvent`, outcome-in-context root output) ship as documented patterns regardless, so
21
+ outgrowing the pool means hand-rolling without penalty, not fighting a framework.
22
+
23
+ ## The source port is generalized; Work Source is one adapter
24
+
25
+ The pool draws from a **Source** port — `next(active) → item | null`, plus an optional `wake` event def (webhook /
26
+ `jr2 send` push seam) and `pollEvery` (the set mutates underneath us) — a queue, a generator, or a re-queried set. tk's
27
+ claim actor is one adapter; GLOSSARY.md's **Work Source** reframes as _the Source adapter for tickets_, and the
28
+ Ready-set doctrine (re-query, never materialize) survives as that adapter's semantics, not the primitive's.
29
+
30
+ A Work Source — `tk`, GitHub issues, Jira, a task queue — is a behavior port, not a canonical data schema: verbs like
31
+ `claimNext`, `updateStatus` and `comment`, each gated by a capability the adapter advertises. It owns the dependency
32
+ graph and hands out the Ready-set, the currently-unblocked items in dependency order, so the Machine never encodes a
33
+ dependency edge. The Ready-set mutates during execution — items can be reopened or created mid-run — so it is not a
34
+ static FIFO queue.
35
+
36
+ ## Terminal triage is three-valued, surfaced as status
37
+
38
+ Forced by parking-is-retention (ADR-0012) plus workflows whose children park on gates (escalation, human review):
39
+
40
+ - **drained** — source empty and all children settled → final, or the run ends `failed` when items failed (below);
41
+ - **deadlocked** — open-but-never-ready items with nothing progressing → surfaced distinctly (jr exit 2), not an
42
+ indistinguishable idle poll, or the run ends `failed` when items failed (below);
43
+ - **waiting** — children parked in gates → the run stays open and the gates list is the "what needs me" surface (jr exit
44
+ 3).
45
+
46
+ The pool can make this call because it owns both the source and the children — the strongest single argument for the
47
+ primitive over an authored loop. Triage surfaces as run _status_ (feed, `jr2 runs`), not machine events; a
48
+ `pool.deadlock` event can be added if a workflow ever needs to react rather than a human.
49
+
50
+ ## The pool catches a worker's Failure
51
+
52
+ A worker that fails ([ADR-0073](0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md)) ends
53
+ only its own item. The pool catches the Failure on `xstate.error.actor.*`, keeps its payload in `failures` by item id,
54
+ stops that worker, and claims again: the output is `{ status, items, failures }`. The pool is a kit-owned parent, so an
55
+ author cannot give it an `onError`; if it let the Failure go up, one lost `task` would end the whole run `failed`, and
56
+ the host would delete the Sandbox of each sibling, also one that waits at its `review` Gate with commits that it did not
57
+ push. That breaks parking-is-retention for work that did not fail. A defect in a worker still goes up and ends the run
58
+ `error`.
59
+
60
+ The option `failures: "fail" | "report"` says how a pool ends when one or more items failed. It catches each worker's
61
+ Failure in both modes, so one failed item never stops its siblings. With `"fail"`, the default, a pool that ends
62
+ `drained` or `deadlocked` with failures invokes `fail` with the reason `"<n> of <m> items failed"` and the detail
63
+ `{ status, items, failures }`, so the run ends `failed` and `jr2 run` exits 1. At either end no worker is live, because
64
+ a pool with a live worker parks, so the Failure deletes no sibling's Sandbox. With `"report"`, the pool ends `done` and
65
+ `failures` in its output says which items did not succeed: for a pool whose job is to try each item, where a failed item
66
+ is a correct answer. The default is `"fail"` because `done` means that the work succeeded (ADR-0073), and a CI loop must
67
+ not exit 0 while every item failed. The deadlock is the usual end of a dependency backlog with a failure: a Source over
68
+ a tracker keeps the failed item open, so its dependents never become ready. The `status` in the detail tells the two
69
+ ends apart, and the two details have one shape.