HTTP API
The Orchestrator's HTTP API: what the jr2 CLI, the Console, a webhook and the Harness call. Each route is in one access band, by the credential it asks for (ADR-0014). Send a token as Authorization: Bearer <token>. An error answer is { error }, with the status the route gives. A path segment :name is a parameter; :name{regex} is a parameter that must match the regex.
Bands: Open, Authenticated, Instance token only, Publish token only. The types the bodies and answers name are in Types.
Open
No token. Structure and observation: no run context, and no control.
GET /healthz
- Summary: Liveness, and what this instance is: its Kit version and image content hash.
- Response: { ok: true, version, hash? }
- Stream: no
- Decisions: ADR-0009, ADR-0019
GET /readyz
- Summary: Readiness.
- Response: { ready: true }
- Stream: no
- Decisions: ADR-0009
GET /workflows
- Summary: The Workflows a run can start, each with its origin, and each Chart wrapper's named Charts in its entry. A named Chart that cannot run now says why.
- Response:
WorkflowListing[] - Stream: no
- Decisions: ADR-0014, ADR-0070
GET /assembly
- Summary: The current Assembly and the Assemblies that live runs hold.
- Response:
AssemblyView - Stream: no
- Decisions: ADR-0069
GET /workflows/:wrapper/chart-schema
- Summary: The Chart grammar, narrowed to the Open body of one Chart wrapper.
- Response: JSON Schema
- Stream: no
- Decisions: ADR-0070
GET /workflows/:name/runs/:runId/machine
- Summary: The Machine that a run binds, with each bound Chart compiled in, which the Console draws.
- Response:
MachineDoc - Stream: no
- Decisions: ADR-0070
GET /
- Summary: The Console page to a browser; a 404 to a JSON caller.
- Response: text/html (the Console shell)
- Stream: no
- Decisions: ADR-0032
GET /workflows/:name
- Summary: The Console page to a browser; to a JSON caller, the workflow and its declared input schema.
- Response: { name, machineId, input: JSON Schema | null }, or text/html
- Stream: no
- Decisions: ADR-0032, ADR-0033
GET /workflows/:name/machine
- Summary: The registered template Machine's structure, which the Console draws.
- Response:
MachineDoc - Stream: no
- Decisions: ADR-0014
GET /workflows/:name/runs
- Summary: The live runs of one workflow, as observations: identity and state value, no context.
- Response:
RunObservation[] - Stream: no
- Decisions: ADR-0014
GET /workflows/:name/events
- Summary: The activity of every run of one workflow, as observations, on one connection.
- Response: SSE: runs, status, emit (type only), retry, gone
- Stream: yes: Server-Sent Events
- Decisions: ADR-0014, ADR-0022
GET /workflows/:name/runs/:runId/events
- Summary: One live run's activity, as observations, until it settles.
- Response: SSE: status, emit (type only), retry
- Stream: yes: Server-Sent Events
- Decisions: ADR-0014, ADR-0022
GET /assets/style.css
- Summary: The Console's stylesheet.
- Response: text/css
- Stream: no
- Decisions: ADR-0032
GET /assets/elk.js
- Summary: The Console's graph layout library (the elkjs bundle).
- Response: text/javascript
- Stream: no
- Decisions: ADR-0032
GET /assets/:file{[\w.-]+\.ts}
- Summary: A Console source module, with its types erased.
- Response: text/javascript
- Stream: no
- Decisions: ADR-0034
GET /assets/components/:file{[\w.-]+\.ts}
- Summary: A Console view module, with its types erased.
- Response: text/javascript
- Stream: no
- Decisions: ADR-0034
GET /assets/vendor/:file
- Summary: Preact's browser ESM, which the Console's import map names.
- Response: text/javascript
- Stream: no
- Decisions: ADR-0034
Authenticated
Any token jr2 minted: the Instance token, or a Sandbox token. A Sandbox token reaches only what its own Sandbox owns.
GET /agents/:instanceId/surface
- Summary: An Agent's Menu and this Turn's Allowed picks. A Sandbox token reads only its own Sandbox's.
- Response:
AgentSurfaceView - Stream: no
- Decisions: ADR-0013, ADR-0029
POST /agents/:instanceId/events
- Summary: Deliver an Agent's pick into the state that invoked it.
- Body: { type, ...payload }, checked against the pick's input schema
- Response:
AgentDeliveryReceipt; 409 { refused: "outstanding", outstanding?, allowed } - Stream: no
- Decisions: ADR-0013, ADR-0024, ADR-0029
GET /agents/:instanceId/deliveries/:deliveryId
- Summary: Long-poll the answer to an outstanding pick; 503 means ask again.
- Response:
AgentDeliveryReceipt - Stream: no
- Decisions: ADR-0013, ADR-0024
POST /sandboxes/:name/fetch
- Summary: Ask the node cache to fetch one Repo that this Sandbox mounts. A Sandbox token asks only for its own.
- Body: { identity }
- Response:
FetchAnswer: { fetched } | { stale, asOf } - Stream: no
- Decisions: ADR-0053
Instance token only
The Instance token alone: run control, starting a run, and full run state. A Sandbox token is refused.
PUT /workflows/:wrapper/charts/:name
- Summary: Admit and keep a runtime Chart under a name. Its Workflow is
<wrapper>/<name>. - Body: the Chart text alone
- Response: 201 { name, origin, hash } when new, 200 when it replaced one; 422 { error, report } when refused
- Stream: no
- Decisions: ADR-0070
DELETE /workflows/:wrapper/charts/:name
- Summary: Remove a runtime Chart. A live run of it keeps its own text.
- Response: 204
- Stream: no
- Decisions: ADR-0070
POST /workflows/:wrapper/charts/:name/runs
- Summary: Start a run of a named Chart, as
POST /workflows/<wrapper>%2F<name>/runsdoes. - Body: the Chart wrapper's input, without the Chart
- Response: 201 { runId, instanceId }; 409 when the named Chart cannot run now
- Stream: no
- Decisions: ADR-0070
POST /workflows/:name/runs
- Summary: Start a run of a registered workflow. A
dispatchfrom another Orchestrator sends the headerjr2-dispatched-by, and a repeated start with its key answers the run that it started. - Body: the workflow's input, checked against its declared input schema
- Response: 201 { runId, instanceId }; 200 { runId, instanceId, existing: true } for a repeated dispatch; 422 { error, report } when the door refuses a Chart in the input
- Stream: no
- Decisions: ADR-0033, ADR-0058, ADR-0070, ADR-0071
GET /runs
- Summary: The live runs, with their full status.
- Response:
RunStatus[] - Stream: no
- Decisions: ADR-0009, ADR-0014
GET /repos
- Summary: The Repos as the cluster reports them.
- Response: { dataPlane, repos:
RepoStatus[] } - Stream: no
- Decisions: ADR-0048, ADR-0051
GET /runs/resolve
- Summary: The run ids that start with a prefix (
?prefix=, at least 4 characters). - Response: { prefix, runIds: string[], truncated }
- Stream: no
- Decisions: ADR-0009
GET /runs/:runId
- Summary: One run's status, live or settled, with its open Gates.
- Response:
RunStatus& { gates:GateView[] } - Stream: no
- Decisions: ADR-0009, ADR-0011
POST /runs/:runId/gates/:gate/events
- Summary: Deliver an event to an open Gate of a run.
- Body: { type, ...payload }, checked against the Gate's schema for that event
- Response: { ok: true }; 409 { error, acceptsNow } when the run does not take it now
- Stream: no
- Decisions: ADR-0011, ADR-0013
GET /runs/:runId/events
- Summary: One run's full activity until it settles; a settled run's final status, once.
- Response: SSE: status, emit, retry, admission, pick, placing, placed, child-error, effect, gone
- Stream: yes: Server-Sent Events
- Decisions: ADR-0022, ADR-0023, ADR-0064, ADR-0067
POST /runs/:runId/events
- Summary: Cancel a run: it ends, and its Workspaces go unless
keepis true. - Body: { type: "CANCEL", keep?: boolean }
- Response: { ok: true }
- Stream: no
- Decisions: ADR-0002, ADR-0025
Publish token only
The publish token alone, which jr2 up reads from the instance Secret. The Instance token is refused, because a publish puts code into the Orchestrator.
PUT /assembly
- Summary: Publish the next Assembly.
jr2 upcalls it, and nothing else. A refused Assembly leaves the current one current. - Body: the Assembly as a tar stream, with the headers
jr2-kit-versionandjr2-assembly-hash - Response: 200 with the Assembly, its Workflows, the held Assemblies and the ones collected; 400, 409 or 422 with the reason
- Stream: no
- Decisions: ADR-0069
Types
WorkflowListing
type WorkflowListing = {
name: string;
origin: "machine" | ChartOrigin;
wrapper?: {
body: string;
oneShot: boolean;
charts: Array<{ name: string; origin: ChartOrigin; unrunnable?: string }>;
};
unrunnable?: string;
};One entry of GET /workflows (ADR-0070): a Workflow and where it came from. A Chart wrapper's entry carries its Open body, whether its door takes a Chart (oneShot), and its named Charts.
Fields
name:stringorigin:"machine" | ChartOriginwrapper(optional):objectbody:stringoneShot:booleancharts:Array<{ name: string; origin: ChartOrigin; unrunnable?: string; }>
unrunnable(optional):string
AssemblyView
type AssemblyView = {
kitVersion: string;
current: { hash: string; kitVersion: string; workflows: string[] } | null;
held: HeldAssembly[];
};GET /assembly's body.
Fields
kitVersion:stringcurrent:{ hash: string; kitVersion: string; workflows: string[]; } | nullheld:HeldAssembly[]
MachineDoc
type MachineDoc = MachineBodyDoc & {
workflow: string;
opaqueStates?: string[];
chartsNotDrawn?: Array<{ path: string; src: string; reason: string }>;
};The serialized structure of a workflow's Machine.
Fields: each field of MachineBodyDoc, and these:
workflow:stringopaqueStates(optional):string[]States whose child list may be incomplete — see
opaqueStates. Computed once here, on the server, so the notice reaches whoever renders the doc without every consumer re-walking it to discover the diagram is lying. Absent on the nestedMachineBodyDocs: it is a whole-doc question, and the root's walk already covers them.chartsNotDrawn(optional):Array<{ path: string; src: string; reason: string; }>A run's doc only (
runMachine, ADR-0070): each bound Chart the doc could not draw, so its Open body shows the slot's own subgraph — the body's path in the run, its slot, and why (a stored Chart the door no longer admits is drift, D-B). Absent when every bound Chart is drawn.
RunObservation
type RunObservation = {
runId: string;
workflow: string;
status: string;
value: unknown;
children: RunChild[];
bound?: true;
chartHashes?: string[];
dispatchedBy?: Omit<DispatchedBy, "effect">;
dispatched?: Array<{ slot: string; run: string }>;
};A run as an UNAUTHENTICATED observer may see it: which run, of what workflow, and where in the Machine it is. That is the whole set — enough to light up a state in the Console, and nothing more.
What is absent is the point. context is the workflow's working data (branch names, ticket bodies, review verdicts, Harness endpoints) and instanceId/fault name live infrastructure and leak error text; all of it is STATE, which ADR-0013 guards behind the Instance token. value is a tree of state KEYS — it is structure, and structure is already public (/workflows/:name/machine serves the whole Machine). So an observer learns nothing here it could not read from the Machine doc, except which states are lit.
children extends that to the child machines (which is where a run mostly lives), and it does NOT widen the line: a RunChild is keys and ids by construction, with no context field at any depth. The one thing it adds is the spawn ids, which are the workflow's own labels for its parallel work ("F-1") — the same class of thing as a state key.
Fields
runId:stringworkflow:stringstatus:stringvalue:unknownchildren:RunChild[]bound(optional):trueThe run binds a Chart (ADR-0070): the Console draws
GET /workflows/:name/runs/:runId/machine. Structure, likevalue.chartHashes(optional):string[]The hashes of the Charts it binds now (see
RunStatus.chartHashes). Structure: a hash of a Chart's text, whichrunMachinealready serves on the open band.dispatchedBy(optional):Omit<DispatchedBy, "effect">The run that dispatched this one, its slot, and the chain's depth (ADR-0071). Routing — two ids, a slot name and a number — the class of thing
runIdandvalueare, so ADR-0014's line is unmoved. The idempotency key is not: it names the caller's ledger, which is the caller's.dispatched(optional):Array<{ slot: string; run: string; }>The runs it dispatched, by slot (ADR-0071): the same routing as
dispatchedBy, the other direction.
RunStatus
type RunStatus = RunRecord & {
status: string;
value: unknown;
context: unknown;
children: RunChild[];
waiting: RunWaiting[];
held: RunHeld[];
fault?: string;
reason?: string;
assembly?: string;
bound?: true;
chartHashes?: string[];
output?: unknown;
dispatched?: Dispatched[];
};A run's current observable state. fault carries the error message when status is "error" (e.g. a gate invoked with a name outside the workflow's manifest — ADR-0011). A run that ends "failed" (ADR-0073) has no fault: its output is the Failure's { reason, detail? }.
Fields: each field of RunRecord, and these:
status:stringvalue:unknowncontext:unknownchildren:RunChild[]The live child machines beneath the root — where most of a run actually is.
waiting:RunWaiting[]Every Workspace waiting for capacity, and why (ADR-0064). Behind the token: the reason is the scheduler's or the quota's own words, which can name nodes and taints, so
observenever carries it — the open band sees only that a Workspace is inplacing.held:RunHeld[]Every pick of this run that is outstanding (ADR-0024): the Agent, its pick, and since when — the Console's "held since". There is no default time bound on a held Turn; this is how a human sees one. Behind the token, like
context: a pick names what the Agent did.fault(optional):stringreason(optional):stringWhy the HOST set this status, for the statuses the Machine did not choose — today
drifted(ADR-0030). Distinct fromfault, which is a running actor's own error; this is the store's account of a run it declined to resume, and until it was surfaced here nothing on any HTTP route could read it.assembly(optional):stringThe hash of the Assembly the run started under (ADR-0069). Absent for a run no Assembly started.
bound(optional):trueThe run binds at least one Chart (ADR-0070): its Machine is the bound one,
runMachine.chartHashes(optional):string[]The hashes of the Charts the run binds now, distinct and sorted (ADR-0070). A run can bind a second Chart in its life (a planner whose first Chart reaches a ceiling picks again), so these, not the run id, say which bound Machine
runMachinedraws. Absent when it binds none.output(optional):unknownThe Machine's output, for a run that is no longer live (ADR-0071): what a
dispatchwithwaitsettles with, and whatGET /runs/<id>serves its foreign caller. Never for a live run, whose Machine has none yet. For afailedrun, the Failure's{ reason, detail? }(ADR-0073), not a value of the Machine's output type. Behind the token, likecontext: it is the workflow's data.dispatched(optional):Dispatched[]The runs this run's
dispatchEffects started, oldest first (ADR-0071): the chain's other direction, read off the caller's own ledger rows. A foreign dispatch's run is a run of the Orchestrator its target names. Absent when it started none.
RepoStatus
type RepoStatus = {
key: string;
url: string;
identity?: string;
bound: boolean;
lastAttached?: string;
nodes: RepoNodeState[];
};One Repo resource as the instance reports it (GET /repos).
Fields
key:stringThe cache key: the resource's name, the hostPath leaf,
/repos/<key>.url:stringThe Binding's own spelling — what the cache clones.
identity(optional):stringbound:booleanA registered Machine binds it — never evicted by
jr2 gc.lastAttached(optional):stringWhen a run last attached it — the eviction clock for a Repo nothing binds. Absent while only the boot has stated it: a bound Repo no run has attached yet.
nodes:RepoNodeState[]
GateView
type GateView = {
gate: string;
path: string[];
accepts: Array<{ name: string; description?: string; input: unknown }>;
acceptsNow: string[];
meta?: Record<string, unknown>;
};One open gate as external callers discover it (GET /runs/:id — ADR-0011): the accepted events with their input schemas as JSON Schema (what drives a form or a jr2 send prompt), plus the workflow-supplied meta (what a UI renders and a webhook translator matches on).
Fields
gate:stringpath:string[]The invoking state's actor path below the run root — where the gate lives in the Machine (what the Console's "parked here" pin resolves against its scope tree), stable whether the id was authored ("F-12") or derived. Never parse
gatefor this.accepts:Array<{ name: string; description?: string; input: unknown; }>acceptsNow:string[]The names of
acceptsthe run would take from this Gate NOW, payload-blind (ADR-0011, ADR-0029): a send of another name is refused 409.acceptsstays whole — it drives the forms.meta(optional):Record<string, unknown>
AgentSurfaceView
type AgentSurfaceView = {
instanceId: string;
runId: string;
sandbox?: string;
menu: SurfaceEvent[];
allowed: string[];
};One live agent surface, as the Harness reads it through its Custodian (GET /agents/:iid/surface — ADR-0013, ADR-0029). Two answers, because they change at different rates:
menuis the Agent's MENU — every pick any state invoking this Agent offers, in a fixed order. The Harness presents it as the tools block, which is then byte-identical for every Turn of the conversation and never breaks the provider's prompt cache.allowedis this Turn's ALLOWED PICKS — the invoking state's derived set, narrowed to what its guards would accept now, in Menu order. The Harness states it in the Briefing's Turn part, after the Frame's prompt (ADR-0062). Enforced at delivery, not here.
sandbox is the Sandbox that may deliver here. It is not a secret from that pod (the pod IS the sandbox), and serving it lets a caller fail loudly on a surface that is not its own.
Fields
instanceId:stringrunId:stringsandbox(optional):stringmenu:SurfaceEvent[]allowed:string[]
AgentDeliveryReceipt
type AgentDeliveryReceipt = {
outstanding?: true;
since?: string;
answered?: true;
straightBack?: true;
answer?: PickAnswer;
stopped?: true;
delivered: true;
event: string;
moved: boolean;
turnComplete: boolean;
allowed: string[];
deliveryId: string;
};The answer to one Agent delivery (POST /agents/:iid/events) — a receipt that DESCRIBES ITSELF (ADR-0024). The Harness renders it as prose, because a bare deliveryId told the Agent nothing about whether it was finished, and the model answered that silence by calling again.
turnComplete is the HINT, never the guarantee (the abort on invocation end is): it is read off the registration table right after delivery, so it says whether the state that asked for this turn has stopped waiting. It fails conservatively — anything that made the read unreliable reads false, which is today's behavior, never a false claim.
deliveryId keeps the outcome ADDRESSABLE after the fact (ADR-0013): a pick the workflow is still running when the POST answers is OUTSTANDING (outstanding: true), and its answer is read at GET /agents/:iid/deliveries/:id — the same shape, answered: true, once the workflow is back at Rest in the Agent's state or has left it (ADR-0024).
Fields
outstanding(optional):trueThe workflow is running the pick: the answer comes on the delivery's poll.
allowedis empty: no other pick is taken until it answers.since(optional):stringISO time the pick was delivered — with
outstanding.answered(optional):trueThis receipt is a pick's answer, read at Rest (or once the Turn left the Agent's state): an outstanding pick's, a reply inside the pick's own send, or a guard straight back with no reply.
straightBack(optional):trueWith
answered: a guard straight back (ADR-0024) — the pick entered a state below the Agent's and came back inside its own send, nothing replying. The one answer that says "moved; nothing ran"; an outstanding pick's answer never carries it.answer(optional):PickAnswerThe last reply since the pick (ADR-0024); absent with
answered: no reply came.stopped(optional):trueWith
answeredandturnComplete: something other than the pick's route left the Agent's state while the pick's work ran, and that exit stopped the work before it finished.answer, if any, is an earlier link's reply, kept (ADR-0024).delivered:trueevent:stringThe event delivered — the Agent reads its own pick back, by name.
moved:booleanA transition accepted it. False means the pick was well-formed, arrived, and moved nothing — every transition for it was guarded false in the current state (ADR-0029). Distinct from
turnComplete: a pick can move the Machine WITHIN the invoking state, which ismoved: true, turnComplete: false. Before this existed the two were indistinguishable, so an Agent whose pick a guard rejected was told the workflow was "still in the state that asked", and its only move was to call again.turnComplete:booleanThe invoking state stopped waiting: this Agent's turn is over.
allowed:string[]The Allowed picks read AFTER the delivery (ADR-0029): what this Turn may pick now — narrowed by a pick that moved the Machine within the invoking state, empty once the Turn is over. The Harness renders it ("Allowed now:
finish"); it does not pre-check against it.deliveryId:string
FetchAnswer
type FetchAnswer = { fetched: string } | { stale: string; asOf: string | null };What one ask settles as. fetched is the landing's timestamp; stale is git's own words (or this port's, when the budget ran out) beside the time the cache's objects are as of — null when nothing has ever fetched it, which the program prints as "an unknown time".
Forms: it is one of these objects.
Form 1:
fetchedfetched:string
Form 2:
stale,asOfstale:stringasOf:string | null
ChartOrigin
type ChartOrigin = "file" | "runtime";Where a named Chart came from (ADR-0070): the Assembly's own file, or a PUT at run time.
HeldAssembly
type HeldAssembly = { hash: string; kitVersion: string; runs: string[] };An Assembly the process holds (ADR-0069), as GET /assembly lists it: the current one, and every one a live run holds, each with the runs that hold it.
Fields
hash:stringkitVersion:stringruns:string[]
MachineBodyDoc
type MachineBodyDoc = {
id: string;
root: MachineStateDoc;
transitions: MachineTransitionDoc[];
events: MachineEventDoc[];
seats: MachineSeat[];
warnings: string[];
};One Machine's structure, independent of what NAMES it (a workflow at the root, a src below).
Fields
id:stringMachine id.
root:MachineStateDoctransitions:MachineTransitionDoc[]events:MachineEventDoc[]The events THIS Machine declares, and only this one (ADR-0049): a nested Machine's ride its own
MachineBodyDoc. Empty for a Machine not built byjr2Setup.seats:MachineSeat[]Who may send what, per state (ADR-0015): one entry per party on each state that invokes one — the audience rule's answer for this Machine, as
jr2 upprints it. Empty for a Machine not built byjr2Setup.warnings:string[]What the build found legal and suspect (ADR-0015) — a mode nothing enters, a pick a Gate above takes from the Agent below, one name written for two parties on one path.
RunChild
type RunChild = { id: string; src: string; status: string; value: unknown; children: RunChild[] };One live child MACHINE of a run, and its own children below it. A workflow's root machine is usually a coordinator — coding spawns a featureWorkspace per feature and the real work happens in there — so the root's value alone says almost nothing about where a run IS. This is the rest.
Context-free BY CONSTRUCTION: there is no context field to forget to redact, which is what lets observe() pass the whole tree through to unauthenticated observers untouched. src is the join key (ChildMachineDoc); id is the spawn id ("F-1"), which is how two live instances of the same child machine are told apart.
Fields
id:stringsrc:stringstatus:stringvalue:unknownchildren:RunChild[]
DispatchedBy
type DispatchedBy = { run: string; slot: string; effect: string; depth: number; instance?: string };Which run dispatched a run (ADR-0071): the caller's run id, the slot of its dispatch Effect, and that Effect's ledger row id, which is the idempotency key — a start that carries a key some run already carries answers that run. depth is the dispatched run's depth in the chain, one below its caller, stated by the caller and recorded as sent: one meaning on the wire, on the binding and on the row. instance is set for a foreign caller, whose Orchestrator this one cannot verify: opaque text, never resolved.
Fields
run:stringslot:stringeffect:stringdepth:numberinstance(optional):string
RunRecord
type RunRecord = { runId: string; workflow: string; instanceId: string; dispatchedBy?: DispatchedBy };The serializable identity of a run — what reconcile sees and what restore rebuilds from. dispatchedBy is the run that started it through a dispatch Effect, and this run's depth in that chain, as the caller sent it (ADR-0071); absent for a run a human or a client started.
Fields
runId:stringworkflow:stringinstanceId:stringdispatchedBy(optional):DispatchedBy
RunWaiting
type RunWaiting = { child: string; on: "node" | "quota"; message: string; since: string };One Workspace of a run waiting for capacity (ADR-0064): its Sandbox has no node. child is the Workspace's actor path below the run root — the RunChild ids down to it, joined by /, and "" when the run's root IS the Workspace. on is what it waits for: a node from the scheduler, or room under the cluster owner's quota; message is the scheduler's or the API server's own words; since (ISO) is when the wait began, kept across a changed reason and a restart.
Fields
child:stringon:"node" | "quota"message:stringsince:string
RunHeld
type RunHeld = { agent: string; pick: string; since: string };One outstanding pick, as RunStatus.held reports it: the Agent slot, its pick, and the ISO time it was delivered.
Fields
agent:stringpick:stringsince:string
Dispatched
type Dispatched = { slot: string; run: string; at: string };One run a dispatch Effect of this run started (ADR-0071): its slot, the run, and when the ledger row was written.
Fields
slot:stringrun:stringat:string
RepoNodeState
type RepoNodeState = {
node: string;
present: boolean;
synced: boolean;
attempted?: "Probe" | "Clone" | "Fetch";
lastAttempt?: string;
lastFetched?: string;
lastError?: string;
};One node's view of a Repo, as the cache agent reports it on the resource's status.
Fields
node:stringpresent:booleanA clone exists on this node.
synced:booleanThe last attempt — a probe, a clone, or a fetch — succeeded.
attempted(optional):"Probe" | "Clone" | "Fetch"Which of the three the last attempt was. A failed Probe is
jr2 status's signal for a Repo no pod on the node mounts yet; only a failed Clone fails a Sandbox waiting on that node.lastAttempt(optional):stringlastFetched(optional):stringThe last successful clone or fetch.
lastError(optional):stringgit's own words; absent when synced.
SurfaceEvent
type SurfaceEvent = {
name: string;
description?: string;
input: unknown;
semantics: EventSemantics;
briefing?: string;
};One pick on the surface. briefing marks a pick whose parameter is a Chart: jr2's text on how a Chart runs (CHART_BRIEFING, ADR-0070), which the Harness says in the Turn part when the pick is Allowed, so only the Turns that may write a Chart carry it.
Fields
name:stringdescription(optional):stringinput:unknownsemantics:EventSemanticsbriefing(optional):string
PickAnswer
type PickAnswer =
| { kind: "event"; event: string; sender?: { party: "agent" | "gate"; id: string }; payload: Record<string, unknown> }
| { kind: "output"; actor: string; output: unknown }
| { kind: "answer"; value: unknown };What an outstanding pick's answer is (ADR-0024): the LAST reply from below the Agent's state since the pick — a party's event (a Gate's answer, a nested Agent's pick) with its payload, or a child's output (a child Machine, an Effect stand-in) — named by who sent it. A child's error is no reply (a defect, ADR-0024). Absent where nothing replied: the answer carries no reply, and says nothing of whether work ran. Plain data; the Harness writes the words. answer: the value the Machine's answer action set (ADR-0067), which replaces the default.
Forms: it is one of these objects.
Form 1:
kind,event,sender,payloadkind:"event"event:stringsender(optional):objectparty:"agent" | "gate"id:string
payload:Record<string, unknown>
Form 2:
kind,actor,outputkind:"output"actor:stringoutput:unknown
Form 3:
kind,valuekind:"answer"value:unknown
MachineStateDoc
type MachineStateDoc = {
id: string;
key: string;
type: "atomic" | "compound" | "parallel" | "final" | "history";
initial?: string;
invoke: Array<{ id: string; src: string; effect?: EffectDoc; fail?: true }>;
tags: string[];
description?: string;
states: MachineStateDoc[];
children: ChildMachineDoc[];
opaqueActions?: boolean;
};One state of the Machine; states nests in document order.
Fields
id:stringUnique state id (custom
id:respected).key:stringRelative key — the segment that appears in a run's
status.value.type:"atomic" | "compound" | "parallel" | "final" | "history"initial(optional):stringInitial child state id (compound only).
invoke:Array<{ id: string; src: string; effect?: EffectDoc; fail?: true; }>Each invoke's id and slot; an Effect's carries its plain-data parts (ADR-0067) — what it runs, where, and for how long, read without running anything.
fail: the slot is afail(), so this state can fail the run (ADR-0073).tags:string[]description(optional):stringstates:MachineStateDoc[]children:ChildMachineDoc[]The child MACHINES this state runs — invoked ones (a subset of
invoke, which stays the complete invocation list) plus spawned ones (which appear nowhere else). SeeChildMachineDoc. A promise/callback actor has no state tree, so it is never here.opaqueActions(optional):booleanThis state runs an
enqueueActionsclosure, so THIS STATE'SchildrenMAY BE INCOMPLETE.Spawning is an action, and
spawnChildis the only static trace it leaves (seespawnedSrcs). A spawn issued asenqueue.spawnChild(...)inside a closure resolves at runtime — it can be conditional, looped, or have a computedsrc— so no amount of static analysis recovers it, and the subgraph beneath it would simply be absent from the diagram while the machine ran correctly. That silence is the hazard, not the omission: it once erased the whole feature pipeline of a multi-stage Machine with no signal at all. So the doc carries the fact it cannot see, and callers say so out loud (opaqueStates, surfaced on the page).
MachineTransitionDoc
type MachineTransitionDoc = {
source: string;
targets: string[];
event: string;
label: string;
guard?: string;
description?: string;
kind: "event" | "always" | "after" | "done" | "error";
};One transition of the Machine, id-addressed at both ends.
Fields
source:stringSource state id.
targets:string[]Target state ids; empty = targetless (self/internal) transition.
event:stringThe raw event descriptor (
""for always/eventless).label:stringDisplay label, precomputed here so the renderer stays dumb.
guard(optional):stringNormalized guard name, if guarded.
description(optional):stringxstate's own transition
description, as authored: what this writing does, where one name has writings that differ (ADR-0015's "one def, one meaning").kind:"event" | "always" | "after" | "done" | "error"
MachineEventDoc
type MachineEventDoc = {
name: string;
description?: string;
audience?: EventAudience;
input: unknown;
};One event a Machine declares — its Vocabulary entry (ADR-0011), as the doc carries it. Same shape a Gate's accepts serves, because it is the same thing seen statically: the name, what a caller sends (JSON Schema), and who may send it.
Fields
name:stringdescription(optional):stringaudience(optional):EventAudienceAbsent: each writing's state decides who may send it (ADR-0015) — see the body's
seats.input:unknownThe def's input schema as JSON Schema — what a form or a
jr2 sendprompt generates from.
MachineSeat
type MachineSeat = { state: string; party: "agent" | "gate"; agent?: string; names: string[] };One party's seat on one state: the state's id, the party, and the names it may send there — for an Agent the derived set its guards narrow; for a Gate its accepted set, unless the Gate's input names accepts: itself.
Fields
state:stringparty:"agent" | "gate"agent(optional):stringnames:string[]
EventSemantics
type EventSemantics = "ack" | "poll";How the party that sends an event gets its answer.
"ack", the default: jr2 delivers the event and answers the sender. When the event starts work inside the Agent's state, the answer comes when that work is done."poll": reserved.createMachinerefuses it:machine "<id>": event "<name>" is `poll`, which is reserved but NOT IMPLEMENTED (ADR-0013). Only `ack` events can be delivered today; nothing drains a steer inbox, and degrading it to `ack` would hand the Agent a lying tool contract.
See also
- The Menu
- ADR-0006: The agent picks from a Machine-defined, schema-backed menu; it does not drive the workflow
EffectDoc
type EffectDoc =
| { kind: "command"; argv: string[]; timeoutMs: number; slot?: string; secrets?: string[]; pin?: true }
| {
kind: "http";
method: string;
target: string;
secrets: string[];
timeoutMs: number;
idempotent: boolean;
file?: true;
}
| { kind: "push"; sign: boolean; squash: boolean; force: boolean; pin: boolean; timeoutMs: number; slot?: string }
| { kind: "dispatch"; workflow: string; wait: boolean; target?: string; timeoutMs: number };An Effect slot's parts, as the doc carries them (ADR-0067): a command's argv (an argv slot reads <name>), its timeout, and the Repo Slot whose Worktree is its cwd, when it names one.
Forms: it is one of these objects.
Form 1:
kind,argv,timeoutMs,slot,secrets,pinkind:"command"argv:string[]timeoutMs:numberslot(optional):stringsecrets(optional):string[]pin(optional):true
Form 2:
kind,method,target,secrets,timeoutMs,idempotent,filekind:"http"method:stringtarget:stringsecrets:string[]timeoutMs:numberidempotent:booleanfile(optional):true
Form 3:
kind,sign,squash,force,pin,timeoutMs,slotkind:"push"sign:booleansquash:booleanforce:booleanpin:booleantimeoutMs:numberslot(optional):string
Form 4:
kind,workflow,wait,target,timeoutMskind:"dispatch"workflow:stringwait:booleantarget(optional):stringtimeoutMs:number
ChildMachineDoc
type ChildMachineDoc = {
src: string;
label: string;
via: "invoke" | "spawn";
machine?: MachineBodyDoc;
recursive?: true;
};A child Machine reached from a state — the pipeline a workflow delegates to, which is most of what it DOES. Attached to the state that runs it, so the renderer nests it where it belongs.
src is the JOIN KEY: it is exactly the src a live child actor reports (RunChild.src), for both kinds — a named actor keeps its name ("body", "worker", "featureWorkspace"), an inline machine object gets xstate's generated key ("xstate.invoke.0.wrapper.running") on both sides. That is what lets the page hang a run's live child state under the right subgraph without the two halves agreeing on anything but this string. Since ADR-0049 every kit wrapper names its child, so the generated key is an author's shape only.
Fields
src:stringThe join key — matches a live
RunChild.srcexactly.label:stringDisplay name: the invoke id (
"body") or the spawned actor's name ("featureWorkspace").via:"invoke" | "spawn"How the state reaches it:
invokebinds it to the state's lifetime,spawnoutlives it.machine(optional):MachineBodyDocThe child's own structure. Absent iff
recursive.recursive(optional):trueThe machine is already an ancestor of this point — the body is up there, not repeated here.
EventAudience
type EventAudience = "agent" | "external" | "any";Who may send an event: "agent" (an Agent, as a pick from its Menu), "external" (the caller of a Gate), or "any" (both).
Leave it unset in most cases. Then the position of each on key decides: the event belongs to the party that its state, or the nearest state above it, invokes. An on key on a state that invokes no party, with no such state above it, belongs to both. Set an audience only to give an event to the other party too ("any"). Or set it to keep an event on the root from one party ("agent" or "external").
See also