Deliver a change

/run build <request> runs the shipped build flow on the request, from the request to a finished working tree. What runs is a flow, a file you can read, copy and replace with your own, without touching any code:

/run build make slugify lowercase the title, turn every run of characters that are not letters or digits into one dash, and trim dashes at both ends; add tests for it --model ilaas/gemma-4-31b

  locate      a scout maps the code the request touches
  plan        a planner splits it into subtasks, a typed list
  deliver     up to two rounds of:
    work        each subtask to a coder and a reviewer, in a copy of the repository
    tests       .pi/checks/test.sh, your project's own script
    audit       an auditor reads the whole change against the request
  report      a synthesiser tells you what was done and what is left

It asks nothing. The request is the brief, the check is a script of your project, and a run left alone runs to its end. It ends with the work in the working tree, uncommitted, and its answer in the conversation. You decide what reaches history after reading the diff.

What it needs

The run stage checks the project before the first spawn, and says what is missing, one fault per line:

  • A git repository. Each subtask works in a copy made from the tree, and the copies land back through git. Outside one, the run is refused at the block that makes them.

  • .pi/checks/test.sh. The check is a script of your project, read before the run starts and run with bash after each round of work: what runs is what was read, whatever an agent does to the file. The shipped flow names that path, so a project without it is refused rather than audited on the reviewers’ word.

#!/usr/bin/env bash
exec node --test

--model puts every agent turn on one model, and --timeout bounds one turn. Nothing else is a flag: how many rounds, how many subtasks at once, whether they get copies, what the check is, all of it is in the file.

What you see

The widget draws the flow’s plan as it fills, the visit running now expanded with what its subagent is doing:

● build · 2 visits · 54s · ↑22k ↓1.9k
✓ locate · scout · 33s · ↑12k ↓831
✓ plan · planner · 10s · ↑9.8k ↓1.1k
● deliver · #1 of 2
  ● deliver#1
    ● deliver#1/work · 0/1 so far
      ● deliver#1/work[1]
        ● deliver#1/work[1]/pair · #1 of 3
          ● deliver#1/work[1]/pair#1
            ● deliver#1/work[1]/pair#1/code
              ● coder#1  grep /node/ in .  ilaas/gemma-4-31b · 11.2s
            ○ deliver#1/work[1]/pair#1/review · agent reviewer (.pi/agents/reviewer.md) · reads item.text, code, diff…
    ○ deliver#1/tests · check .pi/checks/test.sh · timeout 10m · ≤ 20m
    ○ deliver#1/audit · agent auditor (.pi/agents/auditor.md) · reads input, work, tests, diff, deliver.ledger · verd…
○ report · agent synthesiser (.pi/agents/synthesiser.md) · reads input, diff, deliver.output.last.work, deliver.outpu…
esc stops everything · ctrl+↑↓ selects · ctrl+del stops the selected one

A subagent’s line gives its tokens once its turn has ended, since that is when pi’s counters are read; the coder above is still in its first.

When it ends, its answer lands in the conversation: the output of its last root node, report, a few lines a synthesiser wrote from the diff, how each pair of the last round ended and the audit; then ok · runs/<timestamp>; then the last frame:

Result of the build flow, asked to: make slugify lowercase the title, turn every run of characters that are not letters
or digits into one dash, and trim dashes at both ends; add tests for it.

The slugify function in slug.js now lowercases the title, replaces any sequence of non-alphanumeric characters with a
single dash, and trims dashes from both ends. A new test file, slug.test.js, was added to verify these behaviors.
Nothing remains to be done.

ok · runs/2026-09-24_01-06-54

✓ build · 12 visits · 2m19s · ↑69k ↓12k
✓ locate · scout · 33s · ↑12k ↓831
✓ plan · planner · 10s · ↑9.8k ↓1.1k
✓ deliver · 1 iteration · 1m32s · ↑45k ↓9.3k
✓ report · synthesiser · 5s · ↑2.1k ↓456

The report is written from the diff, so it names what a coder did beyond the request too, a file nobody asked for included; it does not replace reading the diff. A build that fails writes no report, and its end line says where and why.

git diff shows exactly what the run did. The run directory holds each subagent’s transcript under the visit or the pair it served, and usage.json with what every visit cost. See Flows.

How the work is judged

  • A pair is a coder and a reviewer who remember each other, for three rounds at most. The reviewer decides through a verdict, reading the task, the coder’s summary and the diff of the copy: the summary is a claim, the diff is the evidence. A review that ends in prose, with no verdict call, is sent back once with the failure named, and so is an audit. A pair that reaches three rounds unapproved still lands its work, marked not converged, and the audit reads that.

  • The check’s verdict is final. The round is over when the tests pass and the auditor approves; no approval turns a failing script into a success. Without it, a pair once wrote a helper and its tests, the reviewer approved, the auditor approved, and the test file imported ./slugify.js for a file named slugify.ts. The suite never even loaded. Both agents had read the code.

  • What the audit raises becomes the next round’s subtasks. Each remark is an obligation with an id in the round’s ledger, and the second round works on the ones nobody closed. There is no third: a build that ends its second round unapproved fails, and one whose ledger is empty with nothing approved gives up rather than running a round on nothing.

Why nothing is committed

A commit is a decision about the work, and whoever takes it has to have read it. So the work stays where the pairs landed it: the report says what to look for, and git diff is what you read.

When you want the decision taken in the run, with somebody there, /run build-attended <request> interviews you on the request, shows the specification and asks “Build this?”. Answered yes, it runs build on the specification, has the committer write the message from the specification, the build’s report and the diff, and commits on the run’s own branch. Our code makes the commit; the committer has no tool that could.

Carrying on after an interruption

Every fact of the run is appended to its journal as it happens, so a Ctrl+C, a dropped connection or a closed terminal costs nothing that already ended.

/run resume
run: resuming build in runs/2026-09-23_20-21-17, from deliver#1/work[1]/pair#1/code

It says what it picked up rather than asking: typing it is already the answer. That run was killed while its coder worked, and the coder’s visit ran again, in the copy it had left open. The summary counts both lives, the first marked partial since it was killed before it could write its measurement:

✓ build · 9 visits · 1m51s · ↑43k ↓11k · 2 lives (1 partial) · resumed from deliver#1/work[1]/pair#1/code

Every visit that ended survives, the plan and each approved pair included, and the first one that did not end runs again with fresh subagents, which re-read the tree rather than a replayed conversation. A copy left open is taken back, and one that is gone starts its subtask over. A run that failed by a decision of its own, the second round unapproved or nothing left to hand round, would decide the same again, and is refused with why. What else is refused, and how, is Resuming a run.

Your own build

A build.md in ~/.pi/agent/flows/ or .pi/flows/ replaces the shipped one, inside build-attended too. Copy the shipped file and change what you mean to: the check’s path, the number of rounds, the concurrency, the agents. A file that does not check is refused with its faults, never silently replaced by the shipped one; /flows build prints the plan it would run. A build.md left in an old pipelines/ directory is refused too, and From pipelines to flows says how to rewrite it.

The interview

/interview is still a command of its own, and build-attended asks through the interview flow, on the same card. One question at a time, because a good second question depends on the first answer, and “That’s enough” is a submit, not a cancel: what was answered still counts, and the specification is still written. The header, the questions and the specification come back in the language of the request, and they travel on to the planner, the coders and the auditor in it: the person correcting the specification is the one who has to read it exactly, and a model takes a request in any language.

A label and its description share one line of the card, and pi-tui cuts what does not fit without an ellipsis, so the interviewer is asked for a label under 30 characters and a description under 60.

From a script

import { bashCheck, checkFlow, checkRun, gitPort, loadFlowCatalogue, measuredRun, runFlow } from "@ai-for-dev/combo";

const flow = checkFlow("build", loadFlowCatalogue({ cwd, scope: "both", builtin: true }));
if (!flow.ok) throw new Error(flow.faults.map((fault) => fault.message).join("\n"));
const run = await checkRun(flow.flow, { cwd, ports: { check: bashCheck(), git: gitPort() }, somebodyThere: false });
if (!run.ok) throw new Error(run.faults.map((fault) => fault.message).join("\n"));
const measured = measuredRun({ dir: runDir });
const result = await runFlow(run.run, "add a cache in front of the loader", { model, runDir, onEvent: measured.onEvent });
measured.finish();

This is what /run build does, with the terminal’s card as the ask port. The deliver, pair and audit combinators the linear pipeline ran are gone with it: this flow is the one implementation of that shape, and the only one with a journal, restored copies and a script as its check.

Reference