execution
This guide is for people connecting an AI agent or another program to Marionette. It explains how to ask what should happen next, record the result, pause for a person's decision, and report completed work.
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 a fallthrough divert) │
│ │ │
│ ├─ 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.
The agent's assignment, as JSON
This is the checkout example from the
walkthrough, 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.2.0", "plan": { "file": "checkout.mar", "project": "checkout-revamp", "hash": "sha256:24d32e0b28205dbad62004391795fe41fc4bfbb40696d30f6e7a62497635efa0", "intent": { "summary": "Lift checkout conversion behind a feature flag — up to five evidence-driven iterations, a human ship gate, and a forced rethink when the budget runs dry.", "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": [] }, "status": "active", "variables": { "attempts": 2 }, "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": {}, "refs": [] }, "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 }, { "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 }, { "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" } ], "divert": 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>", "rules": [ "do the work the current phase describes before recording an outcome", "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:24d32e0b2820…)status: awaiting human decision ✋ progress: 1/3 phases, 5 stepsintent: Lift checkout conversion behind a feature flag — up to five evidence-driven iterations, a human ship gate, and a forced rethink when the budget runs dry.prompt: 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.delivery: 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.
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):
$ cat session.ndjson
{"protocol":"0.1.0","id":1,"op":"initialize","client":{"name":"docs-site","version":"0.1"}}
{"protocol":"0.1.0","id":2,"op":"next","profile":"work"}
{"protocol":"0.1.0","id":3,"op":"choose","choiceId":"build_checkout#0","rationale":"metrics look good","expectedRevision":0,"idempotencyKey":"turn-1","profile":"signal"}
{"protocol":"0.1.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.1.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.1.0","id":1,"ok":true,"result":{"protocol":"0.1.0","capabilities":{"operations":["next","choose","advance","record","events"],"projections":["signal","work","debug"],"idempotency":true,"eventCursor":true},"runId":"docs-demo","graphHash":"sha256:24d32e0b28205dbad62004391795fe41fc4bfbb40696d30f6e7a62497635efa0","replayed":false}}{"protocol":"0.1.0","id":2,"ok":true,"result":{"projection":{"runId":"docs-demo","revision":0,"cursor":2,"graphHash":"sha256:24d32e0b28205dbad62004391795fe41fc4bfbb40696d30f6e7a62497635efa0","status":"active","node":{"id":"build_checkout","title":"Rebuild the checkout flow behind a feature flag.","bodyRef":"marionette://trajectory/sha256%3A24d32e0b28205dbad62004391795fe41fc4bfbb40696d30f6e7a62497635efa0/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":[]},"choices":[{"id":"build_checkout#0","label":"Cohort converts","human":true,"target":"rollout"},{"id":"build_checkout#1","label":"Conversion flat — iterate","human":false,"target":"build_checkout"}],"divert":null,"truncated":false,"omitted":[]},"replayed":false}}{"protocol":"0.1.0","id":3,"ok":false,"error":{"code":"forbidden","message":"choice \"Cohort converts\" is an @human checkpoint; connection principal is agent"}}{"protocol":"0.1.0","id":4,"ok":true,"result":{"revision":1,"eventSeqs":[3,4],"projection":{"runId":"docs-demo","revision":1,"cursor":4,"graphHash":"sha256:24d32e0b28205dbad62004391795fe41fc4bfbb40696d30f6e7a62497635efa0","status":"active","node":{"id":"build_checkout","title":"Rebuild the checkout flow behind a feature flag.","bodyRef":"marionette://trajectory/sha256%3A24d32e0b28205dbad62004391795fe41fc4bfbb40696d30f6e7a62497635efa0/node/build_checkout/body"},"choices":[{"id":"build_checkout#0","label":"Cohort converts","human":true},{"id":"build_checkout#1","label":"Conversion flat — iterate","human":false}],"divert":null,"truncated":false,"omitted":[]},"replayed":false}}{"protocol":"0.1.0","type":"event","event":{"protocol":"0.1.0","seq":3,"at":"2026-07-23T20:55:27.390Z","runId":"docs-demo","kind":"decision.committed","graph":{"trajectoryHash":"sha256:24d32e0b28205dbad62004391795fe41fc4bfbb40696d30f6e7a62497635efa0","nodeId":"build_checkout","choiceId":"build_checkout#1","uri":"marionette://trajectory/sha256%3A24d32e0b28205dbad62004391795fe41fc4bfbb40696d30f6e7a62497635efa0/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":[],"idempotencyKey":"turn-2","commandFingerprint":"3a14cac1760626d0f15e05e506ea2ebd3da7fd093c08193c8addbd7a92481f5f","commandEventSeqs":[3,4]}}}{"protocol":"0.1.0","type":"event","event":{"protocol":"0.1.0","seq":4,"at":"2026-07-23T20:55:27.390Z","runId":"docs-demo","kind":"node.entered","graph":{"trajectoryHash":"sha256:24d32e0b28205dbad62004391795fe41fc4bfbb40696d30f6e7a62497635efa0","nodeId":"build_checkout","uri":"marionette://trajectory/sha256%3A24d32e0b28205dbad62004391795fe41fc4bfbb40696d30f6e7a62497635efa0/node/build_checkout"},"principal":{"id":"coding-agent","role":"agent"},"data":{"from":"build_checkout"}}}{"protocol":"0.1.0","id":5,"ok":true,"result":{"replayed":true,"revision":1,"eventSeqs":[3,4],"projection":{"runId":"docs-demo","revision":1,"cursor":4,"graphHash":"sha256:24d32e0b28205dbad62004391795fe41fc4bfbb40696d30f6e7a62497635efa0","status":"active","node":{"id":"build_checkout","title":"Rebuild the checkout flow behind a feature flag.","bodyRef":"marionette://trajectory/sha256%3A24d32e0b28205dbad62004391795fe41fc4bfbb40696d30f6e7a62497635efa0/node/build_checkout/body"},"choices":[{"id":"build_checkout#0","label":"Cohort converts","human":true},{"id":"build_checkout#1","label":"Conversion flat — iterate","human":false}],"divert":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 | |
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.
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). Two sanctioned paths out:
marionette state rebind plan.mar— migrate the existing state onto the edited plan, keeping the decision log. Vanished taken-choices are dropped (and reported), removed variables dropped, new variables added at their initials, type-changed variables reset. Refused (migration-blocked) when the current phase no longer exists — that reconciliation needs a human.marionette state init --force— start over; history discarded.
See it happen in the walkthrough.
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, once-exhausted,
human-checkpoint, rationale-required,
no-divert, 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.