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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Linux, always on. Node 22 or newer, tmux, ttyd, Tailscale. About 0.5 GB per running agent.
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.
gh auth login
gh auth setup-git
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.
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.
git clone https://github.com/alp82/curia.git
cd curia/daemon
npm install
daemon/.envDISCORD_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.
config/curia.yamlwatch:
- 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.
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: []
config/routing.yamlMaps 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.
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.
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.
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.
bad config config/curia.yaml: ... | The key it names. Config is checked before anything starts. |
skills.install names "x", but ... does not exist | Step 9. |
preview range ... contains attach.serve_port | Move one of the ports. |
attach index ... does not exist | npm run build-attach-index --prefix daemon |
bot is in no guild | The invite did not finish, or CURIA_GUILD_ID is wrong. |
| The bridge refuses to start | DISCORD_ALLOWED_USERS is empty. |
| Replies in a thread do nothing | The message content intent is off. |
spawned ttyd ... but no listener came up | TTYD_BIN points at nothing. |
| No attach or preview links ever appear | Tailscale HTTPS certificates are off, or --operator was never set. |
The config reference and the design records live in the README and docs/adr.