execution
This guide is for people connecting an AI agent or another program to Marionette: ask what happens next, do that bounded piece of work, record the result, and pause when a person needs to decide.
We use executor to mean the tool carrying out the plan. That could be a Claude session using the execution skill, your own agent software, or an automated job. They all use the same commands and receive the same information.
Ask, work, and record
┌─────────────────────────────────────────────────────────────┐
│ marionette brief plan.mar --json ← "what now?" │
│ │ │
│ ├─ status: active → do the phase's work, then │
│ │ marionette state choose plan.mar <choice> │
│ │ --actor agent --rationale "<evidence>" │
│ │ (or `state advance` for an automatic next step) │
│ │ │
│ ├─ status: awaiting-observation → obtain and record │
│ │ each named typed value, then ask again │
│ │ │
│ ├─ status: waiting-timeout → park until the hard exit │
│ │ is due; the host may arrange the wake-up │
│ │ │
│ ├─ status: awaiting-human → deliver the escalation │
│ │ payload to the primary session and STOP │
│ │ │
│ ├─ status: stranded → report; plan needs editing │
│ │ (then `marionette state rebind`) │
│ │ │
│ └─ status: completed → final report; done │
└──────────────── repeat until completed ─────────────────────┘
The brief is the answer to “what should happen now?” The executor can
only use choices that the brief lists as available. Every choice records
who made it and why. Marionette itself enforces choice conditions,
one-time choices, @human decisions, and checks for edits to a
running plan. These rules do not depend on instructions in an AI prompt.
An awaiting-observation brief names each value and type.
The host obtains it from the phase's source and records it with
state observe plus a rationale identifying the lookup. The
plan controls refresh cadence: a batch can be observed once, drained, and
refreshed only after it is flushed. A waiting-timeout brief
means no ordinary route is open yet; Marionette checks time on its next
operation while the host owns scheduling the wake-up.
The agent's assignment, as JSON
This is the checkout example from the
docs, partway through the project. The
JSON contains the original request, the current stage and its instructions,
useful values, progress so far, every possible choice and why it is
available or blocked, and the command needed to record a result. Its exact
format is defined in spec/brief.schema.json.
$ marionette brief checkout.mar --json{ "spec": "0.4.0", "plan": { "file": "checkout.mar", "project": "checkout-revamp", "hash": "sha256:bc46272ad55c291fb91ef18397f1b4ba0a10a8564196468a60a8cc330f673d78", "intent": { "summary": null, "prompt": "Checkout conversion has been flat for two quarters. Rebuild the flow\nbehind a flag and iterate on cohort evidence — five attempts max. I take\nthe ship call, and if it never converts we go back to research rather\nthan grinding out attempt six." }, "refs": [ { "provider": "github", "kind": "repo", "id": "acme/shop", "url": "https://github.com/acme/shop" }, { "provider": "url", "kind": "link", "id": "https://wiki.acme.dev/checkout-revamp", "url": "https://wiki.acme.dev/checkout-revamp" } ] }, "status": "active", "variables": { "attempts": 2 }, "pendingObservations": [], "node": { "id": "build_checkout", "title": "Rebuild the checkout flow behind a feature flag.", "body": "Rebuild the checkout flow behind a feature flag.\nShip one measurable change per attempt to the flag cohort and read the\nconversion funnel after a full week; an attempt is done when its data is in.", "meta": { "github:issue": "41" }, "refs": [ { "provider": "github", "kind": "issue", "id": "acme/shop#41", "url": "https://github.com/acme/shop/issues/41" } ], "timebox": null, "priority": null, "enteredAt": "2026-07-28T09:13:27.521Z" }, "delivery": { "mode": "none", "report": "per-phase", "branch": null }, "frontier": [ { "index": 0, "id": "build_checkout#0", "label": "Cohort converts", "target": "rollout", "targetTitle": "Ramp the flag to 100% and retire the old flow.", "sticky": false, "human": true, "loop": false, "gate": null, "available": true, "blocked": null, "blockedCode": null, "timeout": null }, { "index": 1, "id": "build_checkout#1", "label": "Conversion flat — iterate", "target": "build_checkout", "targetTitle": "Rebuild the checkout flow behind a feature flag.", "sticky": true, "human": false, "loop": true, "gate": "attempts < 5", "available": true, "blocked": null, "blockedCode": null, "timeout": null }, { "index": 2, "id": "build_checkout#2", "label": "Not converging — rethink", "target": "rethink", "targetTitle": "Five iterations without lift: take the flow back to research.", "sticky": false, "human": true, "loop": false, "gate": "attempts >= 5", "available": false, "blocked": "gate {attempts >= 5} is false", "blockedCode": "gate-blocked", "timeout": null } ], "next": null, "escalation": null, "progress": { "steps": 2, "nodesVisited": 1, "nodesTotal": 3, "path": [ "build_checkout", "build_checkout" ] }, "protocol": { "choose": "marionette state choose checkout.mar <choice> --actor <name> --rationale <text>", "advance": "marionette state advance checkout.mar --actor <name> --rationale <text>", "observe": "marionette state observe checkout.mar <name> <json-value> --actor <name> --rationale <text>", "rules": [ "do the work the current phase describes before recording an outcome", "supply only observations the brief requests, with the source in the rationale", "record every taken branch with an honest rationale (it is the audit trail)", "never take an @human choice as the agent: escalate and wait for a human decision", "gates are computed from the variables above: if a choice is blocked, it is not an option", "re-run `marionette brief` after every recorded step: it is the single source of \"what now\"" ] }}
Pause for a person's decision
When all available choices are marked @human, the brief's
status changes to awaiting-human. It includes the context a
person needs to decide without opening the repository. In this example,
the checkout project has used all five attempts, so only a person can
choose what happens next:
$ marionette brief checkout.marwork packet — checkout-revamp (sha256:bc46272ad55c…)status: awaiting human decision ✋ progress: 1/3 phases, 5 stepsprompt: Checkout conversion has been flat for two quarters. Rebuild the flow behind a flag and iterate on cohort evidence — five attempts max. I take the ship call, and if it never converts we go back to research rather than grinding out attempt six.=== build_checkout ===Rebuild the checkout flow behind a feature flag.Ship one measurable change per attempt to the flag cohort and read theconversion funnel after a full week; an attempt is done when its data is in.refs: https://github.com/acme/shop/issues/41delivery: none · report per-phasevariables: attempts=5choices: [0] Cohort converts @human -> rollout — Ramp the flag to 100% and retire the old flow. [1] Conversion flat — iterate ~loop~ {attempts < 5} -> build_checkout — Rebuild the checkout flow behind a feature flag. [unavailable: gate {attempts < 5} is false] [2] Not converging — rethink @human {attempts >= 5} -> rethink — Five iterations without lift: take the flow back to research.escalation required: every currently-available choice is an @human checkpoint pause and escalate: present this phase and its choices to a human; a human records the decision with `marionette state choose checkout.mar <choice> --actor <name> --rationale <text>`. Do not take these choices autonomously. There is no implicit timeout or fallback; silence leaves the run parked.
How you contact that person is up to your software: it could send a chat
message, add a pull-request comment, or create a notification. Until the
person answers, the plan remains paused and visible in
brief, render, and summarize.
Marionette also refuses any attempt to record an agent as the decision
maker for an @human choice.
The answer can come back through the same channel. When the human states
an explicit, unambiguous decision in-session, the executor records it
as their proxy — state choose <plan>
<choice> --actor <their-name> --rationale "<their stated
reasoning>" — no leaving the conversation for a terminal. The
contract at an @human gate is attribution and
evidence, not ceremony: the walker refuses only the
agent actor, and the log carries the human's name and their
words. Inferred intent, ambiguous replies and silence are never
recordable — the executor asks instead.
The local runtime: marionette start
The CLI loop above is one executor shape. The other is a
single-writer runtime process: marionette start
holds the compiled graph and walker behind newline-delimited JSON on
stdin/stdout, so an agent host consumes lifecycle events programmatically
and injects only the compact projection the model needs. Same DSL, same
trajectory contract, same rules — a different transport.
A scripted session against the checkout plan (each input line is one request; the host would normally be an agent framework, not a file):
$ marionette start checkout.mar --run docs-demo --principal coding-agent --role agent < session.ndjson
{"protocol":"0.3.0","id":1,"op":"initialize","client":{"name":"docs-site","version":"0.1"}}
{"protocol":"0.3.0","id":2,"op":"next","profile":"work"}
{"protocol":"0.3.0","id":3,"op":"choose","choiceId":"build_checkout#0","rationale":"metrics look good","expectedRevision":0,"idempotencyKey":"turn-1","profile":"signal"}
{"protocol":"0.3.0","id":4,"op":"choose","choiceId":"build_checkout#1","rationale":"conversion flat; iterating on the payment step","expectedRevision":0,"idempotencyKey":"turn-2","profile":"signal"}
{"protocol":"0.3.0","id":5,"op":"choose","choiceId":"build_checkout#1","rationale":"conversion flat; iterating on the payment step","expectedRevision":0,"idempotencyKey":"turn-2","profile":"signal"}
$ marionette start checkout.mar --run docs-demo --principal coding-agent --role agent < session.ndjson✓ Marionette started · docs-demo · revision 0 · agent:coding-agent listening on stdin/stdout · press Ctrl-C to stop{"protocol":"0.3.0","id":1,"ok":true,"result":{"protocol":"0.3.0","capabilities":{"operations":["next","choose","advance","observe","record","events"],"projections":["signal","work","debug"],"idempotency":true,"eventCursor":true},"runId":"docs-demo","graphHash":"sha256:4e926efcd516b1c7850134c82c6511d36d3335ac861eedea12336c084eae5049","replayed":false}}{"protocol":"0.3.0","id":2,"ok":true,"result":{"projection":{"runId":"docs-demo","revision":0,"cursor":2,"graphHash":"sha256:4e926efcd516b1c7850134c82c6511d36d3335ac861eedea12336c084eae5049","status":"active","node":{"id":"build_checkout","title":"Rebuild the checkout flow behind a feature flag.","bodyRef":"marionette://trajectory/sha256%3A4e926efcd516b1c7850134c82c6511d36d3335ac861eedea12336c084eae5049/node/build_checkout/body","body":"Rebuild the checkout flow behind a feature flag.\nShip one measurable change per attempt to the flag cohort and read the\nconversion funnel after a full week; an attempt is done when its data is in.","refs":[{"provider":"github","kind":"issue","id":"acme/shop#41","url":"https://github.com/acme/shop/issues/41"}]},"choices":[{"id":"build_checkout#0","label":"Cohort converts","human":true,"target":"rollout","timeout":null},{"id":"build_checkout#1","label":"Conversion flat — iterate","human":false,"target":"build_checkout","timeout":null}],"next":null,"observations":[],"escalation":null,"truncated":false,"omitted":[]},"replayed":false}}{"protocol":"0.3.0","id":3,"ok":false,"error":{"code":"forbidden","message":"choice \"Cohort converts\" is an @human checkpoint; connection principal is agent"}}{"protocol":"0.3.0","id":4,"ok":true,"result":{"revision":1,"eventSeqs":[3,4],"projection":{"runId":"docs-demo","revision":1,"cursor":4,"graphHash":"sha256:4e926efcd516b1c7850134c82c6511d36d3335ac861eedea12336c084eae5049","status":"active","node":{"id":"build_checkout","title":"Rebuild the checkout flow behind a feature flag.","bodyRef":"marionette://trajectory/sha256%3A4e926efcd516b1c7850134c82c6511d36d3335ac861eedea12336c084eae5049/node/build_checkout/body"},"choices":[{"id":"build_checkout#0","label":"Cohort converts","human":true},{"id":"build_checkout#1","label":"Conversion flat — iterate","human":false}],"next":null,"observations":[],"escalation":null,"truncated":false,"omitted":[]},"replayed":false}}{"protocol":"0.3.0","type":"event","event":{"protocol":"0.3.0","seq":3,"at":"2026-07-28T12:47:32.839Z","runId":"docs-demo","kind":"decision.committed","graph":{"trajectoryHash":"sha256:4e926efcd516b1c7850134c82c6511d36d3335ac861eedea12336c084eae5049","nodeId":"build_checkout","choiceId":"build_checkout#1","uri":"marionette://trajectory/sha256%3A4e926efcd516b1c7850134c82c6511d36d3335ac861eedea12336c084eae5049/node/build_checkout/choice/build_checkout%231"},"principal":{"id":"coding-agent","role":"agent"},"data":{"from":"build_checkout","to":"build_checkout","label":"Conversion flat — iterate","rationale":"conversion flat; iterating on the payment step","evidence":[],"expectedRevision":0,"idempotencyKey":"turn-2","commandFingerprint":"75dc590050a5e5a993c6b7c3ef49d07811b778c751701913d62fd5ee74287224","commandEventSeqs":[3,4]}}}{"protocol":"0.3.0","type":"event","event":{"protocol":"0.3.0","seq":4,"at":"2026-07-28T12:47:32.839Z","runId":"docs-demo","kind":"node.entered","graph":{"trajectoryHash":"sha256:4e926efcd516b1c7850134c82c6511d36d3335ac861eedea12336c084eae5049","nodeId":"build_checkout","uri":"marionette://trajectory/sha256%3A4e926efcd516b1c7850134c82c6511d36d3335ac861eedea12336c084eae5049/node/build_checkout"},"principal":{"id":"coding-agent","role":"agent"},"data":{"from":"build_checkout"}}}{"protocol":"0.3.0","id":5,"ok":true,"result":{"replayed":true,"revision":1,"eventSeqs":[3,4],"projection":{"runId":"docs-demo","revision":1,"cursor":4,"graphHash":"sha256:4e926efcd516b1c7850134c82c6511d36d3335ac861eedea12336c084eae5049","status":"active","node":{"id":"build_checkout","title":"Rebuild the checkout flow behind a feature flag.","bodyRef":"marionette://trajectory/sha256%3A4e926efcd516b1c7850134c82c6511d36d3335ac861eedea12336c084eae5049/node/build_checkout/body"},"choices":[{"id":"build_checkout#0","label":"Cohort converts","human":true},{"id":"build_checkout#1","label":"Conversion flat — iterate","human":false}],"next":null,"observations":[],"escalation":null,"truncated":false,"omitted":[]}}}Marionette stopped · revision 1
Worth reading closely in that output:
- Request 3 is refused with
forbidden. The connection was launched with--role agent; role is fixed at process start and absent from command payloads, so an agent cannot cross an@humancheckpoint by editing anactorfield. A trusted human-facing host connects separately with--role human. - Request 4 commits. The write returns its receipt and
the next projection together, then emits
decision.committed/node.enteredjournal events — each carrying the immutable trajectory hash and amarionette://trajectory/…URI.expectedRevisionrejects stale writers. - Request 5 replays. Same
idempotencyKey, same command → the original receipt with"replayed": true, and no second decision appended. Retries are safe by construction.
Projections come in three profiles — signal (status, node
identity, available choices), work (the default; adds node
prose, refs, targets) and debug (adds blocked choices, gates,
variables, progress) — with budget bounds for items and body
size, so a host controls exactly how much of the plan enters model context.
The run store is an append-only journal
(.marionette/runs/<run-id>/events.jsonl) with fsynced
snapshots and trajectories archived by content hash; on resume the snapshot
is rebuilt from the journal. One process owns writes for a run;
marionette stop (or Ctrl-C) ends it, and the same
start command resumes it. Full details:
docs/RUNTIME.md
and the versioned protocol schema
spec/runtime-protocol.schema.json.
Portioning out the work: delivery config
How work product is packaged, and how often the executor reports back, is authored in the plan — versioned with it — rather than hard-coded in the executor. Plan-level tags set the default; node-level tags override per phase.
| Tag | Values | Meaning |
|---|---|---|
# delivery: |
pr-per-phase | each phase's work lands as its own pull request |
branch-per-phase | a branch per phase; PRs at the executor's discretion | |
stacked-prs | ordered, dependent GitHub PR layers in one stack-enabled worktree | |
single-pr | one pull request for the whole traversal | |
single-branch | one branch, commits per phase, no PR automation | |
none (default) | no prescribed packaging (non-code plans, executor's discretion) | |
# report: |
per-phase (default) | report back after every phase |
at-checkpoints | report only at @human checkpoints and completion | |
at-end | one report when the traversal completes (or strands) | |
# delivery:branch: |
any string | branch name template; {phase} expands to the node id |
Unknown values warn (MAR019) and fall back to the defaults,
so a typo can't silently change delivery behaviour. Stacked layers stay in
one isolated worktree and use GitHub's official gh stack flow.
External refs
Nodes and plans reference Jira, GitHub, Linear or anything with a URL
through namespaced metadata — no new syntax. The compiler normalises each
into { provider, kind, id, url } in the trajectory and
the brief, synthesising the URL when context tags
(github:repo, jira:site,
linear:workspace) make it derivable. Malformed values warn
(MAR018); unknown namespaces stay available as raw metadata
for your own extensions. Refs are deliberately references, not
sync — the executor decides what to do with them (read the issue for
context, comment progress, close on phase exit).
Editing a live plan: drift and rebind
Plans change mid-flight; any semantic edit changes the content hash and every walk command then refuses with a drift error (exit 3) until the edit is reconciled. The full story — with the recorded demos — is in docs · change it safely. The executor-relevant part: treat exit 3 as "stop and surface", never as a retry.
Conformance: any walker, same behaviour
The reference walker is not the spec — the conformance suite is
(spec/conformance/). Any walker implementation must produce
the same refusal codes (completed, unknown-node,
unknown-choice, ambiguous-choice,
gate-blocked, observation-required,
unknown-observation, observation-type,
timeout-pending, timed-out,
once-exhausted,
human-checkpoint, rationale-required,
no-next-step, migration-blocked), refuse without
mutating state, and append exactly one log entry per successful step. The
baseline case traverses
examples/paas_replatform.mar, which exercises
every DSL construct plus the execution metadata.