@jr2/orchestrator 0.5.1 → 0.7.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/console/components/app.ts +55 -15
- package/console/components/bottom-drawer.ts +259 -0
- package/console/components/drawer.ts +14 -6
- package/console/components/fleet.ts +36 -13
- package/console/components/machine-pane.ts +61 -5
- package/console/components/nav.ts +54 -8
- package/console/conversation.ts +161 -0
- package/console/login.ts +123 -0
- package/console/main.ts +600 -60
- package/console/store.ts +611 -64
- package/console/style.css +231 -0
- package/docs/agents-block.md +2 -2
- package/docs/concepts/agents-and-turns.md +142 -29
- package/docs/concepts/architecture.md +60 -34
- package/docs/concepts/callers-and-hooks.md +205 -0
- package/docs/concepts/charts.md +13 -8
- package/docs/concepts/composition.md +10 -1
- package/docs/concepts/converge.md +7 -2
- package/docs/concepts/durable-runs.md +43 -16
- package/docs/concepts/effects.md +133 -50
- package/docs/concepts/gates.md +95 -25
- package/docs/concepts/machines-and-workflows.md +14 -4
- package/docs/concepts/pools-and-sources.md +193 -17
- package/docs/concepts/secrets-and-trust.md +186 -62
- package/docs/concepts/the-menu.md +14 -0
- package/docs/concepts/workspaces.md +135 -56
- package/docs/get-started/a-model-decides.md +14 -13
- package/docs/get-started/an-agent-in-a-sandbox.md +46 -35
- package/docs/get-started/connect-a-model.md +1 -1
- package/docs/get-started/create-an-instance.md +17 -14
- package/docs/get-started/have-your-assistant-write-a-machine.md +111 -40
- package/docs/get-started/install.md +4 -0
- package/docs/guides/add-tools-to-the-sandbox.md +72 -15
- package/docs/guides/answer-a-gate-from-a-forge.md +304 -0
- package/docs/guides/answer-a-gate.md +74 -37
- package/docs/guides/bind-open-parts.md +29 -12
- package/docs/guides/call-an-mcp-tool-from-a-machine.md +572 -0
- package/docs/guides/cancel-a-run.md +26 -16
- package/docs/guides/cut-a-worktree.md +560 -0
- package/docs/guides/fix-a-failed-converge.md +19 -7
- package/docs/guides/give-a-caller-a-scope.md +203 -0
- package/docs/guides/give-an-agent-mcp-tools.md +422 -0
- package/docs/guides/iterate-on-a-machine-on-kind.md +10 -9
- package/docs/guides/let-an-agent-write-a-chart.md +14 -7
- package/docs/guides/look-inside-a-sandbox.md +115 -42
- package/docs/guides/move-from-kind-to-a-shared-cluster.md +16 -0
- package/docs/guides/open-the-console.md +63 -14
- package/docs/guides/push-and-sign-a-branch.md +67 -21
- package/docs/guides/read-a-conversation.md +225 -0
- package/docs/guides/run-a-chart-by-name.md +17 -16
- package/docs/guides/run-a-command-or-call-a-url.md +84 -41
- package/docs/guides/run-one-worker-per-item.md +34 -27
- package/docs/guides/set-an-agents-access-and-dials.md +69 -22
- package/docs/guides/size-a-workspace.md +19 -6
- package/docs/guides/start-a-run-for-each-alert.md +290 -0
- package/docs/guides/start-a-run-from-a-run.md +18 -16
- package/docs/guides/start-a-run.md +24 -18
- package/docs/guides/steer-an-agent.md +332 -0
- package/docs/guides/use-a-hosted-provider.md +7 -1
- package/docs/guides/watch-a-run.md +146 -117
- package/docs/guides/write-a-hook-kind.md +238 -0
- package/docs/guides/write-a-tool-as-an-mcp-server.md +351 -0
- package/docs/guides/write-a-workflow.md +18 -8
- package/docs/llms.txt +110 -23
- package/docs/reference/authoring-surface/agent.md +35 -12
- package/docs/reference/authoring-surface/answer.md +20 -4
- package/docs/reference/authoring-surface/arg.md +28 -6
- package/docs/reference/authoring-surface/basic.md +1 -1
- package/docs/reference/authoring-surface/bearer.md +1 -1
- package/docs/reference/authoring-surface/branchname.md +15 -0
- package/docs/reference/authoring-surface/carry.md +23 -0
- package/docs/reference/authoring-surface/chartschema.md +1 -1
- package/docs/reference/authoring-surface/chartschemafor.md +1 -1
- package/docs/reference/authoring-surface/command.md +37 -22
- package/docs/reference/authoring-surface/customize.md +4 -1
- package/docs/reference/authoring-surface/defineconfig.md +1 -1
- package/docs/reference/authoring-surface/defineevent.md +1 -1
- package/docs/reference/authoring-surface/dispatch.md +5 -4
- package/docs/reference/authoring-surface/fail.md +18 -5
- package/docs/reference/authoring-surface/file.md +37 -17
- package/docs/reference/authoring-surface/frompick.md +1 -1
- package/docs/reference/authoring-surface/gate.md +118 -0
- package/docs/reference/authoring-surface/group-agents.md +15 -0
- package/docs/reference/authoring-surface/group-charts.md +16 -0
- package/docs/reference/authoring-surface/group-composition.md +25 -0
- package/docs/reference/authoring-surface/group-config.md +17 -0
- package/docs/reference/authoring-surface/group-effects.md +48 -0
- package/docs/reference/authoring-surface/group-failure.md +13 -0
- package/docs/reference/authoring-surface/group-gates.md +16 -0
- package/docs/reference/authoring-surface/group-machine-and-events.md +15 -0
- package/docs/reference/authoring-surface/group-steering.md +12 -0
- package/docs/reference/authoring-surface/group-types-these-signatures-name.md +137 -0
- package/docs/reference/authoring-surface/group-workspaces-and-repos.md +15 -0
- package/docs/reference/authoring-surface/http.md +109 -9
- package/docs/reference/authoring-surface/isfailure.md +1 -1
- package/docs/reference/authoring-surface/isready.md +14 -0
- package/docs/reference/authoring-surface/jr2setup.md +14 -4
- package/docs/reference/authoring-surface/mcp.md +107 -0
- package/docs/reference/authoring-surface/open.md +41 -8
- package/docs/reference/authoring-surface/openbody.md +1 -1
- package/docs/reference/authoring-surface/paramsof.md +23 -5
- package/docs/reference/authoring-surface/pool.md +79 -11
- package/docs/reference/authoring-surface/push.md +31 -13
- package/docs/reference/authoring-surface/readyof.md +21 -0
- package/docs/reference/authoring-surface/reread.md +19 -0
- package/docs/reference/authoring-surface/secret.md +26 -5
- package/docs/reference/authoring-surface/sentby.md +1 -1
- package/docs/reference/authoring-surface/skip.md +22 -0
- package/docs/reference/authoring-surface/source.md +59 -4
- package/docs/reference/authoring-surface/steer.md +93 -0
- package/docs/reference/authoring-surface/text.md +1 -1
- package/docs/reference/authoring-surface/type-agentdefinition.md +16 -3
- package/docs/reference/authoring-surface/type-agentmcpserver.md +54 -0
- package/docs/reference/authoring-surface/type-agentslots.md +4 -1
- 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-bindactors.md +14 -0
- package/docs/reference/authoring-surface/type-binding.md +6 -7
- package/docs/reference/authoring-surface/type-caller.md +40 -0
- package/docs/reference/authoring-surface/type-callerrecord.md +32 -0
- package/docs/reference/authoring-surface/type-carry.md +19 -0
- package/docs/reference/authoring-surface/type-chainlink.md +3 -3
- package/docs/reference/authoring-surface/type-commandinput.md +4 -3
- package/docs/reference/authoring-surface/type-commandoptions.md +0 -22
- package/docs/reference/authoring-surface/type-commandoutput.md +23 -5
- package/docs/reference/authoring-surface/type-customize.md +20 -3
- package/docs/reference/authoring-surface/type-delivered.md +17 -0
- package/docs/reference/authoring-surface/type-deliveredevent.md +18 -0
- package/docs/reference/authoring-surface/type-dispatchdefinition.md +4 -3
- package/docs/reference/authoring-surface/type-dispatchoutput.md +9 -2
- package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
- package/docs/reference/authoring-surface/type-effectbinding.md +12 -2
- package/docs/reference/authoring-surface/type-filebody.md +4 -8
- package/docs/reference/authoring-surface/type-framepath.md +14 -0
- package/docs/reference/authoring-surface/type-gatebinding.md +15 -0
- package/docs/reference/authoring-surface/type-gatehooks.md +12 -0
- package/docs/reference/authoring-surface/type-gatelook.md +37 -0
- package/docs/reference/authoring-surface/type-gateoutcome.md +32 -0
- package/docs/reference/authoring-surface/type-gatereply.md +19 -0
- package/docs/reference/authoring-surface/type-gateslot.md +21 -0
- package/docs/reference/authoring-surface/type-gateslotinput.md +13 -0
- package/docs/reference/authoring-surface/type-gateslotparts.md +18 -0
- package/docs/reference/authoring-surface/type-gateslots.md +12 -0
- package/docs/reference/authoring-surface/type-gatetypesat.md +13 -0
- package/docs/reference/authoring-surface/type-hostinjectedinput.md +3 -5
- package/docs/reference/authoring-surface/type-httpdefinition.md +6 -3
- package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
- package/docs/reference/authoring-surface/type-httpoutput.md +8 -2
- package/docs/reference/authoring-surface/type-jr2config.md +37 -2
- package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
- package/docs/reference/authoring-surface/type-lends.md +5 -1
- package/docs/reference/authoring-surface/type-lookoutput.md +14 -0
- package/docs/reference/authoring-surface/type-mcpcalloutput.md +78 -0
- package/docs/reference/authoring-surface/type-mcpnotreturned.md +42 -0
- package/docs/reference/authoring-surface/type-mcpoptions.md +47 -0
- package/docs/reference/authoring-surface/type-mcpoutput.md +50 -0
- package/docs/reference/authoring-surface/type-mcpreadoutput.md +53 -0
- package/docs/reference/authoring-surface/type-mcpremoteserver.md +50 -0
- package/docs/reference/authoring-surface/type-mcpserver.md +41 -0
- package/docs/reference/authoring-surface/type-mcpstdioserver.md +42 -0
- package/docs/reference/authoring-surface/type-namedsecret.md +4 -4
- package/docs/reference/authoring-surface/type-namestart.md +77 -0
- package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +1 -6
- package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
- package/docs/reference/authoring-surface/type-openbodydeclaration.md +1 -1
- package/docs/reference/authoring-surface/type-openbodyoutput.md +1 -1
- package/docs/reference/authoring-surface/type-opencommandinput.md +7 -2
- package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +1 -6
- package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +5 -10
- package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
- package/docs/reference/authoring-surface/type-poolspec.md +11 -6
- package/docs/reference/authoring-surface/type-pushinput.md +22 -3
- package/docs/reference/authoring-surface/type-pushoutput.md +33 -8
- package/docs/reference/authoring-surface/type-pushsourcespec.md +38 -0
- package/docs/reference/authoring-surface/type-readsourcespec.md +47 -0
- package/docs/reference/authoring-surface/type-reposlot.md +6 -6
- package/docs/reference/authoring-surface/type-reread.md +18 -0
- package/docs/reference/authoring-surface/type-runsconfig.md +41 -0
- package/docs/reference/authoring-surface/type-sandboxoptions.md +11 -4
- package/docs/reference/authoring-surface/type-sender.md +26 -1
- package/docs/reference/authoring-surface/type-sendercallerfield.md +14 -0
- package/docs/reference/authoring-surface/type-size.md +1 -1
- package/docs/reference/authoring-surface/type-skip.md +19 -0
- package/docs/reference/authoring-surface/type-slothandles.md +5 -13
- package/docs/reference/authoring-surface/type-sourcehooks.md +23 -0
- package/docs/reference/authoring-surface/type-sourcein.md +25 -0
- package/docs/reference/authoring-surface/type-sourcereply.md +18 -0
- package/docs/reference/authoring-surface/type-sourcespec.md +26 -17
- package/docs/reference/authoring-surface/type-sourceverdict.md +49 -0
- package/docs/reference/authoring-surface/type-standingpoolmachine.md +12 -0
- package/docs/reference/authoring-surface/type-standingpoolspec.md +51 -0
- package/docs/reference/authoring-surface/type-steerargs.md +20 -0
- package/docs/reference/authoring-surface/type-thinkinglevel.md +1 -1
- package/docs/reference/authoring-surface/type-usercontainer.md +1 -1
- package/docs/reference/authoring-surface/type-workspaced.md +30 -9
- package/docs/reference/authoring-surface/type-workspacehandles.md +5 -6
- package/docs/reference/authoring-surface/type-workspaceoptions.md +1 -13
- 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 +66 -38
- package/docs/reference/authoring-surface/worktree.md +71 -0
- package/docs/reference/authoring-surface.md +88 -147
- package/docs/reference/cli.md +413 -74
- package/docs/reference/config.md +248 -6
- package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +10 -9
- package/docs/reference/decisions/0002-duplex-streaming-actor.md +81 -14
- package/docs/reference/decisions/0004-worktree-provisioning-model.md +27 -21
- package/docs/reference/decisions/0005-sandbox-pod-composition.md +39 -25
- package/docs/reference/decisions/0006-agent-control-surface.md +6 -3
- package/docs/reference/decisions/0007-durable-machine-state.md +1 -1
- package/docs/reference/decisions/0009-cli-and-instance-interface.md +150 -49
- package/docs/reference/decisions/0010-bdd-acceptance-tests.md +5 -3
- package/docs/reference/decisions/0011-workflow-defined-events.md +256 -12
- package/docs/reference/decisions/0012-workspace-wrapper-machine.md +39 -32
- package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +54 -30
- package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +53 -27
- package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +26 -16
- package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +9 -7
- package/docs/reference/decisions/0017-pool-over-a-source-port.md +226 -10
- package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +15 -7
- package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +53 -27
- package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +16 -2
- package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +4 -4
- package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +124 -43
- package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +36 -38
- package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +10 -5
- package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +16 -7
- package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +36 -19
- package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +18 -10
- package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +3 -2
- package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +23 -15
- package/docs/reference/decisions/0032-the-console-unlocks-with-a-callers-credential.md +101 -0
- package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +22 -22
- package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +6 -6
- package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +13 -12
- package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +23 -23
- package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +6 -1
- package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +57 -46
- package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +5 -0
- package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +20 -8
- package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +55 -40
- package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +14 -13
- package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +31 -24
- package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +18 -10
- package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +137 -86
- package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +165 -91
- package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +7 -7
- package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +2 -2
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md +109 -0
- package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +53 -23
- package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +221 -153
- package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +161 -96
- package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +32 -9
- package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +48 -33
- package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +56 -30
- package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +35 -25
- package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +1 -1
- package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +296 -0
- package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +347 -0
- package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
- package/docs/reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md +285 -0
- package/docs/reference/decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md +198 -0
- package/docs/reference/decisions.md +9 -4
- package/docs/reference/glossary.md +138 -45
- package/docs/reference/hooks.md +1077 -0
- package/docs/reference/http-api.md +988 -123
- package/docs/reference/machines.md +0 -1
- package/docs/why.md +7 -5
- package/package.json +10 -3
- package/src/actor.ts +194 -42
- package/src/agent.ts +56 -6
- package/src/ambient.ts +3 -5
- package/src/assembly-checks.ts +530 -4
- package/src/callers.ts +245 -0
- package/src/chart.ts +238 -26
- package/src/charts.ts +9 -2
- package/src/config.ts +566 -7
- package/src/conversations.ts +453 -0
- package/src/custodian.lua +17 -5
- package/src/custodian.ts +257 -77
- package/src/customize.ts +154 -7
- package/src/dispatch-effect.ts +142 -26
- package/src/durability.ts +2 -0
- package/src/duration.ts +20 -0
- package/src/effect-checks.ts +52 -4
- package/src/effect.ts +401 -132
- package/src/effector-wire.ts +278 -30
- package/src/fail.ts +17 -4
- package/src/gate-brand.ts +16 -0
- package/src/gate.ts +634 -48
- package/src/harness-client.ts +84 -35
- package/src/harness-env.ts +33 -0
- package/src/held-secrets.ts +197 -39
- package/src/holds.ts +5 -2
- package/src/hook-kinds/discord.ts +92 -0
- package/src/hook-kinds/door.ts +107 -0
- package/src/hook-kinds/github.ts +89 -0
- package/src/hook-kinds/gitlab.ts +72 -0
- package/src/hook-kinds/hook.ts +7 -0
- package/src/hook-kinds/index.ts +465 -0
- package/src/hook-kinds/slack.ts +151 -0
- package/src/hook-kinds/webhook.ts +113 -0
- package/src/hooks.ts +506 -0
- package/src/http-effect.ts +164 -70
- package/src/http.ts +933 -99
- package/src/index.ts +30 -5
- package/src/instance.ts +196 -26
- package/src/internal.ts +23 -3
- package/src/kube-client.ts +6 -3
- package/src/machine-doc.ts +29 -5
- package/src/mcp-effect.ts +424 -0
- package/src/model-keys.ts +24 -0
- package/src/mount-digest.ts +41 -7
- package/src/names.ts +82 -7
- package/src/notices.ts +576 -59
- package/src/open-body.ts +110 -30
- package/src/open-effect.ts +17 -4
- package/src/open.ts +40 -7
- package/src/parts.ts +125 -35
- package/src/party.ts +35 -1
- package/src/pick.ts +43 -9
- package/src/pool.ts +1172 -172
- package/src/registration.ts +249 -17
- package/src/repo-identity.ts +7 -3
- package/src/repos.ts +20 -2
- package/src/run-host.ts +1614 -190
- package/src/run-list.ts +191 -0
- package/src/sandbox-effect.ts +217 -66
- package/src/sandbox-kube.ts +116 -21
- package/src/secret-placement.ts +248 -35
- package/src/server.ts +53 -4
- package/src/setup.ts +158 -38
- package/src/snapshot-store.ts +565 -48
- package/src/steer.ts +111 -0
- package/src/stub-harness.ts +6 -2
- package/src/tokens.ts +362 -46
- package/src/vocabulary.ts +23 -4
- package/src/wire.ts +27 -10
- package/src/workspace.ts +314 -259
- package/src/worktree.ts +249 -0
- package/docs/reference/authoring-surface/type-noslotspec.md +0 -26
- package/docs/reference/authoring-surface/type-workspacespec.md +0 -37
- package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +0 -61
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +0 -65
package/src/effect.ts
CHANGED
|
@@ -39,11 +39,12 @@ import { randomUUID } from "node:crypto";
|
|
|
39
39
|
import type { ActorLogic, AnyActorRef, EventObject, Snapshot } from "xstate";
|
|
40
40
|
import { ambientHandlesFor, type AmbientHandles } from "./ambient.ts";
|
|
41
41
|
import { boundRun, type AnyActorSystem } from "./registration.ts";
|
|
42
|
-
import type { HttpMethod, HttpOutput, TextPart, WireAuth } from "./effector-wire.ts";
|
|
42
|
+
import type { HttpMethod, HttpOutput, McpOutput, TextPart, WireAuth } from "./effector-wire.ts";
|
|
43
43
|
import type { ExecOutcome, ExecRead, ExecRequest, SyncAnswer } from "./wire.ts";
|
|
44
44
|
import { bindOpenSecretIn, type EffectorPortFactory, type OpenSecretIn } from "./http-effect.ts";
|
|
45
45
|
import { commandEnvParts, credentialedCommandWith, sandboxEffectorPort, type CommandEnv } from "./sandbox-effect.ts";
|
|
46
46
|
import { open } from "./open.ts";
|
|
47
|
+
import type { AnyWorktreePath, WorktreeOutput } from "./worktree.ts";
|
|
47
48
|
import {
|
|
48
49
|
effectBindingShape,
|
|
49
50
|
openEffectLogic,
|
|
@@ -76,30 +77,52 @@ export type Arg<N extends string = string> = { readonly arg: N };
|
|
|
76
77
|
*
|
|
77
78
|
* @example
|
|
78
79
|
* ```ts
|
|
79
|
-
* import {
|
|
80
|
+
* import {
|
|
81
|
+
* arg,
|
|
82
|
+
* command,
|
|
83
|
+
* fail,
|
|
84
|
+
* isReady,
|
|
85
|
+
* jr2Setup,
|
|
86
|
+
* workspace,
|
|
87
|
+
* worktree,
|
|
88
|
+
* type Workspaced,
|
|
89
|
+
* } from "@jr2/orchestrator";
|
|
80
90
|
*
|
|
81
91
|
* type Input = Workspaced<{ file: string }, "app">;
|
|
82
92
|
*
|
|
93
|
+
* const lintTree = (context: Input) => `${context.workspace.repos.app}/lint` as const;
|
|
94
|
+
*
|
|
83
95
|
* const body = jr2Setup({
|
|
84
96
|
* types: {} as { context: Input; input: Input },
|
|
85
|
-
* actors: { lint: command(["npx", "eslint", arg("file")]), fail: fail() },
|
|
97
|
+
* actors: { cut: worktree(), lint: command(["npx", "eslint", arg("file")]), fail: fail() },
|
|
86
98
|
* }).createMachine({
|
|
87
99
|
* id: "lint",
|
|
88
100
|
* context: ({ input }) => input,
|
|
89
101
|
* on: { "workspace.lost": ".lost" },
|
|
90
|
-
* initial: "
|
|
102
|
+
* initial: "cutting",
|
|
91
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
|
+
* },
|
|
92
111
|
* lint: {
|
|
93
|
-
* invoke: {
|
|
112
|
+
* invoke: {
|
|
113
|
+
* src: "lint",
|
|
114
|
+
* input: ({ context }) => ({ file: context.file, worktree: lintTree(context) }),
|
|
115
|
+
* onDone: "done",
|
|
116
|
+
* },
|
|
94
117
|
* },
|
|
95
118
|
* done: { type: "final" },
|
|
119
|
+
* notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
|
|
96
120
|
* lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
|
|
97
121
|
* },
|
|
98
122
|
* });
|
|
99
123
|
*
|
|
100
124
|
* export const machine = workspace(body, {
|
|
101
125
|
* repos: { app: { url: "https://github.com/you/app.git" } },
|
|
102
|
-
* spec: () => ({ branch: "lint" }),
|
|
103
126
|
* });
|
|
104
127
|
* ```
|
|
105
128
|
*
|
|
@@ -109,6 +132,13 @@ export type Arg<N extends string = string> = { readonly arg: N };
|
|
|
109
132
|
*/
|
|
110
133
|
export const arg = <const N extends string>(name: N): Arg<N> => ({ arg: name });
|
|
111
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
|
+
|
|
112
142
|
/** A `command` Effect's plain data. */
|
|
113
143
|
export type CommandParts = {
|
|
114
144
|
readonly kind: "command";
|
|
@@ -124,8 +154,12 @@ export type CommandParts = {
|
|
|
124
154
|
/** A credentialed command's `pin`: its input names the commit (`{ sha }`, an approved one) in
|
|
125
155
|
* place of the Agent's branch tip (ADR-0067). */
|
|
126
156
|
readonly pin?: true;
|
|
127
|
-
/** The
|
|
128
|
-
|
|
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;
|
|
129
163
|
readonly timeoutMs: number;
|
|
130
164
|
/** Characters of the stream's end kept in the output. The full stream goes to the feed. */
|
|
131
165
|
readonly tail: number;
|
|
@@ -156,16 +190,15 @@ export type HttpParts = {
|
|
|
156
190
|
/** Marked by the author: a restore that cannot find the Effect sends it again. */
|
|
157
191
|
readonly idempotent: boolean;
|
|
158
192
|
/** The body is a Worktree file (`body: file(...)`, ADR-0067): the Effect runs in the Sandbox
|
|
159
|
-
* Effector of the Workspace whose body invokes it. `
|
|
160
|
-
* the file — needed only where the Workspace carries several. Absent: a json, text or no body, in
|
|
193
|
+
* Effector of the Workspace whose body invokes it. The input's `worktree` holds the file. Absent: a json, text or no body, in
|
|
161
194
|
* the Instance Effector. */
|
|
162
|
-
readonly file?: { readonly
|
|
195
|
+
readonly file?: { readonly contentType?: string };
|
|
163
196
|
};
|
|
164
197
|
|
|
165
198
|
/**
|
|
166
|
-
* 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
|
|
167
200
|
* Binding. `sign`, `squash`, `force` are the author's; `pin` says the Effect's input names the
|
|
168
|
-
* commit (an approved one) instead of the
|
|
201
|
+
* commit (an approved one) instead of the branch's tip.
|
|
169
202
|
*/
|
|
170
203
|
export type PushParts = {
|
|
171
204
|
readonly kind: "push";
|
|
@@ -207,6 +240,51 @@ export type DispatchParts = {
|
|
|
207
240
|
readonly openSecret: boolean;
|
|
208
241
|
};
|
|
209
242
|
|
|
243
|
+
/** What an `mcp` Effect asks of its server (ADR-0079): call a tool, read a resource, or get a
|
|
244
|
+
* prompt. */
|
|
245
|
+
export type McpOp = "call" | "read" | "prompt";
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* An `mcp` Effect's plain data (ADR-0067, ADR-0079): what `jr2 up` and the Machine doc read — the
|
|
249
|
+
* operation, the server by its name in the `mcp` block of `jr2.config.ts`, the tool (a call), the
|
|
250
|
+
* resource's uri (a read) or the prompt's name (a prompt), the input's keys, the author's timeout,
|
|
251
|
+
* the bound on the output, and whether a restore may send it again. The server's url and secret
|
|
252
|
+
* are the config's, never a part: `jr2 up` reads them from there, and the Instance Effector holds
|
|
253
|
+
* them in its `effector.json` and its secrets.
|
|
254
|
+
*/
|
|
255
|
+
export type McpParts = {
|
|
256
|
+
readonly kind: "mcp";
|
|
257
|
+
readonly op: McpOp;
|
|
258
|
+
readonly server: string;
|
|
259
|
+
/** `mcp.call`'s tool. */
|
|
260
|
+
readonly tool?: string;
|
|
261
|
+
/** `mcp.read`'s resource uri. */
|
|
262
|
+
readonly uri?: string;
|
|
263
|
+
/** `mcp.prompt`'s prompt name. */
|
|
264
|
+
readonly prompt?: string;
|
|
265
|
+
/** The keys of the input schema: what a pick param may fill (ADR-0067's load walk reads them). */
|
|
266
|
+
readonly inputKeys: readonly string[];
|
|
267
|
+
/** The author's timeout, in milliseconds. Absent: the server's `timeout` in `jr2.config.ts`. */
|
|
268
|
+
readonly timeoutMs?: number;
|
|
269
|
+
/** The most bytes of `text`, and of `structured` as JSON, in the output. */
|
|
270
|
+
readonly maxBody: number;
|
|
271
|
+
/** Marked by the author: a restore that cannot find the Effect sends it again. The server's
|
|
272
|
+
* `idempotentHint` is not read (ADR-0079). */
|
|
273
|
+
readonly idempotent: boolean;
|
|
274
|
+
/** Always empty: an `mcp` Effect names no secret of its own. Its server's secret is placed in
|
|
275
|
+
* the Instance Effector by `jr2 up`, from the config (ADR-0079). */
|
|
276
|
+
readonly secrets: readonly string[];
|
|
277
|
+
/** Never: the server, not a `secret(open)`, is what a package leaves Open (ADR-0054). */
|
|
278
|
+
readonly openSecret?: never;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
/** An `mcp` Effect whose server or tool a package left Open (ADR-0054, ADR-0079): the sentinel in
|
|
282
|
+
* that place, until the composer binds it with `customize()`. */
|
|
283
|
+
export type OpenMcpParts = Omit<McpParts, "server" | "tool"> & {
|
|
284
|
+
readonly server: string | typeof open;
|
|
285
|
+
readonly tool?: string | typeof open;
|
|
286
|
+
};
|
|
287
|
+
|
|
210
288
|
/**
|
|
211
289
|
* An Effect whose url or argv a package left Open (ADR-0054): the same parts with the sentinel in
|
|
212
290
|
* that one place. The kind, the placement, the timeout and the secrets the other parts name are
|
|
@@ -231,10 +309,12 @@ export type EffectParts =
|
|
|
231
309
|
| OpenCommandParts
|
|
232
310
|
| OpenHttpParts
|
|
233
311
|
| DispatchParts
|
|
234
|
-
| OpenDispatchParts
|
|
312
|
+
| OpenDispatchParts
|
|
313
|
+
| McpParts
|
|
314
|
+
| OpenMcpParts;
|
|
235
315
|
|
|
236
316
|
/** The kinds, as their parts name them. */
|
|
237
|
-
export const EFFECT_KINDS: ReadonlySet<string> = new Set(["command", "http", "push", "dispatch"]);
|
|
317
|
+
export const EFFECT_KINDS: ReadonlySet<string> = new Set(["command", "http", "push", "dispatch", "mcp"]);
|
|
238
318
|
|
|
239
319
|
/**
|
|
240
320
|
* Where an Effect runs, derived from its kind and what it names, never authored (ADR-0067,
|
|
@@ -242,7 +322,8 @@ export const EFFECT_KINDS: ReadonlySet<string> = new Set(["command", "http", "pu
|
|
|
242
322
|
* `http` Effect with no Worktree file in the Instance Effector; an `http` whose body is a Worktree
|
|
243
323
|
* file, a `push` and a `command` that names a secret in the Sandbox Effector of its Workspace; a
|
|
244
324
|
* `dispatch` with no target in the Orchestrator's own process, and one with a target (an Open one
|
|
245
|
-
* included: it has one once bound) in the Instance Effector (ADR-0071)
|
|
325
|
+
* included: it has one once bound) in the Instance Effector (ADR-0071); an `mcp` Effect in the
|
|
326
|
+
* Instance Effector, which calls remote servers only (ADR-0079).
|
|
246
327
|
*/
|
|
247
328
|
export type Placement = "harness" | "instance" | "sandbox" | "orchestrator";
|
|
248
329
|
export function placementOf(parts: EffectParts): Placement {
|
|
@@ -253,6 +334,8 @@ export function placementOf(parts: EffectParts): Placement {
|
|
|
253
334
|
return parts.file ? "sandbox" : "instance";
|
|
254
335
|
case "push":
|
|
255
336
|
return "sandbox";
|
|
337
|
+
case "mcp":
|
|
338
|
+
return "instance";
|
|
256
339
|
case "command":
|
|
257
340
|
return (parts.secrets?.length ?? 0) > 0 || parts.openSecret ? "sandbox" : "harness";
|
|
258
341
|
}
|
|
@@ -291,10 +374,30 @@ export function targetText(parts: { readonly target: HttpParts["target"] | typeo
|
|
|
291
374
|
* `customize()` binds it (ADR-0054); and for a dispatch with no input schema: the callee's door is
|
|
292
375
|
* the only check, so nothing here says what it reads (ADR-0071). */
|
|
293
376
|
export function inputKeysOf(parts: EffectParts): string[] | undefined {
|
|
294
|
-
|
|
295
|
-
|
|
377
|
+
const shape = inputShapeOf(parts);
|
|
378
|
+
return shape && [...shape.required, ...shape.optional, ...(shape.oneOf ?? [])];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** The shape of a cut's input. */
|
|
382
|
+
const CUT_SHAPE: InputShape = { required: ["path"], optional: ["from"], oneOf: ["branch", "detach"] };
|
|
383
|
+
|
|
384
|
+
/** An Effect's input as a Chart's door and its start hold a Chart to it. Undefined where
|
|
385
|
+
* {@link inputKeysOf} is. `worktree` is optional for a key the Workspace decides: the door makes it
|
|
386
|
+
* required when it knows that the Workspace has Repo Slots, and a start refuses it then. */
|
|
387
|
+
export function inputShapeOf(parts: EffectParts): InputShape | undefined {
|
|
388
|
+
if (parts.kind === "dispatch") {
|
|
389
|
+
return parts.inputKeys === undefined ? undefined : { required: [...parts.inputKeys], optional: [] };
|
|
390
|
+
}
|
|
391
|
+
if (parts.kind === "push" || parts.kind === "mcp") return { required: [...parts.inputKeys], optional: [] };
|
|
392
|
+
if (parts.kind === "http") {
|
|
393
|
+
return { required: [...parts.inputKeys], optional: parts.file ? ["worktree"] : [] };
|
|
394
|
+
}
|
|
395
|
+
if (parts.cut) return CUT_SHAPE;
|
|
296
396
|
if (parts.argv === open) return undefined;
|
|
297
|
-
|
|
397
|
+
const keys = [...parts.argv.flatMap((a) => (typeof a === "string" ? [] : [a.arg])), ...(parts.pin ? ["sha"] : [])];
|
|
398
|
+
// A command with an Effect secret runs on a commit of a Worktree, so it names one in each Workspace.
|
|
399
|
+
const credentialed = (parts.secrets?.length ?? 0) > 0 || parts.openSecret === true;
|
|
400
|
+
return credentialed ? { required: [...keys, "worktree"], optional: [] } : { required: keys, optional: ["worktree"] };
|
|
298
401
|
}
|
|
299
402
|
|
|
300
403
|
// --- Output -------------------------------------------------------------------------------------
|
|
@@ -304,6 +407,11 @@ export function inputKeysOf(parts: EffectParts): string[] | undefined {
|
|
|
304
407
|
* not run the Effect again. */
|
|
305
408
|
export type Unknown = { outcome: "unknown"; reason: string };
|
|
306
409
|
|
|
410
|
+
/** A value of `outcome` that this kit does not give. It stands in the union so that no author can
|
|
411
|
+
* write an exhaustive check, so that a later outcome is no break: route on what you know, and give
|
|
412
|
+
* the rest a last arm. */
|
|
413
|
+
export type LaterOutcome = { outcome: "(a later outcome)"; reason?: string };
|
|
414
|
+
|
|
307
415
|
// `dirty` is a credentialed command's alone (ADR-0067): it runs in the Sandbox Effector against
|
|
308
416
|
// the pinned commit, never the live Worktree.
|
|
309
417
|
/**
|
|
@@ -321,34 +429,48 @@ export type Unknown = { outcome: "unknown"; reason: string };
|
|
|
321
429
|
* characters. `dirty` is `true` when a command that names an Effect secret ran against a commit,
|
|
322
430
|
* and the Worktree held changes that it did not see.
|
|
323
431
|
*
|
|
432
|
+
* The union is open ({@link LaterOutcome}): give the rest a last arm with no guard.
|
|
433
|
+
*
|
|
324
434
|
* @example
|
|
325
435
|
* ```ts
|
|
326
436
|
* import {
|
|
327
437
|
* command,
|
|
328
438
|
* type CommandOutput,
|
|
329
439
|
* fail,
|
|
440
|
+
* isReady,
|
|
330
441
|
* jr2Setup,
|
|
331
442
|
* workspace,
|
|
443
|
+
* worktree,
|
|
332
444
|
* type Workspaced,
|
|
333
445
|
* } from "@jr2/orchestrator";
|
|
334
446
|
*
|
|
335
447
|
* type Input = Workspaced<{}, "app">;
|
|
336
448
|
*
|
|
449
|
+
* const testTree = (context: Input) => `${context.workspace.repos.app}/test` as const;
|
|
450
|
+
*
|
|
337
451
|
* const passed = (output: CommandOutput): boolean =>
|
|
338
452
|
* output.outcome === "exited" && output.code === 0;
|
|
339
453
|
*
|
|
340
454
|
* const body = jr2Setup({
|
|
341
455
|
* types: {} as { context: Input; input: Input },
|
|
342
|
-
* actors: { tests: command(["npm", "test"], { timeout: "15m" }), fail: fail() },
|
|
456
|
+
* actors: { cut: worktree(), tests: command(["npm", "test"], { timeout: "15m" }), fail: fail() },
|
|
343
457
|
* }).createMachine({
|
|
344
458
|
* id: "tests",
|
|
345
459
|
* context: ({ input }) => input,
|
|
346
460
|
* on: { "workspace.lost": ".lost" },
|
|
347
|
-
* initial: "
|
|
461
|
+
* initial: "cutting",
|
|
348
462
|
* states: {
|
|
463
|
+
* cutting: {
|
|
464
|
+
* invoke: {
|
|
465
|
+
* src: "cut",
|
|
466
|
+
* input: ({ context }) => ({ path: testTree(context), branch: "tests" }),
|
|
467
|
+
* onDone: [{ guard: ({ event }) => isReady(event.output), target: "testing" }, { target: "notCut" }],
|
|
468
|
+
* },
|
|
469
|
+
* },
|
|
349
470
|
* testing: {
|
|
350
471
|
* invoke: {
|
|
351
472
|
* src: "tests",
|
|
473
|
+
* input: ({ context }) => ({ worktree: testTree(context) }),
|
|
352
474
|
* onDone: [
|
|
353
475
|
* { guard: ({ event }) => passed(event.output), target: "green" },
|
|
354
476
|
* { target: "red" },
|
|
@@ -358,13 +480,13 @@ export type Unknown = { outcome: "unknown"; reason: string };
|
|
|
358
480
|
* green: { type: "final" },
|
|
359
481
|
* // Not a final state: a final state means `done`, and the run ends `failed` here.
|
|
360
482
|
* red: { invoke: { src: "fail", input: { reason: "npm test did not pass" } } },
|
|
483
|
+
* notCut: { invoke: { src: "fail", input: { reason: "the Worktree was not cut" } } },
|
|
361
484
|
* lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
|
|
362
485
|
* },
|
|
363
486
|
* });
|
|
364
487
|
*
|
|
365
488
|
* export const machine = workspace(body, {
|
|
366
489
|
* repos: { app: { url: "https://github.com/you/app.git" } },
|
|
367
|
-
* spec: () => ({ branch: "tests" }),
|
|
368
490
|
* });
|
|
369
491
|
* ```
|
|
370
492
|
*
|
|
@@ -372,7 +494,7 @@ export type Unknown = { outcome: "unknown"; reason: string };
|
|
|
372
494
|
* @see docs/guides/run-a-command-or-call-a-url.md
|
|
373
495
|
* @see docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md
|
|
374
496
|
*/
|
|
375
|
-
export type CommandOutput = (ExecOutcome & { dirty?: boolean }) | Unknown;
|
|
497
|
+
export type CommandOutput = (ExecOutcome & { dirty?: boolean }) | Unknown | LaterOutcome;
|
|
376
498
|
|
|
377
499
|
// Reach is output, as an http Effect's is (ADR-0067): a body a model wrote can be fixed through
|
|
378
500
|
// the Chain, so a refusal is output too.
|
|
@@ -390,6 +512,8 @@ export type CommandOutput = (ExecOutcome & { dirty?: boolean }) | Unknown;
|
|
|
390
512
|
* - `unreachable`: the Orchestrator of the target did not answer. No run started.
|
|
391
513
|
* - `unknown`: jr2 cannot tell if a run started.
|
|
392
514
|
*
|
|
515
|
+
* The union is open ({@link LaterOutcome}): give the rest a last arm with no guard.
|
|
516
|
+
*
|
|
393
517
|
* @see docs/concepts/effects.md
|
|
394
518
|
*/
|
|
395
519
|
export type DispatchOutput =
|
|
@@ -402,7 +526,8 @@ export type DispatchOutput =
|
|
|
402
526
|
}
|
|
403
527
|
| { outcome: "refused"; status: number; report: string[] }
|
|
404
528
|
| { outcome: "unreachable"; reason: string }
|
|
405
|
-
| Unknown
|
|
529
|
+
| Unknown
|
|
530
|
+
| LaterOutcome;
|
|
406
531
|
|
|
407
532
|
// --- The ledger row (persisted in the run's blob, ADR-0007) --------------------------------------
|
|
408
533
|
|
|
@@ -417,7 +542,7 @@ export type EffectRow =
|
|
|
417
542
|
| { state: "intent"; kind: "command"; slot: string; endpoint: string; placement: string; at: string }
|
|
418
543
|
/** An http Effect's intent names its placement and no replica (ADR-0068): a restore asks every
|
|
419
544
|
* replica. `idempotent` is the author's mark, which the restore rule reads. */
|
|
420
|
-
| { state: "intent"; kind: "http"; slot: string; placement: "instance"; idempotent: boolean; at: string }
|
|
545
|
+
| { state: "intent"; kind: "http" | "mcp"; slot: string; placement: "instance"; idempotent: boolean; at: string }
|
|
421
546
|
/** An Effect of the Sandbox Effector (ADR-0067): its Sandbox, and — for a push or a credentialed
|
|
422
547
|
* command — the commit it runs against, written once the start's answer names it. A restore
|
|
423
548
|
* re-attaches by id, or sends a push or a credentialed command again with only that commit, and
|
|
@@ -445,15 +570,19 @@ export type EffectRow =
|
|
|
445
570
|
run?: string;
|
|
446
571
|
at: string;
|
|
447
572
|
}
|
|
448
|
-
/** `sandbox` and `
|
|
449
|
-
* — every later push of its own
|
|
450
|
-
*
|
|
573
|
+
/** `sandbox`, `repo` and `branch`: the Workspace, the Repo Slot and the branch an Effect of the
|
|
574
|
+
* Sandbox Effector ran on — every later push of its own (slot, branch) sends its pair, and the
|
|
575
|
+
* newest push's unfinished sync runs again in `worktree`. `cut`: a `worktree()` that settled
|
|
576
|
+
* `ready`: its path and branch as fields, because a settled row drops its tail. A push, a
|
|
577
|
+
* credentialed command and a file body read their branch here. */
|
|
451
578
|
| {
|
|
452
579
|
state: "settled";
|
|
453
580
|
kind: EffectParts["kind"];
|
|
454
581
|
slot: string;
|
|
455
582
|
sandbox?: string;
|
|
456
583
|
repo?: string;
|
|
584
|
+
branch?: string;
|
|
585
|
+
cut?: { path: string; branch: string | null };
|
|
457
586
|
output: unknown;
|
|
458
587
|
at: string;
|
|
459
588
|
}
|
|
@@ -511,6 +640,9 @@ const TAILS: Record<EffectRow["kind"], readonly string[]> = {
|
|
|
511
640
|
// settled marker carries both whole. The row keeps the outcome, the run id, its status and the
|
|
512
641
|
// door's HTTP status (ADR-0071).
|
|
513
642
|
dispatch: ["output", "report"],
|
|
643
|
+
// An mcp answer's text and its structured value: the feed's settled marker carries both whole.
|
|
644
|
+
// The row keeps the outcome, `isError` and `truncated` (ADR-0079).
|
|
645
|
+
mcp: ["text", "structured"],
|
|
514
646
|
};
|
|
515
647
|
|
|
516
648
|
/** A kind's tail for one output. The NEWEST push of its Repo Slot whose sync did not finish (`busy`,
|
|
@@ -530,19 +662,19 @@ export type PushedRow = { published: string; source: string; sync?: SyncAnswer["
|
|
|
530
662
|
unknown
|
|
531
663
|
>;
|
|
532
664
|
|
|
533
|
-
/** The ledger's last push of a
|
|
534
|
-
* Effect's (`self`).
|
|
535
|
-
*/
|
|
665
|
+
/** The ledger's last push of a (slot, branch) of a Workspace that published: its row, by `at`, never
|
|
666
|
+
* this Effect's (`self`). Each (slot, branch) has its own pairs: one branch on one remote. */
|
|
536
667
|
export function lastPushOf(
|
|
537
668
|
rows: Readonly<Record<string, EffectRow>>,
|
|
538
669
|
sandbox: string,
|
|
539
670
|
repo: string,
|
|
671
|
+
branch: string,
|
|
540
672
|
self?: string,
|
|
541
673
|
): { rowId: string; row: Extract<EffectRow, { state: "settled" }>; output: PushedRow } | undefined {
|
|
542
674
|
let best: { rowId: string; row: Extract<EffectRow, { state: "settled" }>; output: PushedRow } | undefined;
|
|
543
675
|
for (const [rowId, row] of Object.entries(rows)) {
|
|
544
676
|
if (rowId === self || row.state !== "settled" || row.kind !== "push") continue;
|
|
545
|
-
if (row.sandbox !== sandbox || row.repo !== repo) continue;
|
|
677
|
+
if (row.sandbox !== sandbox || row.repo !== repo || row.branch !== branch) continue;
|
|
546
678
|
const output = row.output as Partial<PushedRow> & { outcome?: unknown };
|
|
547
679
|
if (output?.outcome !== "pushed" || typeof output.published !== "string" || typeof output.source !== "string") {
|
|
548
680
|
continue;
|
|
@@ -552,20 +684,21 @@ export function lastPushOf(
|
|
|
552
684
|
return best;
|
|
553
685
|
}
|
|
554
686
|
|
|
555
|
-
/** Every pair the ledger holds for a
|
|
556
|
-
* published, oldest first by `at`, never this Effect's (`self`) — its published commit, its
|
|
557
|
-
* and its own `squashed`. What a push start carries, so the Effector sees every push's pair. */
|
|
687
|
+
/** Every pair the ledger holds for a (slot, branch) of a Workspace (ADR-0067): each settled push row
|
|
688
|
+
* that published, oldest first by `at`, never this Effect's (`self`) — its published commit, its
|
|
689
|
+
* source, and its own `squashed`. What a push start carries, so the Effector sees every push's pair. */
|
|
558
690
|
export function pushPairsOf(
|
|
559
691
|
rows: Readonly<Record<string, EffectRow>>,
|
|
560
692
|
sandbox: string,
|
|
561
693
|
repo: string,
|
|
694
|
+
branch: string,
|
|
562
695
|
self?: string,
|
|
563
696
|
): Array<{ published: string; source: string; squashed?: "signed" | "unsigned" }> {
|
|
564
697
|
const found: Array<{ at: string; pair: { published: string; source: string; squashed?: "signed" | "unsigned" } }> =
|
|
565
698
|
[];
|
|
566
699
|
for (const [rowId, row] of Object.entries(rows)) {
|
|
567
700
|
if (rowId === self || row.state !== "settled" || row.kind !== "push") continue;
|
|
568
|
-
if (row.sandbox !== sandbox || row.repo !== repo) continue;
|
|
701
|
+
if (row.sandbox !== sandbox || row.repo !== repo || row.branch !== branch) continue;
|
|
569
702
|
const output = row.output as Partial<PushedRow> & { outcome?: unknown; squashed?: unknown };
|
|
570
703
|
if (output?.outcome !== "pushed" || typeof output.published !== "string" || typeof output.source !== "string") {
|
|
571
704
|
continue;
|
|
@@ -579,21 +712,41 @@ export function pushPairsOf(
|
|
|
579
712
|
return found.sort((a, b) => (a.at < b.at ? -1 : a.at > b.at ? 1 : 0)).map((f) => f.pair);
|
|
580
713
|
}
|
|
581
714
|
|
|
582
|
-
/** The row id of each
|
|
715
|
+
/** The row id of each (slot, branch)'s newest push that published (`lastPushOf`), over the ledger. */
|
|
583
716
|
function newestPushesOf(effects: Readonly<Record<string, EffectRow>>): Set<string> {
|
|
584
717
|
const ids = new Set<string>();
|
|
585
718
|
const seen = new Set<string>();
|
|
586
719
|
for (const row of Object.values(effects)) {
|
|
587
720
|
if (row.state !== "settled" || row.kind !== "push" || row.sandbox === undefined || row.repo === undefined) continue;
|
|
588
|
-
|
|
721
|
+
if (row.branch === undefined) continue;
|
|
722
|
+
const key = JSON.stringify([row.sandbox, row.repo, row.branch]);
|
|
589
723
|
if (seen.has(key)) continue;
|
|
590
724
|
seen.add(key);
|
|
591
|
-
const last = lastPushOf(effects, row.sandbox, row.repo);
|
|
725
|
+
const last = lastPushOf(effects, row.sandbox, row.repo, row.branch);
|
|
592
726
|
if (last !== undefined) ids.add(last.rowId);
|
|
593
727
|
}
|
|
594
728
|
return ids;
|
|
595
729
|
}
|
|
596
730
|
|
|
731
|
+
/** The cut that made `path` in this Sandbox: the newest settled `ready` row, whatever its Repo Slot.
|
|
732
|
+
* What a push, a credentialed command and a file body read. A row of another Sandbox says nothing
|
|
733
|
+
* of this one: a re-provision has an empty `/work`. */
|
|
734
|
+
export function cutRowOf(
|
|
735
|
+
rows: Readonly<Record<string, EffectRow>>,
|
|
736
|
+
sandbox: string,
|
|
737
|
+
path: unknown,
|
|
738
|
+
): { repo: string; path: string; branch: string | null } | undefined {
|
|
739
|
+
let best: { at: string; repo: string; path: string; branch: string | null } | undefined;
|
|
740
|
+
for (const row of Object.values(rows)) {
|
|
741
|
+
if (row.state !== "settled" || row.cut === undefined || row.sandbox !== sandbox) continue;
|
|
742
|
+
if (row.cut.path !== path || row.repo === undefined) continue;
|
|
743
|
+
if (best === undefined || row.at > best.at) {
|
|
744
|
+
best = { at: row.at, repo: row.repo, path: row.cut.path, branch: row.cut.branch };
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return best;
|
|
748
|
+
}
|
|
749
|
+
|
|
597
750
|
/** One Effect line on the run's feed (Instance-token band, like the Turn markers). `output` lines
|
|
598
751
|
* carry the stream; `aborted` and an `unknown` settle say the outside world may have acted. */
|
|
599
752
|
export type EffectMarker = {
|
|
@@ -602,7 +755,7 @@ export type EffectMarker = {
|
|
|
602
755
|
effect: string;
|
|
603
756
|
stage: "started" | "output" | "settled" | "aborted";
|
|
604
757
|
text?: string;
|
|
605
|
-
output?: CommandOutput | HttpOutput | DispatchOutput;
|
|
758
|
+
output?: CommandOutput | WorktreeOutput | HttpOutput | DispatchOutput | McpOutput;
|
|
606
759
|
};
|
|
607
760
|
|
|
608
761
|
// --- The port -----------------------------------------------------------------------------------
|
|
@@ -643,19 +796,33 @@ export type EffectLogic<I, O> = ActorLogic<EffectSnapshot<O, I>, EventObject, I>
|
|
|
643
796
|
type ArgNames<A extends readonly unknown[]> = A[number] extends infer E ? (E extends Arg<infer N> ? N : never) : never;
|
|
644
797
|
/** A command's input: one string per argv slot, and with `pin` the approved commit (`{ sha }`);
|
|
645
798
|
* none when there is neither. */
|
|
646
|
-
export type CommandInput<A extends readonly unknown[], P = false> = [
|
|
799
|
+
export type CommandInput<A extends readonly unknown[], P = false, Opt = unknown> = [
|
|
647
800
|
ArgNames<A> | ([P] extends [true] ? "sha" : never),
|
|
648
801
|
] extends [never]
|
|
649
|
-
? void
|
|
650
|
-
: { [K in ArgNames<A> | ([P] extends [true] ? "sha" : never)]: string }
|
|
802
|
+
? (WorktreeKey<Opt> & NotWorktreeKeys) | ([Opt] extends [{ readonly env: object }] ? never : void)
|
|
803
|
+
: { [K in ArgNames<A> | ([P] extends [true] ? "sha" : never)]: string } & WorktreeKey<Opt> &
|
|
804
|
+
Omit<NotWorktreeKeys, ArgNames<A>>;
|
|
805
|
+
|
|
806
|
+
// 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
|
|
807
|
+
// none. A credentialed command runs against one commit of a Worktree, so there it is always required.
|
|
808
|
+
/** @internal */
|
|
809
|
+
export type WorktreeKey<Opt> = [Opt] extends [{ readonly env: object }]
|
|
810
|
+
? { worktree: AnyWorktreePath }
|
|
811
|
+
: { worktree?: AnyWorktreePath };
|
|
812
|
+
|
|
813
|
+
/** The keys of a Turn and a cut, refused by name on an Effect's input: a mapper's excess key is no
|
|
814
|
+
* compile error, and the Worktree would be lost.
|
|
815
|
+
* @internal */
|
|
816
|
+
export type NotWorktreeKeys = { cwd?: never; path?: never; branch?: never };
|
|
651
817
|
|
|
652
818
|
// An Open argv's slots are the composer's (ADR-0054), so any key is one string — what the
|
|
653
819
|
// package's invoke hands it, read by the argv the composer binds (`argvOf`).
|
|
654
820
|
/** The input of a `command(open)`: a string for each `arg()` of the argv that the Instance binds,
|
|
655
821
|
* and with `pin`, the commit (`{ sha }`). The compiler cannot check the names. When the input has
|
|
656
822
|
* no string for an `arg()`, the Effect fails when it starts. */
|
|
657
|
-
export type OpenCommandInput<P = false> = Readonly<Record<string, string>> &
|
|
658
|
-
([P] extends [true] ? { sha: string } : unknown)
|
|
823
|
+
export type OpenCommandInput<P = false, Opt = unknown> = Readonly<Record<string, string>> &
|
|
824
|
+
([P] extends [true] ? { sha: string } : unknown) &
|
|
825
|
+
WorktreeKey<Opt>;
|
|
659
826
|
|
|
660
827
|
/** A length of time: a positive number and a unit, `s`, `m` or `h`. For example `"90s"`, `"10m"`
|
|
661
828
|
* or `"1h"`. */
|
|
@@ -664,28 +831,6 @@ export type Duration = `${number}s` | `${number}m` | `${number}h`;
|
|
|
664
831
|
/** The options of `command()`: the Repo Slot it runs in, how long it can run, how much of its
|
|
665
832
|
* output it keeps, its Effect secrets, and the commit it runs against. */
|
|
666
833
|
export type CommandOptions = {
|
|
667
|
-
/**
|
|
668
|
-
* The Repo Slot whose Worktree is the working directory of the command. Write it only when the
|
|
669
|
-
* Workspace has more than one Repo Slot. In a Workspace with no Repo Slot, the command starts in
|
|
670
|
-
* `/work`, the folder that the steps of the Workspace share.
|
|
671
|
-
*
|
|
672
|
-
* The command starts in the root of that Worktree. To run it in a subfolder, give the path as an
|
|
673
|
-
* argument, or write `["sh", "-c", "cd <dir> && <program>"]`.
|
|
674
|
-
*
|
|
675
|
-
* When it starts, it refuses:
|
|
676
|
-
* - no `slot` under a Workspace with more than one Repo Slot: `command "<slot>" runs under a
|
|
677
|
-
* Workspace carrying more than one Repo Slot (<slots>) and names none — say which Worktree it
|
|
678
|
-
* runs in: command(argv, { slot: "<slot>" }) (ADR-0051)`. When `env` names an Effect secret,
|
|
679
|
-
* the text is `command "<slot>" runs under a Workspace carrying more than one Repo Slot
|
|
680
|
-
* (<slots>) — say which: { slot: "<slot>" } (ADR-0051)`.
|
|
681
|
-
* - with an Effect secret in `env`, a Workspace with no Repo Slot: `command "<slot>" runs under
|
|
682
|
-
* a Workspace with no Repo Slot: a command with an Effect secret runs against one commit of a
|
|
683
|
-
* Repo Slot's branch, and there is none — declare a Repo Slot, or name no secret in its env, and
|
|
684
|
-
* it runs in /work beside the Agent (ADR-0051)`.
|
|
685
|
-
* - a `slot` that the Workspace does not have: `command "<slot>" names Repo Slot "<name>", which
|
|
686
|
-
* its Workspace does not carry (<slots>)`.
|
|
687
|
-
*/
|
|
688
|
-
slot?: string;
|
|
689
834
|
/**
|
|
690
835
|
* How long the command can run: a number and a unit, such as `"90s"`, `"10m"` or `"1h"`.
|
|
691
836
|
* Default `"10m"`. When the time ends, jr2 kills the process group, and the output is
|
|
@@ -735,10 +880,14 @@ export type EnvOpenParts<Opt> = Opt extends { readonly env: infer E }
|
|
|
735
880
|
|
|
736
881
|
/**
|
|
737
882
|
* Why an Effect kind cannot run in a Workspace with no Repo Slot (ADR-0051, ADR-0067), or undefined
|
|
738
|
-
* when it can: a `push` and a credentialed `command` run on a commit of a slot's branch, and
|
|
739
|
-
* Workspace has none. The invoke and the
|
|
883
|
+
* when it can: a `push` and a credentialed `command` run on a commit of a slot's branch, and a
|
|
884
|
+
* `worktree` makes a Worktree of a slot's clone, and that Workspace has none. The invoke and the
|
|
885
|
+
* `jr2 up` walk refuse with the same words.
|
|
740
886
|
*/
|
|
741
887
|
export function needsASlot(kind: string): string | undefined {
|
|
888
|
+
if (kind === "worktree") {
|
|
889
|
+
return 'a cut makes a Worktree of a Repo Slot, and there is none — declare one: repos: { app: "<url>" }';
|
|
890
|
+
}
|
|
742
891
|
if (kind === "push")
|
|
743
892
|
return 'a push publishes a Repo Slot\'s branch, and there is none — declare one: repos: { app: "<url>" }';
|
|
744
893
|
if (kind === "command") {
|
|
@@ -773,7 +922,7 @@ export function commandWith<const Opt extends CommandOptions = CommandOptions>(
|
|
|
773
922
|
argv: typeof open,
|
|
774
923
|
options?: Opt,
|
|
775
924
|
effectorPorts?: EffectorPortFactory,
|
|
776
|
-
): EffectLogic<OpenCommandInput<Opt["pin"]>, CommandOutput> & OpenEffect<"argv" | EnvOpenParts<Opt>>;
|
|
925
|
+
): EffectLogic<OpenCommandInput<Opt["pin"], Opt>, CommandOutput> & OpenEffect<"argv" | EnvOpenParts<Opt>>;
|
|
777
926
|
export function commandWith<
|
|
778
927
|
const A extends readonly (string | Arg)[],
|
|
779
928
|
const Opt extends CommandOptions = CommandOptions,
|
|
@@ -782,12 +931,15 @@ export function commandWith<
|
|
|
782
931
|
argv: A,
|
|
783
932
|
options?: Opt,
|
|
784
933
|
effectorPorts?: EffectorPortFactory,
|
|
785
|
-
|
|
934
|
+
cut?: CommandParts["cut"],
|
|
935
|
+
): EffectLogic<CommandInput<A, Opt["pin"], Opt>, CommandOutput> & OpenOf<EnvOpenParts<Opt>>;
|
|
786
936
|
export function commandWith(
|
|
787
937
|
portFactory: ExecPortFactory,
|
|
788
938
|
argv: readonly (string | Arg)[] | typeof open,
|
|
789
939
|
options: CommandOptions = {},
|
|
790
940
|
effectorPorts: EffectorPortFactory = sandboxEffectorPort,
|
|
941
|
+
// `worktree()` alone passes this: an author's command is at most once.
|
|
942
|
+
cut?: CommandParts["cut"],
|
|
791
943
|
// The implementation of the overloads above: each one states the input type.
|
|
792
944
|
): EffectLogic<any, CommandOutput> {
|
|
793
945
|
if (argv === open) {
|
|
@@ -814,9 +966,9 @@ export function commandWith(
|
|
|
814
966
|
argv: [...argv],
|
|
815
967
|
timeoutMs: options.timeout === undefined ? DEFAULT_TIMEOUT_MS : parseDuration(options.timeout),
|
|
816
968
|
tail: options.tail ?? DEFAULT_TAIL,
|
|
817
|
-
...(options.slot !== undefined ? { slot: options.slot } : {}),
|
|
818
969
|
...(options.env !== undefined && Object.keys(options.env).length > 0 ? commandEnvParts(options.env) : {}),
|
|
819
970
|
...(options.pin === true ? { pin: true as const } : {}),
|
|
971
|
+
...(cut !== undefined ? { cut, idempotent: true as const } : {}),
|
|
820
972
|
};
|
|
821
973
|
if (parts.pin && (parts.secrets?.length ?? 0) === 0 && !parts.openSecret) {
|
|
822
974
|
// A command that names no secret runs in the Harness container, in the live Worktree: there
|
|
@@ -844,7 +996,7 @@ export function commandWith(
|
|
|
844
996
|
) as unknown as EffectLogic<unknown, CommandOutput>;
|
|
845
997
|
}
|
|
846
998
|
type I = unknown;
|
|
847
|
-
type O = CommandOutput;
|
|
999
|
+
type O = CommandOutput | WorktreeOutput;
|
|
848
1000
|
const logic: ActorLogic<EffectSnapshot<O, I>, EventObject, I> = {
|
|
849
1001
|
getInitialSnapshot: (_scope, input) => ({
|
|
850
1002
|
status: "active",
|
|
@@ -878,12 +1030,63 @@ export function commandWith(
|
|
|
878
1030
|
},
|
|
879
1031
|
getPersistedSnapshot: (snapshot) => snapshot,
|
|
880
1032
|
};
|
|
881
|
-
return Object.assign(logic, { effect: parts })
|
|
1033
|
+
return Object.assign(logic, { effect: parts }) as unknown as EffectLogic<any, CommandOutput>;
|
|
882
1034
|
}
|
|
883
1035
|
|
|
884
1036
|
/** The slot's name as its parent invoked it — what the ledger and the feed call the Effect. */
|
|
885
1037
|
const slotOf = (self: AnyActorRef): string => (typeof self.src === "string" ? self.src : self.id);
|
|
886
1038
|
|
|
1039
|
+
/** How many times a marked Effect goes out again after a Harness that lost it. Bounded, so that a
|
|
1040
|
+
* Harness in a crash loop ends the wait: the Effect settles `unknown`, and the Machine routes. */
|
|
1041
|
+
const RESENDS = 3;
|
|
1042
|
+
|
|
1043
|
+
/** What a cut's program answers: its last line. Anything else is a cut that did not end. */
|
|
1044
|
+
function cutOutputOf(raw: CommandOutput): WorktreeOutput {
|
|
1045
|
+
if (raw.outcome === "unknown") return raw as Unknown;
|
|
1046
|
+
const tail = "tail" in raw && typeof raw.tail === "string" ? raw.tail : "";
|
|
1047
|
+
if (raw.outcome === "exited" && raw.code === 0) {
|
|
1048
|
+
try {
|
|
1049
|
+
const answer = JSON.parse(tail.trim().split("\n").pop() ?? "") as { outcome?: unknown };
|
|
1050
|
+
if (answer.outcome === "ready" || answer.outcome === "refused") return answer as WorktreeOutput;
|
|
1051
|
+
} catch {
|
|
1052
|
+
// Falls through: the program said no answer.
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
return { outcome: "unknown", reason: `the cut ended ${raw.outcome} with no answer: ${tail.slice(-400)}` };
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/** Why a cut's input cannot go to the Harness, or undefined. The door checked a Chart's literals; a
|
|
1059
|
+
* reference, a pick param or a mapper can still say this. No exec, no fetch. */
|
|
1060
|
+
function cutFaultOf(input: unknown, ambient: AmbientHandles): string | undefined {
|
|
1061
|
+
const given = (input ?? {}) as { path?: unknown; branch?: unknown; detach?: unknown; from?: unknown };
|
|
1062
|
+
const fault = worktreePathFault(given.path, ambient, { cut: true });
|
|
1063
|
+
if (fault !== undefined) return fault;
|
|
1064
|
+
if ((given.branch === undefined) === (given.detach === undefined))
|
|
1065
|
+
return "a cut names a branch, or a commit in `detach`";
|
|
1066
|
+
if (given.detach !== undefined && given.from !== undefined) return "a detached cut takes no `from`";
|
|
1067
|
+
for (const key of ["branch", "detach", "from"] as const) {
|
|
1068
|
+
if (given[key] !== undefined && typeof given[key] !== "string") return `a cut's \`${key}\` is a string`;
|
|
1069
|
+
}
|
|
1070
|
+
return undefined;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/** The cut's one argument: its input, its id (a re-send answers the first `found`), its static
|
|
1074
|
+
* `expect`, and the base, sent each time, so that no checkout of an Agent's can move it. */
|
|
1075
|
+
function cutRequestOf(id: string, input: unknown, parts: CommandParts, ambient: AmbientHandles): string {
|
|
1076
|
+
const given = (input ?? {}) as { path: string; branch?: string; detach?: string; from?: string };
|
|
1077
|
+
const slot = given.path.split("/")[2]!;
|
|
1078
|
+
const base = ambient.bases?.[slot];
|
|
1079
|
+
return JSON.stringify({
|
|
1080
|
+
id,
|
|
1081
|
+
path: given.path,
|
|
1082
|
+
...(given.branch !== undefined ? { branch: given.branch } : {}),
|
|
1083
|
+
...(given.detach !== undefined ? { detach: given.detach } : {}),
|
|
1084
|
+
...(given.from !== undefined ? { from: given.from } : {}),
|
|
1085
|
+
...(given.branch !== undefined && parts.cut?.expect ? { expect: parts.cut.expect } : {}),
|
|
1086
|
+
...(base !== undefined ? { base } : {}),
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
|
|
887
1090
|
/** Run, re-attach to, or deliver one Effect, by its ledger row (see the module header). */
|
|
888
1091
|
async function drive<I, O>(
|
|
889
1092
|
self: AnyActorRef,
|
|
@@ -900,14 +1103,30 @@ async function drive<I, O>(
|
|
|
900
1103
|
const now = (): string => new Date().toISOString();
|
|
901
1104
|
const mark = (marker: Omit<EffectMarker, "kind" | "slot" | "effect">): void =>
|
|
902
1105
|
binding?.marker?.({ kind: "effect", slot, effect: id, ...marker });
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1106
|
+
/** A settled row of an exec. `sandbox` is the row's own on a restore: the Workspace it ran in. */
|
|
1107
|
+
const settleIn =
|
|
1108
|
+
(sandbox: string | undefined) =>
|
|
1109
|
+
(raw: CommandOutput | WorktreeOutput): void => {
|
|
1110
|
+
if (signal.aborted || self.getSnapshot().status !== "active") return;
|
|
1111
|
+
const output = parts.cut && raw.outcome !== "refused" ? cutOutputOf(raw as CommandOutput) : raw;
|
|
1112
|
+
// The settled row first, saved at the write: a restart before the transition is saved delivers
|
|
1113
|
+
// this output, and runs nothing. A `ready` cut's row keeps its fields: a push reads them.
|
|
1114
|
+
const cutRow =
|
|
1115
|
+
parts.cut && output.outcome === "ready"
|
|
1116
|
+
? { sandbox, repo: output.slot, cut: { path: output.path, branch: output.branch } }
|
|
1117
|
+
: {};
|
|
1118
|
+
binding?.recordEffect?.(id, {
|
|
1119
|
+
state: "settled",
|
|
1120
|
+
kind: parts.kind,
|
|
1121
|
+
slot,
|
|
1122
|
+
...(cutRow as object),
|
|
1123
|
+
output,
|
|
1124
|
+
at: now(),
|
|
1125
|
+
});
|
|
1126
|
+
mark({ stage: "settled", output });
|
|
1127
|
+
self.send({ type: "jr2.effect.settle", output } satisfies Settle);
|
|
1128
|
+
};
|
|
1129
|
+
const settle = settleIn(undefined);
|
|
911
1130
|
|
|
912
1131
|
const row = binding?.effectRow?.(id);
|
|
913
1132
|
if (row?.state === "settled") {
|
|
@@ -923,13 +1142,14 @@ async function drive<I, O>(
|
|
|
923
1142
|
return;
|
|
924
1143
|
}
|
|
925
1144
|
if (row?.state === "intent" && row.kind === "command" && "endpoint" in row) {
|
|
926
|
-
// In flight at an Orchestrator restart: re-attach by id, or settle `unknown`. Never run again
|
|
1145
|
+
// In flight at an Orchestrator restart: re-attach by id, or settle `unknown`. Never run again,
|
|
1146
|
+
// unless the kit marked it: a cut goes on below, under the same id.
|
|
927
1147
|
const port = portFactory(row.endpoint, binding?.harnessBearer?.(row.placement));
|
|
928
1148
|
run.port = port;
|
|
929
1149
|
run.started = true;
|
|
930
1150
|
const first = await retrying(() => port.read(id, undefined, signal), signal);
|
|
931
1151
|
if (signal.aborted) return;
|
|
932
|
-
if (first === undefined) {
|
|
1152
|
+
if (first === undefined && !parts.idempotent) {
|
|
933
1153
|
const output: Unknown = {
|
|
934
1154
|
outcome: "unknown",
|
|
935
1155
|
reason: "it was running when the Orchestrator restarted, and the Harness no longer knows it; not run again",
|
|
@@ -937,8 +1157,11 @@ async function drive<I, O>(
|
|
|
937
1157
|
settle(output);
|
|
938
1158
|
return;
|
|
939
1159
|
}
|
|
940
|
-
|
|
941
|
-
|
|
1160
|
+
if (first !== undefined) {
|
|
1161
|
+
const lost = await follow(port, id, first, signal, mark, settleIn(row.placement), parts.idempotent === true);
|
|
1162
|
+
if (!lost) return;
|
|
1163
|
+
}
|
|
1164
|
+
mark({ stage: "started", text: "sent again: the Harness lost it, and a cut is idempotent" });
|
|
942
1165
|
}
|
|
943
1166
|
|
|
944
1167
|
// Fresh: nothing was sent. Where it runs is the enclosing Workspace's Harness — refused outside one.
|
|
@@ -949,9 +1172,43 @@ async function drive<I, O>(
|
|
|
949
1172
|
`workspace() — invoke it from a Workspace's body (ADR-0067)`,
|
|
950
1173
|
);
|
|
951
1174
|
}
|
|
1175
|
+
const settled = settleIn(ambient.sandbox);
|
|
1176
|
+
// A bad Worktree is a word an Agent or a Chart can say, so it is output (ADR-0067), in the form a
|
|
1177
|
+
// command that could not start has: exited 127, with the reason in the tail. A key that no one
|
|
1178
|
+
// wrote is a defect of the Machine: a throw, and `onError`.
|
|
1179
|
+
const worktree = (snapshot.input as { worktree?: unknown } | undefined | void)?.worktree;
|
|
1180
|
+
if (parts.cut) {
|
|
1181
|
+
if (Object.keys(ambient.repos).length === 0) {
|
|
1182
|
+
throw new Error(
|
|
1183
|
+
`worktree "${slot}" runs under a Workspace with no Repo Slot: ${needsASlot("worktree")} (ADR-0075)`,
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
const fault = cutFaultOf(snapshot.input, ambient);
|
|
1187
|
+
if (fault !== undefined) {
|
|
1188
|
+
settled({ outcome: "refused", reason: fault });
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
} else if (worktree === undefined) {
|
|
1192
|
+
if (Object.keys(ambient.repos).length > 0) {
|
|
1193
|
+
throw new Error(
|
|
1194
|
+
`command "${slot}" runs under a Workspace with Repo Slots (${Object.keys(ambient.repos).join(", ")}) and its ` +
|
|
1195
|
+
"input names no `worktree`: say which, a Worktree that a cut gave or `<slot>/default`",
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
} else {
|
|
1199
|
+
const fault = worktreePathFault(worktree, ambient);
|
|
1200
|
+
if (fault !== undefined) {
|
|
1201
|
+
settled({ outcome: "exited", code: 127, tail: `jr2: ${fault}; nothing ran\n` });
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
952
1205
|
const req: ExecRequest = {
|
|
953
|
-
argv:
|
|
954
|
-
|
|
1206
|
+
argv: parts.cut
|
|
1207
|
+
? [...(parts.argv as string[]), cutRequestOf(id, snapshot.input, parts, ambient)]
|
|
1208
|
+
: argvOf(slot, parts, snapshot.input),
|
|
1209
|
+
// Named, never left to the Harness: a spawn with no cwd runs in the image's own WORKDIR.
|
|
1210
|
+
cwd: parts.cut ? WORK_ROOT : ((worktree as string | undefined) ?? WORK_ROOT),
|
|
1211
|
+
...(!parts.cut && worktree !== undefined ? { root: true as const } : {}),
|
|
955
1212
|
timeoutMs: parts.timeoutMs,
|
|
956
1213
|
tail: parts.tail,
|
|
957
1214
|
};
|
|
@@ -969,26 +1226,38 @@ async function drive<I, O>(
|
|
|
969
1226
|
});
|
|
970
1227
|
run.started = true;
|
|
971
1228
|
mark({ stage: "started", text: req.argv.join(" ") });
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
const
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
if (
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1229
|
+
// One pass for every Effect but the kit's cut: a Harness that restarted knows no id, and a start
|
|
1230
|
+
// that may have reached it is never sent again. A cut goes out again: it is idempotent.
|
|
1231
|
+
const passes = parts.idempotent ? RESENDS + 1 : 1;
|
|
1232
|
+
for (let pass = 1; pass <= passes; pass++) {
|
|
1233
|
+
const sent = await startOnce(port, id, req, signal);
|
|
1234
|
+
if (signal.aborted) return;
|
|
1235
|
+
const first = await retrying(() => port.read(id, 0, signal), signal);
|
|
1236
|
+
if (signal.aborted) return;
|
|
1237
|
+
const last = pass === passes;
|
|
1238
|
+
if (first === undefined) {
|
|
1239
|
+
if (!last) {
|
|
1240
|
+
mark({ stage: "started", text: "sent again: the Harness restarted, and a cut is idempotent" });
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1243
|
+
if (sent === "uncertain" && !parts.idempotent) {
|
|
1244
|
+
// The start may still be on its way to the Harness that answers now: a kill keeps it from
|
|
1245
|
+
// running there (exec.ts keeps an unknown id killed). Best effort — `unknown` either way.
|
|
1246
|
+
void killUntilKept(port, id, parts.timeoutMs).catch(() => {});
|
|
1247
|
+
}
|
|
1248
|
+
settled({
|
|
1249
|
+
outcome: "unknown",
|
|
1250
|
+
reason:
|
|
1251
|
+
sent === "uncertain"
|
|
1252
|
+
? "its start may have reached the Harness, which no longer knows it (it restarted); not run again"
|
|
1253
|
+
: "the Harness lost it after it started (it restarted); not run again",
|
|
1254
|
+
});
|
|
1255
|
+
return;
|
|
981
1256
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
sent === "uncertain"
|
|
986
|
-
? "its start may have reached the Harness, which no longer knows it (it restarted); not run again"
|
|
987
|
-
: "the Harness lost it after it started (it restarted); not run again",
|
|
988
|
-
});
|
|
989
|
-
return;
|
|
1257
|
+
const lost = await follow(port, id, first, signal, mark, settled, !last);
|
|
1258
|
+
if (!lost) return;
|
|
1259
|
+
mark({ stage: "started", text: "sent again: the Harness lost it, and a cut is idempotent" });
|
|
990
1260
|
}
|
|
991
|
-
await follow(port, id, first, signal, mark, settle);
|
|
992
1261
|
}
|
|
993
1262
|
|
|
994
1263
|
/**
|
|
@@ -1018,7 +1287,8 @@ async function startOnce(
|
|
|
1018
1287
|
}
|
|
1019
1288
|
}
|
|
1020
1289
|
|
|
1021
|
-
/** Read the exec to its end: each chunk of stream to the feed, the output to the Machine.
|
|
1290
|
+
/** Read the exec to its end: each chunk of stream to the feed, the output to the Machine. With
|
|
1291
|
+
* `again`, a Harness that lost it settles nothing: the answer is true, and the caller sends it. */
|
|
1022
1292
|
async function follow(
|
|
1023
1293
|
port: ExecPort,
|
|
1024
1294
|
id: string,
|
|
@@ -1026,22 +1296,24 @@ async function follow(
|
|
|
1026
1296
|
signal: AbortSignal,
|
|
1027
1297
|
mark: (marker: Omit<EffectMarker, "kind" | "slot" | "effect">) => void,
|
|
1028
1298
|
settle: (output: CommandOutput) => void,
|
|
1029
|
-
|
|
1299
|
+
again = false,
|
|
1300
|
+
): Promise<boolean> {
|
|
1030
1301
|
let read: ExecRead | undefined = first;
|
|
1031
1302
|
for (;;) {
|
|
1032
1303
|
if (read === undefined) {
|
|
1304
|
+
if (again) return true;
|
|
1033
1305
|
settle({ outcome: "unknown", reason: "the Harness lost it while it ran (it restarted); not run again" });
|
|
1034
|
-
return;
|
|
1306
|
+
return false;
|
|
1035
1307
|
}
|
|
1036
1308
|
if (read.stream) mark({ stage: "output", text: read.stream });
|
|
1037
1309
|
if (read.state === "settled" && read.output) {
|
|
1038
1310
|
settle(read.output);
|
|
1039
1311
|
void port.forget(id).catch(() => {});
|
|
1040
|
-
return;
|
|
1312
|
+
return false;
|
|
1041
1313
|
}
|
|
1042
1314
|
const offset: number = read.next;
|
|
1043
1315
|
read = await retrying(() => port.read(id, offset, signal), signal);
|
|
1044
|
-
if (signal.aborted) return;
|
|
1316
|
+
if (signal.aborted) return false;
|
|
1045
1317
|
}
|
|
1046
1318
|
}
|
|
1047
1319
|
|
|
@@ -1146,30 +1418,27 @@ function argvOf(slot: string, parts: CommandParts, input: unknown): string[] {
|
|
|
1146
1418
|
}
|
|
1147
1419
|
|
|
1148
1420
|
/**
|
|
1149
|
-
*
|
|
1150
|
-
*
|
|
1151
|
-
*
|
|
1152
|
-
* Several and none named is refused, as a Turn's Frame is (ADR-0057): the kit gives no slot a
|
|
1153
|
-
* meaning.
|
|
1421
|
+
* Why `path` is not a Worktree of this Workspace, or undefined when it is. Exactly
|
|
1422
|
+
* `/work/<slot>/<name>`, of one of its Repo Slots, one segment that starts with neither `.` nor `-`.
|
|
1423
|
+
* `default` is a Worktree (the clone), and with `cut` it is not one a cut makes.
|
|
1154
1424
|
*/
|
|
1155
|
-
function
|
|
1425
|
+
export function worktreePathFault(
|
|
1426
|
+
path: unknown,
|
|
1427
|
+
ambient: AmbientHandles,
|
|
1428
|
+
opts: { cut?: boolean } = {},
|
|
1429
|
+
): string | undefined {
|
|
1430
|
+
const match = typeof path === "string" ? path.match(/^\/work\/([^/]+)\/([^/]+)$/) : null;
|
|
1431
|
+
if (match === null) {
|
|
1432
|
+
return `${JSON.stringify(path)} is not a Worktree path: /work/<slot>/<name>, with one segment in <name>`;
|
|
1433
|
+
}
|
|
1434
|
+
const [, slot, name] = match as unknown as [string, string, string];
|
|
1435
|
+
if (name.startsWith(".") || name.startsWith("-")) return `"${path}": a Worktree's name starts with neither . nor -`;
|
|
1436
|
+
if (opts.cut && name === "default") return `"${path}" is the clone: it stays as the attach left it; cut a sibling`;
|
|
1156
1437
|
const slots = Object.keys(ambient.repos);
|
|
1157
|
-
if (
|
|
1158
|
-
|
|
1159
|
-
if (cwd === undefined) {
|
|
1160
|
-
throw new Error(
|
|
1161
|
-
`command "${slot}" names Repo Slot "${parts.slot}", which its Workspace does not carry ` +
|
|
1162
|
-
`(${slots.join(", ") || "none"})`,
|
|
1163
|
-
);
|
|
1164
|
-
}
|
|
1165
|
-
return { cwd };
|
|
1438
|
+
if (!slots.includes(slot)) {
|
|
1439
|
+
return `"${path}" names Repo Slot "${slot}", which its Workspace does not carry (${slots.join(", ") || "none"})`;
|
|
1166
1440
|
}
|
|
1167
|
-
|
|
1168
|
-
if (slots.length === 1) return { cwd: ambient.repos[slots[0]!]! };
|
|
1169
|
-
throw new Error(
|
|
1170
|
-
`command "${slot}" runs under a Workspace carrying more than one Repo Slot (${slots.join(", ")}) and names ` +
|
|
1171
|
-
`none — say which Worktree it runs in: command(argv, { slot: "<slot>" }) (ADR-0051)`,
|
|
1172
|
-
);
|
|
1441
|
+
return undefined;
|
|
1173
1442
|
}
|
|
1174
1443
|
|
|
1175
1444
|
/** Ask again on a transport failure, with backoff, until it answers or the Effect stops. For a
|