istosandClaude Opus 5 755d40dd33 etiquette: the actor's board acts, every other replica renders
Task 19 gave every board the same truth; this makes exactly one of them
react to it. State syncs; reactions don't.

- watch.py: attribution is now the trigger gate. _actor returns (who,
  remote), and a move a pull applied — the arrivals sync files — renders
  and narrates but opens no PR. A plain mv on this disk still acts: inert
  means "happened elsewhere", not "unattributed".
- github.py: the file-carried gates behind that rule, so the rare double
  is harmless rather than loud. The **PR:** line commits itself in team
  mode (taskfiles.commit_edit, sharing the move's pathspec-scoped commit),
  so it reaches the other boards instead of sitting in one working tree —
  where it would also stall sync outright; and a `gh pr create` that races
  anyway adopts the PR GitHub already holds.
- No board finishes the actor's half-done side effect on its own: the
  startup reconcile stands down in team mode and a review card with a
  branch and no PR carries ↑ open PR (POST /api/pr/open), which is a
  person deciding rather than N boards guessing. _open_pr raises its
  reasons now, so the automatic path narrates them and the explicit one
  toasts them.
- agents.py: the claim gates work launches. A card someone else holds
  refuses, naming them; ▸ take over is the deliberate second path (armed
  like everything that costs tokens) and reassigns via taskfiles'
  set_assignee; an unheld card claims itself on launch. Only in team mode
  — with BOARD_COMMIT_MOVES off nothing writes an assignee, so nothing
  reads one as a lock.
- github.complete_task: with BOARD_SYNC on, merge & clean up runs
  `gh pr merge` and lets the beat deliver the result, so local main only
  ever fast-forwards and no board makes a merge commit of its own. A
  branch without a PR is refused with a pointer to ↑ open PR. Sync off
  keeps the local merge path exactly as it was.

Verified with tests/test_actor_acts.py: two real clones of a real bare
upstream and a stub gh — the replica that only renders, the PR line that
travels, the double that adopts, the takeover that reassigns, and both
merge paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 11:18:17 +02:00
2026-07-30 10:56:57 +02:00
2026-07-30 09:47:49 +02:00
2026-07-30 07:40:17 +02:00
2026-07-30 09:47:49 +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.

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.

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: local/driver/start), agent vendor (adapters do: core/adapters/), or project (local/ does). Full docs in AGENTS.md; 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%