a73x

9f6ad3fe

Add server-authoritative collab design and roadmap

a73x   2026-09-05 19:27

Commit message
Add server-authoritative collab design and roadmap

docs/superpowers/specs/2026-09-05-server-authoritative-collab-design.md
Old New
@@ -0,0 +1,278 @@
1 # Server-Authoritative Collaboration — Design & Roadmap
2
3 Date: 2026-09-05
4 Status: Draft
5
6 ## Purpose
7
8 Retire the distributed collab-over-refs machinery and make the git-collab
9 server the single authority for live collaboration state: issues, comments,
10 patches, reviews, and a new primitive, work leases. Git remains the store for
11 code and for patch revision commits; SQLite on the server becomes the store
12 for everything that is *state* rather than *content*.
13
14 The driving use case: agents in ephemeral VMs (provisioned by eitri, reachable
15 via mux) that claim work, do it, and participate in review — plus humans doing
16 the same over the web UI, CLI, and IRC. The current substrate cannot serve it:
17
18 - **Claiming work needs arbitration.** Git's only compare-and-swap is the ref
19 update on push. A claim today is fetch → append event → push → maybe
20 rejected → refetch → replay → discover you lost. Seconds of latency and a
21 lottery, not a lock.
22 - **Review back-and-forth needs subscription.** Git has no watch primitive;
23 the latency floor for a conversation is the polling interval plus a full
24 fetch/reconcile/push cycle per utterance.
25 - **Ephemeral workers can't afford the bootstrap.** Every fresh VM pays
26 clone + `init-key` + trust-allowlist distribution + refspec setup before it
27 can say one word.
28 - **The consistency model is design debt by our own admission.** Concurrent
29 status events resolve by lexicographic timestamp comparison
30 (`docs/design-debt.md`): forgeable, clock-skewed, non-deterministic on ties.
31 - **Signed events freeze the schema forever.** Every historical `event.json`
32 shape is load-bearing permanently (see the `PatchCreate.commit` history in
33 `src/event.rs`). A server-owned store migrates; a signed DAG cannot.
34 - **Per-repo ref islands cannot cross-reference.** An issue about mux filed
35 from inside eitri's repo has no home. One forge, one database makes a
36 cross-repo link a row with two foreign keys.
37
38 All five problems share one cause: the refs substrate makes git act as a
39 replicated multi-writer database. Roughly 7.3k lines (`sync.rs`, `state.rs`'s
40 DAG replay, `trust.rs`, `dag.rs`, `sync_lock.rs`, `merge_scan.rs`'s sync-time
41 half, the signed-event plumbing) exist to prop up that role. With a server we
42 already run, every one of those problems has a boring, standard answer.
43
44 ## The design test, inverted
45
46 The 2026-08-09 patch-revision-refs design stated:
47
48 > If a feature only works when the server is `git-collab-server`, it is not a
49 > git-collab feature.
50
51 That test is hereby retired. It was the honest formulation of the original
52 bet — collaboration between any two clones with no server — and the bet's
53 value did not survive contact with the actual use case. The replacement test:
54
55 > **Contributing code must require nothing but plain git. Everything else may
56 > require the server.**
57
58 Gerrit is the precedent for the whole shape: it stores review data in git
59 (NoteDb, `refs/changes/*`) but behind a single server that is the only writer
60 and the arbiter of all state. Git as *storage* was never the problem; git as
61 the *multi-writer sync protocol* was. This design keeps the Gerrit-style data
62 model already built (numbered revisions, revision-anchored comments,
63 interdiffs, timelines, trailer-recorded merges) and relocates authority.
64
65 ## Decision ledger
66
67 | Concern | Old home | New home |
68 |---|---|---|
69 | Code, branches, tags | git | git (unchanged) |
70 | Patch revision commits | git, refs written by clients + sync | git, refs written **by the server** on push |
71 | Issues, comments, reviews, status | signed event DAG in `refs/collab/*` | SQLite on the server |
72 | Work claims | (impossible) | SQLite lease rows, atomic HTTP endpoint |
73 | Merge recording | sync-time scan on every clone | push-time scan on the server |
74 | Auth / identity | trust allowlist file, distributed by hand | server-side authorized keys (SSH + HTTP); agents are keypairs authorized once |
75 | Conflict resolution | timestamp-wins DAG replay | none needed — the DB serializes writes |
76 | Event schema | frozen forever (signed) | migratable (DB), versioned (webhooks) |
77 | Portability of the record | live sync of `refs/collab/*` | optional one-way `export` to `refs/collab/*` as an archival artifact |
78 | Notifications | none (poll `fetch`) | webhooks (machines) + IRC bridge (humans) |
79
80 ## Architecture
81
82 ### One event bus, three projections
83
84 Every mutation on the forge emits one internal event — `issue.filed`,
85 `issue.claimed`, `patch.created`, `revision.pushed`, `review.posted`,
86 `lease.expired`, `merge.recorded` — with three consumers:
87
88 1. **SQLite** — the authoritative record.
89 2. **Webhooks** — the machine feed. `POST` to subscriber URLs (triage bot,
90 foreman, CI). Payloads reuse the `event.rs` vocabulary, versioned and
91 evolvable now that nothing signs them into permanence. Payloads are signed
92 with the forge's Ed25519 key (`signing.rs`, relocated) so subscribers can
93 verify origin — GitHub's HMAC pattern with the keys we already believe in.
94 3. **IRC bridge** — the human feed. Announcements mirrored into channels;
95 scrollback becomes an ambient audit trail.
96
97 Rule that keeps the seams clean: **machines talk HTTP, humans talk IRC, and
98 the bridge translates only at the edges.** IRC carries intent only when a
99 human is speaking (DM intake, explicit commands like `claim`). No
100 machine-originated action ever rides the channel; automation consumes
101 webhooks. IRC is intake and awareness, never correctness, transport, or
102 record.
103
104 ### Leases
105
106 A claim is one conditional update:
107
108 ```sql
109 UPDATE tasks
110 SET lease_holder = :agent, lease_token = lease_token + 1,
111 lease_expires = :now + :ttl
112 WHERE id = :task
113 AND (lease_holder IS NULL OR lease_expires < :now);
114 ```
115
116 One row affected = claimed (HTTP 200); zero = lost the race (HTTP 409). The
117 database's write serialization is the arbiter; no lock service.
118
119 - **TTL + heartbeat.** Agents die mid-task (VM reaped, OOM, wedged). Leases
120 expire on their own; live holders renew via `PUT /tasks/:id/lease`. Humans
121 get an open-ended lease that renders as "assigned" — one mechanism, two
122 tenure policies.
123 - **Fencing tokens.** `lease_token` increments on every acquire. Every write
124 a holder makes (revision push, status change, completion) carries its
125 token; the forge rejects stale tokens. Expiry alone protects liveness;
126 fencing protects correctness against zombie workers waking up post-expiry.
127 - **Idempotent acquire.** Re-claiming a lease you already hold succeeds, so a
128 client retrying a lost HTTP response does not deadlock against itself.
129
130 Scale is a non-issue by construction: a lease op costs ~1ms of forge time
131 against tasks costing minutes of VM work, and contention is per-task (N racers
132 = one winner, N−1 cheap 409s). The pattern is what GitHub Actions runners,
133 Kubernetes `Lease` objects, and Buildkite agents all do. The thing that
134 actually fails to scale is polling, and the architecture is push-based
135 (webhooks/IRC announce; agents claim on signal, with jitter).
136
137 ### Patch flow (Gerrit-style, magic ref)
138
139 - `git push origin HEAD:refs/for/<base>` — the server's receive hook creates
140 a patch (or matches an existing one by the `Patch:` trailer), pins the
141 commit as the next revision under `refs/collab/patches/<id>/rev/<oid>`,
142 links `--fixes` issues, and prints the patch URL in the push response.
143 Plain git is the only client requirement — this is the new design test
144 holding.
145 - The existing `commit-msg` hook (`hooks.rs`) is our Change-Id equivalent:
146 it stamps the `Patch:` trailer at commit time, and amends carry it. It
147 survives verbatim.
148 - `git-collab patch create/revise` remain as CLI conveniences over the same
149 server operations (set title/body without the web UI).
150 - Local branches are private and never leave the machine. The patch is the
151 unit; revisions are immutable commits; comments anchor to the revision they
152 were written on; interdiff works exactly as today.
153 - Merge: land the commit with its `Patch:` trailer however you merge
154 (including squash). The server scans at push time (`merge_scan.rs`
155 relocated from sync time) and records the merge. `patch merge` stays as
156 the manual fallback.
157
158 ### Identity
159
160 One keypair per principal, human or agent. Humans: the SSH keys the server
161 already authorizes. Agents: a keypair minted (or injected) at VM boot,
162 authorized once on the forge; every clone, push, comment, and claim it makes
163 is attributed to that key. Provenance chains are recorded where actions are
164 brokered: an issue filed by the triage bot from a DM reads *filed by triage,
165 on behalf of alex, via DM*.
166
167 ### Cross-repo
168
169 Issues belong to a repo; references are qualified (`mux#4f2a91`). `relates_to`
170 generalizes to cross-repo foreign keys in the one database. A worker that
171 finds a side-issue in another repo files it (`POST /repos/<repo>/issues`),
172 links it, and stays on task — the default policy is **file, link, continue**;
173 the lease system keeps workers honest about what they claimed.
174
175 ## Companion pieces (out of this repo's scope)
176
177 Per the standalone-tools philosophy, each seam lands as a feature of the tool
178 it belongs to, and the "platform" remains a personal composition:
179
180 - **eitri: job mode.** Boot a VM, inject key + task context, run, report,
181 tear down. Useful to anyone doing CI/sandboxed builds on their own
182 hardware, independent of this forge.
183 - **Bridge bot + foreman: personal glue, unshipped.** The bot mirrors
184 webhook events into IRC and translates human commands/DMs into API calls.
185 The foreman is a dumb elasticity loop: unclaimed tasks > idle workers →
186 ask eitri for a VM (up to a cap); idle worker > N minutes → reap. Lease
187 TTLs already handle worker death, so recruitment can afford to be naive.
188 - **Triage: a webhook subscriber**, not a forge feature. Dedup, labels,
189 severity, clarifying questions — all ordinary API calls.
190
191 Nothing in this repo may depend on any of these existing.
192
193 ## Codebase impact
194
195 **Survives, largely intact** — the parts that were never the problem:
196
197 - `src/server/` — SSH, HTTP, repos, releases, governance, web. Promoted from
198 "optional" to the center of the product.
199 - The Gerrit reconstruction: `patch.rs` diff/interdiff, `timeline.rs`,
200 revision refs (now server-written), TUI and web rendering.
201 - `merge_scan.rs` (relocated to push time), `hooks.rs` (unchanged role).
202 - The CLI: same verbs, thin client over HTTP instead of a local ref-writer.
203 - `event.rs` vocabulary: becomes the webhook payload schema.
204 - `signing.rs`: signs webhook payloads; agent keys remain Ed25519.
205
206 **Dies** — the replicated-database tax (~6k lines):
207
208 - `sync.rs`, `sync_lock.rs`, `dag.rs`, `trust.rs`.
209 - The DAG-replay materializers in `state.rs` (become SQL queries).
210 - Timestamp-wins conflict resolution and its design-debt entry.
211 - Signed event-trees as the live storage format, and with them the
212 frozen-schema archaeology.
213
214 **New, all small:**
215
216 - SQLite schema: issues, comments, patches, revisions, reviews, leases,
217 webhook subscriptions.
218 - The lease endpoints (~50 lines + fencing checks on write paths).
219 - The HTTP API surface for issues/comments/reviews.
220 - `refs/for/<base>` receive-hook handling.
221 - The event bus + webhook dispatcher.
222 - `git-collab export`: one-way materialization of the record into
223 `refs/collab/*` so "clone carries the conversation" survives as an
224 archival feature rather than as the transport.
225
226 ## Roadmap
227
228 Each phase is independently shippable and gets its own implementation plan
229 (in `docs/superpowers/plans/`) when picked up. Order matters: every phase is
230 useful the day it lands, and none blocks on the companions.
231
232 1. **Leases.** Schema + acquire/renew/release endpoints + fencing checks.
233 The agent loop works against the forge as it exists today; humans get
234 `issue claim`. (The seam the whole agent story hangs on, and the one
235 thing git structurally cannot express.)
236 2. **Issues and comments to SQLite.** The API + web UI read/write the DB;
237 a comment becomes one `POST`. Kills the review round-trip burden. Includes
238 a one-time importer that replays existing `refs/collab/*` DAGs into the
239 DB (the current `state.rs` materializer, run once, then retired).
240 3. **Server-maintained revision refs + `refs/for/<base>`.** The receive hook
241 creates/updates patches; push-time merge scanning. Kills the last
242 client-side collab-ref writes. CLI verbs become API calls.
243 4. **Event bus + webhooks + IRC bridge hook points.** Signed payloads,
244 subscription management. Unblocks triage/foreman/CI as external
245 subscribers.
246 5. **Export and burial.** `git-collab export`; delete `sync.rs`,
247 `sync_lock.rs`, `dag.rs`, `trust.rs`, the `state.rs` replay, and the
248 design-debt entry. Update README: the pitch becomes *the lightest forge
249 with real code review*.
250
251 ## Non-goals
252
253 - **Offline multi-writer collaboration.** The property this design
254 deliberately gives up. The record stays portable (export); the *transport*
255 stops being git.
256 - **Nostr / NIP-34 / federation.** The relay-authoritative model here is
257 compatible in spirit (Buzz reaches the same conclusions), and the event
258 vocabulary could become kinds later. Not now; the forge API is smaller and
259 we own both ends.
260 - **Multi-node forge.** SQLite and one process outlive any realistic personal
261 fleet by orders of magnitude. Revisit at ~10⁴ concurrent claimants, i.e.
262 never.
263 - **IRC as a dependency.** The forge must be fully usable with no bridge and
264 no bot.
265
266 ## Open questions
267
268 - **Human lease semantics.** Open-ended lease vs. long TTL with soft nag?
269 Leaning open-ended; an unassign is a manual act either way.
270 - **Agent key authorization flow.** Foreman-signed enrollment vs. manual
271 authorize-once per agent identity. Start manual; automate when it hurts.
272 - **Export format.** Reuse today's signed event-tree layout (readable by
273 existing tooling) vs. a simpler unsigned JSON log. Leaning: keep the tree
274 layout, sign with the forge key — provenance without frozen schemas.
275 - **What happens to `refs/collab/*` data in repos that never import?** The
276 importer is per-repo and opt-in; old refs stay readable by old binaries.
277 - **TUI scope.** Point the dashboard at the API. Cheap if the API mirrors
278 today's read model; decide during phase 2.