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.
This commit is contained in:
+5
-2
@@ -162,9 +162,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
|
||||
|
||||
+3
-1
@@ -113,7 +113,8 @@ STATIC_SKIP = ("*.md", ".DS_Store")
|
||||
|
||||
# The assets a template links directly, and the placeholder each one is
|
||||
# offered under. See stamp() for why they carry a query string.
|
||||
STAMPED = {"stylesheet": "static/site.css", "icon": "static/favicon.svg"}
|
||||
STAMPED = {"stylesheet": "static/site.css", "icon": "static/favicon.svg",
|
||||
"board_shot": "static/board.png"}
|
||||
|
||||
ATX = re.compile(r"^(#{1,6})[ \t]+(.*?)[ \t]*#*[ \t]*$")
|
||||
FENCE = re.compile(r"^ {0,3}(`{3,}|~{3,})")
|
||||
@@ -793,6 +794,7 @@ def render_page(page: dict, manifest: dict, *, site: Path, repo: Path,
|
||||
fields = {
|
||||
"stylesheet": stamps["stylesheet"],
|
||||
"icon": stamps["icon"],
|
||||
"board_shot": stamps["board_shot"],
|
||||
"title": escape(page["title"]),
|
||||
"description": escape(page.get("description")
|
||||
or config.get("description", "")),
|
||||
|
||||
+8
-2
@@ -33,7 +33,13 @@
|
||||
# <script> does not run. One origin besides
|
||||
# 'self' is named: cdn.usefathom.com, which
|
||||
# serves the analytics script and receives its
|
||||
# pageviews. Fathom sets no cookie and collects
|
||||
# pageviews. It appears in THREE directives, and
|
||||
# img-src is the one that is easy to miss:
|
||||
# Fathom sends its pageview as an image request,
|
||||
# so script-src alone loads the script and blocks
|
||||
# everything it was loaded to do — silently, on
|
||||
# the browser's side, where no deploy check
|
||||
# looks. Fathom sets no cookie and collects
|
||||
# nothing about a person, which is why it needs
|
||||
# no banner — but it is a third party, and the
|
||||
# policy says so out loud rather than quietly
|
||||
@@ -43,7 +49,7 @@
|
||||
Referrer-Policy: strict-origin-when-cross-origin
|
||||
Strict-Transport-Security: max-age=31536000; includeSubDomains
|
||||
X-Frame-Options: DENY
|
||||
Content-Security-Policy: default-src 'none'; style-src 'self'; font-src 'self'; img-src 'self'; script-src https://cdn.usefathom.com; connect-src https://cdn.usefathom.com; base-uri 'none'; form-action 'none'; frame-ancestors 'none'
|
||||
Content-Security-Policy: default-src 'none'; style-src 'self'; font-src 'self'; img-src 'self' https://cdn.usefathom.com; script-src https://cdn.usefathom.com; connect-src https://cdn.usefathom.com; base-uri 'none'; form-action 'none'; frame-ancestors 'none'
|
||||
|
||||
# The stylesheet and the icon are linked with a ?v=<hash> of their own
|
||||
# contents (site/build.py, stamp()), and the fonts never change under a
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 164 KiB |
@@ -66,6 +66,9 @@ a:hover{color:var(--text);text-decoration:underline}
|
||||
.dim{color:var(--dim)}
|
||||
.spacer{flex:1}
|
||||
.rule{height:1px;background:var(--border-soft);margin:12px 0}
|
||||
/* No image, anywhere, may widen the page. .prose img keeps its own
|
||||
rule for the article body; this is the floor under every layout. */
|
||||
img{max-width:100%;height:auto}
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
*,*::before,*::after{animation:none !important;transition:none !important}
|
||||
}
|
||||
@@ -362,6 +365,24 @@ a:hover{color:var(--text);text-decoration:underline}
|
||||
.t-muted{color:var(--ink-muted)}
|
||||
.t-dim{color:var(--ink-dim)}
|
||||
|
||||
/* The board itself, directly under the hero — the claim the copy makes,
|
||||
shown rather than described. Sized so its top edge clears the fold on
|
||||
a laptop, which is what the shortened terminal above it buys. */
|
||||
.shot{margin:0;padding:0 44px 30px}
|
||||
.shot-img{
|
||||
display:block;width:100%;height:auto;
|
||||
border:1px solid var(--border);border-radius:12px;
|
||||
box-shadow:var(--shadow);background:var(--ink);
|
||||
}
|
||||
.shot-cap{
|
||||
display:flex;gap:12px;align-items:baseline;padding:10px 2px 0;
|
||||
font-size:var(--t-ui);line-height:1.55;color:var(--muted);
|
||||
}
|
||||
.shot-tag{
|
||||
flex:0 0 auto;font-size:var(--t-micro);letter-spacing:.1em;
|
||||
text-transform:uppercase;color:var(--dim);
|
||||
}
|
||||
|
||||
/* The three sentences between the hero and the doors. Quiet on purpose:
|
||||
no cards, no borders, no colour — the doors below are the page's one
|
||||
grid of boxes, and repeating that shape here would make the reader
|
||||
@@ -503,6 +524,7 @@ a:hover{color:var(--text);text-decoration:underline}
|
||||
.snags{padding:8px 24px 26px}
|
||||
.snag-list{grid-template-columns:repeat(2,minmax(0,1fr));gap:20px}
|
||||
.strip{padding:18px 24px}
|
||||
.shot{padding:0 24px 26px}
|
||||
|
||||
/* Only a code block or a table may scroll sideways, so a token that
|
||||
cannot break — a url, a curl one-liner in running text — breaks
|
||||
@@ -589,6 +611,8 @@ a:hover{color:var(--text);text-decoration:underline}
|
||||
.doors{padding:0 16px 24px}
|
||||
.snags{padding:8px 16px 22px}
|
||||
.strip{padding:16px}
|
||||
.shot{padding:0 16px 22px}
|
||||
.shot-cap{flex-direction:column;gap:4px}
|
||||
.page-article .prose,.page-reference .prose{padding:20px 16px 32px}
|
||||
.menu-summary{padding:9px 14px}
|
||||
.footer{padding:18px 14px}
|
||||
|
||||
@@ -89,23 +89,33 @@ $nav
|
||||
</div>
|
||||
<pre class="terminal-body">$install_block
|
||||
|
||||
No .task-manager/manager/local/.env yet — a few questions and bench writes one.
|
||||
Enter takes the default in [brackets]; Ctrl-D skips the rest.
|
||||
|
||||
solo or team? <span class="t-accent">[solo]</span>:
|
||||
which agent adapter? <span class="t-accent">[claude]</span>:
|
||||
what command runs this project's tests? <span class="t-accent">[python3 -m unittest]</span>:
|
||||
|
||||
Wrote .task-manager/manager/local/.env — every other setting is in there, commented; edit it any time.
|
||||
|
||||
<span class="t-dim"> …</span>
|
||||
|
||||
Task board for ~/your-repo/.task-manager/tasks
|
||||
<span class="t-accent">http://127.0.0.1:26071/</span>
|
||||
<span class="t-dim">Ctrl-C to stop</span></pre>
|
||||
<span class="t-accent">http://127.0.0.1:26071/</span></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- What the command above opens. The shot is stamped like the
|
||||
stylesheet ($$board_shot carries ?v=<hash>), because /static/* is
|
||||
cached immutable for a year and a screenshot that could never be
|
||||
replaced would be a poor thing to publish. -->
|
||||
<figure class="shot">
|
||||
<img class="shot-img" src="$board_shot" width="1600" height="857"
|
||||
alt="The bench board: cards in backlog, to-do, in-progress, review
|
||||
and done, three agents working, each card showing its branch
|
||||
and the command its agent is running." decoding="async">
|
||||
<figcaption class="shot-cap">
|
||||
<span class="mono shot-tag">the board</span>
|
||||
<span>bench's own, running on this repo. Three agents at work, each on
|
||||
its own branch — and every one of them stops at review.</span>
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
<!-- Three sentences a reader has said out loud before finding this page.
|
||||
Each one is answered by a mechanism rather than a promise, and each
|
||||
mechanism has a door below it that explains itself. -->
|
||||
|
||||
+1
-1
@@ -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
|
||||
+1
-1
@@ -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
|
||||
@@ -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: <n> → <stage> (<name>)` 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: <n> → archived
|
||||
(<name>)` and `board: <n> → <stage> (<name>)` 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.
|
||||
@@ -0,0 +1,143 @@
|
||||
# 45 — A past agent session is labelled "You", because "You" is what the board says when it does not know
|
||||
|
||||
**Status:** In Progress
|
||||
**Assignee:** istos
|
||||
**Priority:** High — the flight recorder misattributes work, and
|
||||
misattribution is worse than an absence: the list looks complete
|
||||
**Type:** Bug
|
||||
|
||||
Sessions shows agent names only for agents this board process launched.
|
||||
Every session read back from disk loses its agent id, and the label falls
|
||||
through to `You · <id>` — so past agent runs do not vanish from the list,
|
||||
they appear as the human's own sessions. What looks like "several sessions
|
||||
of me and no old agents" is one bug: the old agents *are* the several
|
||||
sessions of me.
|
||||
|
||||
## Context
|
||||
|
||||
The label is decided in one place, `manager/core/events.py:27`:
|
||||
|
||||
```python
|
||||
def session_label(meta: dict) -> str:
|
||||
agent_id = meta.get("agentId") or ""
|
||||
if agent_id:
|
||||
record = state.AGENTS.get(agent_id) or {}
|
||||
...
|
||||
return f"{who} · #{num.group(1)}" if num else who
|
||||
return f"You · {meta['id'][:8]}"
|
||||
```
|
||||
|
||||
**"You" is not a positive identification. It is the `else`.** Anything the
|
||||
board cannot attribute to an agent it attributes to the person.
|
||||
|
||||
That is fine while the board is up. `ingest_event` (`:41`) reads
|
||||
`raw["agent"]` — the adapter puts `BOARD_AGENT_ID` there
|
||||
(`adapters/claude/emit.py:194`, from the child env set in
|
||||
`agents.py:138`) — and stores it on the session meta, so a live run reads
|
||||
`Reed`, `Juno`, `Basil`.
|
||||
|
||||
It breaks at the disk boundary, in two steps:
|
||||
|
||||
1. **The agent id is never persisted.** The `event` dict built in
|
||||
`ingest_event` (`:47-59`) carries `ts`, `session`, `kind`, `summary`
|
||||
and optionally `file` / `cmd` / `detail` / `ok`. The agent id is read
|
||||
into the *session registry* and never copied into the *event*, and the
|
||||
event is what `state.persist()` writes. Verified on this repo's own
|
||||
state: every persisted event's keys are
|
||||
`['kind', 'session', 'summary', 'ts']`, and of 63 files in
|
||||
`manager/local/state/sessions/`, the only one containing the string
|
||||
`"agent"` is `board.jsonl` — a different log.
|
||||
2. **So the replay cannot recover it.** `load_disk_sessions()` (`:96`)
|
||||
rebuilds each session's meta with `"agentId": None` hardcoded. It is
|
||||
not an oversight in that function — the data is not on disk to read.
|
||||
`session_label()` then takes the `else`.
|
||||
|
||||
The evidence, from this checkout while the card was written. Two rows in
|
||||
Sessions read `You`, and their last persisted event is:
|
||||
|
||||
```
|
||||
be5408af… kind: report "Nell's report on 41-the-drawer-renders-…"
|
||||
d1a65161… kind: report "Piper's report on 35-the-site-reads-on-a-phone.md"
|
||||
```
|
||||
|
||||
Nell and Piper are agents. Their sessions are wearing the user's label,
|
||||
and carrying their own closing reports underneath it.
|
||||
|
||||
A third thing compounds it: `state.AGENTS` is memory-only
|
||||
(`state.py:22`), so even with the id restored, the *name* would be gone
|
||||
after a restart — the best today's fallback could say is `Agent · #41`.
|
||||
The same gap is why a replayed session wears no model chip
|
||||
(`board.html:1575` resolves it through the live agent list).
|
||||
|
||||
**Affected areas:** `manager/core/events.py` (event shape, replay,
|
||||
labelling) and `manager/core/state.py` (what a session's identity is and
|
||||
where it lives).
|
||||
|
||||
## What to build
|
||||
|
||||
- **Persist who a session belonged to.** The smallest fix is one more key
|
||||
on the persisted event; the better one is a per-session identity record
|
||||
written once when the session is first linked — `agentId`, the agent's
|
||||
name, its model, its task — because the name and model are the parts a
|
||||
restart loses and neither can be recovered from an event stream.
|
||||
- **Read it back in `load_disk_sessions()`**, so a replayed session
|
||||
arrives with the same label it had while it ran.
|
||||
- **Stop asserting "You" when the board does not know.** After the fix,
|
||||
absence of an agent id on a *live* session still means the person — a
|
||||
human Claude Code session genuinely sends none. For a session replayed
|
||||
from an old file that predates persistence, absence means unknown, and
|
||||
the label should say something neutral rather than claim it was you.
|
||||
Old logs must not be retro-attributed, in either direction.
|
||||
- **Keep the fallbacks honest and distinct.** `Agent` when the id is
|
||||
known and the name is not; the name when the name was persisted;
|
||||
neutral when nothing was recorded. Three different states, three
|
||||
different words.
|
||||
- **Let the model chip follow.** If the model is persisted with the
|
||||
identity, a replayed session can wear its chip instead of the board
|
||||
saying nothing rather than guessing — that rule was written for a board
|
||||
that had nothing to read.
|
||||
|
||||
**Out of scope** — tempting neighbours left alone:
|
||||
|
||||
- Making `state.AGENTS` a durable registry of every run ever launched.
|
||||
This card needs a session to remember who it was, not the board to
|
||||
remember every agent.
|
||||
- The Sessions list's ordering, filters or timeline rendering.
|
||||
- `board.jsonl` and the board event log, which are a separate stream.
|
||||
- Retro-labelling the sessions already on disk. They have no id to
|
||||
recover; a migration that guesses from `"<Name>'s report on …"` would
|
||||
be a heuristic in the one place that should not have one.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- [ ] Given an agent session that ran before the current board process,
|
||||
when Sessions is opened, then the row carries the agent's name and
|
||||
task — not `You`.
|
||||
- [ ] Given a human session, when it is replayed from disk, then it still
|
||||
reads `You` — this fix does not relabel the person's own sessions.
|
||||
- [ ] Given a session file written before this change (no identity
|
||||
recorded), when it loads, then it is labelled neutrally and no row
|
||||
claims to be you.
|
||||
- [ ] Restarting the board does not change any session's label.
|
||||
- [ ] A replayed agent session shows its model chip, or the board keeps
|
||||
saying nothing — but the same way every time, not by accident of
|
||||
what happens to be in memory.
|
||||
- [ ] Edge case: a session whose events arrived before its agent id did
|
||||
(the id is attached on a later event) is labelled correctly after
|
||||
the fact, as it is today.
|
||||
|
||||
## Notes
|
||||
|
||||
The design principle this breaks is one the project states elsewhere: the
|
||||
board says nothing rather than guessing — that is why a session with no
|
||||
known model wears no chip. Labelling an unattributable session `You` is
|
||||
the opposite of that rule, and it is load-bearing here because the
|
||||
Sessions view is a flight recorder. A gap in it is obvious; a confident
|
||||
wrong name is not.
|
||||
|
||||
**Risks** — session files are append-only JSONL read by
|
||||
`load_disk_sessions()` with a bare `json.JSONDecodeError` guard. Whatever
|
||||
shape the identity takes, a file written by the old code and a file
|
||||
written by the new one both have to load; a header line prepended to
|
||||
existing files would break every reader that assumes line one is an
|
||||
event.
|
||||
+25
-1
@@ -1,6 +1,7 @@
|
||||
# 36 — Add the reference section: settings and the contracts
|
||||
|
||||
**Status:** In Progress
|
||||
**Status:** Review
|
||||
**PR:** https://github.com/12vectors/bench/pull/33
|
||||
**Assignee:** istos
|
||||
**Priority:** Low — deliberately after v1; the guides carry the site until
|
||||
someone needs an exact default
|
||||
@@ -95,3 +96,26 @@ The task cites the design file as `Bench Docs.dc.html`, but the file in the repo
|
||||
## Recommendation
|
||||
|
||||
**Keep as is** (with the one filename tweak above). The card is accurate, unblocked, and correctly scoped; it remains deliberately deferred per its own Priority/Notes ("promote when someone asks 'what's the default for X' twice"). Nothing here is stale enough to warrant a rewrite, and no work has been done that would move it toward done.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Work report — 2026-07-31 16:29 (Reed)
|
||||
|
||||
abled, because the file writes `<git user.name>` meaning a placeholder and a parser honouring HTML would swallow it.
|
||||
- **`site/templates/reference.html`** — the 1c Logbook layout: the article's three columns with the contents gutter given over to a pinned console listing every entry on the page (keys with their defaults on the settings page, headings on a sliced one). It links the same anchors "On this page" carries, so the existing 1080px fold already covers it and no third copy of the links exists.
|
||||
- **`site/pages.json`** — a `Reference` section beside Guides and Concepts: `/reference/settings/`, `/reference/adapters/`, `/reference/driver/`, `/reference/commands/`.
|
||||
- **`site/static/site.css`**, **`site/README.md`** — the console and the light "flag" strip under each setting; the manifest's new `generate` key, the parser's two build failures, and the layout documented.
|
||||
- **`tests/test_site_pages.py`** — its `articles()` helper now means "pages in the article layout" rather than "every page on the flow", since the flow has a second layout on it. The one promise that is site-wide, `test_no_body_is_authored_twice`, was widened rather than narrowed: every flow page must name a source and either a `from` or a `generate`.
|
||||
|
||||
**Two judgement calls worth confirming**
|
||||
|
||||
- The card's third bullet reads as one page ("`/reference/driver` and the local-command contract — the two executables a project writes itself"). I shipped it as **two** pages, `/reference/driver/` and `/reference/commands/`, because the two sections are not adjacent in `AGENTS.md` and a single slice spanning them would have dragged "The activity bar and the archive" onto a contract page. Merging them would mean reordering `AGENTS.md`, which is outside the card's stated affected areas.
|
||||
- Those two pages have no sub-headings, so they render no console — the layout drops the box rather than pinning an empty terminal, the same way the contents strip already handles a body with no `h2`s. If you want a console on all four, the fix is in `AGENTS.md`'s section structure, not in `site/`.
|
||||
|
||||
**The four acceptance criteria, and where each is checked**
|
||||
|
||||
- A new key in `.env.example` appears with its default and no site edit — `ANewSettingReachesTheSiteByItself.test_a_new_key_appears_with_its_default_and_its_comment`.
|
||||
- Every setting exactly once, and a key documented nowhere fails the build — `TheSettingsPageIsTheEnvFile.test_every_setting_appears_exactly_once_in_the_console` (compared against an independent parser written in the test file, sharing no code with the builder's) and `ADriftingEnvFileStopsTheBuild.test_a_key_documented_nowhere_fails_rather_than_being_skipped`.
|
||||
- The contract pages are slices — `TheContractPagesAreSlices`, plus two drift tests proving a renamed heading stops the build naming its route.
|
||||
- A multi-line comment does not swallow the next key — `test_a_multi_line_comment_does_not_swallow_the_next_key`, whose fixture deliberately names the second key inside the first key's comment.
|
||||
+25
-1
@@ -1,6 +1,7 @@
|
||||
# 39 — The browser tab says whether agents are working
|
||||
|
||||
**Status:** In Progress
|
||||
**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
|
||||
@@ -100,3 +101,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◌ · <project> · <view>`; with none running it is character-identical to the previous `<project> · <view>`. 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.
|
||||
+23
-1
@@ -1,6 +1,8 @@
|
||||
# 42 — A long closing report loses its head, which is where it says what happened
|
||||
|
||||
**Status:** Backlog
|
||||
**Status:** Review
|
||||
**PR:** https://github.com/12vectors/bench/pull/34
|
||||
**Assignee:** istos
|
||||
**Priority:** High — the task file is the permanent record, and the part it
|
||||
drops is the part the reader must act on
|
||||
**Type:** Bug
|
||||
@@ -130,3 +132,23 @@ short inputs, where the head and tail windows overlap. Handle "the cap is
|
||||
larger than the text" and "the two windows meet" before anything else;
|
||||
that is where an off-by-one duplicates a paragraph into the permanent
|
||||
record.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Work report — 2026-07-31 16:29 (Olive)
|
||||
|
||||
ks. Its run's log lives in `manager/local/state/agent/logs/` in the main checkout — outside this worktree, and outside what this session was allowed to read — so the 619 lost characters are still only recoverable there. The report in the card still begins `four" — they are` and still omits "nothing has been deployed".
|
||||
2. **Decide whether `reports.CAP = 12000` is the number you want.** It is ~1,800 words, four times the old 3,000 (~450), and documented as such in `manager/core/reports.py`. Card 32's 3,619-byte report now passes through whole, which is the point — the clip is a backstop, not the norm.
|
||||
|
||||
## What to know
|
||||
|
||||
`manager/core/reports.py` is new and is the one place a report is clipped. It sits left of both consumers in the module map (`config → state / reports → taskfiles → …`) and imports `config` alone, so `github.py` never reaches sideways into `agents.py`. It exposes `clean` (the old hook-noise strip), `report` (the head-and-tail clip) and `tail` (a dead run's ending).
|
||||
|
||||
`report` returns a short report cleaned and otherwise byte for byte, with no elision line. Over the cap, it keeps leading whole lines (two-thirds of the budget) and trailing whole lines (the rest), separated by one line of prose: `… 8,214 characters of this report were cut here to keep the record within 12000 characters. The whole report is in \`manager/local/state/agent/logs/32-….log\`.` The budget is sized from that line at its longest before it is written, so the result never exceeds the cap. A report that is one enormous line — the edge case the card names — is cut at a space rather than not shown.
|
||||
|
||||
Both consumers now go through it: `agents.py`'s work report, and the act-PR, PR-review and relevance paths (which previously tail-sliced only when their marker was missing, and were uncapped when it was present), plus `github.py`'s PR body, which used to keep a different 1,500-character tail. `_failure_excerpt` still keeps the log's tail through `reports.tail` — that asymmetry is deliberate and is now asserted by a test.
|
||||
|
||||
Two things worth knowing about the test fixture. The live log for card 32 was not reachable from this worktree, so `tests/fixtures/32-work-report.log` is that report **reassembled**: the 3,000 characters the old clip preserved are verbatim from the done card, and the 619 it discarded are rebuilt from the quotation in card 42. It is byte-sized to the 3,619 the original was, and the test module's docstring says all of this plainly. If you recover the real log, dropping it in place of the fixture should leave every assertion standing.
|
||||
|
||||
**Review first:** `manager/core/reports.py` — the whole fix is there, and `report`'s budget arithmetic is where an off-by-one would duplicate a paragraph into the permanent record. Then `TheWindowsMeet` in `tests/test_report_clip.py`, which sweeps caps from 200 to 1,200 asserting no line is ever kept twice and no output exceeds its cap. `AGENTS.md` gains a "What the record keeps of a report" section under the work-agent steps.
|
||||
@@ -295,6 +295,20 @@ class EveryResponseCarriesTheBaseline(unittest.TestCase):
|
||||
self.assertNotIn("font-src https", policy,
|
||||
"the fonts are self-hosted and stay that way")
|
||||
|
||||
def test_the_analytics_origin_is_allowed_everything_it_uses(self):
|
||||
"""Fathom sends its pageview as an *image* request, so naming the
|
||||
origin under script-src alone loads the script and blocks the one
|
||||
thing it exists to do. Nothing server-side can see that: the
|
||||
response is a clean 200 and the beacon dies in the browser. It
|
||||
cost a live deploy once — hence a test."""
|
||||
policy = self.everything["Content-Security-Policy"]
|
||||
for directive in ("img-src", "script-src", "connect-src"):
|
||||
with self.subTest(directive=directive):
|
||||
found = re.search(rf"{directive}([^;]*)", policy)
|
||||
self.assertIsNotNone(found, f"{directive} is not set")
|
||||
self.assertIn("https://cdn.usefathom.com", found.group(1),
|
||||
f"{directive} does not allow the beacon")
|
||||
|
||||
|
||||
class TheTwoKindsOfFileAreCachedDifferently(unittest.TestCase):
|
||||
"""The edge case in the task: a stale HTML page must not survive a
|
||||
|
||||
@@ -40,21 +40,19 @@ CONFIG = REPO / "manager" / "core" / "config.py"
|
||||
#
|
||||
# The page shows fewer lines than a real first run does; its title bar
|
||||
# says "abridged". It shows none a first run does not.
|
||||
#
|
||||
# Shorter since the board shot joined the hero: the terminal gives up
|
||||
# height so the screenshot below it clears the fold. Trimming lines is
|
||||
# allowed — every line here must still be shown, and still be real — but
|
||||
# a line added back has to earn both halves of that again.
|
||||
TRANSCRIPT = [
|
||||
("a few questions and bench writes one.",
|
||||
"a few questions and bench writes", INSTALL),
|
||||
("Enter takes the default in [brackets]; Ctrl-D skips the rest.",
|
||||
"Enter takes the default in [brackets]; Ctrl-D skips the rest.", INSTALL),
|
||||
("solo or team?", "solo or team?", INSTALL),
|
||||
("which agent adapter?", "which agent adapter?", INSTALL),
|
||||
("what command runs this project's tests?",
|
||||
"what command runs this project's tests?", INSTALL),
|
||||
("python3 -m unittest", "python3 -m unittest", INSTALL),
|
||||
("every other setting is in there, commented; edit it any time.",
|
||||
"every other setting is in there,", INSTALL),
|
||||
("Task board for", "Task board for", BOARD),
|
||||
("http://127.0.0.1:", "http://127.0.0.1:", BOARD),
|
||||
("Ctrl-C to stop", "Ctrl-C to stop", BOARD),
|
||||
("26071", "26071", CONFIG),
|
||||
]
|
||||
|
||||
|
||||
@@ -252,9 +252,14 @@ class OnlyCodeAndTablesScrollSideways(unittest.TestCase):
|
||||
block.get(".prose :not(pre) > code", "").replace(" ", ""))
|
||||
|
||||
def test_an_image_can_never_widen_the_page(self):
|
||||
"""Both the article body's rule and the floor under every layout:
|
||||
the landing page's board shot lives outside .prose, so a rule
|
||||
scoped to prose would not have covered it."""
|
||||
base = rules(base_sheet(self.css))
|
||||
self.assertIn("max-width:100%",
|
||||
base.get(".prose img", "").replace(" ", ""))
|
||||
for selector in (".prose img", "img"):
|
||||
with self.subTest(selector=selector):
|
||||
self.assertIn("max-width:100%",
|
||||
base.get(selector, "").replace(" ", ""))
|
||||
|
||||
|
||||
class EveryTemplateSaysHowWideItIs(unittest.TestCase):
|
||||
|
||||
Reference in New Issue
Block a user