3 Commits
Author SHA1 Message Date
istosandClaude Opus 5 2a4652a15b tasks: card 63 — a phase in flight is work happening
From a board showing 11 of 14 merged with member 31 in review/ and its
CI still running: the phase was advancing and every surface keyed to a
running agent said the board was idle. The tab title went back to the
plain project name, the live chip fell through to "you, working", and
the member the phase was waiting on wore "waiting on you" — which is
the opposite of true, since the phase merges it by itself on green.

The reading already exists client-side (phasesInFlight()), and the
snapshot already carries running/halted/stopped plus per-member states.
Nothing is missing but the question being asked, in three places that
answer it with runningAgents() instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 11:57:18 +02:00
istosandClaude Opus 5 869b7018b4 tasks: card 62 — running a halted phase again resumes it
Found on a board that was hosted by a shell that got killed: the phase
halted at the member whose agent went down with it, and no number of
presses of run phase could get past that member. What cleared it was
launching the member's work agent by hand and only then running the
phase, which is a two-step the board never mentions.

The runner already intends the opposite — _this_run() scopes the log so
that a new run may relaunch a member whose run died, and says so — but
_member_state() reads any existing branch as evidence the phase already
dealt with the card, and a run that died always leaves one. So the
intent is written down and unreachable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 08:05:15 +02:00
istosandClaude Opus 5 dff4a42a86 site: the redirect is a 307, and card 43's line numbers had moved
Two small corrections found while checking 0.4-alpha's deploy.

wrangler.jsonc claimed the slashless form redirects with a 301; measured
against the live site it is a 307. The code is the host's choice rather
than anything this repo sets, so the post-deploy check now says to
confirm that it redirects at all — the property the site depends on —
and records the observed code beside it rather than asserting one.

Card 43 already describes the two test_boards_sync failures a team-mode
checkout sees, and still describes them correctly; only its pointer into
the test file had drifted — reload() sits at 598 now, not 481.
config.py:53-69 is still exact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 07:02:41 +02:00
5 changed files with 275 additions and 5 deletions
+3 -1
View File
@@ -127,7 +127,9 @@ answers rather than files:
1. `https://bench.12vectors.com/` serves the landing page over TLS.
2. `https://bench.12vectors.com/concepts/claiming-a-card` redirects to
the same path with a trailing slash, and no url anywhere ends in
`.html`.
`.html`. Measured, the redirect is a **307** — the host's choice, not
a setting this repo holds, so check that it redirects at all rather
than which code it picks.
3. A path that does not exist — `/nope/` — renders the site's own 404
page **with a 404 status**, not the landing page with a 200.
4. `curl -sI https://bench.12vectors.com/` shows
+1 -1
View File
@@ -19,7 +19,7 @@
// so a deploy from a clean checkout builds first.
"directory": "./dist",
// /concepts/claiming-a-card -> 301 -> /concepts/claiming-a-card/,
// /concepts/claiming-a-card -> 307 -> /concepts/claiming-a-card/,
// which is the url the pages link and the one <link rel=canonical>
// names. One page, one address: the slashless form redirects rather
// than serving a second copy, and no url ever ends in .html.
@@ -18,7 +18,7 @@ tests are about — the assertion reads that file and fails. CI has no
a dict and then does `values.update(os.environ)`. Process environment
beats the file, and the file beats the defaults. That precedence is
right and documented; what follows from it is not obvious.
- `tests/test_boards_sync.py:481-496``TheGateImpliesCommitMoves.reload()`
- `tests/test_boards_sync.py:598-613``TheGateImpliesCommitMoves.reload()`
**pops** `BOARD_SYNC` and `BOARD_COMMIT_MOVES` from `os.environ` and
reloads `config`, to stand for a machine that has set neither. Popping
removes the only layer that was overriding `local/.env`, so the reload
@@ -52,8 +52,8 @@ tests, `tests/test_boards_sync.py` first.
`tests/__init__.py`, which every run imports — that sets the override
before `config` is first imported, so no individual test has to
remember. Note that several test modules import `config` at module
scope (`test_boards_sync.py:27`), so wherever this lands it has to
happen first.
scope (`tests/test_boards_sync.py:27`), so wherever this lands it has
to happen first.
- **Say it where it will be read.** One line in the `_load_env()`
docstring: the process environment is the only layer above the file,
so removing a variable does not reveal the default, it reveals the
@@ -0,0 +1,132 @@
# 62 — Running a halted phase again resumes the member it stopped on
**Status:** Backlog
**Priority:** High — a board that dies mid-run halts its phase on a member
it can never get past, and the way out is a two-step nobody is told about
**Type:** Bug
**▸ run phase** on a halted phase is meant to mean "try that member
again" — `_this_run()` says so in its own docstring, and scopes the log
so that it does. It does not, for the one member it matters most for: a
member whose run died left a branch behind, and `_member_state()` reads
any branch as evidence the phase already dealt with it. So the re-run
recomputes, sees a card in `in-progress/` with no agent on it, and halts
on the same member with the same sentence — once a beat, for as many
times as the button is pressed. The phase cannot be restarted from the
board at all; it can only be restarted by launching the stuck member's
work agent by hand first, which is a step the board never mentions.
## Context
A board hosted by a shell that got killed took its member's agent down
with it. On restart the phase halted at that member — correct, nothing
was running — and every **▸ run phase** after that appended a
`run started` line and halted again about a minute later. What actually
cleared it was **▸ start work** on the member, and *then* **▸ run
phase**: with an agent alive on the card, `_running_on()` short-circuits
at the top of `_member_state()` and the runner waits instead of halting.
- `manager/core/phases.py:144-155``_this_run()` scopes the log to the
entries after the last `run started`, and says why: "a member whose run
died is launchable again, which is exactly what asking for the run
again meant." The intent is already written down. It is `_started()`
that this feeds, and only `_started()`.
- `manager/core/phases.py:263-265` — the clause that defeats it:
`if number not in started and not has_branch: return "pending"`. A run
that died always leaves `task/<stem>` behind — the branch is cut at
launch, before the agent does anything — so `has_branch` is true and
the member never reads as pending, however many times the run is
restarted.
- `manager/core/phases.py:278-281` — where it lands instead: stage is
`in-progress`, so `halt`, "its run ended without reaching review/".
True, and the end of the conversation: the sentence names no way out.
- `manager/core/phases.py:220-230``_failure_note()` is empty after a
restart, by design ("a restart forgets"). So the halt a killed board
produces is also the halt with the least to say, which is the wrong way
round.
- `manager/core/agents.py:391-402` — and the reason this is safe to fix:
`start_agent()` does **not** refuse an existing worktree. It continues
in it, on the same branch, and says so ("is back on … — continuing
branch …"). Relaunching a member whose run died is an operation the
board already supports everywhere except here.
- `AGENTS.md`, "Agents working the board" — says "a work agent's worktree
must not already exist when it starts". That is not what the code does
(above), and it is precisely the sentence that would talk someone out
of this fix. It needs correcting in the same change.
**Affected areas:** `manager/core/phases.py``_member_state()` and the
halt text; `AGENTS.md` for the two sentences that describe both.
## What to build
- **Let a new run resume a member it did not start.** A member that is
not settled, not running, and not started *by this run* is the phase's
next piece of work whether or not a branch exists for it — that is what
`_this_run()`'s scoping already means, and `has_branch` is answering a
different question than the one it is asked here. Keep the branch check
where it earns its keep: deciding `merged`, where containment is the
whole point.
- **Resume rather than restart.** `_launch()` should reach a member with
a branch and a worktree already there and let `start_agent()` continue
in it, so a partially-done run keeps its commits. Nothing here should
delete a worktree or a branch to make room for itself.
- **Say what the halt means when it is still a halt.** A member that
halts inside the run that started it is stopped for a reason and stays
stopped — but the sentence should name the way through the way every
other refusal on this board does: that running the phase again resumes
it, and that **‖ hold** is the other answer.
- **Cover the sequence the board actually produced**: run starts, member
launches, the run record disappears (a killed board), the phase halts,
the phase is run again — and the member comes back up rather than
halting a second time.
**Out of scope** — near neighbours this deliberately leaves alone:
- **The five halting conditions.** `NOT READY`, a non-zero exit, a clean
exit with no commits, CI red and a non-mechanical merge are all still
halts, still within the run that hit them, and still nothing that
retries by itself. This changes what a *new* run may do, which was
always a person's decision.
- **Why the agent died with the board.** A board in a foreground shell
taking its agents down with it is real and is not this card — this one
is about getting the phase moving again afterwards.
- **Anything auto-retrying.** No loop, no backoff, no second attempt the
person did not ask for.
## Acceptance
- [ ] Given a phase halted at a member sitting in `in-progress/` with a
branch and no live agent, when the phase is run again, then that
member is launched and the phase continues — no second halt.
- [ ] Given that member's worktree still has the dead run's commits in
it, when it is relaunched, then the commits are still there and the
agent continues on the same branch.
- [ ] Given a member that halted for one of the five conditions *within
the current run*, when the beat passes again, then it stays halted
and nothing relaunches.
- [ ] Given a member already merged into the phase branch, when the phase
is run again, then it is not relaunched — containment still decides
`merged`.
- [ ] The halt line for a member that ended without reaching `review/`
names running the phase again as the way to resume it.
- [ ] Edge case: a member with a branch that nobody in this run started
and that has no commits on it at all — the empty-branch case a
broken launch leaves — still does not read as merged.
- [ ] `AGENTS.md` no longer says a work agent's worktree must not exist,
and says what a re-run does to a halted member.
## Open questions
- None.
## Notes
The board was right about everything except what to do next: the halt was
accurate, the log was complete, and the Phases view showed the member it
stopped on. What was missing was that the one control offered — run it
again — could not move it, and the thing that could was not offered.
**Risks** — the `has_branch` clause is load-bearing for `merged` and only
wrong for `pending`; a change that removes it from both would make a
merged member eligible to relaunch, which is the one outcome worse than
halting. The acceptance list pins both sides on purpose.
@@ -0,0 +1,136 @@
# 63 — A phase in flight is work happening, and the board should say so without an agent
**Status:** Backlog
**Priority:** High — a phase advancing between members reads as an idle
board on every surface keyed to a running agent, including the one surface
a backgrounded tab has left
**Type:** Bug
The board has one word for "something is happening" and it is spelled
"an agent is running". A phase does not need one: it advances on a beat,
and it spends much of its life with no agent alive at all — while a
member's checks run, between a merge and the next launch, while a
dependency lands. In all of those the phase is working and every
agent-keyed signal on the board says nothing is. The tab title goes back
to the plain project name, the live chip falls through to "you, working
1 session", and the member the phase is actually waiting on wears
`waiting on you` — which is the opposite of true, because nothing is
waiting on you and the phase will merge it by itself the moment CI goes
green.
## Context
Observed on a phase with 11 of 14 merged, member #31 in `review/` with
`CI ◌` in flight and the phase chip reading
`⟶ Phase: platform harde… 11/14 · on #31`. The phase was healthy and
advancing; nothing but that chip said so.
- `manager/core/board.html:1225` — `tabTitle(S.state.project, S.view,
runningAgents().length)`. The count is agents and only agents, so a
phase in flight with no agent produces the quiet title, byte for byte.
This is the surface that matters most: the comment above it argues the
count leads *because* "a backgrounded window" is the only place the
state can still be read — and a phase is exactly the long-running thing
you background the tab on.
- `manager/core/board.html:1026-1028``runningAgents()`, described as
"one reading of 'an agent is working', shared by the header chip and
the tab title, so the two can never disagree". Correct as far as it
goes; there is no equivalent reading of "the board is working".
- `manager/core/board.html:1099-1104``phasesInFlight()` already exists
and already answers it: `p.running || p.halted`, straight off the
snapshot. The title has the fact available and does not consult it.
- `manager/core/board.html:766-768``pillFor(stage, working)`: `working`
is agent-driven, and every other `review/` card is `waiting on you`
unconditionally. A phase member whose checks are still running is not
waiting on anyone.
- `manager/core/phases.py:286-322` — the snapshot the UI reads carries all
of it already: `running`, `halted`, `stopped`, and per-member states
including `waiting` ("its checks are still running") and `ready`. No new
server state is needed for any of this.
- `AGENTS.md`, "Watching one run, and watching it stop" — "Only a run
wears the working vocabulary … because only a run is work happening."
That sentence is the source of the gap: a phase run *is* a run. It
needs saying in a way that does not read as "only an agent".
**Affected areas:** `manager/core/board.html` — the header, the tab title
and `pillFor`; `AGENTS.md` for the working-vocabulary rule and the tab
title's description.
## What to build
- **The tab title says a phase is in flight.** A board with a phase
running and no agent on anything must not render the quiet title.
Reuse `phasesInFlight()` rather than adding a second reading of it.
- **Without ever implying an agent that is not there.** The count is a
count of agents and stays one — `1◌` when a phase runs and nothing is
launched would be a lie, and the tab is the one place nobody can check
it against the board. Use the phase's own glyph (`⟶`, as the chip and
the member chips already do) so the two states are distinguishable at a
glance and can appear together.
- **A halted or held phase wears none of it.** A halt is stopped work and
already says so in `--alarm` in the chip, the switcher and a toast;
putting it in the working position of the title would make "something
is happening" mean "something has stopped".
- **The member the phase is waiting on stops claiming to wait on you.**
A `review/` card whose phase is running and whose checks are still
running should read as what it is — the phase's business, not yours.
It keeps its CI chip; this is the pill only.
- **Say the rule properly in `AGENTS.md`.** The working vocabulary
belongs to work in flight, and a phase beat is work in flight; the tab
title's paragraph should say what it leads with when the work is a
phase.
**Out of scope** — nearby and deliberately untouched:
- **The header's phase chip.** It is already correct and already the
fullest statement of this on the screen. This card is about the
surfaces that contradict it.
- **The breathing animation's meaning.** One looping animation, one
meaning — work in flight. Nothing here adds a second animated state.
- **Any new server-side state or event.** The snapshot already carries
every fact needed; a change to `phases.py` would be a sign of solving
it in the wrong layer.
- **What a phase member's pill says in every other situation.** Only the
running-phase, checks-running case is wrong today.
## Acceptance
- [ ] Given a phase running with a member in `review/` whose CI is still
running and no agent alive anywhere, when the tab is backgrounded,
then the title says work is in flight.
- [ ] Given the same board, the title does not state an agent count — a
reader cannot conclude an agent is running when none is.
- [ ] Given both an agent running and a phase in flight, the title says
both, and still fits a narrow tab well enough to read the leading
mark.
- [ ] Given a phase that has halted, the title carries no working mark;
given a phase held with **‖ hold**, likewise.
- [ ] Given a board with no phase and no agent, the title is exactly the
plain `<project> · bench` it is today, byte for byte.
- [ ] Given a member in `review/` whose phase is running and whose checks
are still running, its pill does not say `waiting on you`.
- [ ] Given a member in `review/` whose phase has halted, or a `review/`
card in no phase at all, the pill is unchanged.
- [ ] Edge case: two phases in flight at once — the title says work is
happening once, not twice, and does not grow with the number of
phases.
## Open questions
- None.
## Notes
The confusion this fixes is specific and worth keeping in mind while
choosing the wording: the board was not wrong anywhere, it was *silent*
in the places a person looks when they are not looking at the board. The
Phases view said 11 of 14 and named the member in flight; the header chip
breathed. Both require the tab to be in front of you, which is the state
a phase is least likely to be in — a phase is the feature you start and
walk away from, and walking away is when the tab title becomes the whole
interface.
**Risks** — the tab title is read at a glance and truncates hard, so the
temptation is to pack it. Every character added to the lead costs the
project name, which is what tells two bench tabs apart. Prefer one glyph
over a word.