curia — self-hosted agent dispatcher

Many repos, one queue,
driven from a phone.

You already run coding agents every day. Curia reads what is takeable across every repo you watch, sends an agent at it, and brings every question, every preview and every approval to the device in your hand.

Your box · your subscription · your keys
GitHub issues in · merged pull requests out

What it gives you

Your tracker is the queue

GitHub issues hold every ticket, every claim and every dependency edge. Curia reads what is takeable across every watched repo, in dependency order, and picks the model for each ticket from a label. There is no second app, and no board to keep in sync.

3 repos watched today · label decides the model · blocked and claimed tickets drop out

Any device is a full seat

Nine verbs over Discord, one thread per ticket. Questions arrive with buttons. Results arrive as HTTPS links on your tailnet. You can attach to a live agent's terminal from a phone and from a desktop at the same time, and neither one is the lesser seat.

tickets · next · start 42 · status · attach 42 · resume 42 · cancel 42 · map 109 · review 42
~0.5 GB per agent measured · four at once on one small box

An agent cannot finish by talking

Every ticket ends the same way: commit, pull request, live preview, your approval, merge. A rejection comes back in your own words and turns into new commits on the same pull request. A stop hook refuses an agent that tries to quit early.

resolved means merged · nothing lands without you

One ticket, done

1 The ticket goes out from a Discord thread.
2 The agent asks. You answer in the thread.
3 The preview link, open on the phone.
4 The review gate. Approved.

Four phone screenshots of one real ticket, on a repo that is not this one. Tap one to read it.

This page was planned, decided and written by curia agents. Each one ended at a review gate, and none of them merged itself. Read the merged pull requests.

From GitHub, since the first commit on 21 Jul 2026 — 33 merged agent pull requests · 175 issues closed · 3 repos watched.
From the daemon journal, since 1 Aug 2026 — 62 agents spawned · 88 questions asked · 31 review gates answered.
Counted 5 Aug 2026.

What you are getting into

  • Curia is fifteen days old. First commit 21 July 2026.
  • One person runs it. You would be the second.
  • Setup is manual and takes an evening. Node, tmux, Tailscale, a Discord bot, a harness, a GitHub repo. It gets simpler. There is no package yet.
  • Tailscale and Discord are required. There is no web UI.
  • Agents use your own harness login. An agent can do what you can do with it.

Set it up

What you need: a Linux box that stays on · Node 22 or newer · tmux · ttyd · a Tailscale account · a Discord server you control · Claude Code or Codex CLI, logged in · an evening.

1Tailscale on the box and on your phone

Turn on HTTPS certificates for your tailnet, then let the daemon publish links without root.

tailscale set --operator=$USER

Every attach link and every preview link is a Tailscale link. Skip this and none of them work.

2Log in, as the user that runs the daemon

gh auth login && gh auth setup-git
claude

Agents use this harness login and this GitHub account. The account needs write access to every repo you watch.

3A Discord bot

Create an application, add a bot, copy the token. Turn on message content intent — without it your replies arrive empty and no question is ever answered. Invite the bot to your server with the bot and applications.commands scopes. It creates the #curia channel itself on first boot.

4The code, and two files

git clone https://github.com/alp82/curia.git
cd curia/daemon && npm install

daemon/.env takes the bot token, your Discord user id, and TTYD_BIN. config/curia.yaml takes the repos you watch and a workspace_root curia can own.

Both files ship pointing at another person's home directory. Change them, or attach is dead and worktrees land in the wrong place.

5Run it

npm start

A good boot logs [bridge] ready. Send a message in #curia: a thread opens and answers you. Then tickets, start 42, status, attach 42.

The complete setup, thirteen steps

1The box

Linux, always on. Node 22 or newer, tmux, ttyd, Tailscale. About 0.5 GB per running agent.

2Tailscale

Install it on the box and on your phone. Turn on HTTPS certificates for your tailnet in the admin console. Then run tailscale set --operator=$USER.

3GitHub

gh auth login
gh auth setup-git

4Your harness

Log in as the user that runs the daemon. Log out and every agent fails. Codex CLI is optional: install it only for the gpt entry in config/routing.yaml.

5The Discord bot

Create an application. Add a bot and copy the token — Discord shows it once. Turn on message content intent. In OAuth2 → URL Generator take the bot and applications.commands scopes, and these permissions: Manage Channels, Manage Webhooks, Send Messages, Create Public Threads, Send Messages in Threads, Read Message History, Embed Links, Attach Files. Open the URL and add the bot. Then turn on Developer Mode and copy your own user id.

6The code

git clone https://github.com/alp82/curia.git
cd curia/daemon
npm install

7daemon/.env

DISCORD_BOT_TOKEN=<the bot token>
DISCORD_ALLOWED_USERS=<your Discord user id>
TTYD_BIN=/home/<you>/.local/bin/ttyd

DISCORD_ALLOWED_USERS is the whole access check. Everyone on it can send agents at your repos. TTYD_BIN defaults to /home/alp/.local/bin/ttyd: leave it and attach is dead. Optional: CURIA_GUILD_ID, CURIA_CHANNEL, PORT. Run one daemon per bot token.

8config/curia.yaml

watch:
  - repo: you/your-repo

dispatch:
  workspace_root: /home/<you>/curia-work

workspace_root is where curia keeps its own clones and worktrees. Do not point it at a checkout you work in. auto_dispatch is false, so nothing starts without you. The ports work as they ship, and the daemon names any clash at boot.

9The skills

The nine skills in skills.install are not in this repo. They come from the public mattpocock/skills collection, installed into ~/.claude/skills. The daemon refuses to boot if one is missing, and names the path it looked for. Install them, cut the list down, or turn them off with an empty list — not a missing key.

skills:
  install: []

10config/routing.yaml

Maps a ticket label to a model, and a model to a CLI. Without Codex, delete the gpt model, the codex harness, and every gpt under defaults and fallbacks.

11The repos you watch

Flat: label an issue ready-for-agent. Curia takes open, unassigned, unblocked issues. Map: the repo needs a wayfinder:map issue with child issues, and a docs/agents/issue-tracker.md file. Dispatch refuses without that file. Blocking uses GitHub's own issue dependencies.

12Run it

cd daemon
npm start

A good boot logs [bridge] ready: guild=<your guild> channel=#curia. Send a normal message in #curia: a thread opens and answers you. The commands work as Discord slash commands or as plain English in a thread — tickets, start 42, status, attach 42, resume 42, cancel 42.

13Keep it running

deploy/curia.service is a systemd unit. Replace the user and the paths, and keep the PATH= line: systemd's default path does not find gh, tmux, tailscale or ttyd under ~/.local/bin. Restarting is safe. daemon/data/events.jsonl is the record, and everything else is rebuilt from GitHub, tmux and Tailscale on boot. Open questions keep their Discord buttons across a restart.

If it does not start

bad config config/curia.yaml: ...The key it names. Config is checked before anything starts.
skills.install names "x", but ... does not existStep 9.
preview range ... contains attach.serve_portMove one of the ports.
attach index ... does not existnpm run build-attach-index --prefix daemon
bot is in no guildThe invite did not finish, or CURIA_GUILD_ID is wrong.
The bridge refuses to startDISCORD_ALLOWED_USERS is empty.
Replies in a thread do nothingThe message content intent is off.
spawned ttyd ... but no listener came upTTYD_BIN points at nothing.
No attach or preview links ever appearTailscale HTTPS certificates are off, or --operator was never set.

The config reference and the design records live in the README and docs/adr.