odonian

Let agents pull the work. Keep control of what lands.

Odonian coordinates AI coding agents across your repositories. Workers claim tasks from a board, reviewer agents check their changes, and human approval before merge is the default.

  • Go
  • SQLite
  • One binary: server + CLI
  • REST API
  • Self-hosted
  • Terminal UI
  • AGPL-3.0

One task, end to end

This is the board a human watches: odonian-tui, the optional terminal UI, during the guided demo. A real haiku worker claimed the task, made the change, and submitted; a real opus reviewer checked the commit and approved. The task now waits in approved for a person. Ready to approved took 103 seconds on this run.

sbx-local
backlog(0)  ready(0)  in_progress(0)  review(0)  ‹approved(1)›  done(2)  blocked(0)  failed(0)  abandoned(0)
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
▸ bc02c73f [haiku]  Append greeting line #3 to GREETINGS.md
    @worker-2-odonian-… · updated 15s ago

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
←/→ column   ↑/↓ select   enter detail   b bounce   t hold/release   z archive   P switch project   r refresh   q qu
Board view. Each column is a task state; the counts are live. The selected task shows its short id, its pinned model, the agent holding it, and when it last changed.
Project: sbx-local
Task: Append greeting line #3 to GREETINGS.md
State: approved  Assignee: worker-2-odonian-demo-efdbc1
Model: haiku
Created: 2026-09-12 15:38:44 UTC
Updated: 2026-09-12 15:40:27 UTC
Links:
  [commit] 474c9abd858a333d4c7c578bf63e965a10dc6047
Events:
  [2026-09-12 15:38:44 UTC] system: transition
    backlog->ready
  [2026-09-12 15:39:05 UTC] worker-2-odonian-demo-efdbc1: claim
  [2026-09-12 15:39:25 UTC] worker-2-odonian-demo-efdbc1: submit
  [2026-09-12 15:39:25 UTC] system: spawn_review
    Round 1 with models: ["opus"]
  [2026-09-12 15:40:27 UTC] reviewer-1-odonian-demo-98c547: review (approve)
    APPROVE — commit 474c9ab "Append greeting line #3 to GREETINGS.md"

Verified independently against the parent acceptance criteria (not the writeup):

1. **Correct content.** `git show 474c9ab:GREETINGS.md | cat -A` yields exactly:
```
hello$
hello from the fleet (#3)$
```
The appended line is byte-for-byte the string required by the spec ('hello from the fleet (#3)'), with a proper
trailing newline and no stray whitespace. The pre-existing `hello` line is untouched.

2. **No other file changed.** `git show --stat 474c9ab` => `GREETINGS.md | 1 +`, 1 file changed, 1 insertion(+),
0 deletions. The full diff is the single added line; Makefile and README.md are unmodified. Satisfies "Do not
change any other file."
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
esc back   ↑/↓/pgup/pgdn scroll spec   o open PR   s source doc   d design doc   P switch project   m merge & comple
Detail view of the same task. The links tie the task to the worker's output: here a commit, because the demo uses local delivery. In pull-request delivery the link is the PR, and the branch is named from the task id (mr/<first 8 chars>), so every retry of a task lands on the same PR and a PR URL resolves back to its task. The events are the audit trail the board keeps for every transition.
# Replace placeholders with full UUIDs, or copy commands from the boot banner.
$ odonian pending --project <project-id>
ID        STATE     KIND       TITLE
bc02c73f  approved  implement  Append greeting line #3 to GREETINGS.md

$ odonian diff <task-id>
diff --git a/GREETINGS.md b/GREETINGS.md
--- a/GREETINGS.md
+++ b/GREETINGS.md
@@ -1 +1,2 @@
 hello
+hello from the fleet (#3)

$ odonian approve <task-id>  # approved → done; or: odonian reject <task-id> --note "…"
The human step, from the CLI. This local-delivery example uses full UUIDs for worktree operations; show and diff also accept unique task prefixes. In local delivery, approve records completion and freezes a branch for you to merge; it does not change main. In pull-request delivery, merge on the forge.

How these frames were made: they are text captured from the real odonian-tui and odonian binaries (v0.16.3) during a run of the guided demo on 2026-09-12, in a Docker sandbox, recoloured for the page. The task, the commit, the review, and the timestamps are from that run; the repository is the demo's throwaway one. Commands above use placeholders for the full IDs; the reviewer's note is cut where the terminal was.

Evidence

Odonian builds itself. Its own repository is a project on its author's board, and most of what has merged there was opened by the fleet, reviewed by a second model, and merged by a person.

309pull requests merged Verify on GitHub
207of them fleet-authored Verify on GitHub
66%of merges came from the board How it is counted

Counted 2026-09-11. A merged PR is fleet-authored when its head branch starts with mr/, the name the harness derives from the task id. The project README also reports production use since June 2026 across fifteen projects on the author's private board; that figure is the author's report and is not independently verifiable from public data.

Worked example: PR #326

prwatch: per-owner backoff on rate-limit responses, abort the sweep early · task 1eec49c9 · branch mr/1eec49c9 · 2026-09-09, times in UTC

  1. A human posts the task: spec written from an incident, pinned to sonnet, reviewers opus and gpt-5.5, agent_merge=false.
  2. A worker claims it, pushes mr/1eec49c9, opens PR #326. The board spawns one review task per reviewer model.
  3. opus-reviewer approves.
  4. gpt-5.5-reviewer requests changes, with a reproduction: a reset timestamp equal to the pass time let a second GitHub call through in the same pass.
  5. sonnet-worker fixes it (commit 92d3069), adds a regression test, and acknowledges the item. odonian submit refuses to resubmit while any item is unacknowledged.
  6. Both reviewers approve. The task moves to approved and waits.
  7. A human merges the PR on GitHub. The PR-watch reconciler sees the merge and records the task as done.

What Odonian coordinated: the claim, the branch and PR naming, review routing to two models from two vendors, the rework gate, and converging the board with what happened on GitHub. Where human judgment entered: writing the spec, and deciding to merge. The agents share their human's GitHub identity and mark their comments with <model>-<role>:, which is how you tell them apart on the PR.

How it works

Projects hold documents, documents are decomposed into tasks, and tasks move through a small state machine that the store enforces. Agents only ever pull.

01 · DECOMPOSE

A design becomes bite-size tasks

A human writes the design or feature spec in the repo and breaks it into tasks a senior engineer would hand off: a spec, a pinned model, the models that must review it, and dependencies. Tasks are posted to the project's board and promoted to ready.

02 · DRAIN

The fleet pulls work

Workers claim tasks and open PRs (or commit locally). Reviewer agents claim the reviews and vote. A rejection sends the task back to ready with the feedback; repeated rejection escalates the model tier or blocks the task and notifies you.

03 · DECIDE

A person approves

Approved work waits. You read the diff and approve, reject with a note, or act on the PR itself; a reconciler keeps the board in step with the forge. Configured notifications tell you when something needs a decision.

Atomic claiming

A claim is one conditional UPDATE whose WHERE clause is the claimability predicate: ready (or a lapsed lease), not held, every dependency done. SQLite serializes writers, so exactly one of N concurrent claimers wins and the rest are told why they lost.

Why it matters: run as many workers as your rate limits allow against one board. One successful claimant per live lease, with no scheduler to keep alive. Workers must renew leases during long tasks to prevent another agent reclaiming their work.

Leases instead of a reaper

Every claim carries a lease. The lease check lives inside the claim predicate itself, so a crashed agent's task becomes claimable the moment its lease lapses, with no sweeper and no race. The rework lands on the same deterministic branch.

Why it matters: after the lease expires, another available worker can reclaim the task on its next poll. Pushed commits can be resumed; unpushed work in a lost container may be lost.

Review routing and a circuit breaker

On submit, the board spawns one review task per reviewer model; approval must be unanimous. Past a per-model rejection threshold the task is superseded by a copy pinned to the next tier of the ladder (haiku → sonnet → opus by default, configurable), or blocked and you are notified. Reviewers need not share a vendor with the worker.

Why it matters: cheap models do the first pass, expensive ones only when needed, and a task that cannot converge pages a human instead of looping.

A mechanical rework gate

When a PR is bounced, the worker must list every unaddressed review item, fix it, and acknowledge it on the PR. odonian submit refuses while any item remains. A PR-watch reconciler applies what humans do on the forge (merge, close, request changes) back to the board.

Why it matters: feedback cannot be skipped by a forgetful prompt, and the board converges with GitHub on later checks. PR-watch needs a token for the repository owner; missing credentials and rate limits delay those checks.

Human approval is the default, not a guarantee

Tasks default to agent_merge=false. After the reviewers approve, the work sits in approved until a person records a verdict or merges the PR. Setting agent_merge=true on a task opts that task into a non-LLM merger that squash-merges once reviewers approve. Review and merge subtasks have their own completion paths.

The board is a workflow, not a permission system. What actually stops an agent from reaching main is branch protection on your forge and the scopes on the tokens you hand the fleet. The shared board token gives access to every project and task, and worktrees do not restrict filesystem or forge access. Agents run with permission prompts disabled and belong in disposable containers with credentials scoped to the work they need to do.

Run the demo

One example task, implemented by a real haiku worker and reviewed by a real opus reviewer, ending in approved for you to decide. Everything runs in a throwaway sandbox with a throwaway repository under /tmp/odonian; the fleet does not push to GitHub. The Odonian source checkout is mounted from your host and remains writable from the sandbox. The full walkthrough, with expected output at each step, is docs/demo.md.

You will need

  • Docker Sandboxes (sbx). The fleet runs claude with permission prompts off, so it belongs in an isolated container.
  • Go 1.25.6+, git, jq, curl inside the sandbox. The script builds the binary there.
  • Claude Code CLI, logged in. Workers and reviewers are claude -p sessions. The run uses real model calls on your Claude account (a capped auth probe, then worker and reviewer sessions; competing claims and rework can add sessions). On a subscription that is plan usage, on an API key it is billed. In three measured runs on 2026-09-12 the task took 68 to 112 seconds from ready to approved, after a boot that includes building the binary.
  • Codex is optional for this demo, whose reviewer is opus. With Claude installed and authenticated, skip sbx-agent-setup.sh and boot directly. That optional setup script installs missing Claude and Codex CLIs using npm, registry access, and passwordless sudo; installation failures stop that script.

1 · Sandbox and boot

# on the host
mkdir -p ~/src
git clone https://github.com/boldfield/odonian ~/src/odonian
cd ~/src/odonian
sbx run --name odonian-demo claude .   # log claude in, then exit
sbx exec -it --workdir "$PWD" odonian-demo bash

# inside the sandbox, already at the mounted checkout's absolute path
# optional broader setup; skip if claude is installed and authenticated
# bash harness/sbx-agent-setup.sh
bash harness/sbx.sh --seed-demo

2 · Watch, inspect, decide

# on the host, from the Odonian checkout: open a second sandbox shell
sbx exec -it --workdir "$PWD" odonian-demo bash

# inside that second shell; the boot banner prints these
export ODONIAN_URL=http://localhost:8080 ODONIAN_TOKEN=sbx-local-token ODONIAN_HOME=/tmp/odonian
export ODONIAN_DELIVERY_MODE=local_commit ODONIAN_REPO=/tmp/odonian/repo ODONIAN_WORKTREE_HOME=/tmp/odonian/worktrees
export PATH=/tmp/odonian/bin:$PATH

tail -f /tmp/odonian/logs/workers.log /tmp/odonian/logs/reviewers.log
# Ctrl-C stops this log viewer; leave the boot shell running.
# copy these commands with their full IDs from the boot banner
odonian pending --project <project-id>   # empty until review or approved
odonian diff <task-id>
odonian approve <task-id>   # freezes a branch; or reject --note "…"

The boot builds the binary, checks that claude is authenticated with a live probe, starts the server on :8080 (reusing its SQLite file on later runs), creates the repo and the project, posts the task, and starts two workers and two reviewers. It ends with [sbx] Odonian fleet is UP. and the exact commands for your board. Approval records done and preserves a branch for you to merge; it does not merge into main. Clean up with bash harness/sbx.sh stop, then rm -rf /tmp/odonian after saving any work you want to keep.

Run it on your own repository

Run the fleet in a disposable sandbox or container with authenticated Claude and GitHub CLIs. The repository you give it must have origin/main.

# Go 1.25.6+; build and start the server
git clone https://github.com/boldfield/odonian && cd odonian && make build
ODONIAN_TOKEN=your-secret-token ./bin/odonian server   # REST API on :8080, SQLite at ./odonian.db

# in each fleet terminal, from the built Odonian checkout
export PATH="$PWD/bin:$PATH"
mkdir -p ~/.odonian
test -e ~/.odonian/env || cp harness/env.example ~/.odonian/env
chmod 600 ~/.odonian/env
${EDITOR:-vi} ~/.odonian/env   # set URL, matching token, full project UUID, absolute repo path
# create a project, register a design doc, post and promote tasks through docs/api.md first
cd harness && ./worker.sh worker-1     # ./reviewer.sh reviewer-1 in another terminal

The running guide covers the fleet on a laptop, in a sandbox, and on Kubernetes; the harness README covers slots, worktrees, per-owner forge tokens, and multi-project mode; configuration lists every variable.

Why “Odonian”

From Ursula K. Le Guin's The Dispossessed: the Odonians of Anarres organize work through voluntary association. Postings are claimed by the people who will do them because the work is worth doing; no boss dispatches anyone. Two convictions from that book shape how this system behaves.

Work is claimed, not assigned. There is no assignment API and no scheduler. An agent asks for the next task it can do and takes it, or is told why it cannot. Capacity is whatever you point at the board, a stuck agent affects only its own lease, and the substrate's only promises are that claiming is atomic and dependencies are honored.

Machines labor; a person makes the living judgment. Agents write and review every line, but the merge decision is a state in the machine that defaults to a human, reviewers can be a different vendor from the author, and the board keeps an audit trail of who did what. The AGPL license is the same politics.

The mark is the syndicate: an unclosed ring of agents, and the moon holding the gap, the human.