12vectors / bench docs for 0.4-alpha
bench docs GitHub ↗
ConceptsRunning a phase

Running a phase

Everything between starting a phase and the one PR it ends with: where each member's branch is cut, what advances the run and what stops it, how a restarted board picks the phase back up, and what merging it does to the cards it carried.

Running a phase works its list into a single integration branch. Starting one cuts phase/<task-stem> from the newest origin/main it can see — the same rule and the same timeout a task branch is cut by — and gives it a worktree beside the task worktrees. From there each member is branched from the phase's tip, run headless exactly as ▸ start work runs any card, and merged back into the phase branch when its checks are green; then the next one starts. At the end one PR, from the phase branch into main, for a human.

That is why the branch exists. Members of a phase are related by definition, so card two branched from main could not see card one's work while card one sat unmerged in review/ — it would conflict, or quietly build the same thing twice. Gating on a merge into main would fix the branch point and destroy the point, because main is merged by a person and the phase would stall on every card. So the human gate moves from every card to the phase boundary, and the promise survives intact: the board merges into a branch it created, inside a scope you opened, and main still waits for your click.

The runner is a beat, not an agent. Everything it decides is already structured state — a card's stage, a PR's CI verdict, whether one branch is contained in another — so an agent paid to poll would be the wrong tool at the wrong price. It is a plain thread (BOARD_PHASE_INTERVAL, 30s), silent when no phase is running.

The beat is stateless. Each pass recomputes which members are finished, which is first unfinished and what that one needs; it holds no registry of where a phase is. Two durable things carry the memory instead, and both are things the board already writes: git, where a member is finished when its branch is contained in the phase branch, and the phase card, which grows a ## Phase log section the runner adds one line to per decision — a run started, a member started, a member merged, a halt. The log is the record a person reads, and the only thing that can tell "this member has run and it ended badly" from "the phase has not reached it yet"; without it a restarted board would relaunch a run that died. So a restart resumes a phase by looking, and the same logic answers "what now?" whether the last event was a launch, a merge or a crash.

Advance on green. A member is finished when its card reaches review/ and its checks are not against it. Green is read from the same PR poll the board already runs: red halts the phase, running holds it, and a member with no checks at all advances — a project without CI must not deadlock every phase it runs. A member with no branch at all that is already in review/ or done/ is simply finished; there is nothing to bring.

Halt, never skip. Five conditions stop a phase, each already a visible state on the card: a member that exits NOT READY, a run that exits non-zero, a clean exit that committed nothing, CI red, and a merge into the phase branch that is not mechanical. A phase that stepped over a failed card would build the rest on a foundation that never landed. The halt is written into the log and then held — said once, not once a beat — and nothing retries by itself. Running the phase again is a person's decision, and it is what appends the line that clears the halt. A member whose **Depends on:** names something unfinished is a wait, not a halt: the phase idles until the dependency lands (merged, for a card inside the phase; done/, for one outside).

Merges are additive, always. Nothing here rebases and nothing force-pushes. main is merged into the phase branch on every beat, so a phase that runs for hours does not drift into one enormous conflict at the end; a conflict there halts the phase like any other, aborted cleanly, with the colliding files named. When every member is in, the branch is pushed, a PR into main is opened with the member list as its body, the **PR:** line is written into the phase card and the card moves to review/ — where the existing apparatus applies unchanged: the CI chip, ◔ review PR, ⚑ copilot, and drag-to-done/ for merge & clean up.

One board runs it. State syncs; reactions don't, so the phase card's Assignee is where "who runs it" is written down — the same claim that gates starting work. A replica renders the phase and advances nothing.

Members run one at a time. Running independent members in parallel is a separate card.

Watching one run, and watching it stop

A phase card is a card, so the PR chip, CI, the review actions and the merge-and-clean-up sheet all reach it by inheritance. Three things do not, and they are the phase's own interface:

And the card itself says which state it is in. The header chip only appears while there is something to say, so it cannot tell "the phase has not been started" from "there is no phase here at all" — which left an in-progress/ phase card nobody had run looking exactly like one mid-run. So the card carries the distinction quietly, in the pill and the line under it: not started (▸ run phase cuts its branch and starts the first card), held, the accent and the member in flight while it runs, and halted in --alarm. Only a run wears the working vocabulary — the breathing mark, the accent border, the caret — because only a run is work happening.

And the card does not move while its work runs. A phase card stands for cards the Board no longer draws, so dragging it to another stage — or onto the archive tray, which is a move like any other — while a member has an agent in it would leave the card in one place and a live run, a worktree and a branch in another. That move is refused, and the refusal names the way through rather than the wall: which member is working, and that ‖ hold stops the phase and the agent it has in flight while leaving the phase branch, everything merged into it and every worktree exactly as they are. The card stays where it was and the reason is in the toast. The answer is the server's — /api/move, /api/archive and merge-and-clean-up all ask it, so a stale page cannot get past a rule the drag alone would enforce. What is refused is narrow on purpose: a phase between members has nothing in flight and moves freely, a halted one has nothing running by construction and moves freely — which is exactly when walking it back is the thing to do — and "running" is read from the runs themselves rather than from what the log says was once started, so a member's run that died cannot lock its phase card. An ordinary card never reaches the question.

Every advance is narrated in the ticker as it happens: the member that came up green, the merge into the phase branch, the member starting next. And the phase card's sheet lists its members in run order with each one's stage — and, while a phase is in flight, the runner's own reading of each (merged in, working, checking, stopped here) — so the card answers "where is this up to" without a hunt across five columns.

Finishing a phase finishes its cards

A member stops at review/ on purpose: done/ has always meant merged into main, and merged into a phase branch is not that. Merge & clean up on the phase card is the moment it becomes that. The phase's PR goes into main, so every card the phase carried is in main too — and since a phase reaching done/ releases its members back onto the Board, the alternative is handing back three or five cards you have already judged, in the column whose note is "your move".

So the merge sweeps. After it has actually succeeded — never before, and never if it conflicts — every member the phase merged moves to done/, and each one's workspace is cleared exactly as completing an ordinary card clears its own: the worktree removed, the local branch deleted, the branch on the remote deleted. That half is not cosmetic. A stale worktree is a trap laid for whoever reopens the card, since a work launch refuses a card whose worktree already exists, and the accumulation is per member per phase.

Four rules keep it honest: