Agents on the board
Start work makes a worktree and a branch, runs the agent headless, and moves the card on the way it exits.
Card actions appear on hover, taking over the status pill's slot (never
stacking on top of it) — at most two per state, only things you'd actually
do without opening the card: ▸ start work on in-progress cards (▸
take over when someone else holds them), ‖ hold while an agent runs,
↩ back on cards waiting on you, ↑ open PR on review cards whose
branch has none, ↺ reopen on done cards, ⟶ phase on unstarted
cards a phase in to-do/ could take, and ◔ still true? everywhere.
Actions that cost tokens or stop work arm on first click and fire on the
second.
Each launched agent wears a short name for its lifetime (Wren, Juno,
Basil, …) — picked per launch, never shared by two running agents, shown as
Wren · #09 on cards, in the sessions list and throughout the ticker. Names
are held in memory, so a restarted board falls back to plain "Agent" for
sessions that predate it.
Beside that name, wherever it identifies a run — the sessions list, the
session and Focus headers, the working card's agent line — sits the model
the launch rode: a small mono chip in the id hash's dim register
(opus-4-8, the vendor's whole string on hover). Which brain did the work
is a review question, not a state, so the chip takes no colour. A launch
that inherited the vendor default, and a session replayed from disk, wear
no chip at all — the board says nothing rather than guessing.
▸ start work launches a headless agent run on the task, through the
configured adapter. It exists only on in-progress/ cards: moving a card
to in-progress is the commitment, and only then does work start — the
server refuses launches from anywhere else. In team mode it also refuses a
card someone else holds, naming them; the action reads ▸ take over
there, and firing it is the deliberate reassignment. An unclaimed card
claims itself on launch. One agent per task at a time, and a work agent's
worktree must not already exist when it starts.
It also refuses a phase card, and the refusal names ▸ run phase —
a phase card's body is a list of other cards, so a work agent handed one
implements the table of contents. The board's own page offers one action
or the other and never both, but a UI layer can be stale or bypassed, so
the rule lives where every other launch refusal lives: with the stage
check, ahead of the claim and well ahead of the worktree, costing nothing
and leaving nothing to clean up. Which headless kinds a phase card may
host is decided rather than left to omission: ▸ start work and ↻ act
on PR refuse it (both are work agents, and a phase's work belongs to its
members' cards), while the read-only pair — ◔ still true? and ◔
review PR, the latter told the phase's own branch — are allowed. The
guard is about starting: a card that gains **Type:** Phase while an
ordinary run is in flight is left alone, and that run ends as it would
have.
- The board creates a git worktree at
.worktrees/<task-stem>/on a new branchtask/<task-stem>from the newest main it can see: with anoriginremote it fetchesorigin/mainfirst (bounded byBOARD_FETCH_TIMEOUT) and branches from that; no remote, a failed fetch or a timeout fall back to current HEAD, so launching never waits on the network. The main checkout itself is never touched, and the ticker names the branch point whenever it isn't just HEAD. (The agent is told not to touch the task file — worktree moves would be invisible to the main checkout anyway.) - The agent works in the worktree: implements, tests, commits. Its hook events stream to the board like any session.
- On clean exit with commits on the branch the board moves the card to
review/; on failure it stays inin-progress/and the card wears the failure (below). A clean exit that committed nothing also stays inin-progress/and is called out loudly — an empty branch reaching review/ is how a broken launch hides. Stdout is kept inlocal/state/agent/logs/.
What the record keeps of a report
The agent's closing report is the permanent record: it is appended to the
task file, shown as the session's last entry, and carried into the PR
body — the same text in all three, from one helper (reports.py) with
one cap. A report that fits arrives whole. One that doesn't keeps both
ends — the headline the report contract puts first, and the pointer
that closes it — and loses the middle, cut on line boundaries, with one
line of prose in its place saying how much went and naming the log under
local/state/agent/logs/ that still holds all of it. The reader is never
left to infer that something was removed. A failed run is the deliberate
exception: its excerpt keeps the log's tail, because for a crash the end
is the story.
A run that died
An agent that exits non-zero is the one outcome a person must not miss, so
it is a state the card wears, not an event that scrolls past. The run's
record keeps the exit code, when it ended, and the cleaned tail of its log
— the excerpt, which for an API outage is the whole story ("API Error:
500 …") and which a launch that died before the agent ever spoke still
answers honestly. From that the board does three things: the card takes the
--alarm border and a run failed pill, with the excerpt on hover and in
full in the card sheet; a toast fires, because failures are rare and
actionable; and the ticker keeps its line, now naming what the log ended on
rather than pointing vaguely at a file. Every headless kind lands here —
work, act-pr, PR review, relevance check — and a card that is not in
in-progress wears it just the same.
The state is scoped to the run and the stage: the next launch supersedes it (the card reads its most recent run), and moving the card to another stage drops it, since the failure was about the work in the stage it died in. Nothing retries by itself — a dead run is a human decision point, and an outage would make auto-retry a thundering herd — but the way is cleared for the human: a failed run that committed nothing has its worktree and empty branch removed, exactly as a decline does, so ▸ start work is one click again. A failed run with commits keeps its worktree; there is work in it.