PRs and review
A card entering review gets a PR opened for it by the board; then review PR, copilot and act on PR, until it settles.
A card entering review/ with a task/<stem> branch gets a PR opened for
it automatically — mechanically, by the board, not by an agent: it pushes
the branch to the repo's remote and runs gh pr create with the task title
and the agent's closing summary as the body. The PR url is written into the
task file as a **PR:** <url> line, so the file stays the source of truth
and the card grows a PR ↗ chip. Cards without a branch pass through
quietly. One guard is loud: if local main is ahead of the remote, the PR
would drag those commits into its diff, so the board refuses and tells you
to push main first (then move the card out and back, or wait for the next
entry into review/).
The board that opens it is the one whose user moved the card (see "State
syncs; reactions don't"), and the **PR:** line is the backstop behind
that: it is checked before every gh pr create, in team mode it commits
itself so it reaches the other boards, and a create that races anyway
adopts the PR GitHub already has rather than failing. A review card that
has a branch but no PR carries an ↑ open PR action — the way to ask
for one after the fact, since no board opens it behind your back.
Review-stage cards with a PR carry two actions:
- ◔ review PR — a read-only agent reads the full diff in context,
checks it against the task and AGENTS.md, posts its verdict to GitHub
(
gh pr review --approve/--request-changes) and appends a## PR reviewsection to the task file ending inPR REVIEW: APPROVE | REQUEST CHANGES. - ⚑ copilot — requests a GitHub Copilot review via the API. Works iff
Copilot code review is enabled for the repo; the error is relayed to the
toast if not. The card tracks the whole arc with a
⚑chip:⚑ ◌asked,⚑ ✓approved,⚑ ✕changes requested,⚑ ·commented — "asked" becomes a verdict when the pending request turns into an authored review.
Once any review is in (a verdict from either agent kind, Copilot, or a
human), the card's actions shift to the loop that matters then:
↻ act on PR replaces the copilot button — an agent re-enters the task's
worktree (recreated from the branch if it was cleaned up), reads every
review and line comment, addresses each point or says why not, commits,
pushes so the PR updates, and appends a ## PR update section to the task
file. Then ◔ review PR again, until it settles.
The board polls open PRs of review-stage cards (reviews + CI checks +
GitHub's mergeable state, every 60s — a plain thread in board.py, no agent
involved, silent when review/ is empty) and folds everything into one
verdict — any changes-requested review or failing check wins over any
approval. A PR GitHub cannot merge cleanly wears an alarm-coloured
conflicts chip and counts as changes-needed-by-you (not a CI failure);
↻ act on PR resolves mechanical conflicts by merging main into the
branch — additively, never rebasing or force-pushing — in a dedicated
resolution commit, and refuses semantic ones, naming the collision for a
human to settle. GitHub computes mergeability lazily, so an UNKNOWN
reading keeps the chip's last state rather than flapping.
A phase member's PR is opened against its phase's branch, not main:
its branch was cut from there, so that is the only base whose diff is the
member's own work — and a PR into main carrying a whole phase is exactly
the merge this design refuses to make. Every other card, the phase card
itself included, opens into main as it always did.
The card wears that verdict in the design system's state colours:
approved → pine (--calm) border and an approved pill; changes asked →
terracotta (--alarm) and a changes asked pill; otherwise it stays the
neutral waiting on you. Tool chips (CI, copilot, PR, drive) are
destinations, not statuses: they live in the card's footer row, never
squeezed into the author row — CI ✓ (pine), CI ✕ (terracotta), ◌
while in flight — with hover actions staying in the status pill's slot.
Merging into main remains yours — the board never merges into main. It
does merge into a branch of its own: a phase's integration branch is the
board's, and merging into it is bookkeeping in the same family as
committing a move (see "A phase runs itself, on a branch of its own").
The agent's first duty is to judge whether the task is actionable. If the
task still has open questions — unresolved decisions only its author can
settle — the agent does no work and exits with a NOT READY: <reason>
marker. The board then moves the card back (to to-do/, or backlog/ if it
started there), records the reason in the ticker, and deletes the untouched
worktree and branch so the task can be refined and relaunched cleanly.
◔ still true? (every stage, done included) fires a read-only relevance
agent instead: no worktree, edit tools disallowed, running in the main
checkout. It checks the task against the actual codebase — already done?
assumptions stale? still worth doing as written? — and its report is
appended to the task file under a ## Relevance review — <date> heading,
with the verdict (Still relevant | Partly done | Already done | Needs rewrite) in the ticker. The card does not move; deciding what to do with
the verdict is yours. One agent per task at a time applies across both
kinds.
Dragging a card with work attached (branch or PR) to done/ opens a
three-way choice instead of just moving: keep it where it is (nothing
changes), just move the card (branch, PR and worktree stay), or
merge & clean up — park the drive if it is this task's, merge the
branch into main, push (which marks the PR merged) and delete the remote
branch, remove the worktree and local branch, then move the card. Every
step narrates in the ticker; a merge conflict aborts cleanly and the card
stays put. On a phase card the same action also finishes the cards the
phase merged — see "Finishing a phase finishes its cards". Cards without
work move silently, and hand-moves on disk are never intercepted — the
board only asks when you act through it.
That work takes as long as it takes, so the card wears it rather than
sitting there looking idle while its branch is disassembled: from the
first step to the last it takes the accent border and a breathing
completing pill — the same vocabulary as an agent working, because that
is what is happening — and carries the latest narrated step on its
activity line ("parking the drive", "merged task/29-… into main",
"cleaned up: worktree and local branch removed"). While it does, it has
no hover actions, no drive or command chips and cannot be dragged, and a
second complete for the same card is refused rather than started. The
claim lives in this board's memory and is released on every exit —
merged, conflicted or crashed — so a failure gives the card straight back
and a board restarted mid-completion leaves nothing stuck. Other replicas
see the card unchanged until the move itself arrives.
With BOARD_SYNC on the merge is made on origin instead: the board
runs gh pr merge on the card's PR, cleans up and moves the card, and
local main fast-forwards to the result on the next beat. Replicas
converge only while main advances by fast-forward, so no board makes a
merge commit of its own. Two consequences the local path hid: whoever
clicks needs merge rights on the repo, not just push rights, and a branch
without a PR is refused with a pointer to ↑ open PR — there is nothing
for origin to merge otherwise. Single-player merges locally, exactly as
above.