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.

Plan

The work, the possible routes, and the decisions reserved for people.

Compiler

Finds missing steps, dead ends, and retry paths that may never finish.

Runner

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.

issue-loop.mar — a small complete planmar
# project: issue-to-pr
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

ConstructSyntaxNotes
Stage (phase)=== name === A unique id using letters, numbers and underscores. END is reserved.
Instructionsplain text below a stage The agent's assignment. The first line becomes its title in diagrams.
VariableVAR name = literal Put it before the first stage. Its initial value sets its type: number, boolean, or string.
Runtime valueVAR 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 loopwhile {expr} -> target · else -> target An exhaustive pair; use until when the true arm is the exit.
Hard timeouttimeout 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 vocabularymar
# prompt: """
The original ask, verbatim. A fenced value is a container for
markdown — blank lines, lists and # characters are plain text.
"""

# github:repo: acme/platform        // context for github refs
# github:issue: 22                  // → https://github.com/acme/platform/issues/22
# github:pr: other/repo#9           // explicit repo wins over context
# jira:site: https://acme.atlassian.net
# jira: PROJ-123, PROJ-124          // comma-separated lists allowed
# linear:workspace: acme
# linear: ENG-42
# ref: https://wiki.acme.dev/brief  // generic link

# delivery: pr-per-phase            // or branch-per-phase | stacked-prs | single-pr | single-branch | none
# delivery:branch: replatform/{phase}
# report: per-phase                 // or at-checkpoints | at-end

# timebox: 3d                       // legacy advisory budget; does not control traversal
# priority: high                    // node-level: critical | high | normal | low
# wake: new "bug" issues in acme/shop   // node-level: what re-activates a standing phase

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:

checkout.mar — first draftmar
# project: checkout-revamp
# 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.
"""
# github:repo: acme/shop
# ref: https://wiki.acme.dev/checkout-revamp
VAR attempts = 0

=== build_checkout ===
# github:issue: 41
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 ===
# github:issue: 42
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.
validate — two structural errorsexit 1
$ 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:

checkout.mar — fixedmar
# project: checkout-revamp
# 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.
"""
# github:repo: acme/shop
# ref: https://wiki.acme.dev/checkout-revamp
VAR attempts = 0

=== build_checkout ===
# github:issue: 41
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 ===
# ref: https://wiki.acme.dev/checkout-usability-study
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 ===
# github:issue: 42
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
validate --strict — cleanexit 0
$ 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:

summarize — the reviewer's viewexit 0
$ 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:

state init — start and show the first choicesexit 0
$ 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.)

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

state choose — the agent cannot approve launchexit 1
$ 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:

state choose — a human records the ship decisionexit 0
$ 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:

state show — the plan changed during the runexit 3
$ 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.

state rebind — accept the edit and keep the historyexit 0
$ 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 helpexit 0
$ 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

CodeMeaning
0success (warnings allowed unless --strict)
1validation errors (or warnings with --strict); refused walk operations
2usage or I/O errors
3plan/state drift detected — reconcile with state rebind (or state init --force)

Compiler diagnostics

CodeSeverityMeaningUsual fix
MAR001errorparse errorfix the line the message points at
MAR002errorduplicate phase idrename one of the phases
MAR003erroran arrow points to a stage that does not existuse the did you mean suggestion, or define the missing stage
MAR004errorundefined variable in a gate or mutationdeclare it with VAR in the preamble
MAR005errorduplicate variable declarationremove the extra VAR
MAR006errora stage has no available way forwardadd a choice or an automatic next step such as -> END
MAR007errorunreachable phaselink it from a real decision, or delete it
MAR008errorundeclared cycleif the loop is intentional, add ~loop~ to the returning choice; otherwise re-point the edge
MAR009errordeclared loop with no exitadd a counter-gated or @human sibling exit
MAR010errorloop exit gate provably unsatisfiablefix the gate so the counter can actually reach it
MAR011warningconstant-false gatethe choice can never be taken — fix or remove it
MAR012error@human choice with no escalation pathgive the choice a -> target
MAR013warning~loop~ on an edge that isn't part of a cycledrop the mark, or point the edge where you meant
MAR014warningunverified gate (dynamic fact the compiler can't decide)expected for real-world facts — review manually, don't churn the plan to silence it
MAR015errortype mismatch in an expression or mutationalign the literal/variable types
MAR016warningunused variablegate on it, or delete it
MAR017warningonce-only (*) loop edgemake it sticky (+) so the loop can repeat
MAR018warningmalformed external ref (github:/jira:/linear:/ref:)fix the value to the namespace's expected shape
MAR019warningunknown delivery:/report: valueuse a listed value; typos fall back to defaults loudly
MAR020warningunknown tracker: valueuse github, jira or linear
MAR021warningmalformed timebox: valuesingle unit: 90m, 4h, 3d, 2w
MAR022warningunknown priority: valueuse critical, high, normal or low
MAR023warningtimeboxed phase with a single exitgive 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.)

CodeMeaning
completedthe plan has already reached END
unknown-nodestate points at a node absent from the compiled plan
unknown-choiceno choice matches the given reference
ambiguous-choicea label prefix matched more than one choice
gate-blockedthe choice's gate is currently false (or failed to evaluate)
once-exhausteda once-only (*) choice was already taken
human-checkpointan agent tried to take an @human choice
rationale-requiredthe step is missing its auditable rationale
no-next-stepadvance was used where there is no automatic next step
migration-blockedstate cannot be rebound onto the edited plan (current phase gone) — needs a human

Files on disk

FileWhat it is
plan.marthe script — durable, diffable source of truth
plan.trajectory.jsoncompiled contract (marionette compile); content-hashed
plan.state.jsontraversal state + decision log, bound to the trajectory hash
.marionette/graphs/<hash>.trajectory.jsonruntime store: compiled trajectories archived by content hash
.marionette/runs/<run-id>/events.jsonlruntime store: append-only event journal (authoritative)
.marionette/runs/<run-id>/snapshot.jsonruntime 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).