Runs 55 (the prompt says the run is one turn), then 54 (start_agent
refuses a phase card), then 46 (a board with no runnable commands says
so). No card depends on another; 55 is prompt-only and goes first, 54
and 46 both edit board.html so they run adjacent.
Also the first exercise of the Phases view: 56-59 landed after phase 53
finished, so hiding, the lane, the move refusal and the sweep have not
yet been watched on a live run.
Merge & clean up on a phase card now ends the phase: after the merge into
main has actually succeeded, every member the phase merged moves to done/
and its workspace is cleared the way completing an ordinary card clears
its own — worktree removed, local branch deleted, branch on the remote
deleted.
Only what the phase merged. A member is swept when its card settled into
review/ or done/ and its branch is contained in the phase branch (or
there was never a branch to bring) — the same pair the runner reads a
member as merged by. One that halted, was held or was walked back keeps
its card, its worktree and its branch: there is work in them.
Nothing uncommitted is thrown away: a member's worktree comes out without
--force, and a dirty one is reported in the ticker and kept with its
branch rather than forced.
One ending, told once. taskfiles.move_together moves the cards in a
single commit naming all of them (`board: 47, 52 → done with phase 53`,
so it publishes in team mode like any other board commit), and the moves
are marked quiet so the watcher does not also scroll five identical move
lines behind the one line the ending gets.
The other endings are untouched: "just move the card" and archiving the
phase card move no member, because neither puts anything in main.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dragging a phase card between stages while one of its members has an
agent in it is a move nobody can mean: the card lands somewhere its
branch, its worktree and its live agent are not. The board refuses it,
and 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 branch, the merges and every worktree as they were.
- phases.assert_not_working() is the refusal: it reads the one file
first, so an ordinary card never reaches the question, then resolves
the phase's list and asks what is actually running.
- agents.working_on() answers that from the processes themselves, not
from the registry's status alone — the reaper flips that a moment
after a run ends, and a rule that only refuses must not hold a card
hostage to a run that has already died. stop_phase() now reads the
same helper.
- httpd asks it on /api/move, /api/archive (archiving is a move) and
/api/task/complete, so a stale page cannot get past it.
- The toast wraps, is bounded to the viewport and stays up for as long
as its text takes to read: a refusal you cannot finish reading is the
wall this was written against.
What still moves: a phase between members, a halted phase, a held one,
a member card, and every ordinary card.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
laneRank() lumped a held (stopped) phase in with unstarted phases at
the same rank, so an actionable "stopped — held by …" lane could sort
below phases nobody has started. The snapshot already carries
snap.stopped, so give a held run its own rank just below running,
keeping it near the top where a person can resume it. Addresses the
Copilot review point on PR #48.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>