From 1ae6a329cac15a7a3f8d8e9e412eaab088601198 Mon Sep 17 00:00:00 2001 From: istos Date: Fri, 31 Jul 2026 16:12:29 +0200 Subject: [PATCH 01/12] tasks: commit the archive moves the board made on disk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../11-failed-agent-runs-must-be-visible.md | 2 +- ...5-distribute-bench-as-packaged-releases.md | 2 +- .../44-archiving-a-card-reaches-git.md | 134 ++++++++++++++++++ 3 files changed, 136 insertions(+), 2 deletions(-) rename tasks/{done => archive}/11-failed-agent-runs-must-be-visible.md (99%) rename tasks/{done => archive}/15-distribute-bench-as-packaged-releases.md (99%) create mode 100644 tasks/backlog/44-archiving-a-card-reaches-git.md diff --git a/tasks/done/11-failed-agent-runs-must-be-visible.md b/tasks/archive/11-failed-agent-runs-must-be-visible.md similarity index 99% rename from tasks/done/11-failed-agent-runs-must-be-visible.md rename to tasks/archive/11-failed-agent-runs-must-be-visible.md index 86d4112..d08557c 100644 --- a/tasks/done/11-failed-agent-runs-must-be-visible.md +++ b/tasks/archive/11-failed-agent-runs-must-be-visible.md @@ -1,6 +1,6 @@ # 11 — A failed agent run must leave a visible trace on the card -**Status:** Done +**Status:** Archived **PR:** https://github.com/12vectors/bench/pull/22 **Assignee:** istos **Priority:** High — three agents died today and the board showed nothing a human would notice diff --git a/tasks/done/15-distribute-bench-as-packaged-releases.md b/tasks/archive/15-distribute-bench-as-packaged-releases.md similarity index 99% rename from tasks/done/15-distribute-bench-as-packaged-releases.md rename to tasks/archive/15-distribute-bench-as-packaged-releases.md index beef081..a9ea841 100644 --- a/tasks/done/15-distribute-bench-as-packaged-releases.md +++ b/tasks/archive/15-distribute-bench-as-packaged-releases.md @@ -1,6 +1,6 @@ # 15 — Distribute bench as packaged releases; update.sh consumes them -**Status:** Done +**Status:** Archived **PR:** https://github.com/12vectors/bench/pull/11 **Priority:** Medium — correctness by construction for every future adopter; urgency rises with the first real second install **Type:** Feature diff --git a/tasks/backlog/44-archiving-a-card-reaches-git.md b/tasks/backlog/44-archiving-a-card-reaches-git.md new file mode 100644 index 0000000..3558e2a --- /dev/null +++ b/tasks/backlog/44-archiving-a-card-reaches-git.md @@ -0,0 +1,134 @@ +# 44 — Archiving a card reaches git, and so does every other write the board makes to a task file + +**Status:** Backlog +**Priority:** High — an archive silently stalls team-mode sync, and the +board says nothing about why +**Type:** Bug + +Moving a card between stages commits itself. Archiving one does not — it +renames the file on disk and stops there. In team mode that leaves an +uncommitted deletion of a tracked file, which is precisely the condition +`sync` refuses to run over, so a single drag onto the archive tray stops +every board commit from publishing until a human notices and commits by +hand. Agent reports appended to a task file have the same gap. + +## Context + +- `manager/core/taskfiles.py:108` — `archive_task()` rewrites the + `**Status:**` line to `Archived`, `shutil.move`s the file into + `tasks/archive/`, and returns. `unarchive_task()` (`:130`) is the same + in reverse. Neither calls `_commit_move()`, `commit_edit()` or + `state.expect_move()`. +- `manager/core/taskfiles.py:261` — `move_task()`, for comparison, sets + the assignee, calls `state.expect_move()` and commits the whole change + in one `board: → ()` commit when `COMMIT_MOVES` is + on. Archiving is the same kind of user action against the same + directory tree, and gets none of it. +- `manager/core/agents.py:35` — `_file_report()` appends a run's closing + report to the task file and never commits either. `commit_edit()` + exists for exactly this and is called from only two places + (`github.py:73` for the `**PR:**` line, `taskfiles.py:199` for a + claim). + +The live evidence, in this repo's own working tree while the card was +written: + +``` + D tasks/done/11-failed-agent-runs-must-be-visible.md + D tasks/done/15-distribute-bench-as-packaged-releases.md + M tasks/to-do/36-the-reference-section.md +?? tasks/archive/11-failed-agent-runs-must-be-visible.md +?? tasks/archive/15-distribute-bench-as-packaged-releases.md +``` + +Two cards archived through the board: the source deleted and +uncommitted, the archived copy untracked, and a third card modified by +an appended report. `sync._clean()` (`sync.py:113`) runs +`git status --porcelain --untracked-files=no`, so those two deletions +alone make it false — this board has not published a commit since, +and the reason is a drag nobody would connect to sync. + +Three consequences, in the order they bite: + +1. **Sync stalls for everyone on this board.** Not just the archive: every + later card move queues behind it. +2. **The archive never reaches the team.** The archived file is untracked, + so other boards keep showing a card its owner archived. +3. **A card can be lost to a clean checkout.** The archived copy exists + only in one working tree. `git checkout .` or a fresh clone loses it, + which is a poor outcome for a feature whose promise is "out of every + column, never deleted". + +**Affected areas:** `manager/core/taskfiles.py` (archive and unarchive), +`manager/core/agents.py` (`_file_report`), and `manager/core/httpd.py` +where the archive routes are served. + +## What to build + +- **Archive and unarchive commit themselves**, exactly as a move does, + under the same `COMMIT_MOVES` gate — solo boards keep committing + `tasks/` by hand and nothing changes for them. One commit, staged by + pathspec, naming both paths so the rename is recorded rather than a + delete and an add. +- **A message that reads like the others.** `board: → archived + ()` and `board: → ()` on the way back. The + `board: ` prefix is not cosmetic: `sync`'s piggyback guard + (`sync.py:157`) refuses to publish any local-ahead commit without it, + so a differently-worded message would stall sync just as thoroughly as + committing nothing. +- **Push it like a move.** `_commit()` already calls + `state.task_committed()`, which is what event-driven push hangs off, so + routing through the same helper gets this for free — worth asserting + rather than assuming. +- **Appended reports commit too.** `_file_report()` should use + `commit_edit()` the way the `**PR:**` line does. A work report is the + permanent record the project keeps on purpose; leaving it uncommitted + both loses it and stalls sync. +- **Decide about `expect_move`.** `archive_task` does not call + `state.expect_move()`, so the watcher sees a card vanish from a stage + directory with no expectation registered. Check what it narrates today + and make the archive attribute itself the way a move does — `you`, not + `disk`. + +**Out of scope** — tempting neighbours left alone: + +- The ⌘Z undo behaviour itself, which works; this is about what it + leaves behind in git. +- Any change to what may be archived (backlog, to-do and done only) or + to the tray. +- Making sync tolerate a dirty tree. It refuses on purpose — the fix is + for the board to stop making the tree dirty behind the user's back. + +## Acceptance + +- [ ] Given `BOARD_COMMIT_MOVES` on, when a card is dragged to the + archive tray, then `git status` is clean afterwards and one + `board: `-prefixed commit records the move into `tasks/archive/`. +- [ ] Given `BOARD_SYNC` on, when a card is archived, then the commit + pushes on its own and a second board shows the card gone on its + next beat. +- [ ] ⌘Z produces its own commit restoring the card to the stage it came + from, and the tree is clean after that too. +- [ ] Given a work agent that finishes, when its report is appended to + the task file, then that write is committed too — no run leaves a + modified task file behind. +- [ ] Given `BOARD_COMMIT_MOVES` off, archiving commits nothing and + behaves exactly as it does today. +- [ ] The archive is attributed on the ticker to the person who did it, + not to `disk`. +- [ ] Edge case: archiving a card that was never committed (a brand-new + backlog file) commits it as an addition in `tasks/archive/` rather + than failing on a pathspec that matches nothing in HEAD. + +## Notes + +The pattern worth taking from this: `tasks/` has exactly one module +allowed to touch it, and inside that module a *move* knows it has to +commit while an *archive* and an *append* do not. Whatever fixes this +should make committing the property of writing to a task file, not +something each caller remembers — otherwise the next writer added to +`taskfiles.py` will forget in the same way. + +**Risks** — the two archived cards and the appended report sitting +uncommitted in this checkout right now need committing by hand before +sync resumes; that is separate from this card and should not wait for it. From 86b7e9f8ab5704dd21a4b83d93e822ded66d6d31 Mon Sep 17 00:00:00 2001 From: istos Date: Fri, 31 Jul 2026 16:13:03 +0200 Subject: [PATCH 02/12] =?UTF-8?q?board:=2039=20=E2=86=92=20review=20(istos?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...the-tab-says-whether-agents-are-working.md | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) rename tasks/{in-progress => review}/39-the-tab-says-whether-agents-are-working.md (64%) diff --git a/tasks/in-progress/39-the-tab-says-whether-agents-are-working.md b/tasks/review/39-the-tab-says-whether-agents-are-working.md similarity index 64% rename from tasks/in-progress/39-the-tab-says-whether-agents-are-working.md rename to tasks/review/39-the-tab-says-whether-agents-are-working.md index 8c343f4..b6b6384 100644 --- a/tasks/in-progress/39-the-tab-says-whether-agents-are-working.md +++ b/tasks/review/39-the-tab-says-whether-agents-are-working.md @@ -1,6 +1,6 @@ # 39 — The browser tab says whether agents are working -**Status:** In Progress +**Status:** Review **Assignee:** istos **Priority:** Medium — the state you most want while the tab is *not* the one you are looking at is the one the tab does not carry @@ -100,3 +100,26 @@ Two follow-ups this deliberately does not do, both worth their own cards: wears, with a toast, precisely because it must not be missed — and a toast fired at a backgrounded tab is missed by definition. That is a sharper version of this same gap. + + +--- + +## Work report — 2026-07-31 16:13 (Juno) + +Committed on `task/39-the-tab-says-whether-agents-are-working` as `72a0d41`; the full suite (497 tests) passes. + +**What changed** + +- `manager/core/board.html` — `renderTitle()` now builds the tab title through a new pure `tabTitle(project, view, working)`. While agents run it reads `2◌ · · `; with none running it is character-identical to the previous ` · `. The mark is `◌`, the in-flight glyph the CI and copilot chips already wear, so the prefix sits in the board's own mono register rather than being an emoji. +- Same file — the `status === 'running'` filter that `renderChip()` used inline is lifted into `runningAgents()`, and both the header chip and the tab title now count through it. That is what makes "the tab and the chip can never disagree" structural rather than coincidental. +- Same file — `document.title` is assigned only when the computed string differs from the last one written (a `shownTitle` cache), since `render()` fires on every SSE frame. +- `tests/test_tab_agents.py` (new) — behaviour run for real in node (skipped when node is absent): the plain title for a quiet board, the count leading for 1/2/3 agents, only `running` records counted, every view carrying it, a finished run returning the tab to the plain title, no write before state arrives, and a 20-frame burst producing exactly one write. Plus source-level wiring checks: one running-filter, one `document.title` writer, and nothing but the count ahead of the project. +- `tests/test_board_title.py` — its "one writer, starting with the project" invariant asserted the literal assignment began with `S.state.project`, which the prefix makes false. It now asserts the single writer builds its string via `tabTitle(S.state.project, …)`, with the ordering claim itself moved to the new file. +- `AGENTS.md` — the "Seeing the board" paragraph on the tab title gains the running-agent prefix. + +**For the reviewer, in order** + +1. `manager/core/board.html:868-897` — the new `tabTitle`/`renderTitle`, and whether `2◌ · ` is the right shape for the prefix. It is the one taste call in the change; the task specified "the count and a mark", and `◌` was picked because it is already bench's in-flight glyph. +2. The edit to `tests/test_board_title.py:129` — an existing assertion was deliberately loosened in one direction (the literal string no longer starts with the project) and its intent re-homed. Worth confirming that trade reads right. + +Nothing here needs running by hand; the definition of done is `python3 -m unittest`, which passes. The out-of-scope neighbours the card named — the favicon and a failed run in the tab — are untouched. From 5c3c5e51c8644b4db400e54cd02638afb98608a8 Mon Sep 17 00:00:00 2001 From: istos Date: Fri, 31 Jul 2026 16:13:12 +0200 Subject: [PATCH 03/12] board: 39 PR opened (istos) --- tasks/review/39-the-tab-says-whether-agents-are-working.md | 1 + 1 file changed, 1 insertion(+) diff --git a/tasks/review/39-the-tab-says-whether-agents-are-working.md b/tasks/review/39-the-tab-says-whether-agents-are-working.md index b6b6384..cc415f6 100644 --- a/tasks/review/39-the-tab-says-whether-agents-are-working.md +++ b/tasks/review/39-the-tab-says-whether-agents-are-working.md @@ -1,6 +1,7 @@ # 39 — The browser tab says whether agents are working **Status:** Review +**PR:** https://github.com/12vectors/bench/pull/32 **Assignee:** istos **Priority:** Medium — the state you most want while the tab is *not* the one you are looking at is the one the tab does not carry From 3bc7e6ec7e77985138a6393853e2feee43db1c94 Mon Sep 17 00:00:00 2001 From: istos Date: Fri, 31 Jul 2026 16:15:50 +0200 Subject: [PATCH 04/12] site: let the analytics beacon through img-src MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- site/README.md | 7 +++++-- site/root/_headers | 10 ++++++++-- tests/test_site_deploy.py | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/site/README.md b/site/README.md index 9cda3c7..3f6fb2b 100644 --- a/site/README.md +++ b/site/README.md @@ -160,9 +160,12 @@ answers rather than files: - **Baseline headers, and one third party by choice.** `nosniff`, a referrer policy, a year of HSTS, `X-Frame-Options`, and a Content-Security-Policy of `default-src 'none'` with `'self'` for - styles, fonts and images. The one origin named besides this one is + styles and fonts. The one origin named besides this one is `cdn.usefathom.com`: Fathom serves the analytics script and receives - its pageviews. It sets no cookie and collects nothing about a person, + its pageviews. It is named under `script-src`, `connect-src` **and + `img-src`** — the beacon is an image request, so an `img-src` that + forgets it loads the script and blocks the pageview, with a clean 200 + on every check. It sets no cookie and collects nothing about a person, which is why the site still needs no consent banner — but it is a third party, and the policy names it rather than opening the door generally. A second one would fail diff --git a/site/root/_headers b/site/root/_headers index 96d6c36..1337d04 100644 --- a/site/root/_headers +++ b/site/root/_headers @@ -33,7 +33,13 @@ #