Compare commits
3
Commits
v0.4-alpha
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a4652a15b | ||
|
|
869b7018b4 | ||
|
|
dff4a42a86 |
+3
-1
@@ -127,7 +127,9 @@ answers rather than files:
|
|||||||
1. `https://bench.12vectors.com/` serves the landing page over TLS.
|
1. `https://bench.12vectors.com/` serves the landing page over TLS.
|
||||||
2. `https://bench.12vectors.com/concepts/claiming-a-card` redirects to
|
2. `https://bench.12vectors.com/concepts/claiming-a-card` redirects to
|
||||||
the same path with a trailing slash, and no url anywhere ends in
|
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
|
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.
|
page **with a 404 status**, not the landing page with a 200.
|
||||||
4. `curl -sI https://bench.12vectors.com/` shows
|
4. `curl -sI https://bench.12vectors.com/` shows
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@
|
|||||||
// so a deploy from a clean checkout builds first.
|
// so a deploy from a clean checkout builds first.
|
||||||
"directory": "./dist",
|
"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>
|
// which is the url the pages link and the one <link rel=canonical>
|
||||||
// names. One page, one address: the slashless form redirects rather
|
// names. One page, one address: the slashless form redirects rather
|
||||||
// than serving a second copy, and no url ever ends in .html.
|
// 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
|
a dict and then does `values.update(os.environ)`. Process environment
|
||||||
beats the file, and the file beats the defaults. That precedence is
|
beats the file, and the file beats the defaults. That precedence is
|
||||||
right and documented; what follows from it is not obvious.
|
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
|
**pops** `BOARD_SYNC` and `BOARD_COMMIT_MOVES` from `os.environ` and
|
||||||
reloads `config`, to stand for a machine that has set neither. Popping
|
reloads `config`, to stand for a machine that has set neither. Popping
|
||||||
removes the only layer that was overriding `local/.env`, so the reload
|
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
|
`tests/__init__.py`, which every run imports — that sets the override
|
||||||
before `config` is first imported, so no individual test has to
|
before `config` is first imported, so no individual test has to
|
||||||
remember. Note that several test modules import `config` at module
|
remember. Note that several test modules import `config` at module
|
||||||
scope (`test_boards_sync.py:27`), so wherever this lands it has to
|
scope (`tests/test_boards_sync.py:27`), so wherever this lands it has
|
||||||
happen first.
|
to happen first.
|
||||||
- **Say it where it will be read.** One line in the `_load_env()`
|
- **Say it where it will be read.** One line in the `_load_env()`
|
||||||
docstring: the process environment is the only layer above the file,
|
docstring: the process environment is the only layer above the file,
|
||||||
so removing a variable does not reveal the default, it reveals the
|
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.
|
||||||
Reference in New Issue
Block a user