language & commands
Write a plan, check that every route is sound, and give an agent one clear assignment at a time.
This page starts with the everyday language, then shows the commands and machine-facing reference. Terminal recordings are exact captures of the real CLI.
The work, the possible routes, and the decisions reserved for people.
Finds missing steps, dead ends, and retry paths that may never finish.
Shows the next assignment and records who chose each route and why.
Write a plan
A plan is a plain-text .mar file. Each named section is a
stage of the project—not an individual coding task. Routes at the end say
what can happen next. You can write one by hand or ask an AI to draft it
from notes; either way it stays readable, reviewable, and stored with the
project.
VAR remaining: number = ?
=== work_issue ===
Work the next ready issue: move it to "In progress",
implement it, open a pull request, and move the issue to "In review".
~ remaining -= 1
while {remaining > 0} -> work_issue
else [Check for newly ready issues] -> refresh_queue
=== refresh_queue ===
Count the ready issues.
? remaining
while {remaining > 0} -> work_issue
else -> share_update
=== share_update ===
Post a short status update with links to the open pull requests.
-> END
Read an arrow as “continue to…”
-> share_update moves to that stage after a choice.
-> END finishes automatically. An arrow on its own line is
the phase's automatic next step.
Language at a glance
| Construct | Syntax | Notes |
|---|---|---|
| Stage (phase) | === name === |
A unique id using letters, numbers and underscores. END is reserved. |
| Instructions | plain text below a stage | The agent's assignment. The first line becomes its title in diagrams. |
| Variable | VAR name = literal |
Put it before the first stage. Its initial value sets its type: number, boolean, or string. |
| Runtime value | VAR name: number = ? · ? name |
Wait for an initial value, or refresh it at an explicit checkpoint. The host records the typed value and its source. |
| Update | ~ name = expr · += · -= |
Changes a value when the stage begins. |
| One-time route | * [Label] -> target |
Can be taken only once during a run. |
| Repeatable route | + [Label] -> target |
Use inside loops and anywhere the same route may be offered again. |
| Rule (gate) | {expr} beside a label |
The route is available only while the expression is true. |
| Human checkpoint | @human on a choice |
Only a person may record this decision; an agent is refused. |
| Intentional loop | ~loop~ on the returning route |
Confirms that returning to an earlier stage is deliberate. |
| Conditional loop | while {expr} -> target · else -> target |
An exhaustive pair; use until when the true arm is the exit. |
| Hard timeout | timeout 3d -> fallback |
Before expiry the route is closed; after expiry it becomes the authoritative exit. |
| Automatic next step | -> target on its own line |
Continue automatically when the stage is done. Put it after any choices. |
| Finish | -> END |
Complete the plan. |
| Metadata | # key: value · # tag · # key: """…""" |
Before the first stage it describes the whole plan; inside a stage it describes that work. Repeated keys become a list. |
Expressions
Operands: numbers (3, 1.5), booleans,
"strings", variables. Operators, loosest to tightest:
||/or · &&/and ·
== != · < <=
> >= · + - ·
* / % · unary
!/not, - · ( ).
Comparisons are type-checked at runtime; + concatenates
strings.
Metadata & quote blocks
Any metadata key — plan-level or node-level — accepts a
fenced """ quote block for well-formed multiline text; an
unterminated fence is a parse error. Use it wherever a value is prose
rather than an identifier: keep lines short and readable instead of packing
a paragraph onto one line. The most important use is prompt —
the original ask, verbatim — which anchors the plan to its origin:
summarize leads with it and the executor's brief carries it as
plan.intent, so the file never operates in a vacuum. (A
one-line summary key also exists; add it only when a long
prompt needs an abstract — don't restate the prompt.) The provider
namespaces normalise into structured refs and delivery config (see
execution); all other namespaces pass through
untouched as extension metadata.
Metadata describes execution context; it does not create control flow.
The legacy # timebox: value travels in the brief as advisory
evidence. When expiry must change the route, put
timeout 3d -> fallback in the phase: before expiry that edge
is closed, and afterwards it becomes the authoritative exit.
wake tells executors what to watch so a service phase can
park instead of polling; scheduling belongs to the executing platform,
never the walker.
Loops
A declared loop passes when its cycle has at least one exit that is
ungated, or whose gate is trivially decidable and satisfiable — the
canonical shape is a monotonic counter:
~ i += 1 inside the loop with an exit gate
{i >= 3}. A loop-continue gate that provably shuts
({i < 3} with an increasing i) also counts, on
any edge of the cycle. Everything else is enumerated as an unverified-gate
warning. Resetting a counter (~ i = 0) anywhere makes its
gates non-monotonic and therefore unverifiable — those warn by design;
review them manually.
Authoring rule of thumb: keep every choice on a path a traversal can
revisit sticky (+). A once-only (*) choice inside
a cycle is consumed on the first pass and can strand a later iteration at
runtime; the compiler warns (MAR017) only when the
~loop~ edge itself is once-only.
Check it
The validate command checks whether every stage can be
reached and whether every path eventually goes somewhere. Here is a first
draft of a checkout-improvement plan with two honest mistakes — the retry
path has not been marked as intentional, and the rollout stage has no next
step:
VAR attempts = 0
=== build_checkout ===
Rebuild the checkout flow behind a feature flag.
Ship one measurable change per attempt to the flag cohort and read the
conversion funnel after a full week; an attempt is done when its data is in.
~ attempts += 1
* [Cohort converts] @human -> rollout
+ [Conversion flat — iterate] -> build_checkout
=== rollout ===
Ramp the flag to 100% and retire the old flow.
Watch error rates and conversion during the ramp; done means the old flow is
deleted, not merely dark.
$ marionette validate checkout.marcheckout.mar:21: error[MAR006]: phase "rollout" has no available way forward 21 | === rollout === help: add a choice or an automatic next step (e.g. "-> END")checkout.mar:19: error[MAR008]: undeclared cycle: build_checkout -> build_checkout 19 | + [Conversion flat — iterate] -> build_checkout help: cycles must be intentional: mark the returning choice with ~loop~✗ checkout.mar: 2 errors, 0 warnings
The first error asks whether returning to an earlier stage is deliberate and, if so, when the retries should stop. The second asks what happens after rollout. Marionette will not give an incomplete plan to an agent. Here is the fixed plan — the team may try up to five versions, a person approves the launch or a change of direction, and every route has a clear ending:
VAR attempts = 0
=== build_checkout ===
Rebuild the checkout flow behind a feature flag.
Ship one measurable change per attempt to the flag cohort and read the
conversion funnel after a full week; an attempt is done when its data is in.
~ attempts += 1
* [Cohort converts] @human -> rollout
+ {attempts < 5} [Conversion flat — iterate] ~loop~ -> build_checkout
* {attempts >= 5} [Not converging — rethink] @human -> rethink
=== rethink ===
Five iterations without lift: take the flow back to research.
Run the checkout usability study and write up why the five attempts failed;
that write-up is the input to whichever door is taken next.
+ [New direction agreed] @human ~loop~ -> build_checkout
* [Park the revamp] @human -> END
=== rollout ===
Ramp the flag to 100% and retire the old flow.
Watch error rates and conversion during the ramp; done means the old flow is
deleted, not merely dark.
-> END
$ marionette validate checkout.mar --strict✓ checkout.mar: 0 errors, 0 warnings
Reviewers do not have to read the plan file. The summarize
command explains the route in plain language and highlights every decision
reserved for a person; render draws a flowchart, in which
✋ marks a human decision and ↻ marks a retry
path:
$ marionette summarize checkout.mar# Plan summary: checkout.mar> 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.Starts at **build_checkout**. 3 phases, 2 decision points (phases with 2+ choices), 5 choices overall.- **Human checkpoints:** "Cohort converts" (at build_checkout); "Not converging — rethink" (at build_checkout); "New direction agreed" (at rethink); "Park the revamp" (at rethink)- **Declared loops:** build_checkout → build_checkout ("Conversion flat — iterate"); rethink → build_checkout ("New direction agreed")- **Gates:** 2 gated choices, of which 0 unverified (review manually)- **Variables:** attempts: number = 0- **Contract hash:** `sha256:bc46272ad55c…`## Walkthrough### build_checkout (start)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.- on entry: `attempts += 1`- **Cohort converts** → rollout (**requires a human decision**)- **Conversion flat — iterate** → build_checkout (only if `attempts < 5`; loops back; repeatable)- **Not converging — rethink** → rethink (only if `attempts >= 5`; **requires a human decision**)### rethinkFive iterations without lift: take the flow back to research.Run the checkout usability study and write up why the five attempts failed;that write-up is the input to whichever door is taken next.- **New direction agreed** → build_checkout (**requires a human decision**; loops back; repeatable)- **Park the revamp** → END (**requires a human decision**)### rolloutRamp the flag to 100% and retire the old flow.Watch error rates and conversion during the ramp; done means the old flow isdeleted, not merely dark.- otherwise → END## Compiler reportNo defects, no warnings. The plan is structurally sound: every phase is reachable, every path has an exit, and all declared loops have a verified exit.
What the compiler guarantees
Structural errors fail the build: dead ends, unreachable
phases, undefined targets and variables, duplicates, undeclared cycles,
loops with no exit or provably-unsatisfiable exits, @human
choices with no escalation path, type mismatches.
Warnings ask for review — or fail the build with
--strict. Zero errors is the authoring bar, and a
plan whose only diagnostics are expected MAR014 dynamic-fact
warnings (real-world facts the compiler cannot decide — it never claims
"verified" for gates beyond constant expressions and monotonic counters) is
a legitimate end state. The full table is in the
reference below.
Run it
state init starts a run and creates a small progress file.
The run begins in the first stage; its attempt counter changes to 1, and
Marionette shows which choices are available and which are currently
blocked:
$ marionette state init checkout.marinitialised checkout.state.json bound to sha256:bc46272ad55c…current: build_checkoutRebuild 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.variables: attempts=1choices: [0] Cohort converts @human -> rollout [1] Conversion flat — iterate ~loop~ {attempts < 5} -> build_checkout [2] Not converging — rethink @human {attempts >= 5} -> rethink [unavailable: gate {attempts >= 5} is false]
If a plan asks for a runtime value, traversal pauses until the host
records it with state observe <plan> <name>
<json-value> --actor <name> --rationale "<source>".
The value is type-checked and audited separately from route decisions.
The agent does not inspect that file. It runs brief to get
its next assignment: the current instructions, useful values such as the
attempt count, the choices available now, and the command to record the
result. (Programs get the same thing as JSON — see
execution.)
$ marionette brief checkout.marwork packet — checkout-revamp (sha256:bc46272ad55c…)status: active progress: 1/3 phases, 1 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=1choices: [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. [2] Not converging — rethink @human {attempts >= 5} -> rethink — Five iterations without lift: take the flow back to research. [unavailable: gate {attempts >= 5} is false]
Choice [0] would approve the launch, and it is marked
@human. When the agent tries to take it, Marionette refuses —
the error is named human-checkpoint, and no progress is
recorded. This rule is enforced by the tool itself; it does not depend on
the agent remembering an instruction:
$ marionette state choose checkout.mar 0 --actor agent --rationale "metrics look good"error: choice "Cohort converts" is an @human checkpoint: an agent may not take it autonomously. Escalate to a human; a human records the decision with --actor <name>.
The agent can take an allowed choice instead — every choice records who made it and why, so anyone checking the project sees the decision and the remaining number of tries. When the evidence is in, a person takes the launch choice and records the reason; Marionette keeps their name, the decision, and the evidence in the project's history:
$ marionette state choose checkout.mar 0 --actor lee --rationale "cohort shows +9% completion; ship it"current: rolloutRamp the flag to 100% and retire the old flow.Watch error rates and conversion during the ramp; done means the old flow isdeleted, not merely dark.variables: attempts=2automatic next step -> END (run marionette state advance)
Three rules apply throughout: an agent cannot take a choice marked
@human; every choice needs a written reason; and if the plan
changes after the run starts, Marionette stops until you accept the edit.
That last one is next.
Change it safely
Plans change mid-flight. Marionette fingerprints the compiled plan, and any semantic edit changes the fingerprint: every walk command then stops with exit code 3 rather than applying the old progress record to a different plan. This mismatch is called drift:
$ marionette state show checkout.marplan/state drift detected: the script changed since this state was recorded. state is bound to sha256:bc46272ad55c291fb91ef18397f1b4ba0a10a8564196468a60a8cc330f673d78 compiled plan is sha256:4e926efcd516b1c7850134c82c6511d36d3335ac861eedea12336c084eae5049Reconcile before continuing: review the plan changes, then either re-initialise the state (marionette state init --force) or restore the previous script version.
If the edit is intentional, state rebind migrates the
progress file onto the new plan, keeping the decision
history, and lists what changed. Vanished taken-choices are
dropped (and reported), removed variables dropped, new variables added at
their initials (or requested as observations), type-changed variables
reset. It refuses
(migration-blocked) when the current phase no longer exists —
that reconciliation needs a human. To start over instead, discarding
history: state init --force.
$ marionette state rebind checkout.mar✓ migrated checkout.state.json from sha256:bc46272ad55c… to sha256:4e926efcd516…current: build_checkoutRebuild 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.variables: attempts=2choices: [0] Cohort converts @human -> rollout [1] Conversion flat — iterate ~loop~ {attempts < 5} -> build_checkout [2] Not converging — rethink @human {attempts >= 5} -> rethink [unavailable: gate {attempts >= 5} is false]
Reference
Command surface
$ marionette helpmarionette — compiled project trajectories for AI agentsUsage: marionette compile <plan.mar> [-o out.trajectory.json] Compile to trajectory JSON marionette validate <plan.mar> [--strict] Check only; print diagnostics marionette facts <plan.mar> [-o out.pl] Prolog fact base (see spec/rules/) marionette oracle <plan.mar> Check via the bundled rule engine marionette query <plan.mar> <goal> [--limit n] Ask the plan a Prolog question marionette render <plan.mar|.json> [--state f] [-o out.mmd] [--lr] marionette summarize <plan.mar|.json> [--state f] [-o out.md] marionette brief <plan.mar|.json> [--state f] [--json] Work packet for the executor marionette sync <plan.mar|.json> [--json] Tracker sync manifest (see docs/SYNC.md) marionette sync bind <plan.mar> --tracker <github|jira|linear> Remember the plan's tracker marionette sync link <plan.mar> <phase> <issue-id> Record a created issue in the plan marionette sync mark <plan.mar|.json> [--cursor n] Advance the applied-audit cursor marionette import <issues.json> [--mode queue|phases] [-o out.mar] Scaffold a plan from issues marionette start <plan.mar|.json> --run <id> [--store dir] [--principal id] [--role agent|human] [--principal-uri uri] marionette stop <plan.mar|.json> --run <id> [--store dir] marionette state init <plan.mar|.json> [--state f] [--force] marionette state show <plan.mar|.json> [--state f] marionette state observe <plan.mar|.json> <name> <json-value> --actor <name> --rationale <text> [--state f] marionette state choose <plan.mar|.json> <choice> --actor <name> --rationale <text> [--state f] marionette state advance <plan.mar|.json> --actor <name> [--rationale <text>] [--state f] marionette state rebind <plan.mar|.json> [--actor <name>] [--rationale <text>] [--state f] Migrate state onto an edited plan; the amendment is logged (G4)Options: -o, --out <file> Output file ('-' for stdout; default varies by command) --state <file> State file (default: <plan>.state.json) --strict Treat warnings as errors (exit 1) --json Emit the machine-readable form (brief) --lr Render left-to-right instead of top-down --force Overwrite an existing state file on init --actor <name> Who takes the step ("agent" may not pass @human gates) --rationale <text> Why the step was taken (required for choices) --tracker <name> Tracker to bind/link against: github, jira or linear --cursor <n> Decision-log position to mark as synced (default: all of it) --mode <mode> Import shape: queue (one loop phase) or phases (one per issue) --run <id> Run id (required by start/stop) --store <dir> Run store (default: <plan-dir>/.marionette) --create Start a new run; error if it already exists --principal <id> Connection principal id (default: agent) --role <role> Bound connection role: agent or human (default: agent) --principal-uri <u> Optional provenance URI for decision records
Three of these deserve a note: a compiled plan is also a database of
facts, and the graph checks are specified as logic rules over it
(spec/rules/marionette.pl — the normative spec, runnable on
the engine bundled with the package). marionette facts emits
the fact base, marionette oracle runs the rule-base report,
and marionette query asks one-shot questions no other
subcommand answers — 'unattended_completion' ("can the agent
finish without a human?"), 'human_gate(C, Phase, Label)'
("what exactly does a human sign off on?"), 'cyclic(P)'
("which phases sit inside loops?").
Exit codes
| Code | Meaning |
|---|---|
0 | success (warnings allowed unless --strict) |
1 | validation errors (or warnings with --strict); refused walk operations |
2 | usage or I/O errors |
3 | plan/state drift detected — reconcile with state rebind (or state init --force) |
Compiler diagnostics
| Code | Severity | Meaning | Usual fix |
|---|---|---|---|
MAR001 | error | parse error | fix the line the message points at |
MAR002 | error | duplicate phase id | rename one of the phases |
MAR003 | error | an arrow points to a stage that does not exist | use the did you mean suggestion, or define the missing stage |
MAR004 | error | undefined variable in a gate or mutation | declare it with VAR in the preamble |
MAR005 | error | duplicate variable declaration | remove the extra VAR |
MAR006 | error | a stage has no available way forward | add a choice or an automatic next step such as -> END |
MAR007 | error | unreachable phase | link it from a real decision, or delete it |
MAR008 | error | undeclared cycle | if the loop is intentional, add ~loop~ to the returning choice; otherwise re-point the edge |
MAR009 | error | declared loop with no exit | add a counter-gated or @human sibling exit |
MAR010 | error | loop exit gate provably unsatisfiable | fix the gate so the counter can actually reach it |
MAR011 | warning | constant-false gate | the choice can never be taken — fix or remove it |
MAR012 | error | @human choice with no escalation path | give the choice a -> target |
MAR013 | warning | ~loop~ on an edge that isn't part of a cycle | drop the mark, or point the edge where you meant |
MAR014 | warning | unverified gate (dynamic fact the compiler can't decide) | expected for real-world facts — review manually, don't churn the plan to silence it |
MAR015 | error | type mismatch in an expression or mutation | align the literal/variable types |
MAR016 | warning | unused variable | gate on it, or delete it |
MAR017 | warning | once-only (*) loop edge | make it sticky (+) so the loop can repeat |
MAR018 | warning | malformed external ref (github:/jira:/linear:/ref:) | fix the value to the namespace's expected shape |
MAR019 | warning | unknown delivery:/report: value | use a listed value; typos fall back to defaults loudly |
MAR020 | warning | unknown tracker: value | use github, jira or linear |
MAR021 | warning | malformed timebox: value | single unit: 90m, 4h, 3d, 2w |
MAR022 | warning | unknown priority: value | use critical, high, normal or low |
MAR023 | warning | timeboxed phase with a single exit | give the spike an abandon door as well — or drop the timebox |
Walker refusal codes
Refusals are machine-coded so executors can react programmatically, and
a refused operation never mutates state. (The runtime protocol carries its
own error codes on the same principles — e.g. forbidden for an
agent connection at an @human checkpoint,
stale-revision for an outdated expectedRevision —
see spec/runtime-protocol.schema.json.)
| Code | Meaning |
|---|---|
completed | the plan has already reached END |
unknown-node | state points at a node absent from the compiled plan |
unknown-choice | no choice matches the given reference |
ambiguous-choice | a label prefix matched more than one choice |
gate-blocked | the choice's gate is currently false (or failed to evaluate) |
once-exhausted | a once-only (*) choice was already taken |
human-checkpoint | an agent tried to take an @human choice |
rationale-required | the step is missing its auditable rationale |
no-next-step | advance was used where there is no automatic next step |
migration-blocked | state cannot be rebound onto the edited plan (current phase gone) — needs a human |
Files on disk
| File | What it is |
|---|---|
plan.mar | the script — durable, diffable source of truth |
plan.trajectory.json | compiled contract (marionette compile); content-hashed |
plan.state.json | traversal state + decision log, bound to the trajectory hash |
.marionette/graphs/<hash>.trajectory.json | runtime store: compiled trajectories archived by content hash |
.marionette/runs/<run-id>/events.jsonl | runtime store: append-only event journal (authoritative) |
.marionette/runs/<run-id>/snapshot.json | runtime store: fsynced snapshot, rebuilt from the journal on resume |
The contracts behind all of this live in the repo:
spec/trajectory.schema.json (the compiled plan),
spec/brief.schema.json (the work packet), and
spec/conformance/ (walker behaviour).