Skip to content

prathamdby/threadcord

Repository files navigation

Threadcord

Self-hosted Discord control plane for Flue coding-agent sessions

threadcord · Node 22+ · Postgres · Flue · discord.js

/task create opens a public thread on your reply, clones the requested GitHub repo into /workspaces, and runs a Flue agent turn. Postgres holds task state and durable turn rows; pg-boss delivers turns with per-task FIFO ordering, retries, and concurrency caps. After a restart, interrupted running turns are requeued automatically.

Threadcord is a Discord bot plus a small Hono server. Use /task create to pick a setup profile, model, and instruction in one modal. The bot opens a thread on the command reply, clones the repo, and dispatches work to a Flue coding agent. Thread messages handle follow-ups; thread commands handle status, cancel, abort, and done.

Configuration lives in .env.example. Zod validation is in src/config.ts.

Get started


Table of contents

Getting started

1. Discord bot

  1. Create a bot in the Discord developer portal.
  2. Turn on the Message Content intent.
  3. Grant View Channel, Send Messages, Create Public Threads, Send Messages in Threads, Read Message History. Optionally grant Manage Messages so the bot can pin task header messages — pinning is best-effort and task operation works without it.
  4. Copy the bot token into .env as DISCORD_BOT_TOKEN. Invite the bot where you will use /task and /setup.

2. Docker Compose (recommended)

cp .env.example .env
# Discord, GitHub, provider keys
docker compose build
docker compose up

Compose sets THREADCORD_HTTP_BEARER to threadcord-dev-bearer when .env leaves it blank. Change it before any network-exposed deploy.

  • GET /health returns 200 when Postgres is up and the Discord client is ready.
  • GET /health/live checks Postgres only.
  • Default port is 3583.
  • Set THREADCORD_HTTP_BEARER before exposing the service. Required when NODE_ENV=production.

Health check:

curl http://localhost:3583/health

Liveness (Postgres only):

curl http://localhost:3583/health/live

3. Local development (optional)

cp .env.example .env
npm install
npm run dev
npm run check
npm run test
npm run build

npm run dev runs flue dev --target node. Flue generates the Node server (dist/server.mjs after build) and dispatch wiring. npm start runs the built server only and does not load .env — export variables or use Docker Compose.

Configure model providers

Models are allowed when their provider is configured in .env. Discord tasks use model: <provider>/<model-id>.

Built-in providers

Provider Env vars Discord example
anthropic ANTHROPIC_API_KEY, ANTHROPIC_MODELS model: anthropic/claude-sonnet-4-5
openai OPENAI_API_KEY, OPENAI_MODELS model: openai/gpt-5-codex

Flue resolves these from its catalog. Set the API key and a comma-separated model list. No extra registration code is needed.

Custom providers

  1. Add the provider ID to PROVIDERS (comma-separated for multiple).
  2. Set PROVIDER_<ID>_BASE_URL, PROVIDER_<ID>_API, and PROVIDER_<ID>_MODELS. Optional PROVIDER_<ID>_API_KEY and PROVIDER_<ID>_HEADERS.
  3. Normalise the ID for env var names: my-gateway becomes PROVIDER_MY_GATEWAY_*.
  4. Use model: <id>/<model-id> in Discord, where <model-id> comes from _MODELS.

Example (local Ollama via OpenAI-compatible endpoint):

PROVIDERS=ollama
PROVIDER_OLLAMA_BASE_URL=http://localhost:11434/v1
PROVIDER_OLLAMA_API=openai-completions
PROVIDER_OLLAMA_MODELS=llama3.1:8b

Common api values: openai-completions, openai-responses, anthropic-messages. See the Flue Provider API for the full list.

To route a catalog provider through a proxy, list its ID in PROVIDERS and set PROVIDER_<ID>_BASE_URL (for example PROVIDERS=anthropic with PROVIDER_ANTHROPIC_BASE_URL=...). Flue layers your transport on the catalog.

Use PROVIDER_<ID>_HEADERS for providers that require custom request headers. The value must be a JSON object with string values:

PROVIDERS=agent-router
PROVIDER_AGENT_ROUTER_BASE_URL=https://router.example.com/v1
PROVIDER_AGENT_ROUTER_API=openai-completions
PROVIDER_AGENT_ROUTER_MODELS=gpt-5-codex
PROVIDER_AGENT_ROUTER_HEADERS={"User-Agent":"Threadcord"}

Inside Docker Compose, localhost in a provider URL points at the container, not your host. Use host.docker.internal, a Compose service name, or host networking.

Allowed models are derived at startup from these provider blocks. The create modal requires a model; it is pre-filled with the default (first configured model).

Message format

When DISCORD_CHANNEL_ID is set, plain messages in that control channel can create tasks in addition to /task create. Put the instruction first, then keyed metadata lines at the end:

Fix the login bug

repo: owner/repo
branch: main
model: anthropic/claude-sonnet-4-5
push: threadcord/feat/login-fix

repo and branch are required. model and push are optional. Attachments can supply content when the instruction prose is empty. Omit DISCORD_CHANNEL_ID to allow only /task create.

Creating tasks

Run /task create in any channel where the bot can post and create threads.

  1. A modal opens with a setup profile dropdown (owner/repo @ branch, up to 25 ready profiles), a model field (defaults to the first allowed model at startup), and a task instruction field.
  2. After you submit, the bot replies with a link to a new public thread. Progress and agent output stream there.

If no setup profile is ready, run /setup create first.

Coding agents normally create branches named threadcord/<type>/<meaningful-name> (for example threadcord/feat/add-auth). Push overrides are not exposed in the slash UI; use follow-up instructions in the thread if you need a specific push target. Only the task base branch and explicit threadcord/* branches are allowed as push targets.

Thread commands (in a Threadcord-created thread):

  • status replies with a jump link to the pinned task header (live status lives there).
  • abort or /abort asks for confirmation, then stops the in-flight agent turn and cancels the task.
  • cancel or /cancel asks for confirmation, then cancels the task without failing the current turn (no further dispatches).
  • done asks for confirmation, then marks a waiting or queued task complete.

Setup profiles

Threadcord stores durable setup profiles in its own Postgres database. A profile belongs to one normalized GitHub repository and one base branch. It contains an environment JSON recipe and Markdown memory for future coding agents.

Normal coding tasks require a ready setup profile. If a task targets a repository and branch without one, Threadcord rejects the task and tells the user to run setup first. Task workspaces can still expire. Setup profile data stays in Postgres.

Target repositories do not need Threadcord files. Setup does not require .cursor, .threadcord, THREADCORD_SETUP.md, AGENTS.md, or any committed compatibility file.

Setup environment JSON:

{
  "install": "npm ci",
  "start": "",
  "checks": {
    "build": "npm run build",
    "test": "npm test",
    "lint": "npm run lint",
    "typecheck": "npm run check"
  },
  "requiredEnv": ["DATABASE_URL"],
  "requiredServices": ["postgres"]
}

install and checks are discovered and verified by the setup agent during first-time setup. Threadcord runs install with bash -c on the initial task turn, so setup profiles can use project-specific bootstrap commands and shell pipelines. Setup install uses the same non-login shell behavior as agent commands, so workspace-local npm globals remain on PATH.

Promotion happens when the setup agent calls save_threadcord_setup_profile. That tool re-runs install, every stored checks command, and (when start is non-empty) a short smoke probe of start in the setup workspace. checks should be commands that passed in that workspace. If a useful command needs missing secrets or services, record the names in requiredEnv, requiredServices, and memory instead of saving a failing check unless you can make it pass during setup. start is optional; leave it empty if there is no long-running dev server to probe. Use /setup update (or /setup edit) when you want to override install or checks before re-running setup.

Threadcord scopes each setup and task workspace with its own HOME, npm global prefix, and cache directory. Commands such as npm install -g <tool> install into that workspace and put the workspace-local bin directory on PATH. Deleting the workspace deletes those globals.

Setup commands (Discord slash command /setup with subcommands; status, view, edit, export, and delete use a profile picker instead of repo/branch args; import still takes repo, branch, and attachments):

Subcommand Purpose
create First-time setup when no profile exists, or when the profile is failed. Opens a modal (repo, branch, skills — no model field), then spawns a public thread on the slash command with a live agent log (same style as coding tasks). The setup agent discovers and verifies install and checks.
update Re-run setup when the profile is ready or failed (not while running or updating). Opens a modal (repo, branch, skills, install, checks — no model field) so you can edit the command recipe before re-running, then spawns a setup thread with live log.
status Pick a profile, then show profile status, revision, and last run state (ephemeral). Re-run anytime for a fresh snapshot; while setup is running, open the setup thread from your create/update command for the live agent log.
view Pick a profile, then view the active profile environment and memory (ephemeral).
edit Pick a profile, then open a private draft editor with buttons and modals.
export Pick a profile, then export environment JSON and memory Markdown as ephemeral attachments.
delete Pick a profile, confirm in a dialog, then remove the profile and its runs/drafts (not allowed while running or updating).
import Import environment and/or memory attachments into a draft (repo and branch required).

Repository names are normalized to lowercase owner/repo. Coding tasks require a profile in ready status.

Draft edits are isolated from the active profile. Applying a draft increments the profile revision only if the active profile still matches the draft base revision. If someone changed the profile first, Threadcord reports a conflict and leaves the active profile unchanged.

Setup profiles store required environment variable names only. They must not contain secret values. Threadcord validates setup JSON and memory before saving, importing, or applying a draft. Draft import and apply perform structural validation only. They do not re-run commands because the setup workspace is gone.

During coding turns, the agent can call append_threadcord_setup_memory to append Markdown to the active profile memory (gotchas, stable fixes, operator preferences). Each append increments the profile revision; new tasks use the latest revision on admission. Follow-up turns in an existing task reload memory on the next dispatch. Appends do not change install or checks.

MCP tool servers

Add external MCP (Model Context Protocol) tool servers at runtime via Discord. No .env changes or restarts needed.

Command What it does
/mcp add Opens a modal to configure a server (id, URL, token, transport, headers). Validates the connection before saving.
/mcp remove Pick a server from a dropdown, confirm removal. Closes the live connection and deletes from DB.
/mcp list Lists configured servers (id, URL, transport), paginated when there are many. Tokens are never shown.

MCP servers are global — every task gets tools from all connected servers. Servers persist in Postgres and reconnect on restart.

Why use Threadcord?

Discord threads per task

Each control-channel message gets its own public thread. Status updates and follow-ups stay in that thread.

Concurrency and follow-ups

MAX_CONCURRENT_TASKS caps parallel agent turns across the process (pg-boss localConcurrency). Extra tasks and follow-ups queue as durable task_turns rows delivered by pg-boss with per-task FIFO ordering (key_strict_fifo). Follow-up messages in a thread queue behind the current turn for that task. On each task’s initial turn only, Threadcord runs the profile’s install command in the task workspace before dispatching the coding agent (follow-up turns reuse the checkout without re-running install).

You run the stack

Postgres, workspace volumes, and API keys stay on your machine or VPS.

Features

Capability Where What happens
New task Control channel Thread created, repo cloned, first turn queued (requires ready setup profile)
Follow-up Task thread Turn row + pg-boss job queued; runs after prior turn for that task finishes
status Task thread Replies with a jump link to the pinned task header
abort Task thread Confirms, then stops in-flight agent work and cancels the task (/abort or abort)
cancel Task thread Confirms, then cancels task; current turn may finish; no further dispatches
done Task thread Confirms, then marks task completed from waiting or queued
Open PR Agent tool create_github_pull_request after push
Learn Agent tool append_threadcord_setup_memory after verified repo lessons
Setup /setup slash Durable per-repo profiles; see Setup profiles
MCP tools /mcp slash Add, remove, and list global MCP tool servers at runtime

How it works

flowchart TD
  discord["Discord messages"] --> gateway["gateway.ts"]
  gateway --> orchestrator["TaskOrchestrator"]
  orchestrator --> intake["task_turns row + pg-boss send"]
  intake --> worker["pg-boss worker"]
  worker --> executor["turn-executor.ts"]
  executor --> bootstrap["git clone and checkout"]
  executor --> flue["Flue coding agent"]
  flue --> githubApi["GitHub API"]
  flue --> observe["observe-bridge.ts"]
  observe --> thread["Discord thread progress"]
Loading
  1. gateway.ts routes channel messages to task creation and thread messages to follow-ups.
  2. orchestrator.ts admits tasks and follow-ups in a Postgres transaction: insert a task_turns row and boss.send atomically.
  3. pg-boss delivers task-turn jobs to turn-executor.ts, which claims the turn, bootstraps the workspace, dispatches the Flue agent, and awaits an in-process completion bridge.
  4. store.ts and turn-store.ts persist task and turn state; boss.ts owns queue delivery, retries, and per-task FIFO.
  5. agents/coding.ts runs the Flue agent with git/bash tools and the GitHub PR tool.
  6. observe-bridge.ts streams agent progress into the thread and resolves turn outcomes on agent end/failure. TaskOrchestrator refreshes the pinned Components-v2 task header for live state.

Data privacy

Self-hosted

Task rows and cloned repos live in your Postgres and workspace volume. No third-party control plane.

LLM providers

Agent turns send your instruction and repo context to whichever model you configured. Check that provider's data policy.

Discord

Instructions and status lines post to your server. redact.ts strips token-shaped strings before send.

GitHub

Clone, push, and PR creation use your GITHUB_TOKEN. Repo access is bounded by that token's scope. Limit who can run /task create and post in task threads accordingly.

Troubleshooting

Task header is not pinned

The bot needs the Manage Messages permission to pin the task header message. Without it, pinning fails silently (logged server-side) and the task continues normally. The header is still posted and editable — only the pin is missing.

Restart requeues interrupted turns

After a restart, turns stuck in running are reset to queued, their pg-boss jobs are re-sent, and the task moves back to queued until the turn runs again. Orphaned queued turns without a pg-boss job are enqueued on boot. If a Discord thread is no longer accessible (archived, deleted, or permissions changed), the restart notification for that task is logged and skipped. Other tasks and queue slots are unaffected.

Agent turns end with generic failure

When the agent hits repeated tool validation errors (wrong argument schemas), the turn is aborted early to stop error spirals. Discord receives a generic failure message; detailed validation text stays in server logs. Adjust AGENT_MAX_VALIDATION_FAILURES in .env to tune the threshold.

License

MIT. See LICENSE.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages