recipes
The trust mechanism

What is a recipe?

A recipe turns "I checked, it works" into something proved on a device as the change is made — and something that objects later if the behaviour breaks.

A recipe proves a task with actions. A JSON graph whose nodes are typed actions drawn only from what this checkout discovered, each carrying a human-written intent, executed against a real build, ending in a verdict computed from what happened rather than asserted in prose.

Every clause there does a job against a specific failure mode. Here they are, one at a time.

What it is for

Proof. Actions drive a real build and assertions check what should be true — counted, where a count is what distinguishes working from nearly working. What lands is on-device evidence, not a claim in a pull request description.

Self-checking, in the same context. The agent does not need a fresh session, a reviewer, or a CI round-trip to know whether its change held. It replays the recipe mid-task, where it is already working, and reads the verdict itself — the inner loop, closed before handoff.

Prevents regressions. The outer loop is the same recipe replayed against every later change. Written once, it keeps refusing the bug it was written for, which is why a recipe is worth authoring rather than checking by hand: the check survives the task.

Trust. Proven, self-checked, and still proving — so the change is trustworthy while it is being built. Everything downstream, review included, inherits that, and every gate stays yours to steer.

The anatomy of a real one

perps.lifecycle ships with the harness. Abridged here — the full graph runs the whole lifecycle, clean state through teardown — but nothing is invented; read it all with mm-harness run perps.lifecycle --describe.

{ "title": "MetaMask Perps lifecycle smoke", "description": "Proves a deterministic Perps prestate, opens a small ETH testnet position, verifies it, closes it, and tears down to a clean state.", "workflow": { "entry": "status", "nodes": { "status": { "action": "app.status", "intent": "Read runner compatibility status", "next": "fixture" }, … wallet fixture, unlock, account select … "start-state": { "action": "metamask.perps.start_state", "market": "ETH", "positions": { "state": "none" }, "intent": "Prepare clean ETH testnet Perps state", "next": "assert-clean-position" }, "assert-clean-position": { "action": "metamask.perps.assert_positions", "market": "ETH", "state": "none", "intent": "Confirm ETH position state is clean", "next": "assert-clean-orders" }, … assert clean orders … "place-order": { "action": "metamask.perps.place_order", "market": "ETH", "side": "long", "amount": "11", "leverage": 3, "intent": "Open a small ETH 3x long position", "next": "assert-open" }, "assert-open": { "action": "metamask.perps.assert_positions", "market": "ETH", "state": "open", "intent": "Confirm the ETH position opened", "next": "read-orders" }, … close orders, close position, assert closed … "teardown-state": { "action": "metamask.perps.teardown_state", "intent": "Return Perps to globally clean home state", "next": "done" }, "done": { "action": "end", "status": "pass" } } } }
entry + next

It is a graph, not a script. Execution starts at entry and follows each node's next. Branching exists too — a switch node picks an edge — which is why the trace records the path actually taken rather than assuming the file's reading order.

action

Always a name from the discovered manifest. app.status, metamask.perps.place_order, and end are all real entries you can look up with mm-harness actions --action <name>. You cannot write an action that does not exist and have it run.

intent

Written by a human, before the run. It is not a comment — it is carried into the trace, so the evidence reads as an argument rather than a log.

assert-clean-position

The pattern worth stealing: assert the starting state before acting. Without it, a position that was already open would make the next step look like a success it did not cause.

assert-open

And assert the effect afterwards, independently. place_order reporting success is the action's own word for it; assert_positions goes and looks. These are deliberately two different nodes.

end

The terminal node. Reaching it is what a passing run means — and it is only reachable by passing through every assertion on the path.

Actions come only from the discovered manifest

The available actions are fixed and discoverable, and a recipe may use nothing else. Validation happens against that manifest before anything executes:

$ mm-harness run my-recipe.json --plan
[static] validate.schema: recipe document schema + action existence/platform vs the adapter manifest

A misspelled or imagined action fails validation with exit code 5, and nothing touches the app. That is the cheap check — run it before the expensive one.

The constraint is the point

A tool free to invent action names is free to invent plausible ones, and a recipe full of plausible steps that never ran is worse than no recipe. Bounding the vocabulary to what exists is what makes a green run mean something — and what makes this safe to hand to an agent.

Intent turns a trace into a narrative

The runner copies each node's intent into the trace beside its result. Read them in order and you have the argument the recipe makes:

PASS status — Read runner compatibility status PASS fixture — Verify wallet fixture availability PASS assert-clean-position — Confirm ETH position state is clean PASS place-order — Open a small ETH 3x long position PASS assert-open — Confirm the ETH position opened

A reviewable claim in five lines, generated from the recipe rather than written about it afterwards. It gives review a precise question: if the intents do not add up to what the pull request claims, the bundle does not support it — however green.

Assertions are first-class — and counting matters

An assertion is a node like any other: it occupies a position in the graph, carries its own intent, and failing it stops the path to end.

AssertionProves
assert_fileA project file exists, and optionally contains given text.
assert_jsonA JSON value satisfies a selector and operator.
assert_outputCaptured stdout or stderr from an earlier command node matches.
assert_exit_codeAn earlier command node exited as expected.
metamask.perps.assert_positionsLive positions are present or absent for an explicit selection.
metamask.analytics.assert_eventsEvent names, property values, and counts.

Counted assertions earn their keep. The analytics assertion takes an exact count, and its own description says why: an exact count catches a double emit that a presence check cannot. "Did the event fire?" answers yes to a bug that fires it twice.

On Core, assert_positions and assert_orders take expect_count; the Extension and Mobile manifests do not declare it, and a recipe that passes it there fails validation with exit 5. assert_orders can also assert a matching order's trigger price, execution mode, and reduce-only flag. Reach for the most specific assertion the surface supports — a vague one passes for the wrong reasons.

Recipes call recipes

call is itself an action: it invokes a named recipe by reference and runs its steps inline, so shared setup is written once instead of copied into every proof. The manifest's own example:

{ "action": "call", "ref": "mydev.open_perps_setup", "intent": "Run a personal Perps setup recipe", "next": "done" }

Running inline means the called recipe's nodes appear in the trace like any others: composition is an author's convenience, not a hole in the evidence. --describe reports both the actions and the recipes pulled in:

$ mm-harness run <recipe> --describe
actions (3) app.status assert_file end called recipes: none

The verdict is derived, never declared

Status is computed from the trace, not asserted by the author or written by an agent afterwards. Every node has an ok; the run passes only if the path reached end with all of them true:

{ "status": "pass", "total": 6, "passed": 6, "failed": 0 }

Two outcomes only: pass or fail. No "inconclusive" hiding place at this level — either the graph reached its terminal node with every assertion satisfied, or it did not.

Where "cannot be proven here" lives

One level up. When a review workflow maps acceptance criteria onto proofs, a criterion the surface cannot demonstrate is reported as needing a human rather than quietly passed. That third answer belongs to the review layer; a recipe either proved the thing or did not.

So every number you cite comes from trace.json; prose around a bundle is commentary. The reviewer's guide walks a real one.

The test of a good recipe

A good recipe fails if the change it proves is reverted. That is the whole quality bar.

A sharper question than "does it pass?", because passing is easy and uninformative. A recipe that navigates to a screen, screenshots it, and ends will pass forever — including after someone deletes the feature. It proves the app still starts.

So the check is adversarial: imagine the change gone and ask which node goes red. If the answer is "none", the recipe is decoration. Common ways this goes wrong:

ShapeWhy it survives a revertFix
Screenshot with no assertion An image is evidence for a human, but nothing in the graph inspects it. Add an assertion on the state the screenshot illustrates.
Presence check where a count belongs Still true when the bug fires the event twice, or leaves two positions open. Use exact; on Core, expect_count.
No starting-state assertion Passes off pre-existing state as the effect of the action under test. Assert clean before acting, as perps.lifecycle does.
Asserting the action's own report Confirms the call returned, not that the world changed. Read the state back with a separate node.

Cheapest way to run this test rather than reason about it: revert your change locally, run the recipe, watch it fail, restore. A recipe you have seen fail for the right reason is one you can trust when it passes.


Author one yourself

You do not have to write the JSON by hand, and mostly should not. Point the recipe-authoring skill at an acceptance criterion and it discovers the actions, composes the graph, validates with --plan, and runs it — following the rules on this page because the harness enforces them. V4 walks that flow.

Expect one failure-and-fix loop on the way; that is the error messages doing their job.

Before you author

Learn the vocabulary for your checkout — you are choosing from this list, not inventing.

$ mm-harness actions --categories
$ mm-harness actions --action assert_orders

Read one that works

The bundled recipes are worked examples. Start from the closest one to your problem.

$ mm-harness run --list
$ mm-harness run perps.lifecycle --describe