12vectors / bench docs for 0.4-alpha
bench docs GitHub ↗
ConceptsAgents on the board

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.

  1. The board creates a git worktree at .worktrees/<task-stem>/ on a new branch task/<task-stem> from the newest main it can see: with an origin remote it fetches origin/main first (bounded by BOARD_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.)
  2. The agent works in the worktree: implements, tests, commits. Its hook events stream to the board like any session.
  3. On clean exit with commits on the branch the board moves the card to review/; on failure it stays in in-progress/ and the card wears the failure (below). A clean exit that committed nothing also stays in in-progress/ and is called out loudly — an empty branch reaching review/ is how a broken launch hides. Stdout is kept in local/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.