A work agent refuses a phase card

`▸ run phase` guarded its own door and left the neighbour's open:
`/api/agent/start` accepted a phase card, cut `task/<stem>` and handed a
list of other cards to a work agent as a brief. It did that once, and the
agent implemented two cards at once in a worktree nobody was watching.

The refusal is a server rule, in `_validate` with the stage check — ahead
of the claim and well ahead of the worktree, so it costs nothing and
leaves nothing to clean up — and it names ▸ run phase rather than just
saying no. Which headless kinds a phase card may host is now decided kind
by kind where the guard lives: ▸ start work and ↻ act on PR refuse it
(both are work agents), while ◔ still true? and ◔ review PR are allowed —
the latter now told the phase's own branch, since its PR is from
`phase/<stem>` and `task/<stem>` was never cut. The guard is about
starting, so a card retyped under a running agent is left alone.

And the card says which state it is in: an `in-progress/` phase nobody has
started read exactly like one mid-run, the header chip being absent in
both cases. It now wears `not started` (or `held`) in the settled
register, with the line under it saying what ▸ run phase would do — never
the accent, the breathing mark or the caret, which mean work is happening.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
istos
2026-08-02 13:32:08 +02:00
co-authored by Claude Opus 5
parent a5f05ea29d
commit ccffbec831
5 changed files with 532 additions and 15 deletions
+26
View File
@@ -378,6 +378,21 @@ 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
@@ -951,6 +966,17 @@ and they are the phase's own interface:
watching is only trustworthy if its halt is impossible to miss, so it is
told three times at three altitudes, exactly as a dead run is.
**And the card itself says which state it is in.** The header chip only
appears while there is something to say, so it cannot tell "the phase has
not been started" from "there is no phase here at all" — which left an
`in-progress/` phase card nobody had run looking exactly like one mid-run.
So the card carries the distinction quietly, in the pill and the line
under it: `not started` (`▸ run phase` cuts its branch and starts the
first card), `held`, the accent and the member in flight while it runs,
and `halted` in `--alarm`. Only a run wears the working vocabulary — the
breathing mark, the accent border, the caret — because only a run is work
happening.
**And the card does not move while its work runs.** A phase card stands
for cards the Board no longer draws, so dragging it to another stage — or
onto the archive tray, which is a move like any other — while a member has