Commit Graph
100 Commits
Author SHA1 Message Date
istos d5ea992618 board: 48 → review (istos) 2026-08-01 09:02:04 +02:00
istos c017639165 board: 48 Work report filed (istos) 2026-08-01 09:02:04 +02:00
istos b4f1282ab4 tasks: card 52 — a redraw throws away where you were looking
renderBoard() wipes #board and rebuilds every column, so each .drop is a
new node with scrollTop 0 — and render() runs on every SSE frame, so the
board resets under you continuously while agents work. The log already
solves this for itself with S.logStick; the columns never got it.
2026-08-01 09:00:43 +02:00
istos 146751c77f board: 48 → in-progress (istos) 2026-08-01 08:48:11 +02:00
istos a7096c0280 board: 51 → to-do (istos) 2026-08-01 08:48:03 +02:00
istos 35a4f1f36c board: 50 → to-do (istos) 2026-08-01 08:47:55 +02:00
istos 19717a30bd board: 49 → to-do (istos) 2026-08-01 08:47:44 +02:00
istos 0eee8d87a3 board: 48 → to-do (istos) 2026-08-01 08:47:36 +02:00
istos 9f4b3cae7f board: 37 → done (istos) 2026-08-01 08:47:23 +02:00
Ronald AshriandGitHub 5d3f22717f Merge pull request #39 from 12vectors/task/37-sync-resolves-its-remote-and-says-when-it-cannot
37 — Sync resolves its remote like the rest of the board, and says so when it cannot
2026-08-01 08:47:15 +02:00
istos 88c625188e board: 28 → done (istos) 2026-08-01 08:47:11 +02:00
Ronald AshriandGitHub 9e0ae30f33 Merge pull request #38 from 12vectors/task/28-a-pinned-port-stays-pinned
28 — A pinned port stays pinned: fix the TIME_WAIT hop that rewrites .env
2026-08-01 08:47:07 +02:00
istos 0224f3a73e tasks: phases — 48 the model, 49 the runner, 50 the UI, 51 the action
A phase is a card with Type: Phase and a ## Cards section listing its
members in run order; members carry Depends on, which guards rather than
orders. It runs on its own branch, each member branched from the phase
tip so card two sees card one's work, merged back on green, one PR into
main at the end — the human gate moves from every card to the phase
boundary, and 'the board never merges' stays true of main.
2026-08-01 08:46:09 +02:00
istos de4e7dc762 board: 37 PR opened (istos) 2026-08-01 08:03:03 +02:00
istos 97263c3691 board: 28 PR opened (istos) 2026-08-01 08:02:59 +02:00
istos 11e2f61ac7 tasks: 47 — 44 is done, so say so
The card warned that archiving does not commit and suggested sequencing
behind card 44. That landed before 0.3-alpha: archive_task goes out
through _relocate, attributed and committed under the same gate a move
is. Nothing blocks 47.
2026-08-01 07:43:01 +02:00
istos 51196cd06c tasks: card 47 — an archive button on the card
The tray works but the gesture is a drag across the whole board. Puts
the same glyph on the card's footer row, on backlog/to-do/done only,
arming on the first click as the other two-step actions do.
2026-08-01 07:39:04 +02:00
istos 038d258cb2 board: 37 → review (istos) 2026-08-01 07:35:57 +02:00
istos d57d3036fd board: 37 Work report filed (istos) 2026-08-01 07:35:57 +02:00
istos 7a27201dff board: 28 → review (istos) 2026-08-01 07:35:25 +02:00
istos f4796485c0 board: 28 Work report filed (istos) 2026-08-01 07:35:24 +02:00
istosandClaude Opus 5 58b658389b sync: ride the resolved remote, and say when there is none
REMOTE/UPSTREAM were module constants, so a team-mode board whose remote
is named anything but origin synced nothing at all — silently, with a
healthy header. The remote is now resolved per use (config's answer, the
one PRs already honour) and threaded through fetch, publish, replay and
integrate; every message names whatever was resolved.

The silence is the other half. Where _converge()/push_now() returned a
bare 'no-origin' that both callers discarded, _remote() now _note()s at
stalled level first: no remote at all names both fixes (add one, or set
BOARD_GIT_REMOTE), and a BOARD_GIT_REMOTE naming a remote this checkout
does not have stalls naming it rather than quietly using another. It
clears with a recovery line like the offline path, and install() asks at
startup so the chip is there from first paint, not the second beat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 07:33:39 +02:00
istosandClaude Opus 5 8194a043da config: one resolver for which remote this board rides
github.remote() decided it for PRs; sync assumed 'origin'. The answer
moves to config, which already owns BOARD_GIT_REMOTE, resolved on demand
so config still shells out nothing at import. github.remote() becomes a
thin call to it, and git_remotes() lets a caller check whether the name
resolves to a remote that exists rather than falling back silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 07:33:32 +02:00
istosandClaude Opus 5 6829136f42 start: a pinned port stays pinned across a restart
The free-port probe bound without SO_REUSEADDR while the board's own
ThreadingHTTPServer sets it, so the socket a just-stopped board left in
TIME_WAIT read as "taken by something else": a routine stop/start walked
the board to the next port and wrote that over the user's BOARD_PORT pin.
The probe now binds exactly as the server does, which is the whole race.

Behind it, a held port gets a few seconds (BOARD_PORT_WAIT, 5s) to clear
before the walk, re-asking is_our_board each beat — a restart races its
own predecessor far more often than a stranger takes the port. Walking
off a pinned port still persists, since the hooks and agents read
BOARD_PORT and must reach the live board, but it now says so in full:
the right file (manager/local/.env, not manager/.env), old → new, and
how to reclaim the pin.

Tested end to end against a scratch host with a stub board.py, over real
sockets: a genuine TIME_WAIT remnant, a listener that lets go mid-wait,
a listener that does not, and our own board answering.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 07:32:54 +02:00
istos 57b3073496 board: 37 → in-progress (istos) 2026-08-01 07:23:10 +02:00
istos adec6a3c37 board: 28 → in-progress (istos) 2026-08-01 07:21:58 +02:00
istos 727354487b README: the docs site, and the board it documents
A link to bench.12vectors.com under the opening paragraph, and the board
screenshot beneath it — the fastest way to say what this is.

The image is an absolute raw.githubusercontent URL rather than a repo
path, for two reasons. README.md ships in the release artifact and
site/ deliberately does not, so a relative link would break in every
vendored install. And the site's own copy is served under an immutable
year-long cache, so linking that would pin readers to whatever
screenshot was current when they first loaded the page.

The three-layer-law paragraph gains a clause and loses one. That section
is sliced into /concepts/three-layer-law/, so "or read them at
bench.12vectors.com" rendered as a page on that site telling you to go
to that site. It now says AGENTS.md is the file the docs site is cut
from, which is true in both places — the rule this repo sets for its own
prose.
2026-07-31 17:42:52 +02:00
istos 4660d0cb34 0.3-alpha
And un-pin the landing page's version test: it named 0.2-alpha as the
string to replace in VERSION, so the first bump after it was written
failed the suite. It reads the file now, which is what the test is about
in the first place.
2026-07-31 17:28:20 +02:00
istos b031711e60 board: 44 → done (istos) 2026-07-31 17:22:33 +02:00
istos 22215753c6 board: 38 → done (istos) 2026-07-31 17:22:33 +02:00
Ronald AshriandGitHub 385acae35a Merge pull request #36 from 12vectors/task/44-archiving-a-card-reaches-git
44 — Archiving a card reaches git, and so does every other write the board makes to a task file
2026-07-31 17:22:25 +02:00
Ronald AshriandGitHub 97f6343275 Merge pull request #37 from 12vectors/task/38-a-card-being-completed-looks-like-it
38 — A card being merged and cleaned up looks like it, and holds still
2026-07-31 17:22:16 +02:00
istos 97d63216b6 site: take the six doors off the landing page
The page is now three things: what bench is, what installing costs, and
why to bother. The header nav is the way into the docs.

Removing them takes their CSS with them (.doors/.door* in the base sheet
and in all three breakpoints) and two tests that existed only for them —
the six-are-distinct check here and TheDoorsOpenOntoArticles in
test_site_pages. The hero-buttons test survives its class, renamed to say
what it now guards: the landing page's only links into the docs.

One test needed retargeting rather than deleting.
test_it_fails_before_anything_is_written removed /concepts/team-mode/
from the manifest to prove a dead internal link stops the build before
anything is written — but the link it relied on was a door. It now aims
at /guides/install/, which the hero button still hard-links.

Side effect worth having: the two reasons in "Why bench?" that nearly
repeated doors 03 and 04 no longer sit above them.
2026-07-31 17:20:49 +02:00
istos 90626b6405 site: a Why bench? section, and a console wide enough to read
Two changes to the landing page and the reference layout.

The three problem one-liners become "Why bench?" — four reasons now,
led by the one no hosted tool can answer: bench lives in your codebase
and is built to be extended, so the workflow it runs is exactly yours.
It stays unboxed on purpose; the doors below are the page's one grid of
cards and repeating that shape would make the eye read it twice.

The reference pages split the room right of the nav three fifths prose,
two fifths console. At the old fixed 208px every BOARD_AGENT_MODEL_* key
was cut mid-word, which makes a console of settings useless for the one
thing it is there for. Articles keep the narrow gutter — a list of
headings needs no more.

The narrow-width overrides are repeated at .page-reference .shell
specificity in both media blocks. Without that the base override (0-2-0)
outranks the bare .shell inside the query (0-1-0), the third track
survives, and with the gutter hidden it becomes two fifths of empty grid
squeezing the prose on every phone.
2026-07-31 17:08:55 +02:00
istos 6413893f19 board: 38 PR opened (istos) 2026-07-31 17:00:22 +02:00
istos 6fca91eed0 board: 44 PR opened (istos) 2026-07-31 17:00:22 +02:00
istos eedec02ef7 board: 38 → review (istos) 2026-07-31 16:57:34 +02:00
istos 9f6838e9fa board: 44 → review (istos) 2026-07-31 16:57:31 +02:00
istos a1124ec991 tests: the claim, the refusal, and the release on every exit
Against a real repo with a real branch: a real merge, a real conflict,
the wrong-branch refusal, a crash, and a second complete arriving from
inside the first one's merge — which is refused, starts no second merge,
and does not release the run it lost to. Plus the registry's own rules
and the card-face invariants in board.html.
2026-07-31 16:57:17 +02:00
istos 5736126142 …and the card wears it, and holds still
The card takes the accent border and a breathing `completing` pill in
the slot the status pill already owns — the working vocabulary, because
that is what is happening — and carries the latest narrated step on its
activity line, so "parking the drive" and "cleaned up: worktree and
local branch removed" land on the card and not only in the ticker.

While it is claimed the card offers nothing: no hover actions, no drive
or command chips (both run against the worktree being removed), and not
draggable, so the sheet cannot be reopened behind the run. Suppressed,
not ignored on click — an action that looks available and does nothing
is the same lie in a different place.
2026-07-31 16:57:16 +02:00
istosandClaude Opus 5 b462b83a7f Archiving a card reaches git, like every other task-file write
An archive renamed the file on disk and stopped there, leaving an
uncommitted deletion of a tracked file — exactly what sync._clean()
refuses to run over, so one drag onto the tray stalled every later board
commit, kept the archive off the other boards, and left the card in a
single working tree. Appended agent reports had the same gap.

taskfiles now has one door out of a stage directory, _relocate(): it
registers the expectation, writes, moves, and commits under the same
COMMIT_MOVES gate a move uses — so archive, unarchive and move all commit
by construction rather than by each caller remembering. The messages read
like the others (`board: <n> -> archived (<name>)`, and the stage slug on
the way back) and carry the `board: ` prefix sync's piggyback guard
requires; routing through _commit means state.task_committed() fires, so
the event-driven push publishes it.

The second door is append_to_task(), which writes into a card where it
stands and commits via commit_edit(); agents._file_report() uses it
instead of touching config.TASKS itself, so a run's closing report is
recorded rather than left modified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 16:57:09 +02:00
istos 7df6e19d0d completing a card is a state the board holds
Merge & clean up is a minute of destructive work behind one click, and
nothing recorded that it was running: /api/task/complete took a second
request as readily as the first.

state.py grows COMPLETING — claimed before the first step, released in a
finally after the last one, on success, conflict and crash alike. The
steps are already narrated as board events against the file, so
record_board_event folds the latest summary into the claim rather than
asking complete_task to report twice. It is memory, not disk: a board
killed mid-completion leaves no card stuck busy.

/api/state carries the registry and every change publishes it, so the
board renders the busy card from the server's truth rather than from
what one tab happened to click.
2026-07-31 16:57:09 +02:00
istos 25f5565fbd site: one thing in the hero, and an install section of its own
The landing page was carrying two large visuals stacked — a full
terminal in the hero and the board beneath it — which is what made it
read as busy. Now the hero is one third words and two thirds board: the
claim this page makes is a board, so the board is what it shows first
and shows large.

The terminal moves into a 50/50 section below, beside the copy that
explains what installing costs: one script, port 26071, two questions,
and the settings file it writes. The six doors are unchanged.

Smaller changes that follow from it: the headline takes a narrower step
(--t-hero-narrow, 40px) because it no longer has half a row; it loses
its hard <br> and wraps on its own; the CTA drops the time claim now
that a whole section makes it; and the caption under the shot becomes
one plain line rather than a tagged row.
2026-07-31 16:55:35 +02:00
istos d3a43ef1ae install: detect the test command instead of asking for it
The third setup question wanted an answer about a repo the person may
have just cloned, thirty seconds in, before anything had explained why
the board needed one. The file that names a project's ecosystem usually
names its runner too, so it is read off the project: package.json → npm
test, Cargo.toml → cargo test, go.mod → go test ./..., a
pyproject/setup.py/tests/ → python3 -m unittest.

Nothing recognisable writes BOARD_AGENT_COMMANDS empty rather than the
example's Python default, which would be quietly wrong in most repos —
and a wrong prefix denies an agent exactly the way an empty one does, so
absent beats wrong here.

Two details the first attempt got wrong, both now covered:

- Detection runs *before* the questions. Placed after them, a Ctrl-D
  part-way through skipped it and left the example's default.
- "Keep what the project already said" reads the existing .env, not the
  example-seeded defaults dict — which already carries the example's
  answer, so every first run looked like it had one and detection never
  ran.

The landing page's terminal, README.md and AGENTS.md follow: two
questions now, and AGENTS.md documents the detection table.
2026-07-31 16:47:30 +02:00
istos 866c4370c6 board: 44 → in-progress (istos) 2026-07-31 16:47:30 +02:00
istos 88cb9e2e05 board: 38 → in-progress (istos) 2026-07-31 16:47:30 +02:00
istos 3b3c1086d1 board: 45 → done (istos) 2026-07-31 16:47:30 +02:00
istos 63426eb9cc board: 42 → done (istos) 2026-07-31 16:47:30 +02:00
Ronald AshriandGitHub 028635e3fc Merge pull request #35 from 12vectors/task/45-a-past-agent-session-is-not-you
45 — A past agent session is labelled "You", because "You" is what the board says when it does not know
2026-07-31 16:40:50 +02:00
Ronald AshriandGitHub 09cf11e73c Merge pull request #34 from 12vectors/task/42-the-report-keeps-its-headline
42 — A long closing report loses its head, which is where it says what happened
2026-07-31 16:40:40 +02:00
istos 0bae23e833 board: 45 PR opened (istos) 2026-07-31 16:35:49 +02:00
istos 89084f9f4a board: 45 → review (istos) 2026-07-31 16:35:39 +02:00
istos f0350c6dc5 board: 36 → done (istos) 2026-07-31 16:35:39 +02:00
Ronald AshriandGitHub edd78136fc Merge pull request #33 from 12vectors/task/36-the-reference-section
36 — Add the reference section: settings and the contracts
2026-07-31 16:35:27 +02:00
istosandClaude Opus 5 f428ee0468 sessions: persist who a session was, so a replayed run is not "You"
"You" was the else-branch of session_label: anything the board could not
attribute to an agent it attributed to the person. Every session read back
from disk was one of those, because the agent id lived only in the session
registry and never reached the persisted events — so past agent runs came
back wearing the human's label, carrying their own closing reports
underneath it.

Identity is now a small whole file beside each event log
(state/sessions/<id>.who.json): agent id, the agent's name, the model it
rode, and the task. A file rather than a key on the events, because the
logs are append-only JSONL whose first line every reader takes for an
event — and because the name and the model are nowhere in the stream, so
this is the only thing a restart can read them back from. It is rewritten
only when what the board knows changes, which also covers an agent id that
arrives on a later event.

load_disk_sessions() reads it back, and the label now has three registers
instead of two: the agent's name (persisted, so a restart no longer costs
it), "You" only for a session positively recorded as carrying no agent,
and a neutral "Session · <id>" for a log written before any of this was
recorded. Old logs are not retro-attributed in either direction.

agentFor() in board.html falls back to the persisted identity when this
board no longer holds the live record, so a replayed agent session wears
its model chip from what was written rather than from what happens to be
in memory. What depends on liveness (Hold, the worktree branch) finds
nothing there and stays silent, as before.

tests/test_session_identity.py drives the real ingest → persist → reload
path and the page's own chip functions in node.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 16:35:19 +02:00
istos 88f6d92b15 board: 39 → done (istos) 2026-07-31 16:35:14 +02:00
Ronald AshriandGitHub 9d94897d45 Merge pull request #32 from 12vectors/task/39-the-tab-says-whether-agents-are-working
39 — The browser tab says whether agents are working
2026-07-31 16:35:06 +02:00
istos a1b17c85cf merge main into task/36: the board shot and the reference section
One conflict, in the 480px block of site.css: main added the landing
page's .shot rules there while this branch widened the prose padding
selector to cover .page-reference. Both belong — the shot is home-only,
the padding is every long-form layout — so the resolution keeps main's
two rules and the branch's wider selector.

Everything else merged clean: the reference layout, its four routes and
the settings parser do not touch what the landing page changed.
2026-07-31 16:34:24 +02:00
istos b3ba838b66 board: 42 PR opened (istos) 2026-07-31 16:30:42 +02:00
istos 3a823d87d9 board: 42 → review (istos) 2026-07-31 16:29:44 +02:00
istos 3ba3a57e39 board: 36 PR opened (istos) 2026-07-31 16:29:28 +02:00
istos c47adcb7bd board: 36 → review (istos) 2026-07-31 16:29:19 +02:00
istosandClaude Opus 5 38745ae0b3 site: add the reference section, generated from the settings file
The third design layout (1c Logbook) had no page. This lands it, and
with it the four routes a reader reaches for when the guides have
stopped answering: /reference/settings and the three contracts a
project implements against.

The settings page is parsed, not transcribed. site/build.py grows a
second way to make a body — a page may say "generate": "settings"
instead of a "from" heading — and manager/core/.env.example is read on
every build: its blank-line blocks become the page's entries, a comment
block documents the keys directly under it, and the file's own
NAME=value lines are the defaults shown. A key with no comment above it,
or one set twice, stops the build naming the route; a settings page that
disagrees with the settings file is worse than no settings page.

Generated bodies render with raw HTML off. The file writes
<git user.name> meaning a placeholder, and a parser honouring HTML
swallows it — it was never written to be markdown.

The reference layout is the article's three columns with the contents
gutter given over to a console: every entry on the page once more in the
machine register, keys with their defaults on the settings page and
headings on a sliced one. It links the same anchors "On this page"
carries, so the 1080px step folds the whole column away and the contents
strip hands them back — no third copy of anything, and the responsive
rules already in the sheet cover it.

The contract pages are slices like every other page: the adapter
contract and its event schema out of manager/core/adapters/README.md,
the driver and local-command contracts out of AGENTS.md. Renaming any of
those headings stops the build naming its route.

tests/test_site_reference.py holds the promise: add a key to
.env.example with its comment, rebuild, and it is on the page with its
default, with nothing in site/ edited.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 16:28:53 +02:00
istosandClaude Opus 5 4f4004d561 tests: the report clip, with card 32 as the regression
33 tests over reports.report: the head survives, the tail survives, the
elision says in words what it cut and names the log, cuts land on line
boundaries, a short report is passed through unchanged, the four marker
lines still parse, and the two windows never duplicate a line — checked
across a sweep of caps, which is where an off-by-one would put a
paragraph into the permanent record twice.

The fixture is card 32's report at the size it was (3,619 bytes). Its
log is gitignored state that did not survive into this worktree, so the
file is that report reassembled: the 3,000 characters the old clip kept
are verbatim from its card in tasks/done/, and the 619 it discarded are
rebuilt from the quotation in card 42, which is where they survived.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 16:27:47 +02:00
istosandClaude Opus 5 d9907fadde core: a long report keeps its head, and says what it cut
The closing report was capped by keeping its last 3000 characters, so a
report longer than that lost exactly the sentence the prompt contract
puts first — the state of the work — and the task file began mid-word.
Card 32's record still shows it.

reports.py is now the one place a report is clipped: it keeps both ends
(the headline and the review-first pointer), cuts the middle on line
boundaries, and leaves one line of prose saying how many characters went
and naming the log under local/state/agent/logs/ that still holds all of
it. One documented cap, CAP = 12000, replaces 3000 in agents.py and 1500
in github.py, so the PR body and the task file carry the same text for
the same run. It sits left of both consumers in the module map and
depends on config alone.

The failure excerpt keeps its tail deliberately — for a crash the end is
the story — and now says so through reports.tail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 16:27:29 +02:00
istos ea93a2479d site: put the board above the fold
The page claimed 'mission control' and showed a terminal. It now shows
the board itself, directly under the hero, on bench's own repo with
three agents working — the claim demonstrated rather than described.

The terminal gives up the height for it: the .env preamble and the
Ctrl-C line go, the install command and the three setup questions stay.
tests/test_site_landing.py's TRANSCRIPT follows, and still holds every
remaining line against the source that prints it.

The shot goes through stamp() like the stylesheet, so its url carries a
?v=<hash>. Without that, /static/*'s year-long immutable cache would
make the screenshot unreplaceable. 1600px wide and palettised to 256
colours — 164K rather than 452K, and a flat dark UI loses nothing to it.

img{max-width:100%} is now a base rule rather than only .prose img: the
shot lives outside the article body, where that rule never reached.
2026-07-31 16:27:02 +02:00
istos 789261d2f5 board: 45 → in-progress (istos) 2026-07-31 16:26:02 +02:00
istos b2c009e1b8 tasks: card 45 — a past agent session is labelled You
The Sessions list shows agent names only for runs this board process
launched; anything replayed from disk loses its agent id and falls
through to the You branch of session_label(). Diagnosis and evidence in
the card.
2026-07-31 16:17:21 +02:00
istos faba0fd5a4 board: 42 → in-progress (istos) 2026-07-31 16:17:10 +02:00
istos 3bc7e6ec7e site: let the analytics beacon through img-src
Fathom sends its pageview as an image request. script-src named the
origin and img-src did not, so the script loaded and the one thing it
loads to do was blocked — visible only in the browser console, with a
200 on every response and no pageview at the other end.

The console said it plainly:

  Loading the image 'https://cdn.usefathom.com/?h=...&sid=ZPKDEHCV...'
  violates the following Content-Security-Policy directive: img-src
  'self'

A test now asserts the origin appears under all three directives it
actually uses, because nothing on this side of the wire can tell that it
does not.
2026-07-31 16:15:50 +02:00
istos 5c3c5e51c8 board: 39 PR opened (istos) 2026-07-31 16:13:12 +02:00
istos 86b7e9f8ab board: 39 → review (istos) 2026-07-31 16:13:03 +02:00
istos 72a0d4195f board: put the running-agent count in the tab title
A running agent was visible on the board and nowhere else, so switching
tabs — the normal thing to do while one works for minutes — left bench
saying the same string whether three agents ran or none.

The count now leads the title (`2◌ · bench · bench`), ahead of even the
project: the reasoning that put the project first (truncation eats the
tail) applies harder to the one state you want from a tab you are not
looking at. The mark is the in-flight glyph the CI and copilot chips
already wear, not an emoji, and a quiet board keeps the plain title byte
for byte.

Both the tab and the header chip now count through one runningAgents(),
so they cannot disagree, and the title is written only when the string
actually changed — render() runs on every SSE frame.

Behaviour is covered in node (skipped where node is absent); the wiring
that keeps one filter and one writer is a source-level invariant, as
elsewhere for board.html.
2026-07-31 16:12:47 +02:00
istos 1ae6a329ca tasks: commit the archive moves the board made on disk
11 and 15 were archived through the board, which renames the file and
stops there — the deletion from done/ and the new file in archive/ never
reached git, and the uncommitted deletion is what sync refuses to run
over. 36 carries an appended agent report, uncommitted for the same
reason. Card 44 writes both up.
2026-07-31 16:12:29 +02:00
istos b0b0927de6 board: 36 → in-progress (istos) 2026-07-31 16:10:10 +02:00
istos cedcfbfe3c board: 39 → in-progress (istos) 2026-07-31 16:05:57 +02:00
istos 8a6d5a6c78 board: 35 → done (istos) 2026-07-31 16:05:39 +02:00
istos 26e6bb333d site: point the landing page at the reader's problem, not the mechanics
The hero explained the architecture before saying why anyone would care:
'task files in stage directories', 'stdlib-only', 'narrates' are words
that mean something once you already use bench. Every line now maps to
something the audience has actually hit — agents outrunning the person
supposed to be tracking them.

- Hero: 'Run a team of coding agents. Stay in control.' The old headline
  keeps its job as the brand line, in the footer of every layout.
- A new section between hero and doors: three sentences a reader has
  said out loud, each answered by a mechanism rather than a promise.
  Deliberately unboxed — the doors below are the page's one grid of
  cards, and repeating that shape would make the eye work twice.
- The six doors lead with the outcome and keep one mechanism as the
  evidence for it, which is the inversion of what they did before.
- Title and description follow, since a pasted url is the same pitch in
  another form.

Two lines that were quietly false are fixed too: the strip's comment
still said this site counts nothing (Fathom counts pageviews now), and
'no per-seat pricing' would have tripped the fiction guard in
tests/test_site_landing.py, which forbids the word outright.
2026-07-31 16:04:03 +02:00
istos d5c999fa79 site: keep maintainers' notes out of the build
The host uploads the assets directory whole, so a .md left in static/
becomes a public url — static/fonts/README.md was live, telling readers
how to refresh fonts they cannot write to. copy_static skips it, and
internal_targets skips it too: the link checker and the copier have to
agree, or a link to a skipped file would pass the build and 404 on the
site. The licences are .txt and still ship, which is the point of
committing them.

Card 43 records the other thing today's run turned up: the suite reads
the developer's own local/.env, so two sync tests fail here and pass in
CI.
2026-07-31 15:56:02 +02:00
istos 662ecc001f site: add Fathom analytics, and fix the caching rule it sits beside
Two changes to what the live site sends.

Analytics: Fathom's tag goes in every template's <head>, deferred. It is
cookieless and collects nothing about a person, so no consent banner —
but it is a third party, so the CSP names cdn.usefathom.com for script
and connect rather than opening the door generally, and the tests that
said 'no script at all' now say 'no script this site depends on, and no
origin nobody chose'.

Caching: the host concatenates a header two matching rules both set
rather than overriding, so /* and /static/* each setting Cache-Control
sent 'max-age=0, must-revalidate, max-age=31536000, immutable' on the
stylesheet — first max-age wins, and the year-long cache never happened.
Measured on the live site, which is where the from-memory assumption in
task 32 said to check it. Now /static/* is the only rule that sets it
and HTML takes the host's revalidating default; the post-deploy checks
in site/README.md verify both ends.
2026-07-31 15:39:46 +02:00
Ronald AshriandGitHub fbd273a6ae Merge pull request #31 from 12vectors/task/35-the-site-reads-on-a-phone
35 — Make the site read on a phone
2026-07-31 15:28:25 +02:00
istos db3b56b737 merge main into task/35: keep both rails' folded strips and 34's lede
article.html: 34 added the lede under the h1, 35 added the folded
contents strip in the same place. Both belong, and site.css settles the
order — its lede adjacency names .menu-contents + p, so the strip sits
between the title and the lede.

Two things the merge exposed rather than caused:

- $toc and $sidebar were spelled inside article.html's own comments,
  and string.Template substitutes there too, so every page carried a
  second copy of both rails inside a comment (28 toc links for 7
  headings). Doubled to $$toc / $$sidebar, as the template contract
  already said to.
- test_site_pages assumed one rendering of each rail. Both are now
  written twice by design, so the tests assert every rendering agrees
  with the page instead of assuming there is one.

The card file keeps main's location: the board owns a card's stage, and
main had already moved 35 to review/.
2026-07-31 15:27:00 +02:00
istos 5f11108d56 board: 41 → done (istos) 2026-07-31 15:16:14 +02:00
Ronald AshriandGitHub d04d665dff Merge pull request #30 from 12vectors/task/41-the-drawer-renders-wrapped-lists-as-lists
41 — The drawer renders a wrapped list item as one item
2026-07-31 15:16:05 +02:00
istos 51206b3dba board: 35 PR opened (istos) 2026-07-31 15:13:05 +02:00
istos e43afb8684 board: 41 PR opened (istos) 2026-07-31 15:13:02 +02:00
istos fbb3446009 site: carry the fonts' OFL texts beside them
Committing the woff2 files is redistribution, and OFL 1.1 asks that its
text and copyright notice travel with the font. IBMPlex.LICENSE.txt from
IBM/plex, ZillaSlab.LICENSE.txt from mozilla/zilla-slab; both ship in
dist/static/fonts/ with the faces they cover.
2026-07-31 15:11:32 +02:00
istos a15147a926 site: commit the seven woff2 files
Fetching them per checkout made every deploy machine ask Google for the
site's own typography, and a clean checkout build silently fell back to
system fonts. 113 KB, immutable under their filenames — which is what
the year-long immutable cache in root/_headers already assumes.
2026-07-31 15:09:02 +02:00
istos 85469537e5 tasks: five cards from this session's review of the board
37 sync resolves its remote like github.py does, and says so when it
cannot; 38 a card being merged wears the state and holds still; 39 the
tab says whether agents are working; 42 a long closing report keeps its
headline instead of its tail. 40 (the open-questions launch gate) is
archived, unmoved by the board and so never committed by one.
2026-07-31 15:06:19 +02:00
istos 6dcf6e4c1a site: keep fetched fonts and wrangler's cache out of git
site/fetch-fonts.py downloads the seven woff2 files once per checkout,
as site/README.md documents, so they are build inputs rather than
sources. .wrangler/ is the cache a deploy leaves behind.
2026-07-31 15:06:11 +02:00
istos 800cbc6429 board: 41 → review (istos) 2026-07-31 15:01:56 +02:00
istosandClaude Opus 5 d25221bb74 The drawer reads a wrapped list as one item
md() split a block into physical lines and made each one a unit. Task
files are hard-wrapped at ~74 columns, so the second line of an item
became its own bullet, `- [ ]` rendered as a literal bracket pair,
nested lists flattened, and prose kept the author's ragged edge as
<br>. "Enough for these task files" was exactly what it was not.

Lists are now grouped into logical items before rendering: a new item
begins only at a marker, and a line without one is continuation text
joined with a space. Indentation is honoured — a marker past its level
opens a nested list, a shallower one closes back to the level that
fits — and one entry point serves both bullets and ordered lists, so
an <ol> nests under a <ul> the same way. Task-list items render as a
glyph in a span, never an <input>: the file is the source of truth and
the drawer is not an editor. A ticked box reads as settled (--calm);
an open one stays neutral. Paragraphs and blockquotes join their
source lines with a space, so prose reflows to the drawer's width.

Fences, tables, headings and rules are untouched, including the fence
state machine that spans blocks.

The tests lift esc() and md() out of the page and run them under node,
because the renderer is a pure function and its output is what to
assert on; node is not a bench dependency, so those checks skip when
it is absent and source-level invariants cover the shape of the fix.
One check renders every card on the board plus AGENTS.md and asserts
one bullet per source marker — the acceptance criterion applied to the
whole corpus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 15:01:26 +02:00
istos 4d60872625 board: 35 → review (istos) 2026-07-31 14:58:26 +02:00
istosandClaude Opus 5 33aebd90d6 site: make the docs read on a phone
The design is drawn at a fixed 1180px with three-column articles and a
two-column hero, and the site's first traffic is a link pasted into a
chat and opened on a handset. This is what happens below that width —
three max-width steps, and nothing that takes effect at or above the
width the design defines.

The two columns an article loses come back as <details> strips: "On
this page" folds under the title at 1080px, where the gutter goes, and
the section nav becomes a menu under the masthead at 760px, where the
sidebar goes. Both are filled from the same $sidebar and $toc as the
columns they replace, so a folded copy cannot say something the column
does not — and one menu that opens and closes needs no script, which
keeps the site's promise of shipping none.

The rest is what the acceptance asks for: a table becomes its own
scroller with an edge shadow that appears only while there is more to
the right; a token that cannot break, breaks; fenced code keeps
scrolling inside its own block; the hero and title steps come down
without leaving Zilla Slab; everything a finger has to hit is --tap
(44px) tall; and the masthead, footer and hero actions wrap instead of
widening the page.

The one thing that would have changed desktop is guarded rather than
noticed later: the contents strip is a sibling between the h1 and the
body's first paragraph even when display:none, so the lede rule names
it too.

tests/test_site_responsive.py mechanises all four promises, including
the one that matters most — every media query is a max-width below the
design.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 14:57:53 +02:00
istos b89e9c6ed4 board: 25 → backlog (istos) 2026-07-31 14:54:25 +02:00
istos e2c0738763 board: 41 → in-progress (istos) 2026-07-31 14:49:15 +02:00
istos 36b9f8c43d board: 35 → in-progress (istos) 2026-07-31 14:40:08 +02:00
istos 6204e1ad50 board: 34 → done (istos) 2026-07-31 14:40:08 +02:00
istos 016b28254d board: 35 → in-progress (istos) 2026-07-31 14:39:26 +02:00
istos 9bf20e8dd7 board: 34 → done (istos) 2026-07-31 14:39:17 +02:00