diff --git a/tasks/backlog/18-cards-claimed-on-move.md b/tasks/backlog/18-cards-claimed-on-move.md new file mode 100644 index 0000000..2a8cd74 --- /dev/null +++ b/tasks/backlog/18-cards-claimed-on-move.md @@ -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:** ` 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. diff --git a/tasks/backlog/19-boards-sync-through-origin-main.md b/tasks/backlog/19-boards-sync-through-origin-main.md new file mode 100644 index 0000000..a3faf99 --- /dev/null +++ b/tasks/backlog/19-boards-sync-through-origin-main.md @@ -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. diff --git a/tasks/backlog/20-replicas-render-actors-act.md b/tasks/backlog/20-replicas-render-actors-act.md new file mode 100644 index 0000000..df1cc1a --- /dev/null +++ b/tasks/backlog/20-replicas-render-actors-act.md @@ -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.