phase: merge origin/main into phase/53-the-board-stops-fighting-you

This commit is contained in:
istos
2026-08-02 07:18:48 +02:00
7 changed files with 506 additions and 1 deletions
@@ -0,0 +1,101 @@
# 55 — A headless run gets one turn, and nothing tells the agent that
**Status:** Backlog
**Priority:** High — it costs a whole run and everything in it, and the
condition that triggers it is getting more likely every week
**Type:** Bug
A work agent finished its turn with the words *"The suite is still
running; I'll wait for the monitor rather than poll further."* There is no
monitor. A headless `claude -p` run ends when the model stops producing
output, so ending a turn to wait ends the run — and this one ended with
314 lines of finished, passing work staged and never committed.
## Context
What actually happened, on card 47 inside phase 53:
- The agent staged changes across `AGENTS.md`, `manager/core/board.html`,
`manager/core/httpd.py`, a new `tests/test_archive_chip.py` and an
edited `tests/test_card_actions.py` — then started the test suite and
ended its turn to wait for it.
- The process exited 0 with no commits on `task/47-…`. The log is 80
bytes: that one sentence.
- Everything downstream behaved correctly. The board refused to move the
card to `review/` — an empty branch reaching review is a broken launch
hiding — and the phase halted at 47 rather than skipping it, saying so
in its `## Phase log`.
- The work was fine. Run by hand afterwards: 37 tests in the two files
it touched, then **875 tests, all passing**, in that worktree.
Why waiting looked reasonable to the agent: **that suite takes four
minutes.** 875 tests, 239 seconds. This morning it was 631 tests in about
105 seconds. At four minutes, backgrounding a run and waiting for it is a
sensible-looking strategy for an agent that believes it will be resumed.
And nothing tells it otherwise. `manager/core/prompts/work.md` says to
run the checks until they pass and to commit in clear, reviewable
commits. It never says the run is a single non-interactive turn, that
there is nobody to hand back to, or that uncommitted work dies with the
process. The one place finality is stated is the `NOT READY` path — "end
immediately with a reply whose FIRST line is exactly…" — which is about
declining, not about the shape of the run.
**Affected areas:** `manager/core/prompts/work.md` first, and the same
paragraph is owed to `act-pr.md` and `review.md`, which run the same way.
## What to build
- **Say what a run is, at the top of the prompt.** One short paragraph:
this is a single non-interactive turn; there is no human and no second
chance; when the reply ends the process exits; anything not committed
is lost, and the board judges the run by what is on the branch.
- **Turn that into an instruction, not a warning.** Commit before
anything long-running, and commit again after it. A commit is cheap and
a lost run is not — an agent that commits early can always improve on
it in the same turn.
- **Name the trap by name.** Do not background a command and end the turn
to wait for it; do not promise to come back to something. If a check
takes minutes, run it in the foreground and wait inside the turn.
- **The same paragraph in the other headless prompts.** `act-pr` pushes
and `review` posts a verdict; both lose everything the same way if the
turn ends early.
- **Check the marker contract still reads clearly** once the paragraph is
added — `NOT READY:` and the closing report are parsed from the same
output, and the new text must not muddy where the first line goes.
**Out of scope** — tempting neighbours left alone:
- Making the runner resume a stalled agent, or giving an agent a second
turn. One-shot is the design; the fix is telling it so.
- Rescuing uncommitted work automatically. Tempting, and wrong: the
board judging a run by its commits is exactly what caught this.
- Changing what the definition-of-done checks are — see Notes.
## Acceptance
- [ ] `prompts/work.md` states, before the task body, that the run is one
non-interactive turn and that uncommitted work is lost when it ends.
- [ ] It instructs the agent to commit before long-running commands, and
not to end a turn waiting on one.
- [ ] `act-pr.md` and `review.md` carry the same paragraph.
- [ ] `tests/test_prompt_report_contract.py` still passes — the four
templates keep their identical closing-report block and their
marker lines still parse.
- [ ] Edge case: the added text does not change where the first line of a
`NOT READY:` reply must sit.
## Notes
The recovered work from that run is committed on
`task/47-an-archive-button-on-the-card` as `661dd89`, by hand, unchanged
from what the run left staged.
**The four-minute suite is the other half of this**, and it deserves its
own card rather than a fix here. `BOARD_AGENT_COMMANDS` is
`python3 -m unittest`, so every work agent runs everything — 875 tests
now, and the number only goes up. A definition-of-done slow enough to
change how an agent behaves is a problem that gets worse quietly: the
prompt fix stops the agent walking away from it, but it does not make the
wait shorter, and the next agent will still spend four minutes of its run
watching a suite that mostly tests things it did not touch.
@@ -0,0 +1,103 @@
# 56 — Phase members leave the main board
**Status:** Backlog
**Priority:** High — the main board is the thing phases currently spoil,
and every other phase-UI card is downstream of this one
**Type:** Feature
A phase's members stop appearing on the Board view. The phase card stays —
one card, in whatever stage the phase is in — and its members are drawn
somewhere else (card 57). The Board goes back to meaning one thing: the
work you are personally holding, in five columns that fit.
## Context
- Membership is already derived, not stored: `taskfiles.weave_phases()`
(`manager/core/taskfiles.py:163`) resolves every phase card's `## Cards`
list against the board and gives each member a
`phase: {file, number, title, index, total}`. Everything this card
needs is on the task by the time `collect()` returns.
- `httpd.state_payload()` already ships `phases: phases.public_state()`
(`:42`) beside the board, and `board.html` reads it in three places, so
the client can tell a phase card from an ordinary one without asking
again.
- `renderBoard()` (`board.html:945`) builds each column from
`stage.tasks` and prints `stage.tasks.length` in the header, with a note
beside it (`STAGE_NOTE[stage.slug]`, or "N agents" when agents are
working there).
- The design exploration that led here, including the two mockups and the
controls split:
https://claude.ai/code/artifact/727a3b64-1354-4fb0-a73d-b700bdfc2b19
**Affected areas:** `manager/core/board.html` — the Board view's
rendering and its column headers. No server change: the state payload
already says everything needed.
## What to build
- **A member is not drawn on the Board view.** It keeps its stage, its
file, its agent and its actions — it is simply not in this view. The
card is not deleted, moved, or marked; the Board just stops listing it.
- **Counts describe what is visible.** A column's number is the number of
cards you can see in it. This is the rule that keeps the header honest:
the count is true by construction rather than true-with-a-footnote.
- **A note where the work went**, beside the count and in the register the
existing note uses — `+2 in phases`. It is a signpost to the other
view, not a correction to the number, and it only appears where members
are actually hidden.
- **Membership is the only thing that hides a card.** So when a phase
reaches `done/`, is archived, or has its `## Cards` list edited, its
former members reappear on the Board in whatever stage they are
genuinely in — with no sweep, no migration and no second rule. Removing
membership *is* the un-hiding.
- **The phase card carries the summary it now owes.** It is the only
thing standing for that work on this view, so it shows progress
(`1 of 2 merged`), the member in flight, and a way through to the
Phases view.
- **Only the Board view hides.** Sessions and Focus are about runs and
sessions, not stages, and a phase member's agent is an agent like any
other.
**Out of scope** — tempting neighbours left alone:
- Where the members are drawn instead — card 57. Until it lands they are
reachable through the phase card and through Sessions, which is thin
but not a dead end.
- Refusing to move a phase card — card 58.
- What happens to members' stages when the phase merges — card 59.
- Hiding anything that is not a phase member.
## Acceptance
- [ ] Given a phase with two members, when the Board view renders, then
the members are absent and the phase card is present.
- [ ] The column that held a member counts one fewer, and its header says
`+1 in phases`.
- [ ] A column with no hidden members shows no such note.
- [ ] Given the phase card reaches `done/`, when the board redraws, then
its former members appear in the columns matching their actual
stages.
- [ ] The same is true when the phase card is archived, and when a number
is removed from its `## Cards` list.
- [ ] The header's live-agents chip still counts an agent working on a
hidden member — hiding cards must not hide activity.
- [ ] Edge case: a card listed by a phase card that does not resolve
(`phaseDrift`) is **not** hidden — an authoring mistake must not
make a card vanish.
- [ ] Edge case: with no phases on the board at all, the Board view is
byte-for-byte what it is today.
## Notes
The risk this card takes is stated plainly and should stay stated: **a
board that does not show everything is a board that can surprise you**,
and "you can see everything the agents are doing" is most of what bench
is for. Three things pay that back — the count note, the phase card's own
progress line, and the halt reaching the header whether or not you are on
the Phases view (57). If those three are not in place, this card makes
the product worse rather than better.
Worth watching for once it lands: a phase with a member in `review/`
means `review/` says `0` and yet something genuinely is waiting on you.
The note covers it, but "your move" as a column note is now a claim about
visible cards only.
@@ -0,0 +1,107 @@
# 57 — The Phases view: a swimlane each
**Status:** Backlog
**Priority:** High — card 56 takes the members off the Board; this is
where they go
**Type:** Feature
**Depends on:** 56 — the members have to have left before they need a room
A fourth view beside Board, Sessions and Focus. One swimlane per phase,
each running the same five stages left to right, holding every card that
phase owns. It is the room where horizontal work gets horizontal space,
which the five columns could never spare.
## Context
- The exploration and both mockups, including the table of which control
belongs to which view:
https://claude.ai/code/artifact/727a3b64-1354-4fb0-a73d-b700bdfc2b19
- Views already exist as a switcher: `setView()` (`board.html:1005`),
`VIEW_TITLES` (`:860`), `S.view`, and the `#views button` elements. A
fourth is an addition to a pattern, not a new one.
- Everything to draw a lane is already in the state payload:
`phases.public_state()` gives the runner's last pass per phase card —
progress, the member in flight, whether it halted and why — and
`weave_phases` has already told each member its phase and position.
- The phase card keeps its own record in a `## Phase log` section, one
line per decision the runner made. It is the thing you read when
something went sideways, and it has nowhere to be shown today.
- `stop_phase()` (`phases.py:662`) holds the phase *and* the agent it has
in flight; `start_phase()` runs it again and clears a halt.
**Affected areas:** `manager/core/board.html` — a new view and its
rendering. The API is already sufficient.
## What to build
- **A Phases view** in the switcher, carrying a count of running phases
and, when one has halted, a mark that says so from the Board view
without switching to find out.
- **One lane per phase**, ordered so a halted phase is not below the fold.
A lane has a head — the phase, its progress, the member in flight — and
five stage columns holding its cards.
- **The cards are the cards.** Full fidelity: the live agent line, the CI
and PR chips, the ordinary hover actions. Nothing is shrunk to a token,
which is the advantage a whole view buys.
- **The last column is the phase's own**, not `done/`: a member merged
into the phase branch is finished as far as the phase is concerned and
is not in `main` yet. Name it for what it is.
- **The phase log under the lane**, the runner's decisions in order. It is
the only thing that can distinguish "not reached yet" from "started and
ended badly".
- **A halted lane says so at the top of itself** — the reason, the card it
stopped on — and offers **▸ run again**. That action lives here and not
on the Board, because clearing a halt should mean having read what
caused it.
- **Per-phase controls in the lane head**: hold, the phase branch, the
phase card. Per-member controls are the card's own and need no
special-casing.
- **Nothing here that ends the phase.** Merging is a board move on the
phase card, and there should be exactly one place where work leaves the
board.
**Out of scope** — tempting neighbours left alone:
- Starting a phase, which stays on the phase card in the Board view: the
commitment is the card reaching `in-progress/`.
- **Merge & clean up**, for the same reason.
- Editing a phase's list — reordering, adding, removing — which is the
file's business and card 51's action.
- A per-phase Focus view. Focus is a heads-up for one session; this is a
different thing wearing similar words.
## Acceptance
- [ ] With at least one phase in `in-progress/`, the switcher offers
Phases and the view draws one lane per phase.
- [ ] A lane shows every card the phase lists, in the stage each is
actually in, with its position in the run.
- [ ] A member's card in the lane offers the same actions it would offer
on the Board.
- [ ] A halted phase names the reason and the card, offers **▸ run
again**, and running it again clears the halt and continues.
- [ ] **‖ hold** in the lane head stops the phase and the agent it has in
flight, and unwinds nothing.
- [ ] The phase log is readable under the lane, in the order the runner
wrote it.
- [ ] Given a phase halts while you are on the Board view, you learn it
there — the switcher marks it, and the existing toast and ticker
still fire.
- [ ] With no phases at all, the view says so plainly rather than
rendering an empty grid.
- [ ] Edge case: a phase whose members are all merged, waiting on its own
PR, still draws a lane rather than disappearing before you have
merged it.
## Notes
The reason this is a view and not a panel: five columns cannot hold two
boards at once, and every attempt to make them — a rail, a band, a thread
— either fights the geometry or hides the order. Given a room of its own,
the swimlane is the obvious drawing, and it was the obvious drawing all
along.
The thing to get right is not the lane, it is the **crossing**: a person
on the Board must learn that a phase halted without being on this view,
and a person here must be able to get back to the card that owns it. Two
signposts, both cheap, and the feature is untrustworthy without either.
@@ -0,0 +1,93 @@
# 58 — A phase card will not move while its work is running
**Status:** Backlog
**Priority:** Medium — the hole is narrow but what falls through it is a
phase whose card and branch disagree about what is happening
**Type:** Feature
Dragging a phase card between stages while one of its members has an
agent in it is a move nobody can mean. Refuse it, name what to do — **
hold** stops the phase and the agent it has in flight — and let the move
through the moment nothing is running.
## Context
- Today a phase card is an ordinary card to `move_task()`. Nothing
consults the phase, so a phase card can be walked back to `to-do/` (or
archived, which is a move) while its second member is mid-run, leaving
a card in one place and a live agent, a worktree and a phase branch in
another.
- The stopping half already exists and does exactly the right thing.
`stop_phase()` (`manager/core/phases.py:662`): *"the agent the phase has
in flight is held exactly as its own card's hold would hold it"*, and
the branch, the merges and the worktrees are left as they were. So the
fix for a refusal is one action the person already has.
- A **halted** phase has nothing running by construction, so it moves
freely — which is exactly when you would want to walk it back.
- The board has almost no refusals on drag. `move_task` raises for a bad
stage or an existing file; the drag-to-`done/` sheet *intercepts* rather
than refuses. This adds the first real "no", which is why the wording
matters more than the check.
**Affected areas:** `manager/core/taskfiles.py` or
`manager/core/phases.py` for the check, `manager/core/httpd.py` where
`/api/move` and `/api/archive` are served, and `manager/core/board.html`
for how a refused drag looks.
## What to build
- **Refuse the move while any member has a live agent.** Not "while the
phase is running" — a phase between members has nothing to lose, and
refusing then would be a rule people learn to resent.
- **Say the whole thing in the refusal**: which member is working, and
that **‖ hold** stops the phase and its agent without unwinding
anything. A refusal that only says no makes the person guess, and the
guess is usually to force it.
- **Archiving is a move.** `archive_task()` takes the same guard, or the
rule has a hole shaped exactly like the tidiest thing to do with a
phase you have lost interest in.
- **The card returns to where it was**, cleanly, with the reason in the
toast — a drag that snaps back with no explanation is worse than one
that is not allowed to start.
- **Guard the server, not just the drag.** The board can be stale, and
`/api/move` is reachable regardless — the same lesson card 54 wrote
down about `▸ start work` on a phase card.
**Out of scope** — tempting neighbours left alone:
- Stopping members individually. `‖ hold` on the phase covers it, and a
per-member stop is card 57's lane doing ordinary card things.
- Refusing to move a *member* card. That is a person overriding the
runner deliberately, and the phase halting on it afterwards is the
honest outcome.
- Anything about what the move does once allowed.
## Acceptance
- [ ] Given a phase with an agent running on a member, when its card is
dragged to another stage, then the move is refused, the card stays,
and the message names the working member and **‖ hold**.
- [ ] The same phase card cannot be archived while that agent runs.
- [ ] Given the phase is held, the card moves — and archives — normally.
- [ ] Given the phase has halted, the card moves normally: nothing is
running.
- [ ] Given a phase between members, with nothing launched, the card
moves normally.
- [ ] `POST /api/move` on that card is refused with the same reason, from
a stale page or by hand.
- [ ] An ordinary card is unaffected in every case.
- [ ] Edge case: a member's agent that has died but not been reaped does
not lock the phase card forever — the check reads what is actually
running, not what was once started.
## Notes
The rule earns its keep on the archive path more than the drag: a phase
you have given up on is precisely the one you would archive, and doing it
mid-run would leave a branch, a worktree and a running agent belonging to
a card that is no longer on the board.
Worth phrasing the refusal as an instruction rather than a prohibition —
"hold the phase first, then move it" reads as help, and "cannot be moved
while running" reads as a wall. The board's other refusals already do
this: work starts from `in-progress/` *— move the card there first*.
@@ -0,0 +1,99 @@
# 59 — Finishing a phase finishes its cards
**Status:** Backlog
**Priority:** Medium — without it a phase ends by handing you a pile of
cards you have already judged
**Type:** Feature
When a phase's PR merges into `main`, its members' work is in `main` too —
but their cards are sitting in `review/`, where the board says they are
waiting on you. **Merge & clean up** on a phase card should move its
members to `done/` in the same operation that moves the phase.
## Context
- A member stops at `review/`. The runner merges its branch into the
phase branch and records it, and deliberately does not move the card:
`done/` has always meant *merged into `main`*, and merged into a phase
branch is not that. That reasoning is right and should survive this
card.
- What makes it wrong at the end is card 56: members are hidden from the
Board while the phase holds them, and reappear when it lets go. A phase
reaching `done/` therefore returns three or five cards to `review/` in
one redraw — all of them merged, none of them needing anything, and all
of them in the column whose note is "your move".
- The operation to extend already exists and already does careful
multi-step git work: `github.complete_task()` (`:387`) parks the drive,
merges, cleans up the worktree and branch, then calls `move_task` for
the one card. It narrates each step and aborts cleanly on a conflict.
- Board-made moves commit themselves under `BOARD_COMMIT_MOVES` and, in
team mode, push — so a sweep of five cards is five commits or one, and
which it is deserves a decision rather than an accident.
**Affected areas:** `manager/core/github.py` (`complete_task`), and
whatever narrates the result.
## What to build
- **The sweep, inside the merge.** When a phase card is completed, every
member the phase lists moves to `done/` as part of the same operation —
after the merge into `main` has actually succeeded, never before.
- **Only what the phase merged.** A member that never reached the phase
branch — halted, held, walked back — is not swept. It stays where it
is, and it is the reason a person will look at the phase afterwards.
- **Narrate it as one thing.** The ticker should say a phase finished and
how many cards went with it, not five separate moves scrolling past. A
person watching should see one event, because one thing happened.
- **Decide the commit shape deliberately.** One commit for the sweep
reads better in `git log` than five `board: NN → done` lines in a row,
but the message must still say what moved; and in team mode it has to
reach the other boards either way.
- **Abort together.** If the merge fails, nothing moves — the existing
behaviour, extended to the members. A half-swept phase is worse than an
unswept one.
- **The other endings do not sweep.** Archiving a phase card, or removing
a member from its list, releases the members to the board in whatever
stage they are actually in (card 56). Only *merging* means the work is
in `main`, and only merging may say `done/`.
**Out of scope** — tempting neighbours left alone:
- Moving members while the phase runs. They stop at `review/` on purpose.
- Sweeping on any path other than **merge & clean up** — including "just
move the card", which explicitly leaves the work alone.
- Closing the members' own PRs. Those were opened against the phase
branch and are closed by their own merges.
## Acceptance
- [ ] Given a phase whose members are all merged, when **merge & clean
up** succeeds, then the phase card and every merged member are in
`done/`.
- [ ] The ticker reports it as one ending, naming the phase and the
number of cards.
- [ ] Given the merge fails or conflicts, nothing moves — not the phase
card, not one member.
- [ ] Given a member that never reached the phase branch, it is left
exactly where it is.
- [ ] Given "just move the card" instead, no member moves.
- [ ] Given the phase card is archived rather than merged, its members
reappear on the board in their own stages and none of them is
marked done.
- [ ] With `BOARD_COMMIT_MOVES` on, the sweep is committed and, in team
mode, published — a sweep that never leaves one working tree is not
a sweep.
- [ ] Edge case: a member already in `done/` — moved by hand — is not
moved again and does not fail the operation.
## Notes
This is the card that makes the ending feel like an ending. Everything
else about phases is about the middle: running, halting, resuming. The
last thing a person does is drag one card to `done/`, and what should
happen is that the whole phase goes quiet — not that five cards they have
already reviewed reappear asking for attention.
The reason it is separate from card 56 rather than folded in: 56 is about
what the *Board view draws*, and this is about what the *board does*. One
is a rendering rule, the other moves files and commits them. Keeping them
apart also keeps 56 shippable on its own.
@@ -65,4 +65,5 @@ review or one too many.
- 2026-08-01 17:00 · 52 started
- 2026-08-01 17:15 · 52 merged into phase/53-the-board-stops-fighting-you
- 2026-08-01 17:15 · 47 started
- 2026-08-01 17:28 · halted at 47 — its run ended without reaching review/
@@ -1,6 +1,7 @@
# 47 — An archive button on the card, where the card is
**Status:** In Progress
**Status:** Review
**PR:** https://github.com/12vectors/bench/pull/45
**Assignee:** istos
**Priority:** Medium — archiving works, but only by dragging a card the
length of the board to a tray at the far corner