quorum-sh
v0.1.0
Published
Coordination for agents that do not share a parent process. Leader election, barriers, and majority votes as URLs.
Maintainers
Readme
curl -X POST -H "X-Name: agent-a" https://quorum.legible.sh/db-migrate-x7k2/first
# first caller -> {"role":"leader","name":"agent-a","epoch":1,"expires":"..."} you do it
# everyone else -> {"role":"follower","leader":{"name":"agent-a","since":"..."}} you stand downThe hosted API at quorum.legible.sh is in soft launch. To run the identical API yourself,
npx quorum-sh servestarts it locally; every example below works againsthttp://127.0.0.1:4186.
Orchestrators are good at coordinating the agents they spawned. But your redundant agents were spawned by cron, by CI, by a teammate's laptop, by three different vendors — there is no parent process to referee them. quorum is the referee as a URL. It does exactly two jobs no shared-parent setup can do:
- Who goes first. Three cron-spawned agents wake up to run the nightly migration. Exactly one should. First
curlto a topic returnsleader; the rest learn who won and stand down. The lease expires, the next epoch begins. - Majority wins. Three agents each produce an answer. Byte-identical outputs are a demo illusion, so quorum is two-phase: each POSTs its answer as a proposal, everyone votes on ids, and
GET /decisionlong-polls until some proposal reaches quorum.
Plus the small third thing both jobs need: a barrier, so N agents can arrive independently and start together.
The whole API
| Call | What happens |
|---|---|
| POST /{topic}/first?ttl=300 | First caller: {"role":"leader","epoch":1,"expires":...}. Everyone else: {"role":"follower","leader":{"name","since"}}. Same X-Name re-POSTing renews its lease. |
| GET /{topic}/first | Current leader (or "leader":null) and epoch. |
| DELETE /{topic}/first | Reset the election. Next winner gets a new epoch. |
| POST /{topic}/barrier?n=3&wait=120 | Blocks until n distinct X-Names arrive, then all get {"arrived":[names],"n":3,"position":k}. 408 on timeout — the arrival is withdrawn. |
| POST /{topic}/propose | Body = your proposal (text, ≤ 8 KB) → 201 {"id":"p1"}. Identical bodies are not merged. |
| GET /{topic}/proposals | {"proposals":[{"id","body","by","votes","voters"}]} |
| POST /{topic}/vote/{id} | One vote per X-Name per topic; re-voting moves it → {"id","votes","voter"}. |
| GET /{topic}/decision?quorum=3&wait=60 | Long-polls until any proposal has quorum votes → {"id","body","votes","voters"}. 408 with current standings on timeout. quorum=majority&of=5 also accepted. |
| GET /{topic}/events | SSE: named events (leader, reset, arrival, withdraw, barrier, proposal, vote, decision), heartbeat comments every 25 s. |
| GET /{topic} | Status — JSON for curl, an HTML page in a browser. |
| GET /README.md · GET /llms.txt | Meta: this document verbatim (text/markdown; also at / with Accept: text/markdown) and a one-screen agent index. Never token-gated. |
Topics match [a-zA-Z0-9_-]{1,64} and are created by first use. X-Name is your identity; omit it and each call is a fresh anonymous one (anon-3f9a1c). Long-polls take ?wait=seconds (capped at 300). Errors are JSON {"error":"...","code":"...","hint":"..."} with real status codes — the hint names the correct next request. All limits live in src/limits.mjs.
Teach your agent
Paste this into your agent's CLAUDE.md / AGENTS.md:
## quorum — coordination with agents outside this process (https://quorum.legible.sh)
Pick one topic per job (unguessable, e.g. nightly-x7k2). Send X-Name to identify yourself.
- Who goes first: POST /{topic}/first?ttl=300 -> {"role":"leader"} means do the job;
{"role":"follower","leader":{...}} means stand down. Re-POST to renew your lease;
DELETE /{topic}/first hands off.
- Meet up: POST /{topic}/barrier?n=3&wait=120 blocks until 3 distinct names arrive,
then everyone gets {"arrived":[...],"position":k}. 408 = timed out, you were withdrawn.
- Majority wins: POST your answer (text) to /{topic}/propose -> {"id":"p1"};
GET /{topic}/proposals to read all; POST /{topic}/vote/{id} (one vote each, re-vote moves it);
GET /{topic}/decision?quorum=2&wait=60 long-polls until a proposal has 2 votes ->
{"id","body","votes"}; 408 = no winner yet, body has current standings.
Errors are JSON {"error","code","hint"} — the hint is the correct next call. Topics auto-create. Works with plain curl.Cross-vendor: Claude Code + Codex + a plain script
No framework, no shared parent — one topic. Each pane can be a different machine, vendor, or decade of shell scripting:
# pane 1 — Claude Code
ANSWER=$(claude -p "How do we fix the flaky auth test?")
curl -X POST -H "X-Name: claude" -d "$ANSWER" https://quorum.legible.sh/flaky-auth-k3v9/propose
# pane 2 — Codex
ANSWER=$(codex exec "How do we fix the flaky auth test?")
curl -X POST -H "X-Name: codex" -d "$ANSWER" https://quorum.legible.sh/flaky-auth-k3v9/propose
# pane 3 — a plain script with an opinion
curl -X POST -H "X-Name: script" -d "pin the clock: fake timers in test setup" \
https://quorum.legible.sh/flaky-auth-k3v9/propose
# everyone reads the field and votes on the ids they like:
curl https://quorum.legible.sh/flaky-auth-k3v9/proposals
curl -X POST -H "X-Name: claude" https://quorum.legible.sh/flaky-auth-k3v9/vote/p3
curl -X POST -H "X-Name: codex" https://quorum.legible.sh/flaky-auth-k3v9/vote/p3
# anyone — including you — collects the majority answer:
curl "https://quorum.legible.sh/flaky-auth-k3v9/decision?quorum=2&wait=60"
# -> {"id":"p3","body":"pin the clock: ...","by":"script","votes":2,"voters":["claude","codex"],"quorum":2}Runnable versions of all three workflows are in examples/.
Self-hosting
npx quorum-sh serve # http://0.0.0.0:4186, in-memory
# or
git clone https://github.com/legible-sh/quorum.git && cd quorum
npm start| Flag | Meaning |
|---|---|
| --port 4186 | Listen port (default 4186). |
| --host 0.0.0.0 | Bind address. |
| --data-dir DIR | Persist to DIR/events.jsonl, replayed on boot. Elections, proposals, and votes survive restarts; in-flight barriers don't (they are blocked HTTP requests). |
| --token T | Require Authorization: Bearer T. Covers all topic routes including GETs — proposal bodies are your agents' output and are treated as sensitive. GET /, /healthz, /README.md, and /llms.txt stay public. |
| --base-url URL | Absolute URL used on help/status pages behind a proxy. |
No token means name-as-capability: anyone who can guess the topic name can join the vote. Unguessable names are the default access model; the token is the opt-in upgrade.
CLI
The CLI is sugar over the same HTTP API — curl remains the contract. Base URL: --url, then $QUORUM_URL, then https://quorum.legible.sh. Identity: --name, then $QUORUM_NAME, then <host>-<pid> (unique per invocation — export QUORUM_NAME when identity must survive re-invocation, e.g. lease renewal).
quorum first nightly-x7k2 --ttl 600 --name cron-a # exit 0 = you lead, 1 = follower
quorum first nightly-x7k2 --release # DELETE — hand off leadership
quorum barrier deploy-x7k2 --n 3 --wait 120
quorum propose review-x7k2 "use fake timers" # prints the id
quorum propose review-x7k2 - # read the proposal from stdin
quorum proposals review-x7k2
quorum vote review-x7k2 p2 --name claude
quorum decision review-x7k2 --quorum 2 --wait 60 # winning body on stdout — pipe it
quorum serve --port 4186 --data-dir ./dataExit codes: 0 ok (for first: you are the leader), 1 follower or timeout, 2 error. quorum decision ... | your-next-step works because standings and chatter go to stderr.
Pro
Planned for the hosted instance — capacity and guarantees, never the core verbs. Self-host stays complete.
- Larger panels — more proposals and voters per topic than the free caps.
- Decision audit retention — who proposed, who voted, what won, kept and queryable after the topic would normally age out.
- Weighted votes — let your reviewer agent count double.
- Per-topic tokens — real auth on single topics without locking the whole instance.
- Hosted SLA — the referee stays up even when your infra doesn't.
Straight talk
- This is not Raft. One quorum instance is the arbiter; it does not replicate itself. It coordinates your agents — if you need consensus across replicated servers, that's etcd's job. Single referee, honest about it.
- The topic name is the password. Without
--token, anyone who can guessdb-migrate-x7k2can vote in it. Mint unguessable names; add the token when it matters. - Votes are honor-system.
X-Nameis self-asserted. Nothing stops one process from voting 50 times under 50 names. Fine among your own agents; do not run a public election on it. - A dead leader holds its lease until TTL. There is no failure detector — pick TTLs you can afford to wait out, and have leaders re-POST to renew.
- Barriers don't survive restarts. They are literally blocked HTTP requests. Elections, proposals, and votes do survive, with
--data-dir. majorityneedsof. The server can't know your group size —quorum=majority&of=5means 3, and it's on you that 5 is true.
License
MIT.
quorum is one of the legible primitives, each an ntfy.sh-shaped answer to one way agents are infrastructurally naked:
| | | |---|---| | gate (4180) | ntfy tells you things. gate asks you things. | | bigred (4181) | The big red button for your agent fleet. | | trail (4182) | The flight recorder for agent runs. | | slate (4183) | The blackboard from the multi-agent papers, as a URL. | | relay (4184) | The work queue your agents can provision themselves. | | mutex (4185) | flock(1) for agents that live on different machines. | | quorum (4186) | Coordination for agents that do not share a parent process. | | meter (4187) | The kill-brake for agent spend. 429-as-a-service. | | stash (4188) | ntfy moves signals. stash moves bytes. | | tally (4189) | StatHat reborn as ntfy. Three months too late — or right on time. |
