istos 419daef5e6 A redraw keeps where you were looking
renderBoard() starts with `board.innerHTML = ''` and rebuilds every
column, so each column's scrolling .drop was a brand-new node on every
pass — and a new node's scrollTop is 0. render() runs on every SSE frame
and a working agent emits an event per tool use, so a column being read
snapped back to the top several times a minute: worst exactly when there
is most to read. The same wipe threw away #board's horizontal position,
and the same demolition costs the session timeline, the sessions rail,
the Focus view, the drawer, and the activity log's place whenever it was
not stuck to the bottom.

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

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

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

    python3 -m unittest discover -s tests   → 854 tests, ok
2026-08-01 17:15:30 +02:00
2026-08-01 16:48:36 +02:00
2026-07-31 15:09:02 +02:00
2026-07-30 07:40:17 +02:00

Bench

A live kanban for coding-agent work: task files in stage directories are the only source of truth; a stdlib-only board narrates everything that happens to them — agents working in git worktrees, PRs opening on review, CI and Copilot state on the cards, drives of the app from a task's own branch, and an archive that is never a delete. Turn on team mode (BOARD_SYNC=1) and the truth is origin/main: moves commit and push themselves, every board pulls on a beat, and the person who claimed a card first keeps it.

Docs: bench.12vectors.com — guides, concepts, and a settings reference, every page of it generated from this repository's own markdown, so what the docs say and what bench does cannot drift apart.

The bench board: cards in backlog, to-do, in-progress, review and done, three agents working, each card showing the branch it is on and the command its agent is running.

bench's own board, running on this repository. Three agents at work, each on its own branch — and every one of them stops at review.

Install into a repo

mkdir .task-manager && curl -L \
  https://github.com/12vectors/bench/releases/latest/download/bench.tar.gz \
  | tar -xz -C .task-manager
./.task-manager/start.sh        # wires the project (idempotent) and serves

No token, no clone: releases are curated artifacts that never contained bench's own cards or settings, so the board starts empty by construction.

The first run asks the two questions it cannot answer for you — solo or team, and which agent adapter — and writes manager/local/.env from the documented example, so every other setting is discoverable in your own copy. What runs your tests is read off the project rather than asked (package.jsonnpm test, and so on); nothing recognisable leaves BOARD_AGENT_COMMANDS empty, and that is the key to set before an agent can run them. Bare Enter takes the default throughout; with no terminal (CI, a script) it asks nothing and install.py --setup asks later.

Commit .task-manager/ into the host repo — core is vendored on purpose, so clones work offline and updates show up in the host's own diffs.

The workflow brief ships as .task-manager/AGENTS.md — the cross-vendor name coding agents read natively — with CLAUDE.md beside it as a one-line compatibility pointer. Both live inside .task-manager/, so a host repo's own root AGENTS.md is never touched.

Update

./.task-manager/update.sh              # latest release
BENCH_REF=v2 ./.task-manager/update.sh # an exact release tag

The artifact is stamped with the repo it was built from, so updating needs no configuration; BENCH_SOURCE=<owner/repo> in manager/local/.env overrides the stamp. Updates replace manager/core/ and the top-level scripts wholesale and touch nothing else — tasks, plans, reference, and everything under manager/local/ (your driver, commands, prompt overrides, settings, state) survive every update. Then python3 .task-manager/install.py and restart the board. If the source repo has no published release yet, update.sh says so and changes nothing.

Working on bench itself

git clone git@github.com:12vectors/bench.git && cd bench && ./start.sh

A clone carries bench's own cards and local/ content — that is dev mode, not an install. (Installing from a clone anyway works: install.py clears the shipped cards on its first boot in a host repo.) Releases are built by ./release.sh from the manifest at manager/core/release-manifest: tag = v<VERSION>, one stable asset name (bench.tar.gz), contents at the tarball root — the two things the install one-liner above depends on.

The three-layer law

Core knows about tasks, worktrees, PRs and events. It knows nothing about any particular app (drivers do: manager/local/driver/start), agent vendor (adapters do: manager/core/adapters/), or project (manager/local/ does).

The consequence is what makes an update safe: update.sh replaces manager/core/ wholesale, and everything a project taught bench about itself lives outside it. Full docs in AGENTS.md — the file the docs site is cut from, and the one an agent working in your repo reads; the adapter contract in manager/core/adapters/README.md.

License

MIT.

S
Description
Bench is a local task manager that integrates with coding agents to make it easier to manage work in a repo.
Readme MIT
1.7 MiB
Languages
Python 78.2%
HTML 17.5%
CSS 2.1%
Shell 2%
JavaScript 0.2%