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.
Eight steps from an empty project to a finished build. Copy each command as you go.
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
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).
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 }]
a bundled demo fixture — or your own
There are two ways to point cook at a plan:
codebase mode · needs a clean git tree
The plan you just generated — cook it in place with --spec=<id>.
bundled demo
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
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 · 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)
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.
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.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
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
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
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:
agent pool
A real diamond: service can't dispatch until both store and validation return.
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
Everything runs through the published binary, npx @hashintel/brunch@latest.
| Command | What it does |
|---|---|
brunch | Launch the web UI in the current project directory — write or refine a spec. |
brunch plan | Emit a plan — writes .brunch/cook/specs/<id>/plan.yaml from a completed spec. |
brunch cook | Run the orchestrator on a plan directory — e.g. ./fixtures/layered-todo, or --spec=<id>. |
brunch agent | Run a JSONL capability session on stdin/stdout, for harness integration. |
One opt-in flag turns on the live view; the rest tune the run.
| Flag / env | What it does | Default |
|---|---|---|
--spec=<id> | Pick .brunch/cook/specs/<id>/plan.yaml (codebase mode). | newest |
--policy=serial|parallel | Firing policy — one step at a time, or all enabled at once (pool-bounded). | serial |
--max-retries=N | Retry budget per slice; exhaustion stops the step. | 3 |
--petrinaut-stream | Stream 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-open | Don't auto-open the browser (the URL still prints). | opens |
--petrinaut-lanes=both|mechanical | Lane projection; mechanical hides the semantic lane. | both |
--petrinaut-fold=color|identity | Petri-net projection for the live view; identity keeps places literal. | identity |
--verbose, -v | Show raw pi-agent output. | off |
Plan flags: --out=<dir> sets the output directory (default: current directory), plus --verbose / -v.