@nvergez/orchestrator
v0.6.1
Published
Slack-driven orchestrator-dispatcher daemon: one Claude Code session per Slack thread, delegating work to Orca worktree agents.
Maintainers
Readme
orchestrator
A Slack-driven orchestrator-dispatcher daemon: one Claude Code session per Slack thread, delegating work to Orca worktree agents. Mention the bot in a channel, describe what you want, and it routes the request to the right repo, dispatches a worker agent in its own worktree, supervises it (questions and escalations relayed back into the thread), and reports the result. Small-team by design: one workspace, the channels you configure, and a shared allow-list of authorized users — anyone else gets one polite refusal.
How it works
The daemon is a single long-lived Node process. It connects to Slack over Socket Mode (no inbound URL) and gives a Claude Code agent a Slack presence — each thread gets its own persistent session, resumable across restarts. The orchestrator never writes code itself: it interprets requests, delegates to Orca worktree agents on an allow-listed set of repos, watches their structured messages (done / blocked / question), and relays anything that needs a human back into the thread. State (sessions, delegations, pending gates) lives in a local SQLite database, so a restart loses nothing.
The full architecture deep-dive is in docs/spec.md.
Prerequisites
- Linux with systemd user services. On other platforms (or without
systemd) run
orcin the foreground under your own supervisor — seedocs/operations.md. - Node.js ≥ 22.18.
- Orca — the
orcaCLI installed and the runtime running on the same machine. Hard prerequisite: the daemon delegates all work to Orca worktree agents. - A Claude subscription with the Claude Code
CLI. Run
claude setup-tokento mint the long-livedsk-ant-…OAuth token the daemon authenticates with. The daemon is subscription-billed — API-key billing is not supported (CLAUDE_CODE_OAUTH_TOKENis hard-required). The token acts as your account: treat it like a password, and expect daemon usage to share your subscription's rate limits. - A Slack workspace where you can create an app. Walkthrough with a
ready-to-paste manifest:
docs/setup-slack.md.
Install
npm install -g @nvergez/orchestrator # 1
orc init # 2
$EDITOR ~/.config/orchestrator/env ~/.config/orchestrator/routing-hints.json # 3
orc doctor # 4
orc service install # 5
sudo loginctl enable-linger $USER # 6 — once- Installs the daemon and the
orcCLI. - Creates
~/.config/orchestrator/and scaffolds the two config files (never overwrites existing ones). - Fill in your tokens and Slack IDs in
env(seedocs/setup-slack.md) and your delegable repos inrouting-hints.json. - Checks the whole setup read-only — env vars, hints file, state dir, node
version,
orcareachability. A non-zero exit means fix before continuing. - Generates and starts the systemd user unit
orchestrator.service. - One-time, and the only step needing sudo: without linger, systemd stops your user services at logout and does not start them at boot — required for reboot survival.
CLI
| Command | What it does |
|---|---|
| orc | Run the daemon in the foreground (reads config from the environment) |
| orc dashboard | Run the dashboard sidecar in the foreground — the read-only web view of live state |
| orc --version | Print the version and exit |
| orc init | Scaffold ~/.config/orchestrator/{env,routing-hints.json} |
| orc doctor | Read-only diagnosis of the whole setup; non-zero exit on any failure |
| orc update | Update to the latest release — install, unit regeneration and restart, as one step |
| orc service install | Generate, enable and start both systemd user units (re-run after node upgrades) |
| orc service uninstall | Stop, disable and remove both systemd user units |
Updating
orc updateOne command runs the whole ritual: check the registry, install the new version, regenerate the systemd unit, restart the service. The restart is the step that actually swaps the running code — which is why it is one indivisible command and not three you might truncate. Already at the latest version is a no-op; without a systemd unit only the package is updated and it tells you to restart your daemon yourself.
A breaking release (major version jump — pre-1.0, the first one graduates
to 1.0.0) is refused with a pointer to the release notes; re-run as
orc update --yes once you have read them.
Changelog = GitHub Releases only. To get notified: Watch → Custom → Releases on the repo.
Operating
systemctl --user status orchestrator # running?
journalctl --user -u orchestrator -f # follow the logsThe Dashboard — a read-only web page answering "what is the orchestrator
doing right now?" (open sessions, in-flight delegations, pending gates, stall
alerts) — is served by a sidecar unit on 127.0.0.1:8787. It installs with
orc service install and exposes nothing beyond the machine; reach it over
Tailscale or an SSH tunnel your own way.
Runbook — service management, the dashboard, orc doctor triage, non-systemd
operation, config/state reference, uninstall:
docs/operations.md.
Development
git clone https://github.com/nvergez/orchestrator && cd orchestrator
npm ci
npm test && npm run typecheck && npm run lintDev loops
Three composite commands, named by the world they put you in (each is a
concurrently composition — when one process dies, the whole loop dies
loud):
| Command | Runs | Database |
| --- | --- | --- |
| npm run dev:all | daemon + sidecar + Vite | .dev/orchestrator.db |
| npm run dev:dashboard | sidecar + Vite | the real one (XDG, or ORCHESTRATOR_DB_PATH) |
| npm run dev:dashboard:demo | seed, then sidecar + Vite | .dev/demo.db, reseeded fresh every run |
npm run dev:all boots the daemon from .env and fails immediately with
the missing-variable list without one. A dev instance coexists with an
installed service by isolation (ADR 0003): its own Slack app, its own
database, its own port. Create a second Slack app for dev (same manifest
as docs/setup-slack.md, Socket Mode, its own
channel) and put its tokens in .env — the daemon refuses to boot while
the service is active on this machine and .env holds the service's
SLACK_APP_TOKEN or points at its database. The dev defaults isolate the
rest: the sidecar listens on 8788 (the service's on 8787; DASHBOARD_PORT
overrides — set it in the shell, not .env: the loop exports one value to
both the sidecar and the Vite proxy, and a .env entry could only reach
the sidecar), and the dev databases live under the git-ignored .dev/.
npm run dev:dashboard:demo needs no daemon, no Slack app, no Orca: it
writes demo state through the real stores — a live session, in-flight and
failed delegations, a decision gate, an escalation, a stall — so every
dashboard section renders from a bare checkout. Timestamps sit relative
to now; reseed any time with npm run seed:demo. The demo database path
is fixed (the seed ignores ORCHESTRATOR_DB_PATH, so ambient daemon env
can never aim the reseed at a real database), and it is a different file
from dev:all's — a demo run never clobbers dev-daemon state.
Dev over a tailnet: WEB_DEV_HOST="$(tailscale ip -4)" npm run dev:all
binds Vite past loopback (*.ts.net Host headers are pre-allowed — bind
the tailnet IP, never 0.0.0.0, on a box with a public interface). By
default nothing binds beyond loopback, and the sidecar never does: the
browser reaches Vite, and Vite proxies /api locally (ADR 0002).
The dashboard frontend lives in the web/ npm workspace (React + Vite),
built into dist/web by npm run build:web and shipped pre-built in the
npm tarball — the published package carries zero frontend dependencies.
PRs are squash-merged and the PR title must follow
Conventional Commits — merged titles
drive version bumps (fix: → patch, feat: → minor, breaking → major;
anything else counts for nothing). Merging alone does not release: a release
happens when a merged PR carries the release label (or via a manual
workflow dispatch), and it ships everything merged since the last tag.
