command
Function, in @jr2/orchestrator/src/harness-client.ts. It is an export of @jr2/orchestrator, on the Authoring surface page, in Effects.
Overload 1
function command<
const A extends readonly (string | Arg)[],
const Opt extends CommandOptions = CommandOptions,
>(
argv: A,
options?: Opt,
): EffectLogic<CommandInput<A, Opt["pin"]>, CommandOutput> & OpenOf<EnvOpenParts<Opt>>Make a command Effect: an actor slot that runs one program in the Workspace's Sandbox when a state invokes it. Put it under actors and invoke it by its key: actors: { tests: command(["node", "--test"]) }, then invoke: { src: "tests", onDone: … }.
argv is a list, never shell text: each entry is one argument. An arg("name") entry is filled from the invoke's input (input: { name: "…" }), as one argument whatever its text. To use a shell, write it: ["sh", "-c", "…"].
Where it runs:
- With no Effect secret in
env, in the Harness container of the enclosing Workspace's Sandbox. It runs in the Worktree of a Repo Slot, or in/workin a Workspace with no Repo Slot. The Agent's Working tools also run in this container. - With an Effect secret in
env, in the Sandbox Effector, against a commit of the branch. This is the branch tip when it starts, or withpin: truethe commit that the invoke'sinput.shanames. The output then saysdirty: truewhen the Worktree held edits that the command did not see.
Every outcome is output for onDone, never an error: { outcome: "exited", code, tail }, { outcome: "timeout", tail }, { outcome: "killed", signal, tail }, or { outcome: "unknown", reason } when jr2 cannot know what happened. tail is the end of the output stream.
After an Orchestrator restart:
- A command with no Effect secret in
envnever runs twice. jr2 attaches to it again, or settlesunknown. - A command with an Effect secret in
envruns again when the Sandbox Effector no longer holds it. It runs against the same commit. If jr2 has no commit for it, it settlesunknown.
When the state exits, the command stops: jr2 kills its process group. onError is only for a Machine or config defect.
It refuses, when you call it:
- an empty
argv:command(argv): argv is a non-empty list — each entry one argument, never shell text - an entry that is not a string or
arg():command(argv): an argv entry is a string or arg("name"), got <entry> - a
timeoutthat is not a positive duration:command: timeout "<timeout>" is not a positive duration (like "90s", "10m", "1h") - an
envname that is not a variable name, orPATH,HOMEorTMPDIR:command(argv, { env }): "<name>" is not a variable the author sets — PATH, HOME and TMPDIR are the Effector's (ADR-0067) pin: truewith no Effect secret inenv:command(argv, { pin: true }) names no secret in its env: `pin` chooses the commit a credentialed command runs against in the Sandbox Effector, and a command that names no secret runs in the live Worktree (ADR-0067) — name its secret, command(argv, { env: { NAME: secret("x") }, pin: true }), or drop `pin`
And when it starts, with no Effect secret in env:
- outside a
workspace():command "<slot>" runs in the Harness container of a Workspace, and its state is not inside a workspace() — invoke it from a Workspace's body (ADR-0067) - no
slotunder a Workspace with more than one Repo Slot:command "<slot>" runs under a Workspace carrying more than one Repo Slot (<slots>) and names none — say which Worktree it runs in: command(argv, { slot: "<slot>" }) (ADR-0051) - a
slotthat the Workspace does not have:command "<slot>" names Repo Slot "<name>", which its Workspace does not carry (<slots>) - an
arg()with no string ininput:command "<slot>": argv slot <<name>> has no string value in its input (got <type>) — map it in the invoke's input. The angle brackets around the arg's name are in the text:argv slot <file>.<type>isnothingwheninputhas no such field, or the type of its value, such asnumber.
And when it starts, with an Effect secret in env:
- outside a
workspace():command "<slot>" runs in the Sandbox Effector of a Workspace, and its state is not inside a workspace() — invoke it from a Workspace's body (ADR-0067) - no
slotunder a Workspace with more than one Repo Slot:command "<slot>" runs under a Workspace carrying more than one Repo Slot (<slots>) — say which: { slot: "<slot>" } (ADR-0051) - a Workspace with no Repo Slot:
command "<slot>" runs under a Workspace with no Repo Slot: a command with an Effect secret runs against one commit of a Repo Slot's branch, and there is none — declare a Repo Slot, or name no secret in its env, and it runs in /work beside the Agent (ADR-0051).jr2 uprefuses it first, with the same reason - a
slotthat the Workspace does not have:command "<slot>" names Repo Slot "<name>", which its Workspace does not carry (<slots>) pin: trueand aninput.shathat is not a full commit id:command "<slot>" (pin): its input's sha is not a full commit id- an
arg()with no string ininput: the same text as above.
Parameters
argv: The program and its arguments: strings andarg("name")entries. Oropen, in a Machine that you publish as a package, for the Instance that registers it to bind withcustomize().options(CommandOptions): Optional.slot: the Repo Slot whose Worktree the command runs in; needed only where the Workspace has more than one.timeout:"90s","10m"or"1h", default"10m"; when it expires, the output istimeout.tail: how many characters of the end of the output stream to keep, default 4096.env: environment variables. Each value issecret("name")for the value of an Effect secret, orsecret("name").pathfor the path of a file that holds it.pin:trueto run against the commit ininput.sha. Usepinonly with an Effect secret inenv.
Returns: The actor logic of the Effect's slot. Its output is a CommandOutput.
Example
import { z } from "zod";
import { arg, command, fail, jr2Setup, workspace, type Workspaced } from "@jr2/orchestrator";
const runInput = z.object({ branch: z.string() });
type BodyInput = Workspaced<z.infer<typeof runInput>, "app">;
const body = jr2Setup({
types: {} as { input: BodyInput },
actors: {
tests: command(["node", "--test", arg("file")], { timeout: "5m" }),
fail: fail(),
},
}).createMachine({
id: "check",
on: { "workspace.lost": ".lost" },
initial: "testing",
states: {
testing: {
invoke: {
src: "tests",
input: { file: "test/greet.test.js" },
onDone: [
{
guard: ({ event }) => event.output.outcome === "exited" && event.output.code === 0,
target: "passed",
},
{ target: "failed" },
],
},
},
passed: { type: "final" },
// Not final states: each one fails the run, and the run ends `failed`.
failed: { invoke: { src: "fail", input: { reason: "the test did not pass" } } },
lost: { invoke: { src: "fail", input: { reason: "the Sandbox is gone" } } },
},
});
export const machine = workspace(body, {
input: runInput,
repos: { app: "https://github.com/you/app.git" },
spec: ({ input }) => ({ branch: input.branch }),
});See also
- Effects
- Run a command or call a URL with an Effect
- ADR-0067: An Effect is an actor slot, and runs at most once unless marked idempotent
- ADR-0068: An Effector holds an Effect's secrets and signs its wire
Overload 2
function command<
const Opt extends CommandOptions = CommandOptions,
>(
argv: typeof open,
options?: Opt,
): EffectLogic<OpenCommandInput<Opt["pin"]>, CommandOutput> & OpenEffect<"argv" | EnvOpenParts<Opt>>Make a command Effect whose argv is Open: command(open, options), in a Machine that you publish as a package. The Instance that registers the Machine binds the argv one time: customize(machine, { effects: { <slot>: { argv } } }). command tells the rest, and what it refuses.
Parameters
argv:open.options(CommandOptions): Optional. The same options as for a written argv.
Returns: The actor logic of the Effect's slot. Its output is a CommandOutput.
See also