5 Commits
Author SHA1 Message Date
istosandClaude Opus 5 b690060be2 Finishing a phase finishes its cards, and clears up after them
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>
2026-08-02 10:08:00 +02:00
istosandClaude Opus 5 8fbfc1085d A phase card does not move while its work runs
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>
2026-08-02 09:23:23 +02:00
istosandClaude Opus 5 c49f5bd014 The board shows a phase running, and shows it stopping
A phase card inherits the PR chip, CI, the review actions and the
merge-and-clean-up sheet by being a card. Three things it does not
inherit, and this is them.

The runner learns to stop. `stopped — held by <name>` joins `halted` and
`run started` in the phase log, and one reader (`run_state`) answers
where a phase is from the last line that says: running, halted, stopped
or never run. `stop_phase` writes that line — so the next beat stands
down — and holds the member agent in flight, because a phase that said
it had stopped while its agent kept working would be lying about the one
thing it was asked. Nothing is unwound: the branch, every card merged
into it and every worktree are left exactly as they were. A halted phase
can be held too, which is the other half of the halt's promise that it
holds until the phase is run again or stopped.

The halt is now told three times, as a dead run is: the state, the
ticker line it already had, and a toast — rare, actionable, and the
whole argument for starting a phase and walking away. Every advance is
narrated: the member that came up green, the merge, the next start.

The page: run phase (take over on someone else's) in the slot start work
occupies, hold while it runs, and a header chip beside the agents chip —
breathing accent with the phase, its progress and the card in flight,
alarm and holding when it halts, absent entirely when there is nothing
to say. One chip per phase, because two phases could in principle run at
once and a chip that showed one of them silently would be worse than
none. Opening a phase card lists its members in run order with each
one's stage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:32:32 +02:00
istosandClaude Opus 4.8 1f8f5f6fab Address Copilot review: no-identity beat, unwritable log, remote phase base
Three points from the PR #41 Copilot review, each a robustness gap on a path
the happy case never takes:

- phases._mine() gated the beat on the assignee even when this checkout has
  no git name — but agents.claim_for_launch() cannot gate a launch there and
  lets it through, so a phase could start (branch cut, run recorded) and then
  advance nowhere. _mine() now treats "no local identity" as the lone actor,
  matching the launch it mirrors.

- phases._record() ignored whether the log line landed. The log is the
  durable memory a restart reads to tell "already started" from "not reached
  yet"; a launch or merge with no line behind it is what a restart repeats.
  _record() now raises _Halt when the write fails — before the action it was
  meant to precede — split from a best-effort _write_log() the halt path and
  _start() use so recording a halt can never itself raise.

- github._pr_base() switched a member PR's base to the phase branch only when
  it existed locally. A board that did not run the phase knows it only through
  the remote (sync fetches origin/main and nothing else), so _pr_base() now
  also honours a phase branch the remote carries, and _open_pr() only pushes
  the base when it is a local branch.

Four new tests in tests/test_phase_runs.py cover each. Full suite green (740).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-01 10:03:08 +02:00
istosandClaude Opus 5 da8984d0e6 A phase runs itself, on a branch of its own
Starting a phase cuts phase/<stem> from the newest main it can see and
works the list into it: each member branched from the phase's tip, run
headless, merged back when its checks are green, the next one started.
At the end one PR into main, for a human. The human gate moves from
every card to the phase boundary, and the promise survives: the board
merges into a branch it created, inside a scope you opened.

The runner is a beat, not an agent — everything it decides is already
structured state, and an agent paid to poll would be the wrong tool at
the wrong price. It holds no registry of where a phase is. Two durable
things carry the memory, and the board already writes both: git, where
a member is finished when its branch is contained in the phase branch,
and the card, which grows a ## Phase log the runner adds one line to
per decision. The log is what tells "this member has run and it ended
badly" from "the phase has not reached it yet" — without it a
restarted board would relaunch a run that died.

Containment alone is not enough to call a member merged: a clean exit
that committed nothing leaves an empty branch that is contained. The
card has to have settled into review/ too, or a broken launch would
hide exactly where it always tries to.

Five conditions halt, each already a visible state on the card, and a
halt is written once and then held. Running the phase again is the
person's decision and is what clears it — the run is scoped to its own
log line, so a member whose run died is launchable again. A dependency
that has not landed is a wait, not a halt.

Merges are additive throughout: main into the phase branch on every
beat so a long run does not drift into one enormous conflict, members
into it as they go green, nothing rebased and nothing force-pushed. A
conflict aborts, leaves the branch as it was, and halts naming the
files that collided.

The actor rule decides who runs it, written where it already lives:
the phase card's assignee. A replica renders the phase and advances
nothing. Reachable through /api/phase/run and the ticker; the header
chip and the card actions are a separate card.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 09:43:57 +02:00