Cards 18-20: the multi-user arc — claim on move, sync through origin, replica etiquette
From the 2026-07-30 design discussion: origin/main becomes the truth and every checkout a replica. 18 makes moving a card claim it (assignee written and committed atomically); 19 pushes board moves and pulls on a beat, with git as the lock server and take-over toasts for lost races; 20 keeps side effects on the actor's board only, gates launches on claims, and routes team-mode merges through origin. All gated off by default; single-player bench is untouched until opted in. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
# 18 — Cards are claimed on move: assignee written, board commits the change
|
||||
|
||||
**Status:** Backlog
|
||||
**Priority:** High — the ownership primitive every other multi-user piece builds on
|
||||
**Type:** Feature
|
||||
|
||||
Cards have no owner: the face shows "nobody yet" and nothing records
|
||||
who picked work up. For one person that's cosmetic; for a team it's
|
||||
the missing primitive. Moving a card out of backlog claims it — the
|
||||
board writes the assignee into the file and commits the move — so
|
||||
ownership travels with the card to every clone, and attribution stops
|
||||
being an in-memory courtesy.
|
||||
|
||||
## Context
|
||||
|
||||
- Task files carry Status/Priority/Type but no assignee; the card
|
||||
face's "nobody yet" slot is the UI waiting for this value.
|
||||
- Board moves today rename the file and rewrite Status
|
||||
(`taskfiles.py`) but git never hears about it — commits of tasks/
|
||||
happen by hand, so the shared history lags the board by hours.
|
||||
- Identity: `git config user.name` — already present on every machine
|
||||
that can commit, already what blame/history show, no new concept.
|
||||
- The 2s disk watcher narrates hand-moves; nothing in this card
|
||||
changes that — it gains attribution in task 19 when remote moves
|
||||
arrive via sync.
|
||||
|
||||
**Affected areas:** `taskfiles.py` (the assignee line, the commit),
|
||||
`board.html` (render assignee on the face), AGENTS.md (the convention).
|
||||
|
||||
## What to build
|
||||
|
||||
- Moving a card backlog → to-do or to-do → in-progress through the
|
||||
board writes `**Assignee:** <git user.name>` into the header (first
|
||||
claim only — an existing assignee is preserved, not overwritten).
|
||||
Walking a card back to backlog clears it.
|
||||
- Board-made task changes commit themselves: the move + claim in ONE
|
||||
commit touching only that task file, message prefixed `board: ` with
|
||||
the actor and transition (`board: 18 → in-progress (ronald)`).
|
||||
Commit only — pushing is task 19's job. Gate the auto-commit behind
|
||||
`BOARD_COMMIT_MOVES` (default off) so single-player behaviour is
|
||||
unchanged until opted in.
|
||||
- The card face replaces "nobody yet" with the assignee; done/archive
|
||||
keep it as history.
|
||||
- AGENTS.md documents the convention: claiming is moving; the assignee
|
||||
launches agents on the card; hand-moves should update the line too.
|
||||
|
||||
**Out of scope** — tempting neighbours left alone:
|
||||
|
||||
- Pushing, pulling, or any cross-machine behaviour (task 19).
|
||||
- Enforcing assignee-only launches (task 20).
|
||||
- Multiple assignees, @-mentions, or any identity beyond git's.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- [ ] Given an unclaimed backlog card, when it is dragged to to-do on
|
||||
a board with `BOARD_COMMIT_MOVES=1`, then the file gains
|
||||
`**Assignee:**` with the mover's git name and exactly one
|
||||
`board: `-prefixed commit exists touching exactly that file.
|
||||
- [ ] Given an already-assigned card, when someone else moves it
|
||||
forward, then the assignee is unchanged (first claim sticks).
|
||||
- [ ] Given a claimed card walked back to backlog, then the assignee
|
||||
line is removed.
|
||||
- [ ] With the gate off (default), moves behave byte-identically to
|
||||
today: no commit, no assignee write unless configured.
|
||||
- [ ] Edge case — a dirty index: the board's commit stages only the
|
||||
task file's paths; a developer's unrelated staged changes are
|
||||
neither committed nor unstaged.
|
||||
|
||||
## Open questions
|
||||
|
||||
- None.
|
||||
|
||||
## Notes
|
||||
|
||||
First card of the multi-user arc (18 claim → 19 sync → 20 etiquette),
|
||||
from the 2026-07-30 design discussion: origin/main becomes the truth
|
||||
and every checkout a replica, with git as the lock server. The claim
|
||||
must be atomic with the move — same commit — because it is the
|
||||
optimistic lock task 19's push races resolve.
|
||||
|
||||
**Risks**
|
||||
|
||||
- Hand-moves (plain `mv`) bypass the claim; the watcher still narrates
|
||||
them but no assignee is written. Acceptable — AGENTS.md says to
|
||||
update the line — but the gap should be stated, not hidden.
|
||||
- `user.name` collisions ("ronald" on two machines) merge identities;
|
||||
fine for teams that also share a git history, worth one doc line.
|
||||
@@ -0,0 +1,99 @@
|
||||
# 19 — Boards sync through origin/main: push on move, pull on a beat
|
||||
|
||||
**Status:** Backlog
|
||||
**Priority:** High — this is the multi-user feature; 18 without it is bookkeeping
|
||||
**Type:** Feature
|
||||
**Depends on:** 18 — the claim commit is what gets pushed, and its
|
||||
same-commit atomicity is what makes the races below resolve correctly
|
||||
|
||||
One machine's `tasks/` is the truth today; everyone else's is stale
|
||||
until someone remembers to push and pull. Make origin/main the truth
|
||||
and every board a converging replica: board-made moves push
|
||||
immediately, every board pulls on a short beat, remote moves appear in
|
||||
the ticker attributed to their author, and losing a same-card race is
|
||||
a toast, not a mystery.
|
||||
|
||||
## Context
|
||||
|
||||
- `watch.py` polls the stage directories every 2s — remote changes
|
||||
that arrive via pull are already noticed and narrated; today they
|
||||
would read "disk", this card upgrades them to the commit author.
|
||||
- Task 14 (landed) already points fresh agent branches at
|
||||
origin/main; this card gives the *board state* the same treatment.
|
||||
- The precondition that makes pulling safe is bench's own discipline:
|
||||
code work lives in worktrees and PRs, so the main checkout stays
|
||||
clean and fast-forwardable. Team mode assumes — and the docs must
|
||||
say — that local main advances only through the board and origin.
|
||||
- Push publishes every local-ahead commit, not just the board's —
|
||||
the piggyback hazard below is the sharpest edge in this card.
|
||||
|
||||
**Affected areas:** a new small `sync.py` (or a sibling thread beside
|
||||
`watch.py`), `config.py` (settings), `state.py`/ticker attribution,
|
||||
AGENTS.md (team-mode discipline).
|
||||
|
||||
## What to build
|
||||
|
||||
- **Gate**: `BOARD_SYNC=1` (default off; implies `BOARD_COMMIT_MOVES`).
|
||||
Off = today's behaviour exactly.
|
||||
- **Push, event-driven**: after each board-made task commit, push. On
|
||||
non-fast-forward: fetch, rebase the board commits, push again. If
|
||||
the rebase conflicts on a task file, the local move loses: revert
|
||||
it, re-read the remote version, and toast who took it
|
||||
("07 claimed by elena — your move was undone").
|
||||
- **Piggyback guard**: before any auto-push, every local-ahead commit
|
||||
on main must be `board: `-prefixed. Anything else → no push, one
|
||||
clear ticker warning naming the stray commit. Never publish a
|
||||
human's unpushed work as a side effect.
|
||||
- **Pull, periodic**: fetch + fast-forward-only merge every
|
||||
`BOARD_SYNC_INTERVAL` (default ~30s) and once at startup. Never
|
||||
pull into a non-clean tree or past a divergence — narrate and wait
|
||||
for a human instead. The watcher then narrates arrived moves with
|
||||
the commit author's name instead of "disk".
|
||||
- **Offline**: unreachable origin degrades to local-only silently
|
||||
sane — commits queue, a quiet ticker note says sync is behind,
|
||||
next successful fetch catches up. No errors every 30s.
|
||||
|
||||
**Out of scope** — tempting neighbours left alone:
|
||||
|
||||
- Reacting to synced state with side effects — replicas render only
|
||||
(task 20 owns the etiquette).
|
||||
- Syncing `local/state/` — liveness stays per-board by design.
|
||||
- Any transport other than git via origin; any branch other than main.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- [ ] Given two clones with `BOARD_SYNC=1`, when A moves a card, then
|
||||
within one beat B's board shows the move and B's ticker
|
||||
attributes it to A's git name.
|
||||
- [ ] Given both move the same card in one window, then exactly one
|
||||
claim survives on origin, the loser's board reverts with the
|
||||
take-over toast, and both converge to the same file bytes.
|
||||
- [ ] Given a human's non-board commit sitting on local main, when
|
||||
the board would push, then it refuses with the named warning
|
||||
and the human's commit stays private.
|
||||
- [ ] Edge case — origin unreachable: boards keep working locally,
|
||||
one quiet ticker note, full convergence after connectivity
|
||||
returns; no error spam.
|
||||
- [ ] With the gate off, no fetch, no push, no behaviour change.
|
||||
|
||||
## Open questions
|
||||
|
||||
- None.
|
||||
|
||||
## Notes
|
||||
|
||||
Middle card of the multi-user arc (18 claim → 19 sync → 20 etiquette).
|
||||
Git is the lock server and main the linearizer: push races are the
|
||||
concurrency control, which is why the claim must ride in the move's
|
||||
own commit (18) and why replicas must not react (20).
|
||||
|
||||
**Risks**
|
||||
|
||||
- ff-only pulls plus the piggyback guard mean a divergent main stalls
|
||||
sync until a human resolves it — correct, but the stall must be
|
||||
loudly visible or two halves of a team quietly stop converging.
|
||||
- A 30s beat × N boards is N fetches/min against origin — fine for
|
||||
GitHub, worth a doc line for rate-limited setups.
|
||||
- Reverting a lost move while the user is mid-drag needs care in the
|
||||
UI — the board re-reads disk on every render, which should make
|
||||
this safe, but test the race deliberately.
|
||||
@@ -0,0 +1,103 @@
|
||||
# 20 — Replica etiquette: the actor's board acts, everyone else renders
|
||||
|
||||
**Status:** Backlog
|
||||
**Priority:** High — without it, task 19 turns every board action into N duplicate side effects
|
||||
**Type:** Feature
|
||||
**Depends on:** 18, 19 — etiquette for a fleet that exists only once
|
||||
claims and sync do
|
||||
|
||||
The board doesn't just render state, it reacts to it: a card entering
|
||||
review opens a PR; entering in-progress arms launches. With N synced
|
||||
boards watching one truth, a move must trigger its side effects on
|
||||
exactly one of them — the actor's — or the team gets N PR-open
|
||||
attempts, duplicate agents, and merge stampedes. And ownership must
|
||||
mean something: launching work on someone else's claimed card should
|
||||
be a deliberate act, not an accident.
|
||||
|
||||
## Context
|
||||
|
||||
- `github.py` opens a PR when a card enters review; today that fires
|
||||
on the board that observed the move. Under task 19, every board
|
||||
observes every move — the trigger must distinguish "I did this"
|
||||
from "this arrived".
|
||||
- The `**PR:**` line already gates re-opening — the idempotency
|
||||
pattern to generalize, as the backstop behind actor-only triggers.
|
||||
- One-agent-per-task lives in board memory (`state.py` registries) —
|
||||
invisible to other machines. The card file's assignee (task 18) is
|
||||
the cross-machine version.
|
||||
- Merge & clean-up merges locally and pushes — in a synced team that
|
||||
fights the "main advances only through origin" discipline task 19
|
||||
documents.
|
||||
|
||||
**Affected areas:** `agents.py` (launch guard), `github.py` (actor-only
|
||||
PR opening, merge via origin), `taskfiles.py`/`watch.py` (marking
|
||||
remote-arrived moves), AGENTS.md.
|
||||
|
||||
## What to build
|
||||
|
||||
- **Remote moves are inert.** A move that arrives via sync (commit
|
||||
author ≠ this board's identity, or applied by the pull rather than
|
||||
the UI) renders and narrates but triggers nothing: no PR opening,
|
||||
no launch arming, no worktree work. Side effects belong to the
|
||||
board whose user made the move.
|
||||
- **Idempotency as the backstop.** The actor-only rule prevents
|
||||
duplication; file-carried gates (`**PR:**` line before `gh pr
|
||||
create`; branch-exists checks before worktree creation) make the
|
||||
rare double harmless. Both layers, deliberately.
|
||||
- **Claims gate launches.** ▸ start work on a card assigned to
|
||||
someone else refuses with who owns it; an explicit second path
|
||||
(arm-style, or clearing the assignee first) allows deliberate
|
||||
takeover — never accidental. Unassigned cards in team mode claim on
|
||||
launch, reusing 18's write.
|
||||
- **Merges go through origin.** With `BOARD_SYNC=1`, merge & clean-up
|
||||
merges via `gh pr merge` (origin-side) instead of locally, then
|
||||
lets the sync beat deliver the result — local main never diverges
|
||||
by design. Single-player keeps the local merge path untouched.
|
||||
|
||||
**Out of scope** — tempting neighbours left alone:
|
||||
|
||||
- Shared liveness (a teammate's running agent stays a static
|
||||
"in-progress, assigned to X" on your board).
|
||||
- Cross-machine agent registries or any coordination service beyond
|
||||
the files and origin.
|
||||
- Permissions/roles — anyone can take over deliberately; git history
|
||||
is the audit.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- [ ] Given A moves a card to review, when the move syncs to B, then
|
||||
exactly one PR exists and B's github poller adopts it read-only.
|
||||
- [ ] Given a card assigned to A, when B clicks ▸ start work, then
|
||||
the launch refuses naming A; the deliberate-takeover path works
|
||||
and reassigns via 18's write.
|
||||
- [ ] Given the rare double-fire anyway, then the file-carried gates
|
||||
make the second attempt a no-op, not an error stampede.
|
||||
- [ ] Given `BOARD_SYNC=1`, when merge & clean-up runs, then the
|
||||
merge commit originates on origin and local main only ever
|
||||
fast-forwards. With sync off, the local merge path is
|
||||
byte-identical to today.
|
||||
- [ ] Edge case — the actor's board dies mid-side-effect (moved the
|
||||
card, crashed before PR opened): any board can complete it
|
||||
explicitly (the review-stage card without a PR shows an "open
|
||||
PR" action), but none does so automatically.
|
||||
|
||||
## Open questions
|
||||
|
||||
- None.
|
||||
|
||||
## Notes
|
||||
|
||||
Closing card of the multi-user arc (18 claim → 19 sync → 20
|
||||
etiquette). The principle worth keeping even beyond this card:
|
||||
**state syncs; reactions don't.** Every future automation hung off a
|
||||
stage transition inherits the same question — "am I the actor?" —
|
||||
and should answer it the same way.
|
||||
|
||||
**Risks**
|
||||
|
||||
- Actor detection must survive restarts (the board that moved a card
|
||||
reboots before its PR opens) — hence the explicit-completion action
|
||||
in the last acceptance item rather than a fragile in-memory flag.
|
||||
- `gh pr merge` needs merge rights on origin for every team member —
|
||||
a real-world constraint the docs must state, since the local-merge
|
||||
path hid it.
|
||||
Reference in New Issue
Block a user