Run guide

From a spec to running code, in eight steps


Write a spec, generate a plan, then build it — one AI coding agent per step, each in its own git worktree, streamed live into Petrinaut. This guide walks the whole loop, from an empty project to a finished build.

🧪 This is an experimental prototype. It works best for building small, self-contained CLI tools in TypeScript (TypeScript/bun runtimes), and may struggle with existing codebases or large projects.

How to run it

Eight steps from an empty project to a finished build. Copy each command as you go.

  1. Set up your environment

    do this first — nothing runs without it

    Before any command, install the two tools cook runs: pi — the coding agent that writes the code for each step (version 0.74 or newer, on your PATH) — and bun, which the demo fixture uses as a runtime.

    npm i -g @earendil-works/pi-coding-agent
    curl -fsSL https://bun.sh/install | bash
    

    Then add your model API key — and, for the live view, the Petrinaut URL and port — to a .env in the directory you'll run from. Shell environment variables work too, and win over .env.

    # required
    ANTHROPIC_API_KEY=sk-ant-…
    
    # required for --petrinaut-stream
    PORT=6006
    PETRINAUT_URL=https://petrinaut.example/brunch?sse=http://localhost:6006/stream
    
  2. Write a spec

    Launch Brunch and describe the small app you want. It interviews you and writes a reviewable specification into the local .brunch/ folder.

    npx @hashintel/brunch@latest
    

    The result is a completed specification in the workspace — the input to the next step.

    Pick New concept from scratch for your first run. The other option — Feature within existing codebase (brownfield) — is also experimental, and requires a clean git working tree to cook (see step 5).

  3. Generate a plan

    Turn the spec into a plan.yaml — a list of steps, with test targets and dependencies. The id comes from the workspace URL: a path like …/specification/39/requirements-review means the id is 39.

    npx @hashintel/brunch@latest plan <id>
    

    This writes .brunch/cook/specs/<id>/plan.yaml. Read the plan before running it — it's a normal YAML file you can edit, and today plans are largely hand-authored:

    slices:
      - id: store
        definition: "Add an in-memory TodoStore in src/store.ts"
        depends_on: [types]
        verification: [{ kind: unit-test, target: tests/store.test.ts }]
    
  4. Pick a plan to run

    a bundled demo fixture — or your own

    There are two ways to point cook at a plan:

    codebase mode · needs a clean git tree

    Your created plan

    The plan you just generated — cook it in place with --spec=<id>.

    bundled demo

    ./fixtures/layered-todo

    A ready-made plan.yaml — a small Todo service plus CLI (the join demo).

    The fixtures/ folder lives in the Brunch repo, not the npm package — so cook ./fixtures/layered-todo only works from inside a checkout. Clone it and run from there, or point cook at your own plan.yaml.

    git clone https://github.com/hashintel/brunch
    cd brunch
    git pull origin main # make sure you're on the latest fixtures
    
  5. Build it, live

    cook compiles the plan into a Petri net and runs it: one pi agent per step, each in its own worktree. Adding the --petrinaut-stream flag boots a local SSE server, prints a launcher URL, and opens Petrinaut.

    # A — cook the plan you just created (codebase mode, clean git tree)
    npx @hashintel/brunch@latest cook --spec=<id> --policy=parallel --petrinaut-stream
    
    # B — run the bundled fixture (from inside the Brunch repo)
    npx @hashintel/brunch@latest cook ./fixtures/layered-todo --policy=parallel --petrinaut-stream
    

    Codebase mode (option A) is still experimental and produces unreliable results. It also needs a clean git tree — commit or stash your changes first. For your first run, use a greenfield New concept from scratch spec, which has no clean-tree requirement.

    A run prints its progress as it goes:

    brunch cook
    brunch cook · policy parallel · 2 epics, 8 slices
    stream    http://127.0.0.1:6006/stream
    launcher  https://…/brunch?runId=3f9c…   ← opens in browser
     9.4s   ✓ tests    store (8.2s)
    17.1s   ● verdict  store → DONE
    ✓ completed (2m 06s)
  6. Watch it in Petrinaut

    With --petrinaut-stream, the browser tab opens on the live run. Places light up as each step starts, holds, and finishes — you watch the build progress through the net in real time. See How it works for what the net is showing you.

  7. Inspect the run

    Each run writes its artifacts to disk under .brunch/cook/runs/<id>/. Per-step output lands in worktree/; the final merged result for each epic is assembled under __epic__/<epic>/ — that's the combined code to review and copy out.

    • worktree/ — per-step generated code.
    • __epic__/<epic>/ — the merged code: completed steps combined, where the epic tests run. This is your final result.
    • reports.jsonl — what each step did, keyed by reportId.
    • net.json — the compiled topology: places, transitions, arcs.
    • petrinaut-events.jsonl — the firing stream, for offline replay.
  8. Clean up

    cook doesn't clean up runs automatically yet, so old worktrees pile up and take disk. .brunch/ is gitignored, so they never show in git status. Drop the latest run with:

    RUN=$(ls -t .brunch/cook/runs | head -1)
    git worktree remove --force .brunch/cook/runs/$RUN/worktree
    rm -rf .brunch/cook/runs/$RUN
    

Try the examples

There are two demos you can run from the fixtures/ folder in the Brunch repo. They're the quickest way to see the difference between running steps one at a time and all at once.

fan-out · wall-clock

parallel-utils

1 setup → 8 independent leaves → barrel. Eight zero-dependency utility functions, each its own file and test, with no edges between them. Run serial, then parallel, to compare wall-clock time.

dependency join

layered-todo

types → {store, validation} → service → cli. service can't start until both store and validation finish — and the cli epic waits behind a cross-epic gate until core is done.

Switch fixtures and flip --policy to compare one step at a time (serial) versus all enabled at once (parallel). Try it below — the same dependency rules, run against a bounded pool of agents:

fixture
policy

agent pool

A real diamond: service can't dispatch until both store and validation return.

available4 / 4

types

queued

store

waiting on types

blocked

validation

waiting on types

blocked

service

waiting on store, validation

blocked

cmd-add

waiting on service

blocked

cmd-list

waiting on service

blocked

cmd-done

waiting on service

blocked

todo-e2e

waiting on cmd-add, cmd-list, cmd-done

blocked

policy parallel · pool 4 agents · core diamond → cli (cross-epic gate)elapsed 0.0s
blocked / queued
building
completed
◆ join / tail (waits on all inputs)
A simulated run of the layered-todo fixture under the parallel policy. A bounded pool of agents builds each slice once its dependencies are complete; serial runs one slice at a time, parallel runs all eligible slices at once up to the pool size, so the wall-clock difference between the two policies is visible.

Commands

Everything runs through the published binary, npx @hashintel/brunch@latest.

CommandWhat it does
brunchLaunch the web UI in the current project directory — write or refine a spec.
brunch planEmit a plan — writes .brunch/cook/specs/<id>/plan.yaml from a completed spec.
brunch cookRun the orchestrator on a plan directory — e.g. ./fixtures/layered-todo, or --spec=<id>.
brunch agentRun a JSONL capability session on stdin/stdout, for harness integration.

Cook flags

One opt-in flag turns on the live view; the rest tune the run.

Flag / envWhat it doesDefault
--spec=<id>Pick .brunch/cook/specs/<id>/plan.yaml (codebase mode).newest
--policy=serial|parallelFiring policy — one step at a time, or all enabled at once (pool-bounded).serial
--max-retries=NRetry budget per slice; exhaustion stops the step.3
--petrinaut-streamStream the live run to Petrinaut over SSE. Without it, cook runs headless to disk.off
--petrinaut-url=<url>Petrinaut route URL incl. path (requires --petrinaut-stream; else PETRINAUT_URL).
--no-petrinaut-openDon't auto-open the browser (the URL still prints).opens
--petrinaut-lanes=both|mechanicalLane projection; mechanical hides the semantic lane.both
--petrinaut-fold=color|identityPetri-net projection for the live view; identity keeps places literal.identity
--verbose, -vShow raw pi-agent output.off

Plan flags: --out=<dir> sets the output directory (default: current directory), plus --verbose / -v.

Next steps

  • New to the concepts? How it works explains the spec → plan → net → build pipeline and the two-lane subnet each step compiles to.
  • Setting up for the first time? Start with Setting up Brunch.