Commit Graph
297 Commits
Author SHA1 Message Date
istos bb35ca1985 phase: merge origin/main into phase/53-the-board-stops-fighting-you 2026-08-01 17:16:30 +02:00
istos 98ceb1bb17 board: 53 phase log (istos) 2026-08-01 17:15:58 +02:00
istos fc9b6fa14a board: 47 → in-progress (istos) 2026-08-01 17:15:58 +02:00
istos 3d29f0f4c0 board: 53 phase log (istos) 2026-08-01 17:15:58 +02:00
istos fa7b92110c board: 52 PR opened (istos) 2026-08-01 17:15:57 +02:00
istos 221151f9b0 phase: merge task/52-a-redraw-throws-away-where-you-were-looking into phase/53-the-board-stops-fighting-you 2026-08-01 17:15:56 +02:00
istos c1cd433a91 phase: merge origin/main into phase/53-the-board-stops-fighting-you 2026-08-01 17:15:53 +02:00
istos 3b01f5c7c5 board: 52 → review (istos) 2026-08-01 17:15:50 +02:00
istos 2350491209 board: 52 Work report filed (istos) 2026-08-01 17:15:50 +02:00
istos 419daef5e6 A redraw keeps where you were looking
renderBoard() starts with `board.innerHTML = ''` and rebuilds every
column, so each column's scrolling .drop was a brand-new node on every
pass — and a new node's scrollTop is 0. render() runs on every SSE frame
and a working agent emits an event per tool use, so a column being read
snapped back to the top several times a minute: worst exactly when there
is most to read. The same wipe threw away #board's horizontal position,
and the same demolition costs the session timeline, the sessions rail,
the Focus view, the drawer, and the activity log's place whenever it was
not stuck to the bottom.

Rather than a reconciling render — the real cure, and a far larger change
to the way the page works — the position is paid for separately: note
where each scroller was before the wipe, put it back once the new nodes
are in, both halves inside the same synchronous render so nothing
flashes. Keys are stable names rather than nodes (the stage slug for a
column, the session id for a timeline), because the node is what does not
survive. Restoring clamps, so a column whose cards moved on lands at its
new bottom instead of past it, and one now shorter than its own viewport
lands at the top instead of at a negative offset.

A `v:` prefix marks a key belonging to a view, and switching views drops
those: coming back is a fresh look, not a stale offset from a previous
visit. The activity log spans every view, so its key carries no prefix
and outlives the switch. The two behaviours that were already right are
left alone and now have tests holding them there — the log still follows
the newest line when it was stuck to the bottom (that reading runs after
the restore, so it wins), and the drawer still opens at the top when you
select a different card, which falls out of keying the drawer per
document rather than being fought for.

The helpers run for real under node, as tests/test_drawer_markdown.py
already does with md(); the source-level invariants beside them hold the
mark/restore pairs in order around each wipe, and one of them fails if a
new scrolling element is added to the CSS without a renderer keeping its
place.

    python3 -m unittest discover -s tests   → 854 tests, ok
2026-08-01 17:15:30 +02:00
istos bf1fb992b9 phase: merge origin/main into phase/53-the-board-stops-fighting-you 2026-08-01 17:01:58 +02:00
istos 21a819080a tasks: card 54 — a work agent must refuse a phase card
/api/phase/run guards itself (phases._phase_card refuses a non-phase);
/api/agent/start has no phase check at all, so a stale tab launched an
ordinary work agent on card 53 and it implemented both members itself.
The gate that was missing is on the older feature — the usual shape:
a new capability guards its own front door and leaves the neighbour's
open.
2026-08-01 17:01:44 +02:00
istos 1eaca0734b phase: merge origin/main into phase/53-the-board-stops-fighting-you 2026-08-01 17:00:51 +02:00
istos 438a1d8a04 board: 53 phase log (istos) 2026-08-01 17:00:44 +02:00
istos ea1f0def96 board: 52 → in-progress (istos) 2026-08-01 17:00:43 +02:00
istos bfdcdec000 board: 53 phase log (istos) 2026-08-01 17:00:41 +02:00
istos bef6a642f7 board: 53 → in-progress (istos) 2026-08-01 16:48:36 +02:00
istos 25311aa2bc board: 53 → to-do (istos) 2026-08-01 16:48:28 +02:00
istos 6744177c50 tasks: phase 53 — the board stops fighting you
Runs 52 (scroll position survives a redraw) then 47 (an archive chip on
the card). Neither depends on the other, so neither carries a Depends on
line — they are phased because they both edit board.html, and two agents
branched from main would collide in it. Sequencing gives the second one
the first one's work as its branch point.
2026-08-01 16:47:34 +02:00
istos cb29504ff0 board: 51 → done (istos) 2026-08-01 16:44:20 +02:00
Ronald AshriandGitHub 09ab2c988f Merge pull request #43 from 12vectors/task/51-add-a-card-to-a-phase-from-the-board
51 — Add a card to a phase without opening the file
2026-08-01 16:44:16 +02:00
istos 6d1ebdef43 board: 51 PR review filed (istos) 2026-08-01 16:39:06 +02:00
istos c8ff9cb356 board: 51 PR opened (istos) 2026-08-01 16:34:15 +02:00
istos 648566e8fb board: 51 → review (istos) 2026-08-01 11:23:41 +02:00
istos a1a9a6f921 board: 51 Work report filed (istos) 2026-08-01 11:23:40 +02:00
istosandClaude Opus 5 463b35bdaf A card joins a phase without the file being opened
Phases are meant to arrive whole — members listed, dependencies filled in, readable in a diff before any of it runs. This is the other case: the card you decide belongs after all.

⟶ phase sits on backlog/ and to-do/ cards that are not already in a phase and are not phase cards themselves, and opens a sheet naming the phase cards waiting in to-do/ with what each already holds. Picking one appends `- <n> — <title>` to the end of that phase's ## Cards — the way a person writes it, because the section is authored and read by hand and a machine-shaped line is how a format stops being pleasant.

Only to-do/. A phase in in-progress/ is running: its branch exists and its members are being worked in the order the list had when it started, so appending mid-flight is a different feature with different questions. Offer it and someone finds that out the hard way. No phase waiting there and the action is absent rather than present and empty.

Nothing else moves. One line into the phase card, nothing at all into the card added — membership runs one direction and joining a phase is not a commitment to start it. The append goes out through append_to_section, the same door the phase log uses, so it commits itself under BOARD_COMMIT_MOVES, reaches the other boards, and reads the phase card off the disk rather than off a render: two boards adding to one phase produce two lines, not a lost one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 11:21:27 +02:00
istos 24f118df48 board: 51 → in-progress (istos) 2026-08-01 11:09:21 +02:00
istos 26aa7cf929 board: 50 → done (istos) 2026-08-01 11:09:21 +02:00
Ronald AshriandGitHub 97c42913ae Merge pull request #42 from 12vectors/task/50-the-board-shows-a-phase-running
50 — The board shows a phase running, and shows it stopping
2026-08-01 11:09:13 +02:00
istos ea905bc5c4 board: 50 PR opened (istos) 2026-08-01 11:07:08 +02:00
istos ba3b050c40 board: 50 → review (istos) 2026-08-01 10:33:03 +02:00
istos 646db08b6e board: 50 Work report filed (istos) 2026-08-01 10:33:03 +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
istos 7198c17332 board: 49 → done (istos) 2026-08-01 10:04:38 +02:00
Ronald AshriandGitHub d2f7ddd4bf Merge pull request #41 from 12vectors/task/49-a-phase-runs-itself-on-its-own-branch
49 — A phase runs itself, on a branch of its own
2026-08-01 10:04:35 +02:00
istos 2be30456e9 board: 49 PR update filed (istos) 2026-08-01 10:03:29 +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
istos df28b6aec6 board: 50 → in-progress (istos) 2026-08-01 09:59:25 +02:00
istos da01ef6c1f board: 49 PR opened (istos) 2026-08-01 09:46:12 +02:00
istos 5621fcb144 board: 49 → review (istos) 2026-08-01 09:44:37 +02:00
istos 89e21753de board: 49 Work report filed (istos) 2026-08-01 09:44:37 +02:00
istosandClaude Opus 5 35c83f5f70 Say what "the board never merges" always meant
It meant main. A phase branch is the board's own, and merging into it
is bookkeeping in the same family as committing a move — so the
absolute in AGENTS.md, and the same claim on the landing page, both
become "never merges into main". Nothing about the human gate changes;
the sentence just stops overstating it.

The rest is the section the runner needs: how a phase is started, why
the branch exists at all, what makes the beat stateless, what green
means and what halts, and who runs it when there are replicas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 09:44:02 +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
istosandClaude Opus 5 af24a3fb39 A card's branch may be a phase's, and a member's PR is based on it
github asked one question in four places by writing task/<stem> out
each time. It asks branch_of() now, which answers phase/<stem> for a
phase card — so from review/ onwards a phase is an ordinary card: its
PR opens, its worktree is driven, and merge & clean up finds the
branch it is meant to take apart.

A phase member's PR is opened against its phase's branch rather than
main. Its branch was cut from there, so that is the only base whose
diff is the member's own work — and a PR into main carrying a whole
phase would invite exactly the merge this design refuses to make. The
base is published first, so the remote has something to open against;
the main-is-ahead guard still stands in front of every PR into main.

A phase's own PR says what is in it: the member list, in run order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 09:43:45 +02:00
istosandClaude Opus 5 ecde8abb53 A member of a phase branches from the phase, not from main
Two pieces the runner needs, in the modules that own them.

taskfiles grows a third door: append_to_section, one line under one
heading. The phase log is a running record, and append_to_task would
scatter its lines through the file as other sections landed between
them — the record would stop being readable in the one place a person
looks. It commits like every other board-made write.

agents learns where a phase member starts. That is the whole reason a
phase has a branch: related cards run one after another, so card two
branched from main could not see card one's work while card one sat
unmerged in review/ — it would conflict, or quietly build the same
thing twice. A card in no phase, or one whose phase has not been
started, takes the ordinary fresh branch point, and the ticker names
the unusual base as it already does.

claim_for_launch loses its underscore: a phase run claims its card the
same way starting work on one does, from another module.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 09:43:35 +02:00
istos 4bccbd4a4c board: 48 → done (istos) 2026-08-01 09:12:34 +02:00
Ronald AshriandGitHub c80f23a313 Merge pull request #40 from 12vectors/task/48-a-phase-is-a-card-that-lists-its-cards
48 — A phase is a card that lists its cards
2026-08-01 09:12:27 +02:00
istos 83de3820df board: 48 PR review filed (istos) 2026-08-01 09:05:57 +02:00
istos 9d4cf30c4c board: 48 PR opened (istos) 2026-08-01 09:02:17 +02:00
istos a76db5efcd board: 49 → in-progress (istos) 2026-08-01 09:02:15 +02:00