@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
@@ -15,32 +15,74 @@
15
15
  // (`{ streamUrl, offset, submissionId }` — jr2's durable re-attach handle, stored in the host
16
16
  // ledger), and `wait(admission)` follows the durable stream from the admission offset to the
17
17
  // Submission's Settlement — a long-poll loop that advances by the `stream-next-offset` header and
18
- // reconnects from the SAME offset on network failure (capped backoff, indefinitely: pod death is
19
- // `workspace.lost`'s job to report, not this loop's to guess). Re-attach after a restart is `wait`
18
+ // reconnects from the SAME offset on network failure (capped backoff, indefinitely: a lost pod is
19
+ // `workspace.lost`'s to report and a restarted Harness the Agent actor's, both off the watch —
20
+ // ADR-0021 — never this loop's to guess). Re-attach after a restart is `wait`
20
21
  // with the SAME persisted admission; replay cost is bounded by one Submission's chunks. A 404 is a
21
- // LOST conversation (ADR-0027: a conversation lives as long as its Harness process) — a
22
- // `SettlementFault`, never an endless poll.
22
+ // LOST conversation (ADR-0027: the Harness no longer holds it — a persisted conversation survives a
23
+ // Harness restart, ADR-0031, and one it cannot rebuild or has freed does not) — a
24
+ // `SettlementFault`, never an endless poll. A Submission a restart cut settles `failed` on the
25
+ // rebuilt stream (ADR-0031), which is read like any other Settlement.
23
26
  //
24
27
  // Both verbs re-send on a network failure, for one reason stated twice: an unanswered request is
25
28
  // not an answer. They differ in bound, and the difference is where the Submission is — `wait`'s is
26
- // already admitted, so the lease may report a dead pod and this loop need never give up, while
29
+ // already admitted, so the watch may report a dead pod or Harness and this loop need never give up, while
27
30
  // `send`'s does not exist yet, so its window closes and faults. `send` also re-sends ONLY what
28
31
  // provably never left this host (`postAdmission` — ADR-0042: it is the first thing ever to dial a
29
32
  // Sandbox's Service, and a CR at `phase: Ready` is not yet routable).
30
33
  //
31
34
  // `abort` is the third verb, and ADR-0024 wires it to the END OF THE INVOCATION: a turn ends with
32
- // the state that asked for it. It sweeps the running Submission and everything queued behind it to
33
- // the distinct `aborted` Settlement — worth having for observability, though jr2 never reads it
34
- // (the actor is stopped by then; see actor.ts).
35
+ // the state that asked for it. It sweeps the running Submission and everything queued behind it,
36
+ // and says its cause: `pick` when the Turn's own pick ended it, which settles the running
37
+ // Submission `ended`; no cause otherwise, which settles it `aborted`. The outcome is worth having for
38
+ // observability, though jr2 never reads it (the actor is stopped by then; see actor.ts).
35
39
 
36
40
  import { agentActorWith } from "./actor.ts";
37
- import type { AgentAdmission, AgentAdmitOptions, AgentLogic, AgentRunInput, AgentRunPort } from "./actor.ts";
41
+ import type {
42
+ AgentAdmission,
43
+ AgentAdmitOptions,
44
+ AgentLogic,
45
+ AgentRunInput,
46
+ AgentRunPort,
47
+ AgentSettleOptions,
48
+ ListedKill,
49
+ } from "./actor.ts";
38
50
  import type { AgentDeclaration, AgentDefinition, ThinkingLevel } from "./agent.ts";
51
+ import {
52
+ commandWith,
53
+ ExecNotSent,
54
+ ExecRefused,
55
+ type Arg,
56
+ type CommandInput,
57
+ type CommandOptions,
58
+ type CommandOutput,
59
+ type OpenCommandInput,
60
+ type EnvOpenParts,
61
+ type EffectLogic,
62
+ type ExecPort,
63
+ } from "./effect.ts";
64
+ import { open } from "./open.ts";
65
+ import type { OpenEffect, OpenOf } from "./open-effect.ts";
39
66
  import {
40
67
  LIVE_LONG_POLL,
41
68
  STREAM_NEXT_OFFSET_HEADER,
69
+ VIEW_HISTORY,
42
70
  VIEW_UPDATES,
71
+ type AbortCause,
72
+ type AbortRequest,
43
73
  type EchoEvent,
74
+ type ExecRead,
75
+ type ExecRequest,
76
+ type LiveConversation,
77
+ type LiveSetRequest,
78
+ type LiveSetResponse,
79
+ type MemoryKillsResponse,
80
+ type NotKeptResponse,
81
+ MAX_KILL_NAME,
82
+ MAX_KILL_STAMP,
83
+ MAX_KILLS_BODY,
84
+ MAX_TOLD,
85
+ type Notice,
44
86
  type Settlement,
45
87
  type StreamEvent,
46
88
  type SubmissionSettledEvent,
@@ -49,8 +91,8 @@ import {
49
91
  /** The three-verb wire client (the injectable seam — structurally what `@flue/sdk`'s
50
92
  * `agents.{send,wait,abort}` was, minus the SDK). */
51
93
  export type HarnessClient = {
52
- /** `POST /agents/:name/:id {message, cwd?, definition, model?, thinkingLevel?}` → the Admission,
53
- * with `streamUrl` resolved absolute. The FRAME is `message` and `cwd` — what this Turn is about
94
+ /** `POST /agents/:name/:id {message, cwd?, definition, model?, thinkingLevel?, notices?, kept?}`
95
+ * → the Admission, with `streamUrl` resolved absolute. The FRAME is `message` and `cwd` — what this Turn is about
54
96
  * and where it works (ADR-0057), flat siblings on the body. The DEFINITION is the slot's,
55
97
  * carried by the Machine and sent with every admission (ADR-0049) — the Harness keeps no roster,
56
98
  * so `:name` alone would name nothing there — and it travels as IDENTITY alone, because this
@@ -67,14 +109,45 @@ export type HarnessClient = {
67
109
  definition: AgentDefinition;
68
110
  model?: string;
69
111
  thinkingLevel?: ThinkingLevel;
112
+ /** The pending notices in this admission's scope (ADR-0062). Omitted when there are none. */
113
+ notices?: Notice[];
114
+ /** The admission is for a conversation this process was told the Harness keeps (ADR-0062).
115
+ * The Harness that keeps none answers 404 and seats none: this verb throws
116
+ * {@link ConversationNotKept}. */
117
+ kept?: boolean;
70
118
  signal?: AbortSignal;
71
119
  },
72
120
  ): Promise<AgentAdmission>;
73
- /** Follow the stream to this Submission's Settlement: resolve on `completed`, reject with
74
- * `SettlementFault` on `failed`/`aborted`/404. */
75
- wait(admission: AgentAdmission, opts?: { signal?: AbortSignal }): Promise<void>;
76
- /** `POST /agents/:name/:id/abort`. The `{ aborted }` answer is dropped (ADR-0024). */
77
- abort(agentName: string, instanceId: string, opts?: { signal?: AbortSignal }): Promise<void>;
121
+ /** Follow the stream to this Submission's Settlement: resolve on `completed` alone — the run
122
+ * returned with the Turn still open, which the Agent actor answers with the nudge or the wake —
123
+ * and reject with `SettlementFault` on `failed`/`ended`/`aborted`/404. `ended` is no fault, and no
124
+ * live actor reads it: a Submission settles `ended` only after the Turn's own pick took the
125
+ * Machine out of the Agent's state. That stopped the actor that waits on it (ADR-0024), and a
126
+ * stopped actor drops every rejection. A guard kill read on the way is handed to `onMemoryLimit` (ADR-0061). */
127
+ wait(admission: AgentAdmission, opts?: AgentSettleOptions): Promise<void>;
128
+ /** `GET /agents/:name/:id?view=history`: does the Harness still hold this conversation? Only a
129
+ * 404 says no (ADR-0027: a GET on an unknown conversation is 404); anything else — another
130
+ * status, a transport failure — says yes, because unknown is never loss (ADR-0062). */
131
+ holds(agentName: string, instanceId: string, opts?: { signal?: AbortSignal }): Promise<boolean>;
132
+ /** `GET /memory-kills`: the pod's guard kills (ADR-0061, ADR-0062). A request that never left
133
+ * this host, and a draining Harness's 503, are sent again until `signal` ends the read — the
134
+ * caller owns the read's limit (the Agent actor's 5 s); with no signal, it is sent once. Any other
135
+ * answer but a 200 that reads — a Harness that keeps none, a transport failure, the caller's
136
+ * timeout — is `undefined`, "could not tell": the Harness still holds what it kept, and the next
137
+ * admission reads again. A kill with no id or time, with an id or a time outside the wire's bounds
138
+ * (`wire.ts`), or with a time that does not parse, is skipped; one whose `peak` or `limit` is not
139
+ * a size is kept without sizes, and one whose `told` holds an entry that is not a conversation, or
140
+ * a name outside the bounds, is kept without that entry — each fault said. Absent sizes are no fault: the pod left them out.
141
+ * Each fault is one line, said when it starts. A read that works says nothing, and the next fault
142
+ * after it is said again. */
143
+ memoryKills(opts?: { signal?: AbortSignal }): Promise<ListedKill[] | undefined>;
144
+ /** `POST /agents/:name/:id/abort`, with the body `{ cause, submissionId }` when a cause is given
145
+ * (`AbortRequest`). The `{ aborted }` answer is dropped (ADR-0024). */
146
+ abort(
147
+ agentName: string,
148
+ instanceId: string,
149
+ opts?: { signal?: AbortSignal; cause?: AbortCause; submissionId?: string },
150
+ ): Promise<void>;
78
151
  };
79
152
 
80
153
  export type HarnessClientOptions = {
@@ -109,18 +182,100 @@ export type HarnessClientOptions = {
109
182
  log?: (line: string) => void;
110
183
  };
111
184
 
112
- /** A Submission that settled `failed`/`aborted` — or whose conversation the Harness no longer
113
- * has (404) — surfaced to the actor as an `agent.fault`. Replaces the SDK's execution error. */
185
+ /** A Submission that settled `failed`/`ended`/`aborted` — or whose conversation the Harness no
186
+ * longer has (404) — surfaced to the actor as an `agent.fault` while it still waits on the Turn.
187
+ * Replaces the SDK's execution error. */
114
188
  export class SettlementFault extends Error {
115
189
  /** The Settlement as the stream carried it; absent when the conversation itself was lost. */
116
190
  readonly settlement?: Settlement;
117
- constructor(message: string, settlement?: Settlement) {
191
+ /** The conversation itself is gone (404): its Harness no longer holds it (ADR-0027). One of the
192
+ * faults the Agent actor asks the Sandbox about, to name a memory kill (ADR-0061). */
193
+ readonly lost: boolean;
194
+ constructor(message: string, settlement?: Settlement, opts: { lost?: boolean } = {}) {
118
195
  super(message);
119
196
  this.name = "SettlementFault";
120
197
  this.settlement = settlement;
198
+ this.lost = opts.lost === true;
121
199
  }
122
200
  }
123
201
 
202
+ /** An admission for a kept conversation (`kept`) that the Harness refused with 404: it keeps no
203
+ * such conversation, and it seated none (ADR-0062). `notKept` is what the Agent actor reads, as it
204
+ * reads `lost`: that module is wire-free. */
205
+ export class ConversationNotKept extends Error {
206
+ readonly notKept = true;
207
+ constructor(message: string) {
208
+ super(message);
209
+ this.name = "ConversationNotKept";
210
+ }
211
+ }
212
+
213
+ /** The Harnesses whose memory kills this process could not read, each said once (`memoryKills`).
214
+ * Per process, not per client: an Agent actor builds a client for each Turn. */
215
+ const killReadFaults = new Set<string>();
216
+
217
+ /** The Harnesses whose kills — on the list or on a stream — held a field this process could not
218
+ * use, each said once until a clean read. Per process, like `killReadFaults`. Neither set is cleaned
219
+ * when a Workspace goes: no hook reaches this module from the Sandbox's destroy, and an entry is
220
+ * one URL per Sandbox this process ever read. */
221
+ const badKillFaults = new Set<string>();
222
+
223
+ /** Say once, until a clean read, that a Harness's kills held a field this process could not use. */
224
+ function badKill(baseUrl: string, log: (line: string) => void, what: string): void {
225
+ if (badKillFaults.has(baseUrl)) return;
226
+ badKillFaults.add(baseUrl);
227
+ log(`harness ${baseUrl}: ${what} (ADR-0062)`);
228
+ }
229
+
230
+ /** A size as the guard writes one (`1.9Gi`, `512Mi`), short. The Agent's uid can write the file
231
+ * the list is read from, and these words go into the next Agent's Briefing as they are, so
232
+ * nothing else passes (ADR-0062). */
233
+ function isSize(value: unknown): value is string {
234
+ return typeof value === "string" && value.length <= 16 && /^\d{1,6}(\.\d{1,3})?(Ki|Mi|Gi|Ti)$/.test(value);
235
+ }
236
+
237
+ /** A string no longer than `max` characters. The bounds are the wire's (`wire.ts`), the same as
238
+ * the Harness's: the Harness counts the bytes of the JSON text, which are never fewer, so a kill it
239
+ * keeps is one this process keeps. An answer larger than `MAX_KILLS_BODY` is no list to act on:
240
+ * the Harness answers no list that large. */
241
+ const fits = (v: unknown, max: number): v is string => typeof v === "string" && v.length <= max;
242
+
243
+ /** One kill of the list as this process can use it, or undefined when it has no id or time, when
244
+ * its id or time is outside the wire's bounds, or when its time does not parse — the only kills
245
+ * that are dropped. Sizes that are not sizes are left out, so the kill is kept with no sizes; a
246
+ * `told` entry that is not a conversation, or whose name is outside the bounds, is left out of
247
+ * `told`. */
248
+ function listedKill(value: unknown): ListedKill | undefined {
249
+ if (typeof value !== "object" || value === null) return undefined;
250
+ const { id, at, peak, limit, told } = value as Record<string, unknown>;
251
+ if (!fits(id, MAX_KILL_STAMP) || !fits(at, MAX_KILL_STAMP) || Number.isNaN(Date.parse(at))) return undefined;
252
+ const conversations = (Array.isArray(told) ? told.slice(0, MAX_TOLD) : []).filter(
253
+ (t): t is { agent: string; instanceId: string } =>
254
+ typeof t === "object" && t !== null && fits(t.agent, MAX_KILL_NAME) && fits(t.instanceId, MAX_KILL_NAME),
255
+ );
256
+ const sized = isSize(peak) && isSize(limit);
257
+ return {
258
+ id,
259
+ at,
260
+ told: conversations.map(({ agent, instanceId }) => ({ agent, instanceId })),
261
+ ...(sized ? { peak, limit } : {}),
262
+ };
263
+ }
264
+
265
+ /** Whether a listed kill came through as the wire states it. */
266
+ function whole(raw: unknown, kill: ListedKill | undefined): boolean {
267
+ if (kill === undefined) return false;
268
+ const { peak, limit, told } = raw as { peak?: unknown; limit?: unknown; told?: unknown };
269
+ // Sizes the pod left out are no fault; sizes it sent that are not sizes are.
270
+ if (kill.limit === undefined && (peak !== undefined || limit !== undefined)) return false;
271
+ return Array.isArray(told) && Math.min(told.length, MAX_TOLD) === kill.told.length && told.length <= MAX_TOLD;
272
+ }
273
+
274
+ /** A timeout's abort reason (`AbortSignal.timeout`): a read that ran out of time, not an abandon. */
275
+ function isTimeout(reason: unknown): boolean {
276
+ return reason instanceof DOMException && reason.name === "TimeoutError";
277
+ }
278
+
124
279
  /** Build the real wire client from connection options. */
125
280
  export function createHarnessClient(options: HarnessClientOptions): HarnessClient {
126
281
  const { baseUrl } = options;
@@ -138,11 +293,11 @@ export function createHarnessClient(options: HarnessClientOptions): HarnessClien
138
293
  * POST the admission, re-sending it while the request DEMONSTRABLY never left this host.
139
294
  *
140
295
  * This is `wait`'s reconnect rule, applied one step earlier and for the same reason: a request
141
- * that could not connect is not a Harness that refused the prompt. It matters here because the
142
- * admission is the FIRST thing jr2 ever sends over the Sandbox's Service — provisioning waits on
143
- * the CR's `phase: Ready`, which the operator computes from the POD, and the attach reaches the
144
- * pod through the API server, so nothing before this has proven the Service dialable. Ready is
145
- * not routable: the EndpointSlice behind the ClusterIP is programmed after the pod passes its
296
+ * that could not connect is not a Harness that refused the prompt. It matters here because
297
+ * provisioning waits on the CR's `phase: Ready`, which the operator computes from the POD, and
298
+ * a Workspace-less Turn (the Instance Harness) has no attach before it — so this can be the first
299
+ * request ever sent over a Service. (A Sandbox's attach, `POST /attach` since ADR-0063, dials it
300
+ * first and retries the same way.) Ready is not routable: the EndpointSlice behind the ClusterIP is programmed after the pod passes its
146
301
  * probe, and until it is, kube-proxy REJECTs — which arrives here as a refused connection.
147
302
  * Un-retried, that single blip lost the whole turn (the actor calls an admission failure a
148
303
  * terminal `agent.fault`), which is exactly the flake that kept the `@kind` tier serial.
@@ -153,9 +308,15 @@ export function createHarnessClient(options: HarnessClientOptions): HarnessClien
153
308
  * question "did this reach the wire at all", never "does this look transient".
154
309
  *
155
310
  * And the window is BOUNDED where `wait`'s is not. `wait` may reconnect forever because its
156
- * Submission is already admitted, so the lease owns the reporting (`workspace.lost` — ADR-0021).
311
+ * Submission is already admitted, so the watch owns the reporting (`workspace.lost`, or the
312
+ * Harness restart that ends the Turn — ADR-0021).
157
313
  * Nothing is admitted yet here, so there is no turn for a lease to be about: an address that
158
314
  * never answers is a fault this call has to name itself.
315
+ *
316
+ * A draining Harness's 503 is re-sent on the same ladder (ADR-0031): the Harness answers it
317
+ * before it reads the body, so it queued nothing, and it closes the connection with it, so the
318
+ * next attempt dials the Service again — which reaches the replacement once it serves. It is the
319
+ * one answer re-sent: every other status is the Harness's word about this admission.
159
320
  */
160
321
  const postAdmission = async (url: string, init: RequestInit, signal?: AbortSignal): Promise<Response> => {
161
322
  const startedAt = Date.now();
@@ -165,12 +326,9 @@ export function createHarnessClient(options: HarnessClientOptions): HarnessClien
165
326
  let lastCode = "?";
166
327
  for (;;) {
167
328
  attempts += 1;
329
+ let res: Response;
168
330
  try {
169
- const res = await fetchImpl(url, init);
170
- // Only when it actually cost something. A window that is never approached should be silent,
171
- // so that a line appearing at all is already the signal (see `routabilityLine`).
172
- if (attempts > 1) log(routabilityLine("admission", url, attempts, Date.now() - startedAt, lastCode));
173
- return res;
331
+ res = await fetchImpl(url, init);
174
332
  } catch (err) {
175
333
  lastCode = transportCode(err);
176
334
  // A local abandon propagates untranslated, exactly as in `wait` — the stopped actor
@@ -186,7 +344,19 @@ export function createHarnessClient(options: HarnessClientOptions): HarnessClien
186
344
  }
187
345
  await sleep(jittered(backoffMs), signal);
188
346
  backoffMs = Math.min(backoffMs * 2, backoffMaxMs);
347
+ continue;
189
348
  }
349
+ if (res.status === DRAINING && Date.now() < deadline) {
350
+ lastCode = `HTTP${DRAINING}`;
351
+ await res.body?.cancel();
352
+ await sleep(jittered(backoffMs), signal);
353
+ backoffMs = Math.min(backoffMs * 2, backoffMaxMs);
354
+ continue;
355
+ }
356
+ // Only when it actually cost something. A window that is never approached should be silent,
357
+ // so that a line appearing at all is already the signal (see `routabilityLine`).
358
+ if (attempts > 1) log(routabilityLine("admission", url, attempts, Date.now() - startedAt, lastCode));
359
+ return res;
190
360
  }
191
361
  };
192
362
 
@@ -215,11 +385,29 @@ export function createHarnessClient(options: HarnessClientOptions): HarnessClien
215
385
  // Omitted when unset, so an admission with no dials is byte-identical to before.
216
386
  ...(sendOptions.model ? { model: sendOptions.model } : {}),
217
387
  ...(sendOptions.thinkingLevel ? { thinkingLevel: sendOptions.thinkingLevel } : {}),
388
+ // The Briefing's news (ADR-0062): typed data, the Harness writes the words. Omitted
389
+ // when there is none, so an admission without notices is byte-identical to before.
390
+ ...(sendOptions.notices?.length ? { notices: sendOptions.notices } : {}),
391
+ // Omitted when unset, so an admission that is not for a kept conversation is
392
+ // byte-identical to before.
393
+ ...(sendOptions.kept ? { kept: true } : {}),
218
394
  }),
219
395
  signal: sendOptions.signal,
220
396
  },
221
397
  sendOptions.signal,
222
398
  );
399
+ if (sendOptions.kept && res.status === 404) {
400
+ // "Not kept" only with the Harness's marker: a 404 from anything else — a proxy, a route
401
+ // that moved — is a failed admission, as before.
402
+ const body = (await res.json().catch(() => undefined)) as Partial<NotKeptResponse> | undefined;
403
+ const detail = typeof body?.error === "string" ? body.error : res.statusText || "no detail";
404
+ if (body?.kept === false) {
405
+ throw new ConversationNotKept(
406
+ `harness admission refused (404): ${detail} — the Harness does not keep this conversation (ADR-0062)`,
407
+ );
408
+ }
409
+ throw new Error(`harness admission failed (404): ${detail}`);
410
+ }
223
411
  if (!res.ok) {
224
412
  throw new Error(`harness admission failed (${res.status}): ${await errorDetail(res)}`);
225
413
  }
@@ -247,7 +435,9 @@ export function createHarnessClient(options: HarnessClientOptions): HarnessClien
247
435
  if (res.status === 404) {
248
436
  throw new SettlementFault(
249
437
  `conversation lost: the harness answered 404 for submission "${admission.submissionId}" — ` +
250
- `a conversation lives as long as its Harness process (ADR-0027)`,
438
+ `the Harness no longer holds this conversation (ADR-0027, ADR-0031)`,
439
+ undefined,
440
+ { lost: true },
251
441
  );
252
442
  }
253
443
  // Refused, not unreachable: the bearer is derived, so a Harness that rejects it now
@@ -276,6 +466,22 @@ export function createHarnessClient(options: HarnessClientOptions): HarnessClien
276
466
  backoffMs = backoffInitialMs;
277
467
 
278
468
  for (const event of events) {
469
+ if (event.type === "memory-limit") {
470
+ const { id, peak, limit, conversationId, position } = event;
471
+ // A size that is not one is not put in a Briefing (the Agent's uid can write the record):
472
+ // the kill is kept, with no sizes, and the fault is said.
473
+ const sized = isSize(peak) && isSize(limit);
474
+ if (!sized)
475
+ badKill(baseUrl, log, "a memory kill on a stream has sizes that are not sizes — kept without them");
476
+ // An older Harness's event has no id: keyed by its place on the stream, exactly as the
477
+ // Orchestrator before raised it, so a replayed read is the same kill and it can never
478
+ // mark a later kill seen. An id too long to be one is no id.
479
+ const kill = fits(id, MAX_KILL_STAMP)
480
+ ? { id }
481
+ : { id: `${conversationId}@${position.batch}.${position.index}`, legacy: true as const };
482
+ opts?.onMemoryLimit?.({ ...kill, ...(sized ? { peak, limit } : {}) });
483
+ continue;
484
+ }
279
485
  if (event.type !== "submission-settled" || event.submissionId !== admission.submissionId) continue;
280
486
  if (event.outcome === "completed") return;
281
487
  throw new SettlementFault(faultMessage(event), toSettlement(event));
@@ -284,10 +490,111 @@ export function createHarnessClient(options: HarnessClientOptions): HarnessClien
284
490
  }
285
491
  },
286
492
 
493
+ async holds(agentName, instanceId, opts) {
494
+ const url = new URL(conversationUrl(agentName, instanceId));
495
+ url.searchParams.set("view", VIEW_HISTORY);
496
+ // Asked as a restart is seen, when the new container may not listen yet: a request that
497
+ // never left this host, and a draining Harness's 503, are sent again until the caller's
498
+ // signal ends the wait — then, and on any other failure, unknown is never loss.
499
+ let backoffMs = backoffInitialMs;
500
+ for (;;) {
501
+ let again = false;
502
+ try {
503
+ const res = await fetchImpl(url.toString(), { headers: authorization, signal: opts?.signal });
504
+ // Drain the body so the socket is released; the snapshot itself is not the question.
505
+ await res.body?.cancel().catch(() => undefined);
506
+ if (res.status !== DRAINING) return res.status !== 404;
507
+ again = true;
508
+ } catch (err) {
509
+ if (opts?.signal?.aborted && !isTimeout(opts.signal.reason)) throw err;
510
+ again = !opts?.signal?.aborted && neverDelivered(err);
511
+ }
512
+ if (!again || opts?.signal === undefined) return true;
513
+ try {
514
+ await sleep(jittered(backoffMs), opts.signal);
515
+ } catch (err) {
516
+ if (opts.signal.aborted && !isTimeout(opts.signal.reason)) throw err;
517
+ return true;
518
+ }
519
+ backoffMs = Math.min(backoffMs * 2, backoffMaxMs);
520
+ }
521
+ },
522
+
523
+ async memoryKills(opts) {
524
+ const url = new URL("/memory-kills", baseUrl).toString();
525
+ // Idempotent, so re-sent on what `postAdmission` re-sends — a request that never left this
526
+ // host, a draining Harness's 503 — on the same ladder, until the caller's signal ends it. The
527
+ // limit is the caller's alone, so there is one clock for it.
528
+ let backoffMs = backoffInitialMs;
529
+ let why: string;
530
+ for (;;) {
531
+ let again = false;
532
+ try {
533
+ const res = await fetchImpl(url, { headers: authorization, signal: opts?.signal });
534
+ if (res.status === 200 && Number(res.headers.get("content-length") ?? 0) > MAX_KILLS_BODY) {
535
+ await res.body?.cancel().catch(() => undefined);
536
+ why = `the answer is larger than ${MAX_KILLS_BODY} bytes`;
537
+ } else if (res.status === 200) {
538
+ const text = await res.text();
539
+ const kills =
540
+ text.length > MAX_KILLS_BODY ? undefined : (JSON.parse(text) as Partial<MemoryKillsResponse>).kills;
541
+ if (Array.isArray(kills)) {
542
+ // A read that works ends the fault: the next fault is said again.
543
+ killReadFaults.delete(baseUrl);
544
+ const read = kills.map(listedKill);
545
+ const usable = read.filter((kill): kill is ListedKill => kill !== undefined);
546
+ const flawed = kills.filter((raw, i) => !whole(raw, read[i])).length;
547
+ if (flawed === 0) badKillFaults.delete(baseUrl);
548
+ else {
549
+ badKill(
550
+ baseUrl,
551
+ log,
552
+ `${flawed} of its memory kills are not the wire's shape — those with no id or time, or with one outside the wire's bounds or a time that does not parse, are skipped, the others are kept without the fields that were wrong`,
553
+ );
554
+ }
555
+ return usable;
556
+ }
557
+ why = "the answer is not a list of kills";
558
+ } else {
559
+ why = `HTTP ${res.status}: ${await errorDetail(res)}`;
560
+ again = res.status === DRAINING;
561
+ }
562
+ } catch (err) {
563
+ // A local abandon propagates, as in every other verb; a timeout the caller set is none.
564
+ if (opts?.signal?.aborted && !isTimeout(opts.signal.reason)) throw err;
565
+ why = transportDetail(err);
566
+ again = !opts?.signal?.aborted && neverDelivered(err);
567
+ }
568
+ if (!again || opts?.signal === undefined) break;
569
+ try {
570
+ await sleep(jittered(backoffMs), opts?.signal);
571
+ } catch (err) {
572
+ if (opts?.signal?.aborted && !isTimeout(opts.signal.reason)) throw err;
573
+ why = "the read's time ran out";
574
+ break;
575
+ }
576
+ backoffMs = Math.min(backoffMs * 2, backoffMaxMs);
577
+ }
578
+ // One line for each fault: said when it starts. A read that works says nothing, and the next
579
+ // fault after it is said again.
580
+ if (!killReadFaults.has(baseUrl)) {
581
+ killReadFaults.add(baseUrl);
582
+ log(
583
+ `harness ${baseUrl}: cannot read its memory kills (${why}) — no notice from them now; the Harness keeps them, and the next admission reads again (ADR-0062)`,
584
+ );
585
+ }
586
+ return undefined;
587
+ },
588
+
287
589
  async abort(agentName, instanceId, opts) {
590
+ // No cause, no body: the request an Orchestrator from before `cause` sent, byte for byte.
591
+ const body: AbortRequest | undefined = opts?.cause
592
+ ? { cause: opts.cause, ...(opts.submissionId ? { submissionId: opts.submissionId } : {}) }
593
+ : undefined;
288
594
  const res = await fetchImpl(`${conversationUrl(agentName, instanceId)}/abort`, {
289
595
  method: "POST",
290
- headers: authorization,
596
+ headers: body ? { ...authorization, "content-type": "application/json" } : authorization,
597
+ ...(body ? { body: JSON.stringify(body) } : {}),
291
598
  signal: opts?.signal,
292
599
  });
293
600
  if (!res.ok) {
@@ -324,19 +631,37 @@ export function harnessAgentRunPort(client: HarnessClient): AgentRunPort {
324
631
  definition: opts.definition,
325
632
  model: input.model,
326
633
  thinkingLevel: input.thinkingLevel,
634
+ ...(opts.notices?.length ? { notices: opts.notices } : {}),
635
+ ...(opts.kept ? { kept: true } : {}),
327
636
  signal: opts.signal,
328
637
  });
329
638
  },
330
639
 
331
- async settle(admission: AgentAdmission, opts?: { signal?: AbortSignal }): Promise<void> {
332
- await client.wait(admission, { signal: opts?.signal });
640
+ async settle(admission: AgentAdmission, opts?: AgentSettleOptions): Promise<void> {
641
+ await client.wait(admission, opts);
642
+ },
643
+
644
+ async holds(agentName: string, instanceId: string, opts?: { signal?: AbortSignal }): Promise<boolean> {
645
+ return await client.holds(agentName, instanceId, opts);
333
646
  },
334
647
 
335
- async abort(agentName: string, instanceId: string, opts?: { signal?: AbortSignal }): Promise<void> {
648
+ async memoryKills(opts?: { signal?: AbortSignal }): Promise<ListedKill[] | undefined> {
649
+ return await client.memoryKills(opts);
650
+ },
651
+
652
+ async abort(
653
+ agentName: string,
654
+ instanceId: string,
655
+ opts?: { signal?: AbortSignal; cause?: AbortCause; submissionId?: string },
656
+ ): Promise<void> {
336
657
  // The result (`{ aborted }` — whether there was work to end) is dropped: by the time this
337
658
  // runs the state has already moved on, and an idle instance is exactly as fine as a
338
659
  // stopped one. Settlement is asynchronous and nothing here is listening (ADR-0024).
339
- await client.abort(agentName, instanceId, { signal: opts?.signal });
660
+ await client.abort(agentName, instanceId, {
661
+ signal: opts?.signal,
662
+ ...(opts?.cause ? { cause: opts.cause } : {}),
663
+ ...(opts?.submissionId ? { submissionId: opts.submissionId } : {}),
664
+ });
340
665
  },
341
666
  };
342
667
  }
@@ -387,28 +712,419 @@ export function createEchoPush(options: {
387
712
  }
388
713
 
389
714
  /**
390
- * THE authoring surface for an Agent (ADR-0049): one definition in, one actor slot out —
391
- * `jr2Setup({ actors: { coder: agent({ model, instructions }) } })`, invoked as `src: "coder"`.
392
- * The slot key is the Agent's name (the Harness route, the minted iid, the markers), so a name
393
- * the Machine does not carry is a compile error on `src`, and two Machines in one run may each
394
- * carry their own `coder`.
715
+ * Build the live-set statement for the Instance Harness (ADR-0031): `PUT /agents
716
+ * { live: [{ agent, instanceId }] }` → `{ freed }`, and the Harness frees memory and files of every
717
+ * conversation not named. The whole set every time — level-triggered, so a missed statement costs
718
+ * nothing and the next one says it all again. Un-retried, like the echo: the caller is a timer.
719
+ */
720
+ export function createLiveSetPush(options: {
721
+ baseUrl: string;
722
+ /** The Instance Harness's bearer (ADR-0058). Absent only against the stub Harness. */
723
+ token?: string;
724
+ /** Injectable for socket-free tests. Default: global `fetch`. */
725
+ fetch?: typeof fetch;
726
+ }): (live: LiveConversation[]) => Promise<LiveSetResponse> {
727
+ const fetchImpl = options.fetch ?? fetch;
728
+ return async (live) => {
729
+ const url = new URL("/agents", options.baseUrl).toString();
730
+ const res = await fetchImpl(url, {
731
+ method: "PUT",
732
+ headers: {
733
+ "content-type": "application/json",
734
+ ...(options.token ? { authorization: `Bearer ${options.token}` } : {}),
735
+ },
736
+ body: JSON.stringify({ live } satisfies LiveSetRequest),
737
+ }).catch((err: unknown) => {
738
+ throw new Error(`live-set statement to ${url} failed: ${transportDetail(err)}`, { cause: err });
739
+ });
740
+ if (!res.ok) {
741
+ throw new Error(`live-set statement failed (${res.status}): ${await errorDetail(res)}`);
742
+ }
743
+ const { freed } = (await res.json().catch(() => ({}))) as Partial<Record<keyof LiveSetResponse, unknown>>;
744
+ return { freed: typeof freed === "number" ? freed : 0 };
745
+ };
746
+ }
747
+
748
+ /**
749
+ * Make an Agent: an actor slot that runs one Turn of a model each time a state invokes it. Put it
750
+ * under `actors` in `jr2Setup`, and invoke it by its key: `actors: { coder: agent(…) }`, then
751
+ * `invoke: { src: "coder", input: { prompt } }`. The key is the Agent's name.
752
+ *
753
+ * The invoke's `input` is the Turn: `prompt` (required), `cwd`, `model`, `thinkingLevel` and
754
+ * `continue`. `cwd` is the absolute directory where the Working tools work. Absent, it is the
755
+ * Worktree of the Workspace's one Repo Slot. `continue: true` continues the Agent's conversation
756
+ * in this Machine. Absent, each Turn starts a new conversation. `continue` is the literal `true`,
757
+ * not a `boolean`. A Turn can join only a conversation that an earlier Turn started with
758
+ * `continue: true`. Thus in a retry loop, put `continue: true` on each Turn, also on the first, so
759
+ * that attempt 2 sees attempt 1. Then a Runaway on the first Turn gets no second start, because a
760
+ * continued Turn gets none (ADR-0035). Spread it only when a state must start a clean conversation
761
+ * on some Turns only: `...(context.clean ? {} : { continue: true as const })`. A clean Turn does not
762
+ * start the continued conversation again: the next Turn with `continue: true` joins the earlier
763
+ * one. The Turn ends when the state that invoked the Agent exits, usually on a pick from the
764
+ * Agent's Menu.
765
+ *
766
+ * The actor of an Agent never finishes, so an `onDone` on its invoke never runs. Give the Agent a
767
+ * pick, such as `finish`, and route on it. When a Turn fails with no pick, jr2 sends the event
768
+ * `agent.fault` to the Machine. It is an event, not an error of the invoke, so an `onError` does
769
+ * not get it. Route it on each state that invokes an Agent, or once on a state above them:
770
+ * `on: { "agent.fault": "failed" }`. `jr2 check` and `jr2 up` refuse a state that invokes an
771
+ * Agent with no route for it, and an `agent.fault` that no transition takes ends the run `error`.
772
+ *
773
+ * An Agent with `workspace: "write"` or `"read"` works in a Sandbox, so invoke it inside a
774
+ * `workspace()` body. An Agent with `workspace: "none"` can run anywhere. A model can lack the
775
+ * definition's thinking level: see {@link ThinkingLevel}.
395
776
  *
396
- * Everything live is constructed per-invocation from serializable input: the wire client from
397
- * `input.endpoint` (which Sandbox's Harness — or the wire-compatible dev stub; either way it's
398
- * only a URL, one code path). Lives here, not in actor.ts, so the actor logic and its unit tests
399
- * never touch the wire.
777
+ * When a Turn starts, it refuses:
778
+ * - a key in `input` that a Turn does not have: ``agent "<slot>": the Turn's input carries
779
+ * `<key>`, which a Turn has no use for — its input is its Frame (`prompt`, `cwd`), its Dials
780
+ * (`model`, `thinkingLevel`), and `continue` (ADR-0057)``
781
+ * - no `cwd` under a Workspace with more than one Repo Slot: ``turn <id>: agent "<slot>" works
782
+ * under a Workspace carrying more than one Repo Slot (<slots>) and its Turn framed no `cwd` —
783
+ * say which Worktree it works in (`cwd: context.workspace.repos.<slot>`, one of those), because
784
+ * the kit gives no slot a meaning (ADR-0051) and a Turn's Frame is where it works (ADR-0057)``
785
+ * - a `cwd` that is not an absolute path: ``agent "<slot>": the Turn's Frame carries a `cwd` that
786
+ * is not an absolute path — the Frame says WHERE this Turn works (ADR-0057), and a Turn rooted
787
+ * anywhere else would work in the wrong directory silently``. The Harness makes this refusal
788
+ * when it admits the Turn, and the error starts with `harness admission failed (400): `.
789
+ * - an Agent with Working tools outside a `workspace()`: ``turn <id>: agent "<slot>" has
790
+ * workspace: "<access>" — invoke it inside a workspace() (ambient resolution), or pass an
791
+ * explicit `endpoint` (workspace-less stub path)``
792
+ * - a `model` that is still `open`: ``agent "<slot>" has an Open model — nobody bound it; bind it
793
+ * where the Machine is registered (`customize(<machine>, { agents: { <slot>: { model:
794
+ * "<provider>/<model>" } } })`), because a packaged Machine cannot pick a model on your behalf
795
+ * (ADR-0054)``. `jr2 up` refuses this before it builds anything.
400
796
  *
401
- * It takes a DECLARATION (ADR-0054), so a packaged Machine may write `agent({ model: open, … })`
402
- * and leave the one part it cannot honestly fill to whoever registers it. What rides the Turn is
403
- * still an `AgentDefinition`: the actor narrows on start and refuses an Open one there.
797
+ * @param declaration - The Agent's definition (see {@link AgentDefinition}). `model` may be `open`
798
+ * in a Machine that you publish as a package.
799
+ * @returns The actor logic of the Agent's slot.
800
+ *
801
+ * @example
802
+ * ```ts
803
+ * import { z } from "zod";
804
+ * import { agent, defineEvent, fail, jr2Setup, workspace, type Workspaced } from "@jr2/orchestrator";
805
+ *
806
+ * const finish = defineEvent({
807
+ * name: "finish",
808
+ * description: "Call this when `node --test` passes.",
809
+ * input: z.object({}),
810
+ * });
811
+ * const runInput = z.object({ branch: z.string() });
812
+ * type BodyInput = Workspaced<z.infer<typeof runInput>, "app">;
813
+ *
814
+ * const body = jr2Setup({
815
+ * types: {} as { input: BodyInput; context: BodyInput },
816
+ * events: [finish],
817
+ * actors: {
818
+ * coder: agent({
819
+ * model: "anthropic/claude-haiku-5-5",
820
+ * instructions: "You fix failing tests. You change no test.",
821
+ * }),
822
+ * fail: fail(),
823
+ * },
824
+ * }).createMachine({
825
+ * id: "fix",
826
+ * context: ({ input }) => input,
827
+ * on: { "workspace.lost": ".lost", "agent.fault": ".failed" },
828
+ * initial: "coding",
829
+ * states: {
830
+ * coding: {
831
+ * invoke: {
832
+ * src: "coder",
833
+ * input: ({ context }) => ({
834
+ * prompt: "Make `node --test` pass, then commit.",
835
+ * cwd: context.workspace.repos.app,
836
+ * thinkingLevel: "high",
837
+ * }),
838
+ * },
839
+ * on: { finish: "done" },
840
+ * },
841
+ * done: { type: "final" },
842
+ * failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
843
+ * lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
844
+ * },
845
+ * });
846
+ *
847
+ * export const machine = workspace(body, {
848
+ * input: runInput,
849
+ * repos: { app: "https://github.com/you/app.git" },
850
+ * spec: ({ input }) => ({ branch: input.branch }),
851
+ * });
852
+ * ```
853
+ *
854
+ * @see docs/concepts/agents-and-turns.md
855
+ * @see docs/guides/write-a-workflow.md
856
+ * @see docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md
857
+ * @see docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md
404
858
  */
405
859
  export function agent(declaration: AgentDeclaration): AgentLogic {
860
+ // THE authoring surface for an Agent (ADR-0049): one definition in, one actor slot out. The
861
+ // slot key is the Agent's name (the Harness route, the minted iid, the markers), so a name the
862
+ // Machine does not carry is a compile error on `src`, and two Machines in one run may each carry
863
+ // their own `coder`.
864
+ //
865
+ // Everything live is constructed per-invocation from serializable input: the wire client from
866
+ // `input.endpoint` (which Sandbox's Harness — or the wire-compatible dev stub; either way it's
867
+ // only a URL, one code path). Lives here, not in actor.ts, so the actor logic and its unit
868
+ // tests never touch the wire.
869
+ //
870
+ // It takes a DECLARATION (ADR-0054), so a packaged Machine may write `agent({ model: open, … })`
871
+ // and leave the one part it cannot honestly fill to whoever registers it. What rides the Turn
872
+ // is still an `AgentDefinition`: the actor narrows on start and refuses an Open one there.
406
873
  return agentActorWith(
407
874
  (endpoint, bearer) => createHarnessAgentRunClient({ baseUrl: endpoint, ...(bearer ? { token: bearer } : {}) }),
408
875
  declaration,
409
876
  );
410
877
  }
411
878
 
879
+ /**
880
+ * The Harness's exec (ADR-0067), as a `command` Effect drives it: `POST /exec/:id` to start,
881
+ * `GET /exec/:id` long-polled by offset, `POST /exec/:id/kill` when the Effect's state exits (sent
882
+ * again until the Harness keeps it), `DELETE /exec/:id` once the output is ledgered. A start that
883
+ * never reached the wire rejects `ExecNotSent`, the one start failure sent again: a Harness runs one
884
+ * id once only within its own process, so a start that reached one that then restarted would run
885
+ * twice. A read's transport failure or 5xx rejects, and the Effect asks again; a 404 on a read is
886
+ * an exec the Harness does not know (it restarted); any other 4xx is a jr2 defect, never retried.
887
+ */
888
+ export function createExecPort(options: {
889
+ baseUrl: string;
890
+ /** The Workspace's Harness bearer (ADR-0058). Absent only against a Harness that checks nothing. */
891
+ token?: string;
892
+ /** Injectable for socket-free tests. Default: global `fetch`. */
893
+ fetch?: typeof fetch;
894
+ }): ExecPort {
895
+ const fetchImpl = options.fetch ?? fetch;
896
+ const headers = {
897
+ "content-type": "application/json",
898
+ ...(options.token ? { authorization: `Bearer ${options.token}` } : {}),
899
+ };
900
+ const url = (id: string, suffix = "") => new URL(`/exec/${encodeURIComponent(id)}${suffix}`, options.baseUrl);
901
+ const call = async (target: URL, init: RequestInit): Promise<Response> =>
902
+ fetchImpl(target.toString(), { ...init, headers }).catch((err: unknown) => {
903
+ throw new Error(`harness exec ${init.method ?? "GET"} ${target.pathname} failed: ${transportDetail(err)}`, {
904
+ cause: err,
905
+ });
906
+ });
907
+ const refusedOrRetry = async (res: Response, what: string): Promise<never> => {
908
+ const detail = await errorDetail(res);
909
+ if (res.status >= 400 && res.status < 500)
910
+ throw new ExecRefused(`harness refused the ${what} (${res.status}): ${detail}`);
911
+ throw new Error(`harness ${what} failed (${res.status}): ${detail}`);
912
+ };
913
+ return {
914
+ start: async (id, req: ExecRequest, signal) => {
915
+ const res = await call(url(id), { method: "POST", body: JSON.stringify(req), signal }).catch((err: unknown) => {
916
+ // Only a request that never reached the wire is safe to send again (see `startOnce`).
917
+ if (!signal.aborted && neverDelivered(err)) throw new ExecNotSent((err as Error).message, { cause: err });
918
+ throw err;
919
+ });
920
+ if (!res.ok) return refusedOrRetry(res, "exec");
921
+ await res.json().catch(() => undefined);
922
+ },
923
+ read: async (id, offset, signal) => {
924
+ const target = url(id);
925
+ if (offset !== undefined) target.searchParams.set("offset", String(offset));
926
+ target.searchParams.set("live", LIVE_LONG_POLL);
927
+ const res = await call(target, { method: "GET", signal });
928
+ if (res.status === 404) {
929
+ await res.body?.cancel();
930
+ return undefined;
931
+ }
932
+ if (!res.ok) return refusedOrRetry(res, "exec read");
933
+ return (await res.json()) as ExecRead;
934
+ },
935
+ kill: async (id) => {
936
+ // Not kept until the Harness answers 2xx: the Effect sends it again until then.
937
+ const res = await call(url(id, "/kill"), { method: "POST" });
938
+ if (!res.ok) return refusedOrRetry(res, "exec kill");
939
+ await res.body?.cancel();
940
+ },
941
+ forget: async (id) => {
942
+ const res = await call(url(id), { method: "DELETE" });
943
+ await res.body?.cancel();
944
+ },
945
+ };
946
+ }
947
+
948
+ /**
949
+ * Make a `command` Effect: an actor slot that runs one program in the Workspace's Sandbox when a
950
+ * state invokes it. Put it under `actors` and invoke it by its key: `actors: { tests:
951
+ * command(["node", "--test"]) }`, then `invoke: { src: "tests", onDone: … }`.
952
+ *
953
+ * `argv` is a list, never shell text: each entry is one argument. An `arg("name")` entry is filled
954
+ * from the invoke's `input` (`input: { name: "…" }`), as one argument whatever its text. To use a
955
+ * shell, write it: `["sh", "-c", "…"]`.
956
+ *
957
+ * Where it runs:
958
+ * - With no Effect secret in `env`, in the Harness container of the enclosing Workspace's Sandbox.
959
+ * It runs in the Worktree of a Repo Slot, or in `/work` in a Workspace with no Repo Slot. The
960
+ * Agent's Working tools also run in this container.
961
+ * - With an Effect secret in `env`, in the Sandbox Effector, against a commit of the branch. This
962
+ * is the branch tip when it starts, or with `pin: true` the commit that the invoke's `input.sha`
963
+ * names. The output then says `dirty: true` when the Worktree held edits that the command did
964
+ * not see.
965
+ *
966
+ * Every outcome is output for `onDone`, never an error: `{ outcome: "exited", code, tail }`,
967
+ * `{ outcome: "timeout", tail }`, `{ outcome: "killed", signal, tail }`, or `{ outcome: "unknown",
968
+ * reason }` when jr2 cannot know what happened. `tail` is the end of the output stream.
969
+ *
970
+ * After an Orchestrator restart:
971
+ * - A command with no Effect secret in `env` never runs twice. jr2 attaches to it again, or settles
972
+ * `unknown`.
973
+ * - A command with an Effect secret in `env` runs again when the Sandbox Effector no longer holds
974
+ * it. It runs against the same commit. If jr2 has no commit for it, it settles `unknown`.
975
+ *
976
+ * When the state exits, the command stops: jr2 kills its process group. `onError` is only for a
977
+ * Machine or config defect.
978
+ *
979
+ * It refuses, when you call it:
980
+ * - an empty `argv`: `command(argv): argv is a non-empty list — each entry one argument, never
981
+ * shell text`
982
+ * - an entry that is not a string or `arg()`: `command(argv): an argv entry is a string or
983
+ * arg("name"), got <entry>`
984
+ * - a `timeout` that is not a positive duration: `command: timeout "<timeout>" is not a positive
985
+ * duration (like "90s", "10m", "1h")`
986
+ * - an `env` name that is not a variable name, or `PATH`, `HOME` or `TMPDIR`: ``command(argv, {
987
+ * env }): "<name>" is not a variable the author sets — PATH, HOME and TMPDIR are the Effector's
988
+ * (ADR-0067)``
989
+ * - `pin: true` with no Effect secret in `env`: ``command(argv, { pin: true }) names no secret in
990
+ * its env: `pin` chooses the commit a credentialed command runs against in the Sandbox
991
+ * Effector, and a command that names no secret runs in the live Worktree (ADR-0067) — name its
992
+ * secret, command(argv, { env: { NAME: secret("x") }, pin: true }), or drop `pin` ``
993
+ *
994
+ * And when it starts, with no Effect secret in `env`:
995
+ * - outside a `workspace()`: `command "<slot>" runs in the Harness container of a Workspace, and
996
+ * its state is not inside a workspace() — invoke it from a Workspace's body (ADR-0067)`
997
+ * - no `slot` under a Workspace with more than one Repo Slot: `command "<slot>" runs under a
998
+ * Workspace carrying more than one Repo Slot (<slots>) and names none — say which Worktree it
999
+ * runs in: command(argv, { slot: "<slot>" }) (ADR-0051)`
1000
+ * - a `slot` that the Workspace does not have: `command "<slot>" names Repo Slot "<name>", which
1001
+ * its Workspace does not carry (<slots>)`
1002
+ * - an `arg()` with no string in `input`: `command "<slot>": argv slot <<name>> has no string
1003
+ * value in its input (got <type>) — map it in the invoke's input`. The angle brackets around the
1004
+ * arg's name are in the text: `argv slot <file>`. `<type>` is `nothing` when `input` has no such
1005
+ * field, or the type of its value, such as `number`.
1006
+ *
1007
+ * And when it starts, with an Effect secret in `env`:
1008
+ * - outside a `workspace()`: `command "<slot>" runs in the Sandbox Effector of a Workspace, and its
1009
+ * state is not inside a workspace() — invoke it from a Workspace's body (ADR-0067)`
1010
+ * - no `slot` under a Workspace with more than one Repo Slot: `command "<slot>" runs under a
1011
+ * Workspace carrying more than one Repo Slot (<slots>) — say which: { slot: "<slot>" }
1012
+ * (ADR-0051)`
1013
+ * - a Workspace with no Repo Slot: `command "<slot>" runs under a Workspace with no Repo Slot: a
1014
+ * command with an Effect secret runs against one commit of a Repo Slot's branch, and there is
1015
+ * none — declare a Repo Slot, or name no secret in its env, and it runs in /work beside the Agent
1016
+ * (ADR-0051)`. `jr2 up` refuses it first, with the same reason
1017
+ * - a `slot` that the Workspace does not have: `command "<slot>" names Repo Slot "<name>", which
1018
+ * its Workspace does not carry (<slots>)`
1019
+ * - `pin: true` and an `input.sha` that is not a full commit id: `command "<slot>" (pin): its
1020
+ * input's sha is not a full commit id`
1021
+ * - an `arg()` with no string in `input`: the same text as above.
1022
+ *
1023
+ * @param argv - The program and its arguments: strings and `arg("name")` entries. Or `open`, in a
1024
+ * Machine that you publish as a package, for the Instance that registers it to bind with
1025
+ * `customize()`.
1026
+ * @param options - Optional. `slot`: the Repo Slot whose Worktree the command runs in; needed only
1027
+ * where the Workspace has more than one. `timeout`: `"90s"`, `"10m"` or `"1h"`, default
1028
+ * `"10m"`; when it expires, the output is `timeout`. `tail`: how many characters of the end of
1029
+ * the output stream to keep, default 4096. `env`: environment variables. Each value is
1030
+ * `secret("name")` for the value of an Effect secret, or `secret("name").path` for the path of a
1031
+ * file that holds it. `pin`: `true` to run against the commit in `input.sha`. Use `pin` only
1032
+ * with an Effect secret in `env`.
1033
+ * @returns The actor logic of the Effect's slot. Its output is a `CommandOutput`.
1034
+ *
1035
+ * @example
1036
+ * ```ts
1037
+ * import { z } from "zod";
1038
+ * import { arg, command, fail, jr2Setup, workspace, type Workspaced } from "@jr2/orchestrator";
1039
+ *
1040
+ * const runInput = z.object({ branch: z.string() });
1041
+ * type BodyInput = Workspaced<z.infer<typeof runInput>, "app">;
1042
+ *
1043
+ * const body = jr2Setup({
1044
+ * types: {} as { input: BodyInput },
1045
+ * actors: {
1046
+ * tests: command(["node", "--test", arg("file")], { timeout: "5m" }),
1047
+ * fail: fail(),
1048
+ * },
1049
+ * }).createMachine({
1050
+ * id: "check",
1051
+ * on: { "workspace.lost": ".lost" },
1052
+ * initial: "testing",
1053
+ * states: {
1054
+ * testing: {
1055
+ * invoke: {
1056
+ * src: "tests",
1057
+ * input: { file: "test/greet.test.js" },
1058
+ * onDone: [
1059
+ * {
1060
+ * guard: ({ event }) => event.output.outcome === "exited" && event.output.code === 0,
1061
+ * target: "passed",
1062
+ * },
1063
+ * { target: "failed" },
1064
+ * ],
1065
+ * },
1066
+ * },
1067
+ * passed: { type: "final" },
1068
+ * // Not final states: each one fails the run, and the run ends `failed`.
1069
+ * failed: { invoke: { src: "fail", input: { reason: "the test did not pass" } } },
1070
+ * lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
1071
+ * },
1072
+ * });
1073
+ *
1074
+ * export const machine = workspace(body, {
1075
+ * input: runInput,
1076
+ * repos: { app: "https://github.com/you/app.git" },
1077
+ * spec: ({ input }) => ({ branch: input.branch }),
1078
+ * });
1079
+ * ```
1080
+ *
1081
+ * @see docs/concepts/effects.md
1082
+ * @see docs/guides/run-a-command-or-call-a-url.md
1083
+ * @see docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md
1084
+ * @see docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md
1085
+ */
1086
+ export function command<const A extends readonly (string | Arg)[], const Opt extends CommandOptions = CommandOptions>(
1087
+ argv: A,
1088
+ options?: Opt,
1089
+ ): EffectLogic<CommandInput<A, Opt["pin"]>, CommandOutput> & OpenOf<EnvOpenParts<Opt>>;
1090
+ /**
1091
+ * Make a `command` Effect whose argv is Open: `command(open, options)`, in a Machine that you
1092
+ * publish as a package. The Instance that registers the Machine binds the argv one time:
1093
+ * `customize(machine, { effects: { <slot>: { argv } } })`. {@link command} tells the rest, and
1094
+ * what it refuses.
1095
+ *
1096
+ * @param argv - `open`.
1097
+ * @param options - Optional. The same options as for a written argv.
1098
+ * @returns The actor logic of the Effect's slot. Its output is a `CommandOutput`.
1099
+ *
1100
+ * @see docs/guides/bind-open-parts.md
1101
+ * @see docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md
1102
+ */
1103
+ export function command<const Opt extends CommandOptions = CommandOptions>(
1104
+ argv: typeof open,
1105
+ options?: Opt,
1106
+ ): EffectLogic<OpenCommandInput<Opt["pin"]>, CommandOutput> & OpenEffect<"argv" | EnvOpenParts<Opt>>;
1107
+ export function command(
1108
+ argv: readonly (string | Arg)[] | typeof open,
1109
+ options: CommandOptions = {},
1110
+ // The implementation of the overloads above: each one states the input type.
1111
+ ): EffectLogic<any, CommandOutput> {
1112
+ // THE authoring surface for a `command` Effect (ADR-0067). `argv` is never shell text — an
1113
+ // `arg("x")` entry is ONE argument, filled from the invoke's input — and `sh -c` is the author's
1114
+ // explicit choice. Bound to the real exec port here; `commandWith` in effect.ts is the seam the
1115
+ // unit tests share.
1116
+ //
1117
+ // An Open argv (ADR-0054) is the composer's to bind with `customize()`; see open-effect.ts.
1118
+ return commandWith(
1119
+ (endpoint, bearer) => createExecPort({ baseUrl: endpoint, ...(bearer ? { token: bearer } : {}) }),
1120
+ argv as readonly (string | Arg)[],
1121
+ options,
1122
+ );
1123
+ }
1124
+
1125
+ /** What a draining Harness answers an admission (ADR-0031): refused before anything is queued. */
1126
+ const DRAINING = 503;
1127
+
412
1128
  /** A stream read worth retrying (server hiccup) — internal to the reconnect loop, never thrown out. */
413
1129
  class ReconnectableError extends Error {}
414
1130