@jr2/orchestrator 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -2
- package/console/components/app.ts +2 -0
- package/console/components/fleet.ts +14 -1
- package/console/components/machine-pane.ts +41 -2
- package/console/main.ts +13 -1
- package/console/store.ts +102 -6
- package/console/style.css +21 -0
- package/docs/agents-block.md +5 -0
- package/docs/concepts/agents-and-turns.md +478 -0
- package/docs/concepts/architecture.md +243 -0
- package/docs/concepts/charts.md +146 -0
- package/docs/concepts/composition.md +357 -0
- package/docs/concepts/converge.md +257 -0
- package/docs/concepts/durable-runs.md +189 -0
- package/docs/concepts/effects.md +658 -0
- package/docs/concepts/gates.md +208 -0
- package/docs/concepts/machines-and-workflows.md +298 -0
- package/docs/concepts/pools-and-sources.md +230 -0
- package/docs/concepts/secrets-and-trust.md +306 -0
- package/docs/concepts/the-menu.md +381 -0
- package/docs/concepts/workspaces.md +756 -0
- package/docs/get-started/a-model-decides.md +563 -0
- package/docs/get-started/an-agent-in-a-sandbox.md +383 -0
- package/docs/get-started/connect-a-model.md +248 -0
- package/docs/get-started/create-an-instance.md +194 -0
- package/docs/get-started/have-your-assistant-write-a-machine.md +993 -0
- package/docs/get-started/install.md +130 -0
- package/docs/guides/add-tools-to-the-sandbox.md +308 -0
- package/docs/guides/answer-a-gate.md +304 -0
- package/docs/guides/bind-open-parts.md +224 -0
- package/docs/guides/cancel-a-run.md +146 -0
- package/docs/guides/clean-up.md +206 -0
- package/docs/guides/cut-a-worktree.md +560 -0
- package/docs/guides/fix-a-failed-converge.md +361 -0
- package/docs/guides/give-the-cluster-git-credentials.md +222 -0
- package/docs/guides/iterate-on-a-machine-on-kind.md +190 -0
- package/docs/guides/let-an-agent-write-a-chart.md +230 -0
- package/docs/guides/look-inside-a-sandbox.md +177 -0
- package/docs/guides/mirror-the-kit-images.md +176 -0
- package/docs/guides/move-from-kind-to-a-shared-cluster.md +318 -0
- package/docs/guides/open-the-console.md +91 -0
- package/docs/guides/publish-a-machine-as-an-npm-package.md +350 -0
- package/docs/guides/push-and-sign-a-branch.md +532 -0
- package/docs/guides/run-a-chart-by-name.md +270 -0
- package/docs/guides/run-a-command-or-call-a-url.md +587 -0
- package/docs/guides/run-one-worker-per-item.md +322 -0
- package/docs/guides/set-an-agents-access-and-dials.md +420 -0
- package/docs/guides/size-a-workspace.md +217 -0
- package/docs/guides/start-a-run-from-a-run.md +237 -0
- package/docs/guides/start-a-run.md +207 -0
- package/docs/guides/upgrade-the-kit-version.md +240 -0
- package/docs/guides/use-a-hosted-provider.md +214 -0
- package/docs/guides/use-a-local-model.md +313 -0
- package/docs/guides/watch-a-run.md +349 -0
- package/docs/guides/write-a-workflow.md +318 -0
- package/docs/index.md +44 -0
- package/docs/llms.txt +237 -0
- package/docs/reference/authoring-surface/agent.md +151 -0
- package/docs/reference/authoring-surface/answer.md +156 -0
- package/docs/reference/authoring-surface/arg.md +82 -0
- package/docs/reference/authoring-surface/basic.md +62 -0
- package/docs/reference/authoring-surface/bearer.md +53 -0
- package/docs/reference/authoring-surface/branchname.md +15 -0
- package/docs/reference/authoring-surface/chartschema.md +23 -0
- package/docs/reference/authoring-surface/chartschemafor.md +48 -0
- package/docs/reference/authoring-surface/command.md +206 -0
- package/docs/reference/authoring-surface/customize.md +64 -0
- package/docs/reference/authoring-surface/defineconfig.md +41 -0
- package/docs/reference/authoring-surface/defineevent.md +114 -0
- package/docs/reference/authoring-surface/dispatch.md +126 -0
- package/docs/reference/authoring-surface/fail.md +96 -0
- package/docs/reference/authoring-surface/file.md +142 -0
- package/docs/reference/authoring-surface/frompick.md +277 -0
- package/docs/reference/authoring-surface/http.md +259 -0
- package/docs/reference/authoring-surface/isfailure.md +58 -0
- package/docs/reference/authoring-surface/isready.md +14 -0
- package/docs/reference/authoring-surface/jr2setup.md +237 -0
- package/docs/reference/authoring-surface/open.md +105 -0
- package/docs/reference/authoring-surface/openbody.md +86 -0
- package/docs/reference/authoring-surface/paramsof.md +143 -0
- package/docs/reference/authoring-surface/pool.md +186 -0
- package/docs/reference/authoring-surface/push.md +130 -0
- package/docs/reference/authoring-surface/readyof.md +21 -0
- package/docs/reference/authoring-surface/secret.md +139 -0
- package/docs/reference/authoring-surface/sentby.md +83 -0
- package/docs/reference/authoring-surface/source.md +51 -0
- package/docs/reference/authoring-surface/text.md +62 -0
- package/docs/reference/authoring-surface/type-agentdeclaration.md +27 -0
- package/docs/reference/authoring-surface/type-agentdefinition.md +80 -0
- package/docs/reference/authoring-surface/type-agentlogic.md +15 -0
- package/docs/reference/authoring-surface/type-agentslots.md +15 -0
- package/docs/reference/authoring-surface/type-answeraction.md +12 -0
- package/docs/reference/authoring-surface/type-anyframepath.md +14 -0
- package/docs/reference/authoring-surface/type-anyworktreepath.md +15 -0
- package/docs/reference/authoring-surface/type-arg.md +19 -0
- package/docs/reference/authoring-surface/type-argnames.md +12 -0
- package/docs/reference/authoring-surface/type-bindactors.md +14 -0
- package/docs/reference/authoring-surface/type-binding.md +61 -0
- package/docs/reference/authoring-surface/type-bindingof.md +12 -0
- package/docs/reference/authoring-surface/type-bodyacceptsdoor.md +15 -0
- package/docs/reference/authoring-surface/type-bodyacceptsslots.md +17 -0
- package/docs/reference/authoring-surface/type-ceilings.md +41 -0
- package/docs/reference/authoring-surface/type-chainlink.md +19 -0
- package/docs/reference/authoring-surface/type-chartaction.md +46 -0
- package/docs/reference/authoring-surface/type-chartagent.md +28 -0
- package/docs/reference/authoring-surface/type-chartevent.md +20 -0
- package/docs/reference/authoring-surface/type-chartguard.md +20 -0
- package/docs/reference/authoring-surface/type-chartinvoke.md +28 -0
- package/docs/reference/authoring-surface/type-chartjson.md +14 -0
- package/docs/reference/authoring-surface/type-chartstate.md +46 -0
- package/docs/reference/authoring-surface/type-charttext.md +40 -0
- package/docs/reference/authoring-surface/type-charttransition.md +22 -0
- package/docs/reference/authoring-surface/type-charttransitions.md +14 -0
- package/docs/reference/authoring-surface/type-chartvalue.md +15 -0
- package/docs/reference/authoring-surface/type-childat.md +12 -0
- package/docs/reference/authoring-surface/type-childslots.md +12 -0
- package/docs/reference/authoring-surface/type-commandenv.md +15 -0
- package/docs/reference/authoring-surface/type-commandinput.md +20 -0
- package/docs/reference/authoring-surface/type-commandoptions.md +56 -0
- package/docs/reference/authoring-surface/type-commandoutput.md +122 -0
- package/docs/reference/authoring-surface/type-customize.md +111 -0
- package/docs/reference/authoring-surface/type-dispatchdefinition.md +39 -0
- package/docs/reference/authoring-surface/type-dispatchinput.md +14 -0
- package/docs/reference/authoring-surface/type-dispatchopenparts.md +14 -0
- package/docs/reference/authoring-surface/type-dispatchoutput.md +72 -0
- package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
- package/docs/reference/authoring-surface/type-duration.md +15 -0
- package/docs/reference/authoring-surface/type-effectbinding.md +93 -0
- package/docs/reference/authoring-surface/type-effectlogic.md +12 -0
- package/docs/reference/authoring-surface/type-envopenparts.md +14 -0
- package/docs/reference/authoring-surface/type-eventaudience.md +27 -0
- package/docs/reference/authoring-surface/type-eventdef.md +45 -0
- package/docs/reference/authoring-surface/type-eventfrom.md +16 -0
- package/docs/reference/authoring-surface/type-eventsemantics.md +26 -0
- package/docs/reference/authoring-surface/type-execoutcome.md +34 -0
- package/docs/reference/authoring-surface/type-failslot.md +14 -0
- package/docs/reference/authoring-surface/type-failure.md +19 -0
- package/docs/reference/authoring-surface/type-failureinput.md +20 -0
- package/docs/reference/authoring-surface/type-filebody.md +37 -0
- package/docs/reference/authoring-surface/type-framepath.md +14 -0
- package/docs/reference/authoring-surface/type-frominput.md +14 -0
- package/docs/reference/authoring-surface/type-hostinjectedinput.md +56 -0
- package/docs/reference/authoring-surface/type-httpdefinition.md +104 -0
- package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
- package/docs/reference/authoring-surface/type-httpinput.md +12 -0
- package/docs/reference/authoring-surface/type-httpmethod.md +14 -0
- package/docs/reference/authoring-surface/type-httpopenparts.md +12 -0
- package/docs/reference/authoring-surface/type-httpoutput.md +95 -0
- package/docs/reference/authoring-surface/type-httprequestparts.md +33 -0
- package/docs/reference/authoring-surface/type-jr2config.md +126 -0
- package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
- package/docs/reference/authoring-surface/type-lends.md +38 -0
- package/docs/reference/authoring-surface/type-logicat.md +12 -0
- package/docs/reference/authoring-surface/type-markedif.md +12 -0
- package/docs/reference/authoring-surface/type-mergedactors.md +12 -0
- package/docs/reference/authoring-surface/type-namedsecret.md +25 -0
- package/docs/reference/authoring-surface/type-namestart.md +77 -0
- package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +27 -0
- package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
- package/docs/reference/authoring-surface/type-openbodydeclaration.md +40 -0
- package/docs/reference/authoring-surface/type-openbodylogic.md +14 -0
- package/docs/reference/authoring-surface/type-openbodyoutput.md +42 -0
- package/docs/reference/authoring-surface/type-openbodyslot.md +18 -0
- package/docs/reference/authoring-surface/type-opencommandinput.md +22 -0
- package/docs/reference/authoring-surface/type-openeffect.md +12 -0
- package/docs/reference/authoring-surface/type-openeffectslots.md +12 -0
- package/docs/reference/authoring-surface/type-openof.md +12 -0
- package/docs/reference/authoring-surface/type-opensecret.md +23 -0
- package/docs/reference/authoring-surface/type-opensecretmark.md +12 -0
- package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +26 -0
- package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +27 -0
- package/docs/reference/authoring-surface/type-poolmachine.md +28 -0
- package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
- package/docs/reference/authoring-surface/type-poolspec.md +60 -0
- package/docs/reference/authoring-surface/type-pushinput.md +34 -0
- package/docs/reference/authoring-surface/type-pushoptions.md +79 -0
- package/docs/reference/authoring-surface/type-pushoutput.md +149 -0
- package/docs/reference/authoring-surface/type-pushwireoutput.md +12 -0
- package/docs/reference/authoring-surface/type-quantity.md +14 -0
- package/docs/reference/authoring-surface/type-reached.md +12 -0
- package/docs/reference/authoring-surface/type-reposlot.md +70 -0
- package/docs/reference/authoring-surface/type-reposlotsof.md +16 -0
- package/docs/reference/authoring-surface/type-sandboxoptions.md +84 -0
- package/docs/reference/authoring-surface/type-secret.md +25 -0
- package/docs/reference/authoring-surface/type-sender.md +19 -0
- package/docs/reference/authoring-surface/type-sentbyguards.md +12 -0
- package/docs/reference/authoring-surface/type-size.md +85 -0
- package/docs/reference/authoring-surface/type-slothandles.md +24 -0
- package/docs/reference/authoring-surface/type-sourcenextresult.md +17 -0
- package/docs/reference/authoring-surface/type-sourcespec.md +44 -0
- package/docs/reference/authoring-surface/type-syncstate.md +12 -0
- package/docs/reference/authoring-surface/type-text.md +19 -0
- package/docs/reference/authoring-surface/type-textpart.md +24 -0
- package/docs/reference/authoring-surface/type-thinkinglevel.md +39 -0
- package/docs/reference/authoring-surface/type-toparameterizedobject.md +14 -0
- package/docs/reference/authoring-surface/type-toprovidedactor.md +14 -0
- package/docs/reference/authoring-surface/type-unknown.md +21 -0
- package/docs/reference/authoring-surface/type-usercontainer.md +77 -0
- package/docs/reference/authoring-surface/type-withoutemptyindex.md +20 -0
- package/docs/reference/authoring-surface/type-workflowevent.md +46 -0
- package/docs/reference/authoring-surface/type-workspaceaccess.md +16 -0
- package/docs/reference/authoring-surface/type-workspaced.md +115 -0
- package/docs/reference/authoring-surface/type-workspacehandles.md +22 -0
- package/docs/reference/authoring-surface/type-workspacemachine.md +29 -0
- package/docs/reference/authoring-surface/type-workspaceof.md +13 -0
- package/docs/reference/authoring-surface/type-workspaceoptions.md +27 -0
- package/docs/reference/authoring-surface/type-worktreefound.md +16 -0
- package/docs/reference/authoring-surface/type-worktreeinput.md +56 -0
- package/docs/reference/authoring-surface/type-worktreekey.md +14 -0
- package/docs/reference/authoring-surface/type-worktreeoptions.md +29 -0
- package/docs/reference/authoring-surface/type-worktreeoutput.md +45 -0
- package/docs/reference/authoring-surface/type-worktreepath.md +16 -0
- package/docs/reference/authoring-surface/type-worktreeready.md +37 -0
- package/docs/reference/authoring-surface/type-worktreerefused.md +21 -0
- package/docs/reference/authoring-surface/workspace.md +287 -0
- package/docs/reference/authoring-surface/worktree.md +71 -0
- package/docs/reference/authoring-surface.md +217 -0
- package/docs/reference/cli.md +433 -0
- package/docs/reference/config.md +765 -0
- package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +114 -0
- package/docs/reference/decisions/0002-duplex-streaming-actor.md +117 -0
- package/docs/reference/decisions/0004-worktree-provisioning-model.md +130 -0
- package/docs/reference/decisions/0005-sandbox-pod-composition.md +240 -0
- package/docs/reference/decisions/0006-agent-control-surface.md +50 -0
- package/docs/reference/decisions/0007-durable-machine-state.md +109 -0
- package/docs/reference/decisions/0008-orchestrator-as-deployed-app.md +60 -0
- package/docs/reference/decisions/0009-cli-and-instance-interface.md +275 -0
- package/docs/reference/decisions/0010-bdd-acceptance-tests.md +160 -0
- package/docs/reference/decisions/0011-workflow-defined-events.md +141 -0
- package/docs/reference/decisions/0012-workspace-wrapper-machine.md +103 -0
- package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +175 -0
- package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +95 -0
- package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +271 -0
- package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +126 -0
- package/docs/reference/decisions/0017-pool-over-a-source-port.md +71 -0
- package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +111 -0
- package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +287 -0
- package/docs/reference/decisions/0020-private-ca-trust-is-a-named-concept.md +60 -0
- package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +101 -0
- package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +103 -0
- package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +175 -0
- package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +436 -0
- package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +65 -0
- package/docs/reference/decisions/0026-a-turn-that-is-over-is-never-prompted.md +62 -0
- package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +245 -0
- package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +90 -0
- package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +154 -0
- package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +100 -0
- package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +193 -0
- package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +61 -0
- package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +126 -0
- package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +57 -0
- package/docs/reference/decisions/0035-a-runaway-turn-is-ended-rerolled-once-then-a-fault.md +92 -0
- package/docs/reference/decisions/0036-a-turn-compacts-its-own-context-mid-flight.md +138 -0
- package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +196 -0
- package/docs/reference/decisions/0038-jr2-up-builds-every-image-it-deploys.md +170 -0
- package/docs/reference/decisions/0039-image-garbage-collects-by-reachability.md +115 -0
- package/docs/reference/decisions/0041-a-build-the-host-already-holds-is-not-spent-again.md +64 -0
- package/docs/reference/decisions/0042-ready-is-not-routable-an-admission-retries-its-connection.md +142 -0
- package/docs/reference/decisions/0043-the-kit-is-tested-as-installed-a-local-registry-stands-in-for-npm.md +170 -0
- package/docs/reference/decisions/0044-kit-images-live-at-a-canonical-home-a-self-host-mirrors-it.md +85 -0
- package/docs/reference/decisions/0045-the-platform-joins-the-image-address-and-the-cluster-chooses-it.md +82 -0
- package/docs/reference/decisions/0046-a-converge-failure-carries-its-diagnosis.md +34 -0
- package/docs/reference/decisions/0047-the-git-ssh-key-source-is-the-users-choice.md +47 -0
- package/docs/reference/decisions/0048-the-orchestrator-boots-without-its-repos.md +36 -0
- package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +126 -0
- package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +73 -0
- package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +238 -0
- package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +86 -0
- package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +123 -0
- package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +126 -0
- package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +107 -0
- package/docs/reference/decisions/0056-a-global-jr2-hands-off-to-the-instances-own-and-an-instance-has-one-kit-version.md +62 -0
- package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +152 -0
- package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +108 -0
- package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +427 -0
- package/docs/reference/decisions/0060-a-workspace-states-its-size-and-jr2-reserves-all-of-it.md +111 -0
- package/docs/reference/decisions/0061-a-memory-kill-ends-the-agents-processes-not-the-harness.md +83 -0
- package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +239 -0
- package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +82 -0
- package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +79 -0
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +65 -0
- package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +552 -0
- package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +458 -0
- package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +475 -0
- package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +180 -0
- package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +334 -0
- package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +183 -0
- package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +144 -0
- package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +122 -0
- package/docs/reference/decisions/0074-an-unrouted-mechanism-event-is-refused-at-load-and-faults-the-run.md +76 -0
- package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +295 -0
- package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +320 -0
- package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
- package/docs/reference/decisions.md +86 -0
- package/docs/reference/glossary.md +687 -0
- package/docs/reference/http-api.md +1386 -0
- package/docs/reference/machines.md +189 -0
- package/docs/reference/mechanism-events.md +72 -0
- package/docs/why.md +207 -0
- package/package.json +5 -3
- package/src/actor.ts +694 -106
- package/src/agent.ts +114 -38
- package/src/ambient.ts +3 -4
- package/src/assembly-checks.ts +189 -7
- package/src/cause.ts +234 -0
- package/src/chart.ts +345 -46
- package/src/charts.ts +43 -8
- package/src/config.ts +440 -178
- package/src/custodian.lua +17 -5
- package/src/custodian.ts +325 -99
- package/src/customize.ts +199 -49
- package/src/dispatch-effect.ts +945 -0
- package/src/door-compat.ts +217 -0
- package/src/durability.ts +11 -4
- package/src/effect-checks.ts +50 -3
- package/src/effect.ts +641 -128
- package/src/effector-wire.ts +165 -13
- package/src/fail.ts +222 -0
- package/src/harness-client.ts +587 -50
- package/src/harness-env.ts +33 -0
- package/src/held-secrets.ts +125 -16
- package/src/holds.ts +3 -0
- package/src/http-effect.ts +556 -58
- package/src/http.ts +480 -30
- package/src/index.ts +74 -82
- package/src/instance.ts +68 -8
- package/src/internal.ts +85 -0
- package/src/kube-client.ts +6 -3
- package/src/machine-doc.ts +24 -7
- package/src/mechanism-routes.ts +168 -0
- package/src/model-keys.generated.ts +53 -0
- package/src/model-keys.ts +42 -58
- package/src/names.ts +19 -6
- package/src/notices.ts +483 -41
- package/src/open-body.ts +242 -40
- package/src/open-effect.ts +81 -8
- package/src/open.ts +90 -6
- package/src/parts.ts +213 -30
- package/src/party.ts +101 -2
- package/src/pick.ts +497 -11
- package/src/pool.ts +282 -50
- package/src/registration.ts +133 -12
- package/src/repo-identity.ts +7 -3
- package/src/repos.ts +22 -3
- package/src/run-host.ts +665 -45
- package/src/run-output.ts +38 -0
- package/src/sandbox-effect.ts +457 -71
- package/src/sandbox-kube.ts +66 -17
- package/src/secret-placement.ts +59 -17
- package/src/server.ts +3 -0
- package/src/setup.ts +325 -62
- package/src/size.ts +121 -9
- package/src/snapshot-store.ts +54 -10
- package/src/stub-harness.ts +44 -16
- package/src/timers.ts +14 -1
- package/src/tokens.ts +107 -32
- package/src/vocabulary.ts +88 -14
- package/src/wire.ts +100 -26
- package/src/workspace.ts +771 -238
- package/src/worktree.ts +249 -0
package/src/effect.ts
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
// route on, the input kept in the persisted snapshot, a `src` typo as a compile error, and
|
|
5
5
|
// `.provide()` as the unit-test seam.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// The kinds: `command`, run in the Harness container of the enclosing Workspace's Sandbox (the
|
|
8
|
+
// Harness's `/exec/:id`, ADR-0067), here; `http`, run in the Instance Effector (http-effect.ts,
|
|
9
|
+
// ADR-0068); `push`, in the Sandbox Effector (sandbox-effect.ts); and `dispatch`, which starts a run
|
|
10
|
+
// of a registered Workflow, in the Orchestrator's own process or, toward another Orchestrator, in
|
|
11
|
+
// the Instance Effector (dispatch-effect.ts, ADR-0071). What this module adds to xstate is what makes an Effect safe to
|
|
10
12
|
// restart:
|
|
11
13
|
//
|
|
12
14
|
// - THE LEDGER. The run host keeps one row per Effect, in the run's blob beside the snapshot
|
|
@@ -42,6 +44,7 @@ import type { ExecOutcome, ExecRead, ExecRequest, SyncAnswer } from "./wire.ts";
|
|
|
42
44
|
import { bindOpenSecretIn, type EffectorPortFactory, type OpenSecretIn } from "./http-effect.ts";
|
|
43
45
|
import { commandEnvParts, credentialedCommandWith, sandboxEffectorPort, type CommandEnv } from "./sandbox-effect.ts";
|
|
44
46
|
import { open } from "./open.ts";
|
|
47
|
+
import type { AnyWorktreePath, WorktreeOutput } from "./worktree.ts";
|
|
45
48
|
import {
|
|
46
49
|
effectBindingShape,
|
|
47
50
|
openEffectLogic,
|
|
@@ -53,10 +56,89 @@ import {
|
|
|
53
56
|
|
|
54
57
|
// --- Parts: what `jr2 up` and the Machine doc read off a slot, without running anything ---------
|
|
55
58
|
|
|
56
|
-
|
|
59
|
+
// ONE argument, whatever its bytes: never shell text.
|
|
60
|
+
/** One entry of a command's argv that the input of the Effect fills, as `arg("name")` makes it. It
|
|
61
|
+
* is one argument, whatever its text. No shell reads it. */
|
|
57
62
|
export type Arg<N extends string = string> = { readonly arg: N };
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Mark one argv entry of a `command` that the input of the Effect fills. `arg("file")` is one
|
|
66
|
+
* argument: the value of the `file` key of the input, whatever its characters. No shell reads it.
|
|
67
|
+
*
|
|
68
|
+
* The input of the Effect then has a string key for each `arg`. Give it in the `input` of the
|
|
69
|
+
* `invoke`, or use `fromPick` to give it from a pick.
|
|
70
|
+
*
|
|
71
|
+
* When it starts, with no string in the input for an entry, the Effect fails (its `onError`
|
|
72
|
+
* route): `command "<slot>": argv slot <<name>> has no string value in its input (got <type>) —
|
|
73
|
+
* map it in the invoke's input`. The angle brackets around the name are in the text.
|
|
74
|
+
*
|
|
75
|
+
* @param name - The key of the input that fills this entry.
|
|
76
|
+
* @returns The argv entry, for the argv list of `command`.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* import {
|
|
81
|
+
* arg,
|
|
82
|
+
* command,
|
|
83
|
+
* fail,
|
|
84
|
+
* isReady,
|
|
85
|
+
* jr2Setup,
|
|
86
|
+
* workspace,
|
|
87
|
+
* worktree,
|
|
88
|
+
* type Workspaced,
|
|
89
|
+
* } from "@jr2/orchestrator";
|
|
90
|
+
*
|
|
91
|
+
* type Input = Workspaced<{ file: string }, "app">;
|
|
92
|
+
*
|
|
93
|
+
* const lintTree = (context: Input) => `${context.workspace.repos.app}/lint` as const;
|
|
94
|
+
*
|
|
95
|
+
* const body = jr2Setup({
|
|
96
|
+
* types: {} as { context: Input; input: Input },
|
|
97
|
+
* actors: { cut: worktree(), lint: command(["npx", "eslint", arg("file")]), fail: fail() },
|
|
98
|
+
* }).createMachine({
|
|
99
|
+
* id: "lint",
|
|
100
|
+
* context: ({ input }) => input,
|
|
101
|
+
* on: { "workspace.lost": ".lost" },
|
|
102
|
+
* initial: "cutting",
|
|
103
|
+
* states: {
|
|
104
|
+
* cutting: {
|
|
105
|
+
* invoke: {
|
|
106
|
+
* src: "cut",
|
|
107
|
+
* input: ({ context }) => ({ path: lintTree(context), branch: "lint" }),
|
|
108
|
+
* onDone: [{ guard: ({ event }) => isReady(event.output), target: "lint" }, { target: "notCut" }],
|
|
109
|
+
* },
|
|
110
|
+
* },
|
|
111
|
+
* lint: {
|
|
112
|
+
* invoke: {
|
|
113
|
+
* src: "lint",
|
|
114
|
+
* input: ({ context }) => ({ file: context.file, worktree: lintTree(context) }),
|
|
115
|
+
* onDone: "done",
|
|
116
|
+
* },
|
|
117
|
+
* },
|
|
118
|
+
* done: { type: "final" },
|
|
119
|
+
* notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
|
|
120
|
+
* lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
|
|
121
|
+
* },
|
|
122
|
+
* });
|
|
123
|
+
*
|
|
124
|
+
* export const machine = workspace(body, {
|
|
125
|
+
* repos: { app: { url: "https://github.com/you/app.git" } },
|
|
126
|
+
* });
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* @see docs/concepts/effects.md
|
|
130
|
+
* @see docs/guides/run-a-command-or-call-a-url.md
|
|
131
|
+
* @see docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md
|
|
132
|
+
*/
|
|
58
133
|
export const arg = <const N extends string>(name: N): Arg<N> => ({ arg: name });
|
|
59
134
|
|
|
135
|
+
/** What an Effect's input takes: `required` keys, `optional` keys, and exactly one of `oneOf`. */
|
|
136
|
+
export type InputShape = {
|
|
137
|
+
readonly required: readonly string[];
|
|
138
|
+
readonly optional: readonly string[];
|
|
139
|
+
readonly oneOf?: readonly string[];
|
|
140
|
+
};
|
|
141
|
+
|
|
60
142
|
/** A `command` Effect's plain data. */
|
|
61
143
|
export type CommandParts = {
|
|
62
144
|
readonly kind: "command";
|
|
@@ -72,8 +154,12 @@ export type CommandParts = {
|
|
|
72
154
|
/** A credentialed command's `pin`: its input names the commit (`{ sha }`, an approved one) in
|
|
73
155
|
* place of the Agent's branch tip (ADR-0067). */
|
|
74
156
|
readonly pin?: true;
|
|
75
|
-
/** The
|
|
76
|
-
|
|
157
|
+
/** The kit's cut (`worktree()`): its `expect`, static, so that a Chart cannot change it. A command
|
|
158
|
+
* with this is the kit's program, and its output is a `WorktreeOutput`. */
|
|
159
|
+
readonly cut?: { readonly expect?: "new" | "existing" };
|
|
160
|
+
/** Marked by the kit, on a cut alone: a restore or a Harness restart sends it again, under its
|
|
161
|
+
* id. An author has no such option: a command is at most once (ADR-0067). */
|
|
162
|
+
readonly idempotent?: true;
|
|
77
163
|
readonly timeoutMs: number;
|
|
78
164
|
/** Characters of the stream's end kept in the output. The full stream goes to the feed. */
|
|
79
165
|
readonly tail: number;
|
|
@@ -103,17 +189,16 @@ export type HttpParts = {
|
|
|
103
189
|
readonly maxBody: number;
|
|
104
190
|
/** Marked by the author: a restore that cannot find the Effect sends it again. */
|
|
105
191
|
readonly idempotent: boolean;
|
|
106
|
-
/** The body is a Worktree file (`body: file(...)`, ADR-0067): the Effect runs in the Sandbox
|
|
107
|
-
* of the Workspace whose body invokes it. `
|
|
108
|
-
*
|
|
109
|
-
|
|
110
|
-
readonly file?: { readonly slot?: string; readonly contentType?: string };
|
|
192
|
+
/** The body is a Worktree file (`body: file(...)`, ADR-0067): the Effect runs in the Sandbox
|
|
193
|
+
* Effector of the Workspace whose body invokes it. The input's `worktree` holds the file. Absent: a json, text or no body, in
|
|
194
|
+
* the Instance Effector. */
|
|
195
|
+
readonly file?: { readonly contentType?: string };
|
|
111
196
|
};
|
|
112
197
|
|
|
113
198
|
/**
|
|
114
|
-
* A `push` Effect's plain data (ADR-0067, ADR-
|
|
199
|
+
* A `push` Effect's plain data (ADR-0067, ADR-0075): a cut Worktree's branch to its Repo Slot's
|
|
115
200
|
* Binding. `sign`, `squash`, `force` are the author's; `pin` says the Effect's input names the
|
|
116
|
-
* commit (an approved one) instead of the
|
|
201
|
+
* commit (an approved one) instead of the branch's tip.
|
|
117
202
|
*/
|
|
118
203
|
export type PushParts = {
|
|
119
204
|
readonly kind: "push";
|
|
@@ -128,6 +213,33 @@ export type PushParts = {
|
|
|
128
213
|
readonly inputKeys: readonly string[];
|
|
129
214
|
};
|
|
130
215
|
|
|
216
|
+
/** A `dispatch` Effect's target: another Orchestrator's origin and the secret that holds its
|
|
217
|
+
* Instance token, by name (ADR-0071). */
|
|
218
|
+
export type DispatchTarget = { readonly url: string; readonly token: string };
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* A `dispatch` Effect's plain data (ADR-0071): the Workflow it starts a run of, as `jr2 run` names
|
|
222
|
+
* it (`review`, or `<wrapper>/<name>` for a named Chart), whether it waits for that run to end, the
|
|
223
|
+
* bound on the start (or the wait), and the target. No target: it runs in the Orchestrator's own
|
|
224
|
+
* process, through the run host, with no wire and no token. A target: it runs in the Instance
|
|
225
|
+
* Effector, which holds the token as an Effect secret.
|
|
226
|
+
*/
|
|
227
|
+
export type DispatchParts = {
|
|
228
|
+
readonly kind: "dispatch";
|
|
229
|
+
readonly workflow: string;
|
|
230
|
+
readonly wait: boolean;
|
|
231
|
+
readonly timeoutMs: number;
|
|
232
|
+
readonly target?: DispatchTarget;
|
|
233
|
+
/** The keys of the slot's input schema: what a pick param may fill. Undefined with no schema:
|
|
234
|
+
* the callee's door alone checks the body, so the load walk reads everything (ADR-0071). */
|
|
235
|
+
readonly inputKeys: readonly string[] | undefined;
|
|
236
|
+
/** `[target.token]`, or none in-process: what places the token (ADR-0068). */
|
|
237
|
+
readonly secrets: readonly string[];
|
|
238
|
+
/** Always false: a target's token is never `secret(open)` — the whole target is the Open part
|
|
239
|
+
* (ADR-0071). */
|
|
240
|
+
readonly openSecret: boolean;
|
|
241
|
+
};
|
|
242
|
+
|
|
131
243
|
/**
|
|
132
244
|
* An Effect whose url or argv a package left Open (ADR-0054): the same parts with the sentinel in
|
|
133
245
|
* that one place. The kind, the placement, the timeout and the secrets the other parts name are
|
|
@@ -140,21 +252,36 @@ export type OpenHttpParts = Omit<HttpParts, "url" | "target"> & {
|
|
|
140
252
|
readonly target: typeof open;
|
|
141
253
|
};
|
|
142
254
|
|
|
255
|
+
/** A `dispatch` whose target a package left Open (ADR-0071): the composer binds the url and the
|
|
256
|
+
* token together, once. */
|
|
257
|
+
export type OpenDispatchParts = Omit<DispatchParts, "target"> & { readonly target: typeof open };
|
|
258
|
+
|
|
143
259
|
/** Every kind's parts, bound or with an Open part. */
|
|
144
|
-
export type EffectParts =
|
|
260
|
+
export type EffectParts =
|
|
261
|
+
| CommandParts
|
|
262
|
+
| HttpParts
|
|
263
|
+
| PushParts
|
|
264
|
+
| OpenCommandParts
|
|
265
|
+
| OpenHttpParts
|
|
266
|
+
| DispatchParts
|
|
267
|
+
| OpenDispatchParts;
|
|
145
268
|
|
|
146
269
|
/** The kinds, as their parts name them. */
|
|
147
|
-
export const EFFECT_KINDS: ReadonlySet<string> = new Set(["command", "http", "push"]);
|
|
270
|
+
export const EFFECT_KINDS: ReadonlySet<string> = new Set(["command", "http", "push", "dispatch"]);
|
|
148
271
|
|
|
149
272
|
/**
|
|
150
273
|
* Where an Effect runs, derived from its kind and what it names, never authored (ADR-0067,
|
|
151
274
|
* ADR-0068): a `command` that names no secret in the Harness container of its Workspace; an
|
|
152
275
|
* `http` Effect with no Worktree file in the Instance Effector; an `http` whose body is a Worktree
|
|
153
|
-
* file, a `push` and a `command` that names a secret in the Sandbox Effector of its Workspace
|
|
276
|
+
* file, a `push` and a `command` that names a secret in the Sandbox Effector of its Workspace; a
|
|
277
|
+
* `dispatch` with no target in the Orchestrator's own process, and one with a target (an Open one
|
|
278
|
+
* included: it has one once bound) in the Instance Effector (ADR-0071).
|
|
154
279
|
*/
|
|
155
|
-
export type Placement = "harness" | "instance" | "sandbox";
|
|
280
|
+
export type Placement = "harness" | "instance" | "sandbox" | "orchestrator";
|
|
156
281
|
export function placementOf(parts: EffectParts): Placement {
|
|
157
282
|
switch (parts.kind) {
|
|
283
|
+
case "dispatch":
|
|
284
|
+
return parts.target === undefined ? "orchestrator" : "instance";
|
|
158
285
|
case "http":
|
|
159
286
|
return parts.file ? "sandbox" : "instance";
|
|
160
287
|
case "push":
|
|
@@ -192,23 +319,165 @@ export function targetText(parts: { readonly target: HttpParts["target"] | typeo
|
|
|
192
319
|
}
|
|
193
320
|
|
|
194
321
|
/** The keys an Effect's input has, read off its parts: a command's argv slot names (and `sha` with
|
|
195
|
-
* `pin`); an http Effect's input schema keys; a push's `sha` and `message
|
|
196
|
-
* argv: its slots are the composer's, unknown until
|
|
322
|
+
* `pin`); an http Effect's input schema keys; a push's `sha` and `message`; a dispatch's input
|
|
323
|
+
* schema keys. Undefined for an Open argv: its slots are the composer's, unknown until
|
|
324
|
+
* `customize()` binds it (ADR-0054); and for a dispatch with no input schema: the callee's door is
|
|
325
|
+
* the only check, so nothing here says what it reads (ADR-0071). */
|
|
197
326
|
export function inputKeysOf(parts: EffectParts): string[] | undefined {
|
|
198
|
-
|
|
327
|
+
const shape = inputShapeOf(parts);
|
|
328
|
+
return shape && [...shape.required, ...shape.optional, ...(shape.oneOf ?? [])];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** The shape of a cut's input. */
|
|
332
|
+
const CUT_SHAPE: InputShape = { required: ["path"], optional: ["from"], oneOf: ["branch", "detach"] };
|
|
333
|
+
|
|
334
|
+
/** An Effect's input as a Chart's door and its start hold a Chart to it. Undefined where
|
|
335
|
+
* {@link inputKeysOf} is. `worktree` is optional for a key the Workspace decides: the door makes it
|
|
336
|
+
* required when it knows that the Workspace has Repo Slots, and a start refuses it then. */
|
|
337
|
+
export function inputShapeOf(parts: EffectParts): InputShape | undefined {
|
|
338
|
+
if (parts.kind === "dispatch") {
|
|
339
|
+
return parts.inputKeys === undefined ? undefined : { required: [...parts.inputKeys], optional: [] };
|
|
340
|
+
}
|
|
341
|
+
if (parts.kind === "push") return { required: [...parts.inputKeys], optional: [] };
|
|
342
|
+
if (parts.kind === "http") {
|
|
343
|
+
return { required: [...parts.inputKeys], optional: parts.file ? ["worktree"] : [] };
|
|
344
|
+
}
|
|
345
|
+
if (parts.cut) return CUT_SHAPE;
|
|
199
346
|
if (parts.argv === open) return undefined;
|
|
200
|
-
|
|
347
|
+
const keys = [...parts.argv.flatMap((a) => (typeof a === "string" ? [] : [a.arg])), ...(parts.pin ? ["sha"] : [])];
|
|
348
|
+
// A command with an Effect secret runs on a commit of a Worktree, so it names one in each Workspace.
|
|
349
|
+
const credentialed = (parts.secrets?.length ?? 0) > 0 || parts.openSecret === true;
|
|
350
|
+
return credentialed ? { required: [...keys, "worktree"], optional: [] } : { required: keys, optional: ["worktree"] };
|
|
201
351
|
}
|
|
202
352
|
|
|
203
353
|
// --- Output -------------------------------------------------------------------------------------
|
|
204
354
|
|
|
205
|
-
/** The
|
|
206
|
-
*
|
|
355
|
+
/** The output `{ outcome: "unknown", reason }`: the Effect may have acted, and jr2 cannot tell.
|
|
356
|
+
* For example, the Harness lost the exec, or its state exited while it ran. Route on it. jr2 does
|
|
357
|
+
* not run the Effect again. */
|
|
207
358
|
export type Unknown = { outcome: "unknown"; reason: string };
|
|
208
359
|
|
|
209
|
-
/** A `
|
|
210
|
-
*
|
|
211
|
-
|
|
360
|
+
/** A value of `outcome` that this kit does not give. It stands in the union so that no author can
|
|
361
|
+
* write an exhaustive check, so that a later outcome is no break: route on what you know, and give
|
|
362
|
+
* the rest a last arm. */
|
|
363
|
+
export type LaterOutcome = { outcome: "(a later outcome)"; reason?: string };
|
|
364
|
+
|
|
365
|
+
// `dirty` is a credentialed command's alone (ADR-0067): it runs in the Sandbox Effector against
|
|
366
|
+
// the pinned commit, never the live Worktree.
|
|
367
|
+
/**
|
|
368
|
+
* The output of a `command` Effect: what the `onDone` of its state gets as `event.output`. Every
|
|
369
|
+
* outcome is output. A failed program is output too. Route on `outcome`:
|
|
370
|
+
*
|
|
371
|
+
* - `exited`: the program ended. `code` is its exit code. A program that the container does not
|
|
372
|
+
* have is `exited` with code 127.
|
|
373
|
+
* - `timeout`: the command ran past its `timeout`, and jr2 killed it.
|
|
374
|
+
* - `killed`: a signal ended it. `signal` names the signal.
|
|
375
|
+
* - `unknown`: jr2 cannot tell what the command did, for example because the Harness restarted
|
|
376
|
+
* while it ran. `reason` tells why. jr2 does not run it again.
|
|
377
|
+
*
|
|
378
|
+
* `tail` is the end of what the command wrote, at most the `tail` option of `command` in
|
|
379
|
+
* characters. `dirty` is `true` when a command that names an Effect secret ran against a commit,
|
|
380
|
+
* and the Worktree held changes that it did not see.
|
|
381
|
+
*
|
|
382
|
+
* The union is open ({@link LaterOutcome}): give the rest a last arm with no guard.
|
|
383
|
+
*
|
|
384
|
+
* @example
|
|
385
|
+
* ```ts
|
|
386
|
+
* import {
|
|
387
|
+
* command,
|
|
388
|
+
* type CommandOutput,
|
|
389
|
+
* fail,
|
|
390
|
+
* isReady,
|
|
391
|
+
* jr2Setup,
|
|
392
|
+
* workspace,
|
|
393
|
+
* worktree,
|
|
394
|
+
* type Workspaced,
|
|
395
|
+
* } from "@jr2/orchestrator";
|
|
396
|
+
*
|
|
397
|
+
* type Input = Workspaced<{}, "app">;
|
|
398
|
+
*
|
|
399
|
+
* const testTree = (context: Input) => `${context.workspace.repos.app}/test` as const;
|
|
400
|
+
*
|
|
401
|
+
* const passed = (output: CommandOutput): boolean =>
|
|
402
|
+
* output.outcome === "exited" && output.code === 0;
|
|
403
|
+
*
|
|
404
|
+
* const body = jr2Setup({
|
|
405
|
+
* types: {} as { context: Input; input: Input },
|
|
406
|
+
* actors: { cut: worktree(), tests: command(["npm", "test"], { timeout: "15m" }), fail: fail() },
|
|
407
|
+
* }).createMachine({
|
|
408
|
+
* id: "tests",
|
|
409
|
+
* context: ({ input }) => input,
|
|
410
|
+
* on: { "workspace.lost": ".lost" },
|
|
411
|
+
* initial: "cutting",
|
|
412
|
+
* states: {
|
|
413
|
+
* cutting: {
|
|
414
|
+
* invoke: {
|
|
415
|
+
* src: "cut",
|
|
416
|
+
* input: ({ context }) => ({ path: testTree(context), branch: "tests" }),
|
|
417
|
+
* onDone: [{ guard: ({ event }) => isReady(event.output), target: "testing" }, { target: "notCut" }],
|
|
418
|
+
* },
|
|
419
|
+
* },
|
|
420
|
+
* testing: {
|
|
421
|
+
* invoke: {
|
|
422
|
+
* src: "tests",
|
|
423
|
+
* input: ({ context }) => ({ worktree: testTree(context) }),
|
|
424
|
+
* onDone: [
|
|
425
|
+
* { guard: ({ event }) => passed(event.output), target: "green" },
|
|
426
|
+
* { target: "red" },
|
|
427
|
+
* ],
|
|
428
|
+
* },
|
|
429
|
+
* },
|
|
430
|
+
* green: { type: "final" },
|
|
431
|
+
* // Not a final state: a final state means `done`, and the run ends `failed` here.
|
|
432
|
+
* red: { invoke: { src: "fail", input: { reason: "npm test did not pass" } } },
|
|
433
|
+
* notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
|
|
434
|
+
* lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
|
|
435
|
+
* },
|
|
436
|
+
* });
|
|
437
|
+
*
|
|
438
|
+
* export const machine = workspace(body, {
|
|
439
|
+
* repos: { app: { url: "https://github.com/you/app.git" } },
|
|
440
|
+
* });
|
|
441
|
+
* ```
|
|
442
|
+
*
|
|
443
|
+
* @see docs/concepts/effects.md
|
|
444
|
+
* @see docs/guides/run-a-command-or-call-a-url.md
|
|
445
|
+
* @see docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md
|
|
446
|
+
*/
|
|
447
|
+
export type CommandOutput = (ExecOutcome & { dirty?: boolean }) | Unknown | LaterOutcome;
|
|
448
|
+
|
|
449
|
+
// Reach is output, as an http Effect's is (ADR-0067): a body a model wrote can be fixed through
|
|
450
|
+
// the Chain, so a refusal is output too.
|
|
451
|
+
/**
|
|
452
|
+
* The output of a `dispatch` Effect. Route on `outcome`:
|
|
453
|
+
*
|
|
454
|
+
* - `started`: the Orchestrator accepted the new run. `run` is its id.
|
|
455
|
+
* - `settled`: with `wait`, the new run ended. `status` is `done`, `failed`, `error`, `cancelled`,
|
|
456
|
+
* `lost` or `drifted`, and `output` is the output of its Machine. For `failed`, `output` is the
|
|
457
|
+
* Failure, `{ reason, detail? }`. `status` is `live` when the timeout came first, and the run
|
|
458
|
+
* continues.
|
|
459
|
+
* - `refused`: the Orchestrator started no run. `status` is the HTTP status: 400 for an input that
|
|
460
|
+
* the Workflow does not take, 404 for a Workflow that it does not have, 409 for a named Chart that
|
|
461
|
+
* cannot run now, 422 for a Chart that it refuses. `report` holds the lines of the refusal.
|
|
462
|
+
* - `unreachable`: the Orchestrator of the target did not answer. No run started.
|
|
463
|
+
* - `unknown`: jr2 cannot tell if a run started.
|
|
464
|
+
*
|
|
465
|
+
* The union is open ({@link LaterOutcome}): give the rest a last arm with no guard.
|
|
466
|
+
*
|
|
467
|
+
* @see docs/concepts/effects.md
|
|
468
|
+
*/
|
|
469
|
+
export type DispatchOutput =
|
|
470
|
+
| { outcome: "started"; run: string }
|
|
471
|
+
| {
|
|
472
|
+
outcome: "settled";
|
|
473
|
+
run: string;
|
|
474
|
+
status: "done" | "failed" | "error" | "cancelled" | "lost" | "drifted" | "live";
|
|
475
|
+
output?: unknown;
|
|
476
|
+
}
|
|
477
|
+
| { outcome: "refused"; status: number; report: string[] }
|
|
478
|
+
| { outcome: "unreachable"; reason: string }
|
|
479
|
+
| Unknown
|
|
480
|
+
| LaterOutcome;
|
|
212
481
|
|
|
213
482
|
// --- The ledger row (persisted in the run's blob, ADR-0007) --------------------------------------
|
|
214
483
|
|
|
@@ -238,19 +507,38 @@ export type EffectRow =
|
|
|
238
507
|
pinned?: string;
|
|
239
508
|
at: string;
|
|
240
509
|
}
|
|
241
|
-
/**
|
|
242
|
-
*
|
|
243
|
-
*
|
|
510
|
+
/** A dispatch's intent (ADR-0071): where it runs, and — once the start's answer names it — the
|
|
511
|
+
* run it started. A dispatch is idempotent by construction: its ledger id rides every start as
|
|
512
|
+
* the door's key, so a restore sends it again and gets the same run. */
|
|
513
|
+
| {
|
|
514
|
+
state: "intent";
|
|
515
|
+
kind: "dispatch";
|
|
516
|
+
slot: string;
|
|
517
|
+
placement: "orchestrator" | "instance";
|
|
518
|
+
idempotent: true;
|
|
519
|
+
wait: boolean;
|
|
520
|
+
run?: string;
|
|
521
|
+
at: string;
|
|
522
|
+
}
|
|
523
|
+
/** `sandbox`, `repo` and `branch`: the Workspace, the Repo Slot and the branch an Effect of the
|
|
524
|
+
* Sandbox Effector ran on — every later push of its own (slot, branch) sends its pair, and the
|
|
525
|
+
* newest push's unfinished sync runs again in `worktree`. `cut`: a `worktree()` that settled
|
|
526
|
+
* `ready`: its path and branch as fields, because a settled row drops its tail. A push, a
|
|
527
|
+
* credentialed command and a file body read their branch here. */
|
|
244
528
|
| {
|
|
245
529
|
state: "settled";
|
|
246
530
|
kind: EffectParts["kind"];
|
|
247
531
|
slot: string;
|
|
248
532
|
sandbox?: string;
|
|
249
533
|
repo?: string;
|
|
534
|
+
branch?: string;
|
|
535
|
+
cut?: { path: string; branch: string | null };
|
|
250
536
|
output: unknown;
|
|
251
537
|
at: string;
|
|
252
538
|
}
|
|
253
|
-
|
|
539
|
+
/** `run`: for a dispatch, the run it started, kept past the abort — the callee runs on, and the
|
|
540
|
+
* caller's row still names it (ADR-0071). */
|
|
541
|
+
| { state: "aborted"; kind: EffectParts["kind"]; slot: string; run?: string; at: string };
|
|
254
542
|
|
|
255
543
|
/**
|
|
256
544
|
* Retention (ADR-0067): drop, in place, the output tail of every settled row once a save on disk
|
|
@@ -297,36 +585,43 @@ const TAILS: Record<EffectRow["kind"], readonly string[]> = {
|
|
|
297
585
|
// every later push of its Repo Slot sends the pair (`pushPairsOf`), and the newest push's sync
|
|
298
586
|
// runs again when it did not finish. The sync's bundle and git's words are its tail.
|
|
299
587
|
push: ["bundle", "detail"],
|
|
588
|
+
// A dispatch's tail is the callee's output, which `GET /runs/<id>` serves for as long as the run
|
|
589
|
+
// is kept, and a refusal's report, which can carry the callee door's whole schema: the feed's
|
|
590
|
+
// settled marker carries both whole. The row keeps the outcome, the run id, its status and the
|
|
591
|
+
// door's HTTP status (ADR-0071).
|
|
592
|
+
dispatch: ["output", "report"],
|
|
300
593
|
};
|
|
301
594
|
|
|
302
|
-
/** A kind's tail for one output. The NEWEST push of its Repo Slot whose sync did not finish
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
595
|
+
/** A kind's tail for one output. The NEWEST push of its Repo Slot whose sync did not finish (`busy`,
|
|
596
|
+
* `not-synced`) keeps its bundle: the sync that runs again before the next push of that slot moves
|
|
597
|
+
* the branch from it, and the Agent's store may not hold the published commits without it. Once a
|
|
598
|
+
* sync finished, or once a newer push of the slot published, the bundle goes: only the newest
|
|
599
|
+
* push's sync runs again (`lastPushOf`), so an older bundle has no reader. */
|
|
307
600
|
function tailsOf(kind: EffectRow["kind"], output: Record<string, unknown>, newest: boolean): readonly string[] {
|
|
308
601
|
if (kind === "push" && newest && (output.sync === "busy" || output.sync === "not-synced")) return ["detail"];
|
|
309
602
|
return TAILS[kind];
|
|
310
603
|
}
|
|
311
604
|
|
|
312
|
-
/** A settled push row that published: its pair, and its sync's answer and bundle when it has them.
|
|
605
|
+
/** A settled push row that published: its pair, and its sync's answer and bundle when it has them.
|
|
606
|
+
*/
|
|
313
607
|
export type PushedRow = { published: string; source: string; sync?: SyncAnswer["sync"]; bundle?: string } & Record<
|
|
314
608
|
string,
|
|
315
609
|
unknown
|
|
316
610
|
>;
|
|
317
611
|
|
|
318
|
-
/** The ledger's last push of a
|
|
319
|
-
* Effect's (`self`).
|
|
612
|
+
/** The ledger's last push of a (slot, branch) of a Workspace that published: its row, by `at`, never
|
|
613
|
+
* this Effect's (`self`). Each (slot, branch) has its own pairs: one branch on one remote. */
|
|
320
614
|
export function lastPushOf(
|
|
321
615
|
rows: Readonly<Record<string, EffectRow>>,
|
|
322
616
|
sandbox: string,
|
|
323
617
|
repo: string,
|
|
618
|
+
branch: string,
|
|
324
619
|
self?: string,
|
|
325
620
|
): { rowId: string; row: Extract<EffectRow, { state: "settled" }>; output: PushedRow } | undefined {
|
|
326
621
|
let best: { rowId: string; row: Extract<EffectRow, { state: "settled" }>; output: PushedRow } | undefined;
|
|
327
622
|
for (const [rowId, row] of Object.entries(rows)) {
|
|
328
623
|
if (rowId === self || row.state !== "settled" || row.kind !== "push") continue;
|
|
329
|
-
if (row.sandbox !== sandbox || row.repo !== repo) continue;
|
|
624
|
+
if (row.sandbox !== sandbox || row.repo !== repo || row.branch !== branch) continue;
|
|
330
625
|
const output = row.output as Partial<PushedRow> & { outcome?: unknown };
|
|
331
626
|
if (output?.outcome !== "pushed" || typeof output.published !== "string" || typeof output.source !== "string") {
|
|
332
627
|
continue;
|
|
@@ -336,20 +631,21 @@ export function lastPushOf(
|
|
|
336
631
|
return best;
|
|
337
632
|
}
|
|
338
633
|
|
|
339
|
-
/** Every pair the ledger holds for a
|
|
340
|
-
* published, oldest first by `at`, never this Effect's (`self`) — its published commit, its
|
|
341
|
-
* and its own `squashed`. What a push start carries, so the Effector sees every push's pair. */
|
|
634
|
+
/** Every pair the ledger holds for a (slot, branch) of a Workspace (ADR-0067): each settled push row
|
|
635
|
+
* that published, oldest first by `at`, never this Effect's (`self`) — its published commit, its
|
|
636
|
+
* source, and its own `squashed`. What a push start carries, so the Effector sees every push's pair. */
|
|
342
637
|
export function pushPairsOf(
|
|
343
638
|
rows: Readonly<Record<string, EffectRow>>,
|
|
344
639
|
sandbox: string,
|
|
345
640
|
repo: string,
|
|
641
|
+
branch: string,
|
|
346
642
|
self?: string,
|
|
347
643
|
): Array<{ published: string; source: string; squashed?: "signed" | "unsigned" }> {
|
|
348
644
|
const found: Array<{ at: string; pair: { published: string; source: string; squashed?: "signed" | "unsigned" } }> =
|
|
349
645
|
[];
|
|
350
646
|
for (const [rowId, row] of Object.entries(rows)) {
|
|
351
647
|
if (rowId === self || row.state !== "settled" || row.kind !== "push") continue;
|
|
352
|
-
if (row.sandbox !== sandbox || row.repo !== repo) continue;
|
|
648
|
+
if (row.sandbox !== sandbox || row.repo !== repo || row.branch !== branch) continue;
|
|
353
649
|
const output = row.output as Partial<PushedRow> & { outcome?: unknown; squashed?: unknown };
|
|
354
650
|
if (output?.outcome !== "pushed" || typeof output.published !== "string" || typeof output.source !== "string") {
|
|
355
651
|
continue;
|
|
@@ -363,21 +659,41 @@ export function pushPairsOf(
|
|
|
363
659
|
return found.sort((a, b) => (a.at < b.at ? -1 : a.at > b.at ? 1 : 0)).map((f) => f.pair);
|
|
364
660
|
}
|
|
365
661
|
|
|
366
|
-
/** The row id of each
|
|
662
|
+
/** The row id of each (slot, branch)'s newest push that published (`lastPushOf`), over the ledger. */
|
|
367
663
|
function newestPushesOf(effects: Readonly<Record<string, EffectRow>>): Set<string> {
|
|
368
664
|
const ids = new Set<string>();
|
|
369
665
|
const seen = new Set<string>();
|
|
370
666
|
for (const row of Object.values(effects)) {
|
|
371
667
|
if (row.state !== "settled" || row.kind !== "push" || row.sandbox === undefined || row.repo === undefined) continue;
|
|
372
|
-
|
|
668
|
+
if (row.branch === undefined) continue;
|
|
669
|
+
const key = JSON.stringify([row.sandbox, row.repo, row.branch]);
|
|
373
670
|
if (seen.has(key)) continue;
|
|
374
671
|
seen.add(key);
|
|
375
|
-
const last = lastPushOf(effects, row.sandbox, row.repo);
|
|
672
|
+
const last = lastPushOf(effects, row.sandbox, row.repo, row.branch);
|
|
376
673
|
if (last !== undefined) ids.add(last.rowId);
|
|
377
674
|
}
|
|
378
675
|
return ids;
|
|
379
676
|
}
|
|
380
677
|
|
|
678
|
+
/** The cut that made `path` in this Sandbox: the newest settled `ready` row, whatever its Repo Slot.
|
|
679
|
+
* What a push, a credentialed command and a file body read. A row of another Sandbox says nothing
|
|
680
|
+
* of this one: a re-provision has an empty `/work`. */
|
|
681
|
+
export function cutRowOf(
|
|
682
|
+
rows: Readonly<Record<string, EffectRow>>,
|
|
683
|
+
sandbox: string,
|
|
684
|
+
path: unknown,
|
|
685
|
+
): { repo: string; path: string; branch: string | null } | undefined {
|
|
686
|
+
let best: { at: string; repo: string; path: string; branch: string | null } | undefined;
|
|
687
|
+
for (const row of Object.values(rows)) {
|
|
688
|
+
if (row.state !== "settled" || row.cut === undefined || row.sandbox !== sandbox) continue;
|
|
689
|
+
if (row.cut.path !== path || row.repo === undefined) continue;
|
|
690
|
+
if (best === undefined || row.at > best.at) {
|
|
691
|
+
best = { at: row.at, repo: row.repo, path: row.cut.path, branch: row.cut.branch };
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return best;
|
|
695
|
+
}
|
|
696
|
+
|
|
381
697
|
/** One Effect line on the run's feed (Instance-token band, like the Turn markers). `output` lines
|
|
382
698
|
* carry the stream; `aborted` and an `unknown` settle say the outside world may have acted. */
|
|
383
699
|
export type EffectMarker = {
|
|
@@ -386,14 +702,15 @@ export type EffectMarker = {
|
|
|
386
702
|
effect: string;
|
|
387
703
|
stage: "started" | "output" | "settled" | "aborted";
|
|
388
704
|
text?: string;
|
|
389
|
-
output?: CommandOutput | HttpOutput;
|
|
705
|
+
output?: CommandOutput | WorktreeOutput | HttpOutput | DispatchOutput;
|
|
390
706
|
};
|
|
391
707
|
|
|
392
708
|
// --- The port -----------------------------------------------------------------------------------
|
|
393
709
|
|
|
394
710
|
/** The Harness's exec, as the Effect drives it. `read` answers undefined for an exec the Harness
|
|
395
711
|
* does not know (404): it restarted, and jr2 settles `unknown`. `start` rejects with `ExecNotSent`
|
|
396
|
-
* only when the request certainly never reached the Harness — the one failure it is sent again on.
|
|
712
|
+
* only when the request certainly never reached the Harness — the one failure it is sent again on.
|
|
713
|
+
*/
|
|
397
714
|
export interface ExecPort {
|
|
398
715
|
start(id: string, req: ExecRequest, signal: AbortSignal): Promise<void>;
|
|
399
716
|
read(id: string, offset: number | undefined, signal: AbortSignal): Promise<ExecRead | undefined>;
|
|
@@ -416,48 +733,122 @@ type Fault = { type: "jr2.effect.error"; error: unknown };
|
|
|
416
733
|
|
|
417
734
|
/** An Effect slot's logic: xstate `ActorLogic`, typed by its input and output, branded with its
|
|
418
735
|
* parts. The brand is a runtime property (`isEffect`) and part of the type, as `definition` is
|
|
419
|
-
* not on an Agent's: nothing substitutes a fake for an Effect's parts.
|
|
736
|
+
* not on an Agent's: nothing substitutes a fake for an Effect's parts.
|
|
737
|
+
*
|
|
738
|
+
* @internal
|
|
739
|
+
*/
|
|
420
740
|
export type EffectLogic<I, O> = ActorLogic<EffectSnapshot<O, I>, EventObject, I> & EffectSlot;
|
|
421
741
|
|
|
742
|
+
/** @internal */
|
|
422
743
|
type ArgNames<A extends readonly unknown[]> = A[number] extends infer E ? (E extends Arg<infer N> ? N : never) : never;
|
|
423
744
|
/** A command's input: one string per argv slot, and with `pin` the approved commit (`{ sha }`);
|
|
424
745
|
* none when there is neither. */
|
|
425
|
-
export type CommandInput<A extends readonly unknown[], P = false> = [
|
|
746
|
+
export type CommandInput<A extends readonly unknown[], P = false, Opt = unknown> = [
|
|
426
747
|
ArgNames<A> | ([P] extends [true] ? "sha" : never),
|
|
427
748
|
] extends [never]
|
|
428
|
-
? void
|
|
429
|
-
: { [K in ArgNames<A> | ([P] extends [true] ? "sha" : never)]: string }
|
|
749
|
+
? (WorktreeKey<Opt> & NotWorktreeKeys) | ([Opt] extends [{ readonly env: object }] ? never : void)
|
|
750
|
+
: { [K in ArgNames<A> | ([P] extends [true] ? "sha" : never)]: string } & WorktreeKey<Opt> &
|
|
751
|
+
Omit<NotWorktreeKeys, ArgNames<A>>;
|
|
752
|
+
|
|
753
|
+
// A command takes its Worktree in the invoke's input. `jr2Setup` makes the key required under a body with Repo Slots, and drops it under a body with
|
|
754
|
+
// none. A credentialed command runs against one commit of a Worktree, so there it is always required.
|
|
755
|
+
/** @internal */
|
|
756
|
+
export type WorktreeKey<Opt> = [Opt] extends [{ readonly env: object }]
|
|
757
|
+
? { worktree: AnyWorktreePath }
|
|
758
|
+
: { worktree?: AnyWorktreePath };
|
|
759
|
+
|
|
760
|
+
/** The keys of a Turn and a cut, refused by name on an Effect's input: a mapper's excess key is no
|
|
761
|
+
* compile error, and the Worktree would be lost.
|
|
762
|
+
* @internal */
|
|
763
|
+
export type NotWorktreeKeys = { cwd?: never; path?: never; branch?: never };
|
|
430
764
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
765
|
+
// An Open argv's slots are the composer's (ADR-0054), so any key is one string — what the
|
|
766
|
+
// package's invoke hands it, read by the argv the composer binds (`argvOf`).
|
|
767
|
+
/** The input of a `command(open)`: a string for each `arg()` of the argv that the Instance binds,
|
|
768
|
+
* and with `pin`, the commit (`{ sha }`). The compiler cannot check the names. When the input has
|
|
769
|
+
* no string for an `arg()`, the Effect fails when it starts. */
|
|
770
|
+
export type OpenCommandInput<P = false, Opt = unknown> = Readonly<Record<string, string>> &
|
|
771
|
+
([P] extends [true] ? { sha: string } : unknown) &
|
|
772
|
+
WorktreeKey<Opt>;
|
|
436
773
|
|
|
774
|
+
/** A length of time: a positive number and a unit, `s`, `m` or `h`. For example `"90s"`, `"10m"`
|
|
775
|
+
* or `"1h"`. */
|
|
437
776
|
export type Duration = `${number}s` | `${number}m` | `${number}h`;
|
|
438
777
|
|
|
778
|
+
/** The options of `command()`: the Repo Slot it runs in, how long it can run, how much of its
|
|
779
|
+
* output it keeps, its Effect secrets, and the commit it runs against. */
|
|
439
780
|
export type CommandOptions = {
|
|
440
|
-
/**
|
|
441
|
-
|
|
442
|
-
|
|
781
|
+
/**
|
|
782
|
+
* How long the command can run: a number and a unit, such as `"90s"`, `"10m"` or `"1h"`.
|
|
783
|
+
* Default `"10m"`. When the time ends, jr2 kills the process group, and the output is
|
|
784
|
+
* `timeout`. It refuses a value that is not a positive duration: `command: timeout "<timeout>"
|
|
785
|
+
* is not a positive duration (like "90s", "10m", "1h")`.
|
|
786
|
+
*/
|
|
443
787
|
timeout?: Duration;
|
|
444
|
-
/**
|
|
788
|
+
/** The number of characters that `tail` keeps from the end of what the command wrote. Default
|
|
789
|
+
* 4096. */
|
|
445
790
|
tail?: number;
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
*
|
|
791
|
+
// Naming a secret is the placement (ADR-0067, ADR-0068): there is no placement key.
|
|
792
|
+
/**
|
|
793
|
+
* The environment variables of the command, each an Effect secret. `secret("x")` gives the
|
|
794
|
+
* value, and `secret("x").path` gives the path of a file that holds the value. When `env` names
|
|
795
|
+
* one, the command runs in the Sandbox Effector against one commit, not in the Worktree that the
|
|
796
|
+
* Agent changes. Without `env`, it runs in the Harness container, beside the Agent.
|
|
797
|
+
*
|
|
798
|
+
* It refuses a name that is not a variable name, and `PATH`, `HOME` and `TMPDIR`: ``command(argv,
|
|
799
|
+
* { env }): "<name>" is not a variable the author sets — PATH, HOME and TMPDIR are the
|
|
800
|
+
* Effector's (ADR-0067)``.
|
|
801
|
+
*/
|
|
449
802
|
env?: CommandEnv;
|
|
450
|
-
/**
|
|
451
|
-
*
|
|
803
|
+
/**
|
|
804
|
+
* Only for a command whose `env` names an Effect secret. `true`: the input of the Effect names
|
|
805
|
+
* the commit to run against (`{ sha }`, a full commit id, for example one that a Gate approved).
|
|
806
|
+
* Default: the tip of the Agent's branch when the Effect starts.
|
|
807
|
+
*
|
|
808
|
+
* It refuses `pin: true` on a command that names no Effect secret: ``command(argv, { pin: true
|
|
809
|
+
* }) names no secret in its env: `pin` chooses the commit a credentialed command runs against in
|
|
810
|
+
* the Sandbox Effector, and a command that names no secret runs in the live Worktree (ADR-0067)
|
|
811
|
+
* — name its secret, command(argv, { env: { NAME: secret("x") }, pin: true }), or drop `pin` ``.
|
|
812
|
+
*
|
|
813
|
+
* When it starts, it refuses an input `sha` that is not a full commit id: `command "<slot>"
|
|
814
|
+
* (pin): its input's sha is not a full commit id`.
|
|
815
|
+
*/
|
|
452
816
|
pin?: boolean;
|
|
453
817
|
};
|
|
454
818
|
|
|
455
819
|
/** The Open part a command's options state (ADR-0054): its secret, when its env holds
|
|
456
|
-
* `secret(open)` or its `.path`.
|
|
820
|
+
* `secret(open)` or its `.path`.
|
|
821
|
+
*
|
|
822
|
+
* @internal
|
|
823
|
+
*/
|
|
457
824
|
export type EnvOpenParts<Opt> = Opt extends { readonly env: infer E }
|
|
458
825
|
? OpenSecretIn<E extends Readonly<Record<string, infer V>> ? V : never>
|
|
459
826
|
: never;
|
|
460
827
|
|
|
828
|
+
/**
|
|
829
|
+
* Why an Effect kind cannot run in a Workspace with no Repo Slot (ADR-0051, ADR-0067), or undefined
|
|
830
|
+
* when it can: a `push` and a credentialed `command` run on a commit of a slot's branch, and a
|
|
831
|
+
* `worktree` makes a Worktree of a slot's clone, and that Workspace has none. The invoke and the
|
|
832
|
+
* `jr2 up` walk refuse with the same words.
|
|
833
|
+
*/
|
|
834
|
+
export function needsASlot(kind: string): string | undefined {
|
|
835
|
+
if (kind === "worktree") {
|
|
836
|
+
return 'a cut makes a Worktree of a Repo Slot, and there is none — declare one: repos: { app: "<url>" }';
|
|
837
|
+
}
|
|
838
|
+
if (kind === "push")
|
|
839
|
+
return 'a push publishes a Repo Slot\'s branch, and there is none — declare one: repos: { app: "<url>" }';
|
|
840
|
+
if (kind === "command") {
|
|
841
|
+
return (
|
|
842
|
+
"a command with an Effect secret runs against one commit of a Repo Slot's branch, and there is none — " +
|
|
843
|
+
"declare a Repo Slot, or name no secret in its env, and it runs in /work beside the Agent"
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
return undefined;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/** The work root of a Sandbox pod (ADR-0004): where a command runs in a Workspace with no Repo Slot. */
|
|
850
|
+
const WORK_ROOT = "/work";
|
|
851
|
+
|
|
461
852
|
const DEFAULT_TIMEOUT_MS = 10 * 60_000;
|
|
462
853
|
const DEFAULT_TAIL = 4096;
|
|
463
854
|
/** How long a re-asked request waits, at most, after a transport failure. */
|
|
@@ -478,7 +869,7 @@ export function commandWith<const Opt extends CommandOptions = CommandOptions>(
|
|
|
478
869
|
argv: typeof open,
|
|
479
870
|
options?: Opt,
|
|
480
871
|
effectorPorts?: EffectorPortFactory,
|
|
481
|
-
): EffectLogic<OpenCommandInput<Opt["pin"]>, CommandOutput> & OpenEffect<"argv" | EnvOpenParts<Opt>>;
|
|
872
|
+
): EffectLogic<OpenCommandInput<Opt["pin"], Opt>, CommandOutput> & OpenEffect<"argv" | EnvOpenParts<Opt>>;
|
|
482
873
|
export function commandWith<
|
|
483
874
|
const A extends readonly (string | Arg)[],
|
|
484
875
|
const Opt extends CommandOptions = CommandOptions,
|
|
@@ -487,12 +878,15 @@ export function commandWith<
|
|
|
487
878
|
argv: A,
|
|
488
879
|
options?: Opt,
|
|
489
880
|
effectorPorts?: EffectorPortFactory,
|
|
490
|
-
|
|
881
|
+
cut?: CommandParts["cut"],
|
|
882
|
+
): EffectLogic<CommandInput<A, Opt["pin"], Opt>, CommandOutput> & OpenOf<EnvOpenParts<Opt>>;
|
|
491
883
|
export function commandWith(
|
|
492
884
|
portFactory: ExecPortFactory,
|
|
493
885
|
argv: readonly (string | Arg)[] | typeof open,
|
|
494
886
|
options: CommandOptions = {},
|
|
495
887
|
effectorPorts: EffectorPortFactory = sandboxEffectorPort,
|
|
888
|
+
// `worktree()` alone passes this: an author's command is at most once.
|
|
889
|
+
cut?: CommandParts["cut"],
|
|
496
890
|
// The implementation of the overloads above: each one states the input type.
|
|
497
891
|
): EffectLogic<any, CommandOutput> {
|
|
498
892
|
if (argv === open) {
|
|
@@ -519,9 +913,9 @@ export function commandWith(
|
|
|
519
913
|
argv: [...argv],
|
|
520
914
|
timeoutMs: options.timeout === undefined ? DEFAULT_TIMEOUT_MS : parseDuration(options.timeout),
|
|
521
915
|
tail: options.tail ?? DEFAULT_TAIL,
|
|
522
|
-
...(options.slot !== undefined ? { slot: options.slot } : {}),
|
|
523
916
|
...(options.env !== undefined && Object.keys(options.env).length > 0 ? commandEnvParts(options.env) : {}),
|
|
524
917
|
...(options.pin === true ? { pin: true as const } : {}),
|
|
918
|
+
...(cut !== undefined ? { cut, idempotent: true as const } : {}),
|
|
525
919
|
};
|
|
526
920
|
if (parts.pin && (parts.secrets?.length ?? 0) === 0 && !parts.openSecret) {
|
|
527
921
|
// A command that names no secret runs in the Harness container, in the live Worktree: there
|
|
@@ -549,7 +943,7 @@ export function commandWith(
|
|
|
549
943
|
) as unknown as EffectLogic<unknown, CommandOutput>;
|
|
550
944
|
}
|
|
551
945
|
type I = unknown;
|
|
552
|
-
type O = CommandOutput;
|
|
946
|
+
type O = CommandOutput | WorktreeOutput;
|
|
553
947
|
const logic: ActorLogic<EffectSnapshot<O, I>, EventObject, I> = {
|
|
554
948
|
getInitialSnapshot: (_scope, input) => ({
|
|
555
949
|
status: "active",
|
|
@@ -583,12 +977,63 @@ export function commandWith(
|
|
|
583
977
|
},
|
|
584
978
|
getPersistedSnapshot: (snapshot) => snapshot,
|
|
585
979
|
};
|
|
586
|
-
return Object.assign(logic, { effect: parts })
|
|
980
|
+
return Object.assign(logic, { effect: parts }) as unknown as EffectLogic<any, CommandOutput>;
|
|
587
981
|
}
|
|
588
982
|
|
|
589
983
|
/** The slot's name as its parent invoked it — what the ledger and the feed call the Effect. */
|
|
590
984
|
const slotOf = (self: AnyActorRef): string => (typeof self.src === "string" ? self.src : self.id);
|
|
591
985
|
|
|
986
|
+
/** How many times a marked Effect goes out again after a Harness that lost it. Bounded, so that a
|
|
987
|
+
* Harness in a crash loop ends the wait: the Effect settles `unknown`, and the Machine routes. */
|
|
988
|
+
const RESENDS = 3;
|
|
989
|
+
|
|
990
|
+
/** What a cut's program answers: its last line. Anything else is a cut that did not end. */
|
|
991
|
+
function cutOutputOf(raw: CommandOutput): WorktreeOutput {
|
|
992
|
+
if (raw.outcome === "unknown") return raw as Unknown;
|
|
993
|
+
const tail = "tail" in raw && typeof raw.tail === "string" ? raw.tail : "";
|
|
994
|
+
if (raw.outcome === "exited" && raw.code === 0) {
|
|
995
|
+
try {
|
|
996
|
+
const answer = JSON.parse(tail.trim().split("\n").pop() ?? "") as { outcome?: unknown };
|
|
997
|
+
if (answer.outcome === "ready" || answer.outcome === "refused") return answer as WorktreeOutput;
|
|
998
|
+
} catch {
|
|
999
|
+
// Falls through: the program said no answer.
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return { outcome: "unknown", reason: `the cut ended ${raw.outcome} with no answer: ${tail.slice(-400)}` };
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/** Why a cut's input cannot go to the Harness, or undefined. The door checked a Chart's literals; a
|
|
1006
|
+
* reference, a pick param or a mapper can still say this. No exec, no fetch. */
|
|
1007
|
+
function cutFaultOf(input: unknown, ambient: AmbientHandles): string | undefined {
|
|
1008
|
+
const given = (input ?? {}) as { path?: unknown; branch?: unknown; detach?: unknown; from?: unknown };
|
|
1009
|
+
const fault = worktreePathFault(given.path, ambient, { cut: true });
|
|
1010
|
+
if (fault !== undefined) return fault;
|
|
1011
|
+
if ((given.branch === undefined) === (given.detach === undefined))
|
|
1012
|
+
return "a cut names a branch, or a commit in `detach`";
|
|
1013
|
+
if (given.detach !== undefined && given.from !== undefined) return "a detached cut takes no `from`";
|
|
1014
|
+
for (const key of ["branch", "detach", "from"] as const) {
|
|
1015
|
+
if (given[key] !== undefined && typeof given[key] !== "string") return `a cut's \`${key}\` is a string`;
|
|
1016
|
+
}
|
|
1017
|
+
return undefined;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/** The cut's one argument: its input, its id (a re-send answers the first `found`), its static
|
|
1021
|
+
* `expect`, and the base, sent each time, so that no checkout of an Agent's can move it. */
|
|
1022
|
+
function cutRequestOf(id: string, input: unknown, parts: CommandParts, ambient: AmbientHandles): string {
|
|
1023
|
+
const given = (input ?? {}) as { path: string; branch?: string; detach?: string; from?: string };
|
|
1024
|
+
const slot = given.path.split("/")[2]!;
|
|
1025
|
+
const base = ambient.bases?.[slot];
|
|
1026
|
+
return JSON.stringify({
|
|
1027
|
+
id,
|
|
1028
|
+
path: given.path,
|
|
1029
|
+
...(given.branch !== undefined ? { branch: given.branch } : {}),
|
|
1030
|
+
...(given.detach !== undefined ? { detach: given.detach } : {}),
|
|
1031
|
+
...(given.from !== undefined ? { from: given.from } : {}),
|
|
1032
|
+
...(given.branch !== undefined && parts.cut?.expect ? { expect: parts.cut.expect } : {}),
|
|
1033
|
+
...(base !== undefined ? { base } : {}),
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
|
|
592
1037
|
/** Run, re-attach to, or deliver one Effect, by its ledger row (see the module header). */
|
|
593
1038
|
async function drive<I, O>(
|
|
594
1039
|
self: AnyActorRef,
|
|
@@ -605,14 +1050,30 @@ async function drive<I, O>(
|
|
|
605
1050
|
const now = (): string => new Date().toISOString();
|
|
606
1051
|
const mark = (marker: Omit<EffectMarker, "kind" | "slot" | "effect">): void =>
|
|
607
1052
|
binding?.marker?.({ kind: "effect", slot, effect: id, ...marker });
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
1053
|
+
/** A settled row of an exec. `sandbox` is the row's own on a restore: the Workspace it ran in. */
|
|
1054
|
+
const settleIn =
|
|
1055
|
+
(sandbox: string | undefined) =>
|
|
1056
|
+
(raw: CommandOutput | WorktreeOutput): void => {
|
|
1057
|
+
if (signal.aborted || self.getSnapshot().status !== "active") return;
|
|
1058
|
+
const output = parts.cut && raw.outcome !== "refused" ? cutOutputOf(raw as CommandOutput) : raw;
|
|
1059
|
+
// The settled row first, saved at the write: a restart before the transition is saved delivers
|
|
1060
|
+
// this output, and runs nothing. A `ready` cut's row keeps its fields: a push reads them.
|
|
1061
|
+
const cutRow =
|
|
1062
|
+
parts.cut && output.outcome === "ready"
|
|
1063
|
+
? { sandbox, repo: output.slot, cut: { path: output.path, branch: output.branch } }
|
|
1064
|
+
: {};
|
|
1065
|
+
binding?.recordEffect?.(id, {
|
|
1066
|
+
state: "settled",
|
|
1067
|
+
kind: parts.kind,
|
|
1068
|
+
slot,
|
|
1069
|
+
...(cutRow as object),
|
|
1070
|
+
output,
|
|
1071
|
+
at: now(),
|
|
1072
|
+
});
|
|
1073
|
+
mark({ stage: "settled", output });
|
|
1074
|
+
self.send({ type: "jr2.effect.settle", output } satisfies Settle);
|
|
1075
|
+
};
|
|
1076
|
+
const settle = settleIn(undefined);
|
|
616
1077
|
|
|
617
1078
|
const row = binding?.effectRow?.(id);
|
|
618
1079
|
if (row?.state === "settled") {
|
|
@@ -628,13 +1089,14 @@ async function drive<I, O>(
|
|
|
628
1089
|
return;
|
|
629
1090
|
}
|
|
630
1091
|
if (row?.state === "intent" && row.kind === "command" && "endpoint" in row) {
|
|
631
|
-
// In flight at an Orchestrator restart: re-attach by id, or settle `unknown`. Never run again
|
|
1092
|
+
// In flight at an Orchestrator restart: re-attach by id, or settle `unknown`. Never run again,
|
|
1093
|
+
// unless the kit marked it: a cut goes on below, under the same id.
|
|
632
1094
|
const port = portFactory(row.endpoint, binding?.harnessBearer?.(row.placement));
|
|
633
1095
|
run.port = port;
|
|
634
1096
|
run.started = true;
|
|
635
1097
|
const first = await retrying(() => port.read(id, undefined, signal), signal);
|
|
636
1098
|
if (signal.aborted) return;
|
|
637
|
-
if (first === undefined) {
|
|
1099
|
+
if (first === undefined && !parts.idempotent) {
|
|
638
1100
|
const output: Unknown = {
|
|
639
1101
|
outcome: "unknown",
|
|
640
1102
|
reason: "it was running when the Orchestrator restarted, and the Harness no longer knows it; not run again",
|
|
@@ -642,8 +1104,11 @@ async function drive<I, O>(
|
|
|
642
1104
|
settle(output);
|
|
643
1105
|
return;
|
|
644
1106
|
}
|
|
645
|
-
|
|
646
|
-
|
|
1107
|
+
if (first !== undefined) {
|
|
1108
|
+
const lost = await follow(port, id, first, signal, mark, settleIn(row.placement), parts.idempotent === true);
|
|
1109
|
+
if (!lost) return;
|
|
1110
|
+
}
|
|
1111
|
+
mark({ stage: "started", text: "sent again: the Harness lost it, and a cut is idempotent" });
|
|
647
1112
|
}
|
|
648
1113
|
|
|
649
1114
|
// Fresh: nothing was sent. Where it runs is the enclosing Workspace's Harness — refused outside one.
|
|
@@ -654,9 +1119,43 @@ async function drive<I, O>(
|
|
|
654
1119
|
`workspace() — invoke it from a Workspace's body (ADR-0067)`,
|
|
655
1120
|
);
|
|
656
1121
|
}
|
|
1122
|
+
const settled = settleIn(ambient.sandbox);
|
|
1123
|
+
// A bad Worktree is a word an Agent or a Chart can say, so it is output (ADR-0067), in the form a
|
|
1124
|
+
// command that could not start has: exited 127, with the reason in the tail. A key that no one
|
|
1125
|
+
// wrote is a defect of the Machine: a throw, and `onError`.
|
|
1126
|
+
const worktree = (snapshot.input as { worktree?: unknown } | undefined | void)?.worktree;
|
|
1127
|
+
if (parts.cut) {
|
|
1128
|
+
if (Object.keys(ambient.repos).length === 0) {
|
|
1129
|
+
throw new Error(
|
|
1130
|
+
`worktree "${slot}" runs under a Workspace with no Repo Slot: ${needsASlot("worktree")} (ADR-0075)`,
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
const fault = cutFaultOf(snapshot.input, ambient);
|
|
1134
|
+
if (fault !== undefined) {
|
|
1135
|
+
settled({ outcome: "refused", reason: fault });
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
} else if (worktree === undefined) {
|
|
1139
|
+
if (Object.keys(ambient.repos).length > 0) {
|
|
1140
|
+
throw new Error(
|
|
1141
|
+
`command "${slot}" runs under a Workspace with Repo Slots (${Object.keys(ambient.repos).join(", ")}) and its ` +
|
|
1142
|
+
"input names no `worktree`: say which, a Worktree that a cut gave or `<slot>/default`",
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
} else {
|
|
1146
|
+
const fault = worktreePathFault(worktree, ambient);
|
|
1147
|
+
if (fault !== undefined) {
|
|
1148
|
+
settled({ outcome: "exited", code: 127, tail: `jr2: ${fault}; nothing ran\n` });
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
657
1152
|
const req: ExecRequest = {
|
|
658
|
-
argv:
|
|
659
|
-
|
|
1153
|
+
argv: parts.cut
|
|
1154
|
+
? [...(parts.argv as string[]), cutRequestOf(id, snapshot.input, parts, ambient)]
|
|
1155
|
+
: argvOf(slot, parts, snapshot.input),
|
|
1156
|
+
// Named, never left to the Harness: a spawn with no cwd runs in the image's own WORKDIR.
|
|
1157
|
+
cwd: parts.cut ? WORK_ROOT : ((worktree as string | undefined) ?? WORK_ROOT),
|
|
1158
|
+
...(!parts.cut && worktree !== undefined ? { root: true as const } : {}),
|
|
660
1159
|
timeoutMs: parts.timeoutMs,
|
|
661
1160
|
tail: parts.tail,
|
|
662
1161
|
};
|
|
@@ -674,26 +1173,38 @@ async function drive<I, O>(
|
|
|
674
1173
|
});
|
|
675
1174
|
run.started = true;
|
|
676
1175
|
mark({ stage: "started", text: req.argv.join(" ") });
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
if (
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
1176
|
+
// One pass for every Effect but the kit's cut: a Harness that restarted knows no id, and a start
|
|
1177
|
+
// that may have reached it is never sent again. A cut goes out again: it is idempotent.
|
|
1178
|
+
const passes = parts.idempotent ? RESENDS + 1 : 1;
|
|
1179
|
+
for (let pass = 1; pass <= passes; pass++) {
|
|
1180
|
+
const sent = await startOnce(port, id, req, signal);
|
|
1181
|
+
if (signal.aborted) return;
|
|
1182
|
+
const first = await retrying(() => port.read(id, 0, signal), signal);
|
|
1183
|
+
if (signal.aborted) return;
|
|
1184
|
+
const last = pass === passes;
|
|
1185
|
+
if (first === undefined) {
|
|
1186
|
+
if (!last) {
|
|
1187
|
+
mark({ stage: "started", text: "sent again: the Harness restarted, and a cut is idempotent" });
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
if (sent === "uncertain" && !parts.idempotent) {
|
|
1191
|
+
// The start may still be on its way to the Harness that answers now: a kill keeps it from
|
|
1192
|
+
// running there (exec.ts keeps an unknown id killed). Best effort — `unknown` either way.
|
|
1193
|
+
void killUntilKept(port, id, parts.timeoutMs).catch(() => {});
|
|
1194
|
+
}
|
|
1195
|
+
settled({
|
|
1196
|
+
outcome: "unknown",
|
|
1197
|
+
reason:
|
|
1198
|
+
sent === "uncertain"
|
|
1199
|
+
? "its start may have reached the Harness, which no longer knows it (it restarted); not run again"
|
|
1200
|
+
: "the Harness lost it after it started (it restarted); not run again",
|
|
1201
|
+
});
|
|
1202
|
+
return;
|
|
686
1203
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
sent === "uncertain"
|
|
691
|
-
? "its start may have reached the Harness, which no longer knows it (it restarted); not run again"
|
|
692
|
-
: "the Harness lost it after it started (it restarted); not run again",
|
|
693
|
-
});
|
|
694
|
-
return;
|
|
1204
|
+
const lost = await follow(port, id, first, signal, mark, settled, !last);
|
|
1205
|
+
if (!lost) return;
|
|
1206
|
+
mark({ stage: "started", text: "sent again: the Harness lost it, and a cut is idempotent" });
|
|
695
1207
|
}
|
|
696
|
-
await follow(port, id, first, signal, mark, settle);
|
|
697
1208
|
}
|
|
698
1209
|
|
|
699
1210
|
/**
|
|
@@ -723,7 +1234,8 @@ async function startOnce(
|
|
|
723
1234
|
}
|
|
724
1235
|
}
|
|
725
1236
|
|
|
726
|
-
/** Read the exec to its end: each chunk of stream to the feed, the output to the Machine.
|
|
1237
|
+
/** Read the exec to its end: each chunk of stream to the feed, the output to the Machine. With
|
|
1238
|
+
* `again`, a Harness that lost it settles nothing: the answer is true, and the caller sends it. */
|
|
727
1239
|
async function follow(
|
|
728
1240
|
port: ExecPort,
|
|
729
1241
|
id: string,
|
|
@@ -731,22 +1243,24 @@ async function follow(
|
|
|
731
1243
|
signal: AbortSignal,
|
|
732
1244
|
mark: (marker: Omit<EffectMarker, "kind" | "slot" | "effect">) => void,
|
|
733
1245
|
settle: (output: CommandOutput) => void,
|
|
734
|
-
|
|
1246
|
+
again = false,
|
|
1247
|
+
): Promise<boolean> {
|
|
735
1248
|
let read: ExecRead | undefined = first;
|
|
736
1249
|
for (;;) {
|
|
737
1250
|
if (read === undefined) {
|
|
1251
|
+
if (again) return true;
|
|
738
1252
|
settle({ outcome: "unknown", reason: "the Harness lost it while it ran (it restarted); not run again" });
|
|
739
|
-
return;
|
|
1253
|
+
return false;
|
|
740
1254
|
}
|
|
741
1255
|
if (read.stream) mark({ stage: "output", text: read.stream });
|
|
742
1256
|
if (read.state === "settled" && read.output) {
|
|
743
1257
|
settle(read.output);
|
|
744
1258
|
void port.forget(id).catch(() => {});
|
|
745
|
-
return;
|
|
1259
|
+
return false;
|
|
746
1260
|
}
|
|
747
1261
|
const offset: number = read.next;
|
|
748
1262
|
read = await retrying(() => port.read(id, offset, signal), signal);
|
|
749
|
-
if (signal.aborted) return;
|
|
1263
|
+
if (signal.aborted) return false;
|
|
750
1264
|
}
|
|
751
1265
|
}
|
|
752
1266
|
|
|
@@ -851,28 +1365,27 @@ function argvOf(slot: string, parts: CommandParts, input: unknown): string[] {
|
|
|
851
1365
|
}
|
|
852
1366
|
|
|
853
1367
|
/**
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
*
|
|
1368
|
+
* Why `path` is not a Worktree of this Workspace, or undefined when it is. Exactly
|
|
1369
|
+
* `/work/<slot>/<name>`, of one of its Repo Slots, one segment that starts with neither `.` nor `-`.
|
|
1370
|
+
* `default` is a Worktree (the clone), and with `cut` it is not one a cut makes.
|
|
857
1371
|
*/
|
|
858
|
-
function
|
|
1372
|
+
export function worktreePathFault(
|
|
1373
|
+
path: unknown,
|
|
1374
|
+
ambient: AmbientHandles,
|
|
1375
|
+
opts: { cut?: boolean } = {},
|
|
1376
|
+
): string | undefined {
|
|
1377
|
+
const match = typeof path === "string" ? path.match(/^\/work\/([^/]+)\/([^/]+)$/) : null;
|
|
1378
|
+
if (match === null) {
|
|
1379
|
+
return `${JSON.stringify(path)} is not a Worktree path: /work/<slot>/<name>, with one segment in <name>`;
|
|
1380
|
+
}
|
|
1381
|
+
const [, slot, name] = match as unknown as [string, string, string];
|
|
1382
|
+
if (name.startsWith(".") || name.startsWith("-")) return `"${path}": a Worktree's name starts with neither . nor -`;
|
|
1383
|
+
if (opts.cut && name === "default") return `"${path}" is the clone: it stays as the attach left it; cut a sibling`;
|
|
859
1384
|
const slots = Object.keys(ambient.repos);
|
|
860
|
-
if (
|
|
861
|
-
|
|
862
|
-
if (cwd === undefined) {
|
|
863
|
-
throw new Error(
|
|
864
|
-
`command "${slot}" names Repo Slot "${parts.slot}", which its Workspace does not carry ` +
|
|
865
|
-
`(${slots.join(", ") || "none"})`,
|
|
866
|
-
);
|
|
867
|
-
}
|
|
868
|
-
return { cwd };
|
|
1385
|
+
if (!slots.includes(slot)) {
|
|
1386
|
+
return `"${path}" names Repo Slot "${slot}", which its Workspace does not carry (${slots.join(", ") || "none"})`;
|
|
869
1387
|
}
|
|
870
|
-
|
|
871
|
-
if (slots.length === 1) return { cwd: ambient.repos[slots[0]!]! };
|
|
872
|
-
throw new Error(
|
|
873
|
-
`command "${slot}" runs under a Workspace carrying more than one Repo Slot (${slots.join(", ")}) and names ` +
|
|
874
|
-
`none — say which Worktree it runs in: command(argv, { slot: "<slot>" }) (ADR-0051)`,
|
|
875
|
-
);
|
|
1388
|
+
return undefined;
|
|
876
1389
|
}
|
|
877
1390
|
|
|
878
1391
|
/** Ask again on a transport failure, with backoff, until it answers or the Effect stops. For a
|