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.

brief --json — the agent's assignmentexit 0
$ 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:

brief — status: awaiting human decisionexit 0
$ 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 proxystate 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"}
start — an NDJSON runtime sessionexit 0
$ 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:

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.

TagValuesMeaning
# delivery: pr-per-phaseeach phase's work lands as its own pull request
branch-per-phasea branch per phase; PRs at the executor's discretion
single-prone pull request for the whole traversal
single-branchone 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-checkpointsreport only at @human checkpoints and completion
at-endone report when the traversal completes (or strands)
# delivery:branch: any stringbranch 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:

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.