From ee8469d009829845aa91ff0a7c23f3bd41e12d5c Mon Sep 17 00:00:00 2001 From: istos Date: Thu, 30 Jul 2026 09:47:49 +0200 Subject: [PATCH] docs: team mode through origin/main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AGENTS.md gets the "Syncing boards" section — what pushes, what pulls, what a lost race looks like, and the two disciplines that make it safe: local main advances only through the board and origin, and sync never merges. The module map gains sync.py, the settings paragraph and the Board view's description gain the sync chip and the author attribution, and .env.example documents BOARD_SYNC / BOARD_SYNC_INTERVAL, including the fetch-rate line for rate-limited remotes. --- AGENTS.md | 59 +++++++++++++++++++++++++++++++++++---- README.md | 5 +++- manager/core/.env.example | 21 ++++++++++++++ 3 files changed, 79 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d004803..4dabc10 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,7 +44,7 @@ Drivers know apps, adapters know vendors, `local/` knows this project. Module map for `manager/core/` (dependencies flow strictly left to right): ``` -config → state → taskfiles → events / github / drive → agents → watch / httpd → board.py +config → state → taskfiles → events / github / drive / sync → agents → watch / httpd → board.py ``` - `config.py` — paths, stages, settings, prompt/adapter/driver resolution @@ -53,6 +53,7 @@ config → state → taskfiles → events / github / drive → agents → watch - `events.py` — ingests NORMALIZED events (the adapter contract), session registry - `github.py` — PR opening, Copilot requests, review/CI polling - `drive.py` — runs the project driver, tracks the one live drive +- `sync.py` — origin/main as the shared board: push on move, pull on a beat - `agents.py` — headless work/review jobs, launched through the adapter - `watch.py` — 2s disk poller narrating moves made outside the API - `httpd.py` — HTTP routes, the SSE stream, serving the page @@ -175,7 +176,8 @@ view swaps only the tail (` · sessions`). All settings live in `manager/core/.env.example` with their defaults documented — the port, the binaries agents launch with, the commands agents may run, -the worktrees directory, whether moves claim and commit themselves, the +the worktrees directory, whether moves claim and commit themselves, +whether boards sync through origin/main and how often, the watch interval and the in-memory caps. Copy it to `manager/local/.env` (gitignored) to override locally; real environment variables beat `.env`, which beats the defaults. The hook bridge reads the @@ -204,7 +206,11 @@ The board has three views (header switcher): - **Board** — the kanban, live. Active Claude Code sessions appear as chips in the header; a card an agent is working on carries a live activity line; the bottom ticker narrates the latest events and every move is attributed - (`you` / `agent` / `disk`). + (`you` / `agent` / `disk`, or the teammate's git name when a sync brought + it). With `BOARD_SYNC` on, a second header chip appears — and only + appears — when sync stops converging, saying whether it is behind + (origin unreachable, self-healing) or stalled on something only a human + can settle. - **Sessions** — a flight recorder per session: a chronological timeline of reads, edits, test runs, commits and card moves, with filters and expandable output. Sessions persist to `.sessions/*.jsonl`, so past ones can be replayed. @@ -433,13 +439,56 @@ the move and the claim land in one commit touching only that task file, messaged `board: ()`, staged by pathspec so unrelated staged work is neither committed nor unstaged (hooks are skipped — this is bookkeeping, not code). Pushing is not part of it: those commits sit -on your local `main` until you push it, which the PR guard above will tell -you about if you forget. The setting is off by default: a single-player +on your local `main` until you push it (or until `BOARD_SYNC` pushes them — +see below), which the PR guard above will tell you about if you forget. The +setting is off by default: a single-player board neither writes nor clears the assignee and makes no commits, exactly as before, and `tasks/` is committed by hand. The gate governs only whether a *move* writes the line — an **Assignee:** added to a file by hand is still read and shown on the card whether the gate is on or off. +## Syncing boards + +`BOARD_SYNC=1` makes `origin/main` the truth and every board a converging +replica. It implies `BOARD_COMMIT_MOVES` — a move that never commits has +nothing to publish — and off (the default) nothing below runs: no fetch, +no push, no thread, no behaviour change at all. + +- **Push is event-driven.** The commit a move makes is pushed as soon as + it lands. A rejected push means another board got there first, so the + board fetches, replays its own commits on top and pushes again. +- **Pull is a beat.** Every `BOARD_SYNC_INTERVAL` (30s by default) and + once at startup: fetch, then fast-forward. The watcher narrates what + arrived, attributed to the commit's author rather than `disk`. +- **Losing a race is a toast, not a mystery.** When replaying collides + with a card someone else already moved, origin wins: the local move is + dropped, the file reverts to origin's version and the board says + `07 claimed by elena — your move was undone`. +- **A human's unpushed commit is never published.** Before any auto-push + every local-ahead commit on `main` must be `board: `-prefixed. One + that isn't stops the push (and the replay), names itself in the ticker + and holds the header's `sync stalled` chip until you push it yourself + or move it off main. +- **Offline is quiet.** An unreachable origin says so once, then works + locally; commits queue on `main` and go out on the next reachable + fetch. + +Two disciplines make this safe, and team mode assumes both: + +- **Local `main` advances only through the board and origin.** Code work + lives in worktrees and PRs — that is what keeps the main checkout clean + and fast-forwardable. Uncommitted changes to tracked files, a checkout + sitting on another branch, or a divergence the guard won't replay all + stall sync rather than risking your work; each one is narrated once and + shown as a chip in the header until it clears. +- **Sync never merges.** It fast-forwards, or rebases the board's own + bookkeeping commits. Nothing here force-pushes, and nothing reacts to + what it pulled beyond narrating it. + +One board fetches twice a minute at the default interval; N boards make +N times that. Against GitHub this is nothing, but on a rate-limited or +metered remote raise `BOARD_SYNC_INTERVAL`. + ## Task file format Each task is a markdown file with a descriptive filename diff --git a/README.md b/README.md index f6dbd5c..6ded7a0 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,10 @@ 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. +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 diff --git a/manager/core/.env.example b/manager/core/.env.example index bfd3cad..d6bfaa8 100644 --- a/manager/core/.env.example +++ b/manager/core/.env.example @@ -76,6 +76,27 @@ BOARD_FETCH_TIMEOUT=10 # empty/0/false/no/off turns it on. BOARD_COMMIT_MOVES= +# Sync boards through origin/main: the second half of team mode, and off +# by default. On, it implies BOARD_COMMIT_MOVES above (a move that never +# commits has nothing to publish) and: each board commit is pushed as it +# is made; a beat fetches and fast-forwards every BOARD_SYNC_INTERVAL +# seconds and once at startup; moves that arrive are narrated with their +# commit author's name; and a card someone else moved first wins the race +# — the local move is dropped with a toast naming who took it. +# +# Two things it will never do. It never publishes a commit the board did +# not make: one local-ahead commit on main without the `board: ` prefix +# stops the push and says which. And it never merges past a divergence or +# into a checkout with uncommitted changes — it says so and waits for a +# human. Both assume the discipline bench already has: code work lives in +# worktrees and PRs, so local main advances only through the board and +# origin. +# +# One board fetches twice a minute at the default interval; raise it on a +# rate-limited or metered remote. Sync rides `origin` and `main` only. +BOARD_SYNC= +BOARD_SYNC_INTERVAL=30 + # Seconds between disk polls of the stage directories. BOARD_WATCH_INTERVAL=2