You've got one program (the moderator), a bunch of helpers that wake up on timers (daemons), a chat surface (the concierge), and Claude subprocesses that do the actual work (workers). This page explains who does what, when, and why, with diagrams.
A daemon (pronounced "dee-mon") is a program that runs in the background, on a timer, doing one specific repetitive job. The name comes from Greek mythology, a daimon was a helpful spirit doing background work for you. Nothing demonic about it. Unix folks just liked the metaphor.
The key thing: a daemon doesn't sit waiting for you to ask it to do something. It just wakes up on its own every N seconds, checks if there's work to do, does it, goes back to sleep. You can have many daemons running side-by-side, each handling one job.
In Nyx, the auto-merge daemon wakes up every 60 seconds, looks at every nyx-feat/* branch, decides if any are ready to merge, merges them, sleeps. The watchdog daemon wakes up every 60 seconds, checks if any workers have been silent too long, kills the stuck ones, sleeps. They never talk to each other; they each do their own job.
The person giving Nyx things to do. Talks to it through the TUI, the mobile page, or (for now) through Claude Code as a stand-in concierge.
When you type a message in the TUI, the moderator spawns a Claude subprocess with the Nyx persona prepended (from ~/.nyx/concierge/CLAUDE.md), your recent memory loaded, and tools attached to dispatch more workers. That subprocess IS the concierge. There's no separate concierge process running all the time; it's spun up fresh for each turn.
Right now, while the TUI is still being polished, Claude Code (the CLI you're chatting with right now) is filling the concierge role. Once the new TUI lands, you'll talk to the concierge there instead.
One long-running Node process on port 8787. It owns the database, the queue, the worker pool, all the daemon loops, the SSE/WS hub for live UI updates, and the HTTP routes the TUI, mobile page, dashboard, and concierge all hit. If the moderator dies, everything dies. Keep-alive respawns it within ~5 seconds.
You can now run up to 2 moderators at once (one per project) once the multi-moderator feature lands, each on its own port and database. Default is just nyx on 8787.
A claude --print subprocess spawned by the moderator. Gets a prompt, a working directory, an allowed-tool list, and a model (sonnet by default, opus when the work is high-stakes). Runs until it finishes the job or hits the turn limit, then exits. Every tool call and every assistant message gets streamed back to the moderator and stored in sqlite.
Hard cap: 4 workers per moderator at once. The 5th dispatch refuses with a 429-style error. This is the budget knob. Hard runtime cap: MAX_WORKER_RUNTIME_MS (default 60min). Exceeded workers are killed and marked failed with reason='max_runtime_exceeded'.
An opus worker invoked when the concierge sees a multi-part request ("build me a 6-page site", "add 5 NPCs"). The planner reads the goal and the repo, then outputs a JSON plan with N subtasks, each with its own prompt, its own owned file list, and explicit interfaces between them. The plan is validated (no two subtasks can claim the same file) before the fanout step. If the planner returns 0 subtasks, the orchestrator exits immediately instead of waiting. Total fanout wall-clock is capped at NYX_FANOUT_MAX_MS (default 45min).
Takes a Plan from the Planner and spawns N workers in parallel, each in its own git worktree on its own branch, each working only on its owned files. Because file ownership is declared up front, the workers can't trample each other.
After all fanout workers finish, the synthesizer cherry-picks each sub-branch onto a single integration branch in plan order. Pure git, zero tokens, when ownership was respected. Only when cherry-pick hits a conflict does the synthesizer call an opus pass to resolve, and only for the conflicted hunks, not the whole diff.
Before any diverged branch gets merged into main, the Critic reads the diff and votes approve, revise, or abstain. Approve = ship. Revise = the auto-revise loop spawns a fixer worker with the critique. Abstain = leave the branch for you to look at manually.
Every 60 seconds: enumerates all nyx-feat/* branches, tries to fast-forward each into main. If a branch diverged, runs the test suite + Critic. If both pass: no-ff merge. After any successful merge that touched apps/moderator/src/**, schedules a 30-second debounced restart so the moderator picks up its own new code (keep-alive respawns).
Every 60 seconds: looks at the queue table for pending items, picks the highest-priority one, spawns a worker, marks the row in_progress. Won't pick more than 4 at once per moderator. This is what lets you "drop tasks and walk away." Has a 1-hour cooldown on recently-completed items so it never re-runs a task that just finished.
Every 60 seconds: scans for worker sessions that haven't emitted an event in 5+ minutes. Kills them. If the stuck session was the moderator working on itself, triggers a self-iter to recover. Now tracks tool_use events separately: workers with no tool_use in 15min (NYX_WATCHDOG_TOOLUSE_IDLE_MS) are killed as spinning-but-not-progressing. Any-event idle is kept as a longer secondary fallback.
Every 60 seconds: checks the schedules table for cron entries due to fire. Currently runs the @weekly digest. Anything you want on a recurring schedule (nightly memory garbage collection, etc.) goes here.
Runs at the start of every auto-merge tick. Deletes branches that are already ancestors of main (merged, safe to drop). Renames branches that exhausted their auto-revise budget to nyx-stale/* so the daemon stops re-evaluating them.
Event-driven. Fires when the Critic blocks a branch with revise. Spawns a sonnet fixer worker with the critique + original diff, working in a fresh worktree off the same branch. The worker commits a v2; the daemon picks it up on the next sweep. Budget: 3 attempts per branch, then quarantined.
When the watchdog finds a stuck session AND the system is idle, self-iter picks one item off Nyx's own backlog and dispatches an opus worker on a nyx-self/* branch to ship it. This is how Nyx grew from ~12k lines to ~16k lines in a day. Triggers also include the model probe finding a new Claude version (it self-upgrades) and the auto-merge gate landing successful self-fixes.
Every 5 minutes: flips in_progress queue rows to done when their dispatched worker's branch landed in main. This is what catches the case where a worker finished, the daemon merged its branch, but the queue row was never updated.
Every 24 hours: pings the Claude API to see if a new model version shipped. If yes, dispatches a self-iter worker to upgrade Nyx's routing config. This is how it knows to use opus-4-8 instead of an older opus when one comes out.
On boot + every 12 hours: checks for a new Claude Code release. When one's available, prompts you (Tier 2 push gate) before installing. Keeps the underlying CLI fresh without you having to remember to run brew upgrade.
Event-driven. After any successful auto-merge whose diff touched apps/moderator/src/**, the daemon schedules a 30-second debounced process.exit(0). Keep-alive respawns the moderator within ~5s with the new code loaded. Multiple back-to-back merges coalesce into a single restart. The operator never has to type "kill" again. Off-switch: NYX_AUTO_RESTART_ON_MERGE=0. On DELETE /agents/:id, the subprocess is SIGTERMed before the DB row is archived, so no orphan processes accumulate.
Every 20 minutes, a pure-SQL sweep generates a one-sentence health summary ("3 workers active, 2 branches awaiting critic, last merge 14min ago, 1 stuck over 60min") and broadcasts it on the SSE 'status' topic. Zero LLM tokens in the common path. A small haiku call fires only when something looks wrong (stuck workers, critic_blocked over 5, queue stalled with idle workers). GET /status/pulse anytime for an instant snapshot.
Runs daily via the scheduler. Rule-based pattern detection across the audit log answers "what hurt this week": worker timeouts, branches that burned all 3 revise attempts, queue items stalled over 6h, merge velocity drops, repeat Critic block reasons. One haiku call synthesizes these into a 3-sentence narrative ("biggest hurt + one concrete improvement to try"). GET /reflection/digest?window=7d anytime.
Runs after Critic approves but before auto-merge fires. Boots the changed app in the right mode: headless Playwright for web routes, spawn+exit-code for CLI, expect-style stdin/stdout for the TUI. Exercises only the touched surface area (routes/components that the diff actually changed, not the whole app). Rule-based assertions: HTTP 200, no console errors, no broken images, clean exit codes. haiku-low only for visual screenshot judgment, capped at 5 calls per run. 8-minute hard wall-clock cap. Block/approve verdict is the same shape as Critic's so the daemon treats both gates identically.
Each project (Nyx, FBM, Atlas, Artist Exchange) gets its own moderator process on its own port with its own SQLite DB. Hard caps: at most 2 moderators concurrent, at most 4 workers per moderator. The 5th dispatch returns 429. New CLI: nyx project create/start/stop/list/switch. Crash isolation between projects; separate rate-limit budgets.
Tonight a real example of the safety mechanism caught a bug in its own upgrade. The Critic was abstaining on big branches because it could only see the first 12,000 characters of a diff. I shipped a one-line config change to bump that to 80,000, with a tiny refactor extracting the constant. The new branch went to the auto-merge daemon, which sent the diff to the Critic. The Critic (still running the OLD 12k version) read my diff and caught a NaN footgun I'd written: parseInt(env_var, 10) returns NaN if the env var is empty, and diff.slice(0, NaN) silently returns an empty string. The Critic's verdict: revise, with a specific line cite. Auto-revise then dispatched a sonnet fixer worker with the critique. The fixer is now patching it autonomously.
That only works because of a specific structural rule: each self-upgrade has to be small enough that the previous version of the gate can still see it. My one-line change was under 12k chars, so the old Critic could review it fully. If I'd tried to ship a giant refactor of the Critic in one branch, the old Critic would have abstained on its own truncated view, and the upgrade would be stuck.
Same rule applies to the moderator itself. The auto-restart-on-merge feature shipped on a single-file diff. Once it landed, the running moderator restarted, and from that moment forward every future src-touching merge restarts automatically. The feature uses the feature it just installed. This is how Nyx grew from 12k to 24k lines over two days without a human ever running kill.
Each colored row is one loop, ticking on its own schedule. Time runs left to right. The shorter intervals (60s) produce a row of dots; the longer ones (12h, 24h) show as single hits.
The chat path stays in the conversation pane. The single path produces a commit on a branch. The plan path spawns parallel workers and a synthesizer. All three roads end at the auto-merge daemon, which lands the work in main while you sleep.
The TUI is live. Run /Users/ethanashihundu/projects/agent-orchestrator/apps/tui-go/nyx-tui in any terminal. The concierge lives in there. Claude Code (the thing you're reading this with) is now your architecture/strategy partner, not the primary interface. Use the TUI for dispatch; use Claude Code for conversation about what to build next.