🎼 Conductor

Ten terminals running Claude Code, no idea which one is stuck or waiting on you. Conductor reads all of them β€” and lets you reply β€” from one window.

Conductor β€” run one command, see every Claude Code window grouped by status

The Conductor cockpit β€” your live Claude Code windows grouped by status

You've got 10 terminals running Claude Code. You lose track of what each one is doing. Open a fresh window and ask Conductor to sort them out:

🎼 Conductor β€” 3 windows Β· last 1h
   cockpit: conductor up   Β·   control: conductor run <label> / conductor say <label> yes

WORKING NOW ───────────────────────────────────────────────────────────
β”Œβ”€ 1d177c35 ──────────────────────────────────────────────────────────┐
β”‚ ● Build SOAG Agent trading grid with character art  conv-fix Β· 4s ago β”‚
β”‚ SOAG Β· Grid                                                          β”‚
β”‚ β€Ί Read: src/characters.js                                            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

OPEN ──────────────────────────────────────────────────────────────────
β”Œβ”€ ede6faa0 ──────────────────────────────────────────────────────────┐
β”‚ ● Tech week NYC schedule planning                      main Β· 17h ago β”‚
β”‚ Good Rooms                                                           β”‚
β”‚ β€Ί Done. Here's what I built…                                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Each window is a box that leads with what it's actually about (the session's own summary), grouped into Working now / Open / Recently active / Idle. Open the visual version with conductor up.

No new infrastructure. Conductor reads the trail each worker already writes (for Claude Code, the transcript under ~/.claude/projects/). Read-only observation, opt-in control: watching is always-on and free; control (replies, launch, flatten) is opt-in and only where a real command channel exists. Zero dependencies. The server binds to 127.0.0.1 only, and state-changing requests require a local origin + an X-Conductor header (CSRF / DNS-rebinding guard); destructive control (flatten / broadcast) additionally requires a confirm token. The header stops hostile webpages, not local processes β€” anything already running code on your machine can call the API, same as it could call tmux directly. Run the cockpit for yourself, not as a service.

Under the hood, Conductor is a source-agnostic supervisory core with a pluggable adapter interface: the engine owns grouping, status ranking, sectioning, and the three surfaces (CLI table, web cockpit, MCP server); an adapter owns where the trails live and how to read them. Claude Code is one adapter; a trading-bot fleet, MEV searchers and Solana validators are others β€” see The abstraction.

Conductor watches windows you already opened. Its sibling Conductor V2 flips the order: design the fleet first (topology + preset + mission), press FIRE, and steer the swarm from V2's bundled board. V2 is self-contained on its own tmux/registry namespace β€” fleets you fire live there; V1 stays the watcher for windows you open by hand.

How it works

Every Claude Code window logs a live .jsonl transcript under ~/.claude/projects/<dir>/<session-id>.jsonl. Conductor:

  1. Lists those transcripts and filters by modification time (folders hold thousands of historical sessions β€” only recently-touched ones are candidates).
  2. Excludes subagent threads (/subagents/, sidechains) so each window counts once.
  3. Groups by session id and streams each file (never loads 8MB into memory), pulling each session's ai-title, latest prompt, recent tool calls, and last action.
  4. Reports one row per window.

πŸ—Ί The whole thing on one page

Conductor architecture β€” your windows write transcripts and run as processes; the zero-dep engine reads them; three surfaces (CLI, web cockpit, MCP); a tmux control plane for windows you launch or adopt

Install

git clone <repo> ~/conductor && cd ~/conductor && npm link

npm link puts a global conductor command on your PATH. No build, no dependencies.

Usage

One command, three modes:

conductor              # glance: table of your live windows
conductor up           # launch the visual web cockpit (opens your browser)
conductor mcp          # run the MCP server (for agent integration)
conductor help         # all options

Table options

conductor --minutes 60   # widen the time window
conductor ls --all       # every session, ignore the filter
conductor ls --json      # structured JSON

Web cockpit (the visual)

A live, glanceable dashboard. Big friendly label per window, color-coded status (🟒 working now · 🟑 idle), click a card for full detail (goal, last action, recent timeline). Auto-refreshes every 4s (read-only observation).

conductor up                # starts on :7591 and opens your browser
conductor up --port 8080    # custom port
conductor up --no-open      # don't auto-open

Tap πŸ“– in the header for a one-page quick manual (also at /manual, or as a printable PDF).

Custom labels (the "key")

The big label on each card comes from the working directory, auto-prettified. To give a project a human name, edit ~/.conductor/labels.json β€” a flat map of <dir-basename> β†’ <friendly name>:

{
  "agentsoag": "SOAG Β· Website",
  "inmusic-pitch": "inMusic Β· Pitch",
  "survivors": "DegenScreener"
}

Changes are picked up live (no restart). Unmapped projects fall back to a prettified directory name.

As a Claude Code skill (recommended)

Install the skill so any window can summarize the others in natural language:

mkdir -p ~/.claude/skills/conductor
cp skill/SKILL.md ~/.claude/skills/conductor/SKILL.md

Then in any Claude Code session: "sort out my windows" / /conductor. Claude runs the scanner and renders a doing-now / done / what's-left summary per window.

As an MCP server (use it inside any agent)

Conductor speaks the Model Context Protocol over stdio, so any MCP-aware agent can call it natively β€” to watch its windows and to drive them.

Read tools: list_sessions, summarize_session, whats_left, pending_questions (only the windows blocked waiting on a human, with the question text β€” the triage feed), and risk_snapshot (fleet PnL / drawdown / wedged units, for the trading-bot adapter). The read tools take an optional adapter ("claude-code" default, or "fleet").

Control tools (write, via the same tmux channel as the cockpit): reply_to_session (reply to a window, adopting a read-only one first β€” ungated, for a human-authorized reply), auto_continue (advance a window under the gate β€” see below), send_key (Escape / C-c / Enter to a managed window), and run_window (launch a new managed window with an optional first prompt).

The driving loop, and the gate that makes it safe. An orchestrator runs windows end-to-end like this: poll pending_questions β†’ each waiting window comes back flagged irreversible? β†’ auto_continue the safe ones (it sends "continue" to keep ordinary work moving) and hand the flagged ones to the human. auto_continue consults the irreversibility gate (policy.js): if the window's question β€” or the reply you propose β€” touches deploy, send, delete, or spend, it refuses to send and returns the question + reason so you escalate. Continuing ordinary work is automatic; approving an irreversible action is always a human decision. The bias is to stop when unsure: a false gate costs one manual reply, a false pass can ship a bad deploy or move real money. reply_to_session stays the raw, ungated channel for relaying a decision the human has already made.

auto_continue across a fleet β€” ordinary work auto-continues; a "spend 0.4 SOL?" window is GATED back to the human

Add it to Claude Code (user scope = available everywhere):

claude mcp add conductor --scope user -- node ~/conductor/mcp.js

Or add it by hand to a client config (e.g. Claude Desktop claude_desktop_config.json):

{
  "mcpServers": {
    "conductor": { "command": "node", "args": ["/Users/you/conductor/mcp.js"] }
  }
}

Then in any session: "use conductor to list my sessions" / "what's left across my windows?"

To run an agent that drives your windows end-to-end (triage β†’ continue the safe stuff β†’ stop and ask you for irreversible steps), paste the orchestrator prompt in docs/orchestrator-prompt.md into a window that has this MCP.

Control β€” reply to managed windows

Conductor can also steer windows, not just watch them. Because a plain-terminal Claude TUI can't have input injected, control works on managed windows β€” ones you launch through Conductor into a tmux session:

conductor run soag            # launch a managed Claude window labelled "soag" (in tmux)
conductor adopt <id> soag      # take an EXISTING session under management (see below)
conductor say soag yes         # send a quick reply
conductor say soag "review and test it before deploying"
conductor attach soag          # drop into the window to type longer commands
conductor managed              # list managed windows
conductor stop soag            # close it

In the cockpit, managed windows get a MANAGED badge and a reply bar β€” one-tap Yes / No / Continue / Review / Re-iterate / Test+deploy plus a free-text box. Clicks send keystrokes straight into the live window.

Quick replies are short by design. For long, complex instructions, conductor attach and type in the window directly. Requires tmux (brew install tmux).

Adopting an existing window

A Claude window you opened yourself (in a plain terminal) can't be controlled β€” the OS won't let anything inject input into it. conductor adopt works around this by forking the session into a managed tmux window, keeping the full history:

conductor ls                  # find the session (note its 8-char id or label)
conductor adopt 1a2b3c4d work  # re-open it (forked) as managed window "work"
# ...then close the original tab; control "work" from CLI + cockpit

It runs `claude --resume --fork-session` in the session's own project directory. Forking means no collision with the still-open original β€” but you should close that tab and continue in the managed window.

Honest limits (v1)

  • Control is managed-only. Conductor can reply to windows you launched via conductor run (tmux). Plain terminal windows you opened yourself stay read-only β€” there's no reliable way to inject input into them.
  • "What's left" is inferred from the transcript, not a real todo list. Treat it as best-effort.
  • "Live" = recently touched. A window that's been open but idle for hours may not appear (widen with --minutes). Per-row time shows true last activity.
  • Claude Code only, local machine only.
  • It only reads your own ~/.claude β€” never another user's transcripts.
  • The "open" signal needs lsof (and a claude-named process) β€” primarily macOS. If lsof is missing/unavailable, liveness falls back to recent file writes (nothing shows as "open", just "recent"/"idle").
  • Control = a control plane, not read-only. The cockpit's POST endpoints inject keystrokes into managed windows. They're guarded (localhost bind + origin + X-Conductor header) so a stray web page can't reach them, but treat the cockpit as something you run for yourself, not a public service.

The abstraction

Conductor is supervisory awareness over a fleet of semi-autonomous workers that already emit an append-only activity trail, where the operator's scarce resource is attention. Observation is always-on and free; control is a separate, opt-in plane added only where a real command channel exists.

A worker fits Conductor when four things hold:

  1. Many autonomous-ish units β€” you're outnumbered.
  2. Each already emits a readable trail β€” no instrumentation to add.
  3. Each is pursuing a task with intent β€” so "doing now / done / what's left" is meaningful.
  4. You supervise by exception β€” surface the one that's stuck, ignore the 19 that are fine.

Claude Code windows fit (transcripts + intent + you-have-10-open). Trading bots fit (event logs + a mandate + you-can't-watch-all-day). So do CI runners, scrapers, data pipelines β€” anything that narrates itself to disk.

The engine / adapter split

                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ engine.js ──────────────────────┐
   adapter   ───▢ β”‚ discover β†’ liveness β†’ parse β†’ group β†’ status β†’ sort   β”‚ ───▢ rows
 (the trail)      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β”‚
                              owns: grouping, ranking, sectioning           β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”
                                                                         CLI Β· cockpit Β· MCP

The engine (engine.js) is domain-blind: loadAdapter(name) resolves adapters/<name>.js, collect(adapter, opts) runs the pipeline and returns sorted public rows. The three surfaces (scan.js, server.js, mcp.js) render those rows and are adapter-selectable with --adapter.

Adapters

Adapter Reads Liveness Control
claude-code (default) ~/.claude/projects/**/*.jsonl transcripts a live claude process (lsof) tmux send-keys (managed windows)
fleet ~/.fleet/bots/*/events.jsonl event logs newest heartbeat freshness append commands to control.jsonl
mev-searcher ~/.fleet/searchers/*/events.jsonl event logs fresh heartbeat/opportunity (feed alive) append commands to control.jsonl
validator-fleet the chain (getVoteAccounts/getEpochInfo/… per rpcUrl) currently in the cluster vote set opt-in, gated: control file or SSH exec
conductor                          # claude-code (default)
conductor --adapter fleet          # the trading-bot fleet
conductor --adapter mev-searcher   # the MEV / liquidation searcher fleet
conductor --adapter validator-fleet # Solana validator ops (chain-side)
conductor up --adapter fleet       # the cockpit, fleet mode

The fleet adapter

A convention-over-config trading desk. Each bot appends to ~/.fleet/bots/<bot>/events.jsonl, one JSON record per line β€” { ts, type, ... } where type ∈ signal | order | fill | pnl | heartbeat | error β€” and an optional ~/.fleet/bots/<bot>/meta.json gives { strategy, mandate, venue, symbol }. The adapter derives:

  • liveness from heartbeat freshness;
  • a wedged signal β€” an order/signal stuck with no fill past a threshold;
  • a drawdown signal β€” equity off its running peak;
  • position, session PnL, and venue as context chips.

Units are sectioned WEDGED β†’ DRAWDOWN β†’ TRADING β†’ IDLE (problems first β€” supervise by exception). Control appends pause | resume | flatten | set-param to the bot's control.jsonl, which the bot polls; the cockpit's broadcast-flatten is the desk-wide panic button (confirm token + double-confirm). Try it without a venue:

node tools/fakebot.js alpha --scenario healthy
node tools/fakebot.js beta  --scenario wedged
node tools/fakebot.js gamma --scenario drawdown
conductor --adapter fleet

The MCP server adds risk_snapshot (total PnL + worst drawdowns + wedged units) for an orchestrator agent driving the desk.

The mev-searcher adapter

A sibling of the fleet adapter β€” same ~/.fleet file-trail plumbing (factored into adapters/_filetrail.js), different domain. Each searcher appends to ~/.fleet/searchers/<bot>/events.jsonl β€” { ts, type, ... } where type ∈ opportunity | bundle | submit | land | revert | pnl | gas | heartbeat | error β€” and an optional meta.json gives { strategy, mandate, chain, venue }. The adapter derives, over a recent window:

  • feed-dead (critical) β€” no heartbeat/opportunity in the window: disconnected from the mempool / Geyser / orderflow;
  • wedged β€” submitting a flurry of bundles but ~nothing landing (losing every race);
  • bleeding β€” net flow negative after tips + gas;
  • racing (active) β€” landing bundles, positive flow; idle β€” connected, quiet market.

Sectioned FEED-DEAD β†’ WEDGED β†’ BLEEDING β†’ RACING β†’ IDLE. Control appends pause | resume | set-param | kill | unwind to the searcher's control.jsonl. unwind (flatten seized collateral) is destructive β†’ it carries an adapter-layer confirm-token gate on top of the cockpit guard, and can never be broadcast β€” broadcast('pause') is the desk-wide stop (gas spike / reorg / bad oracle), nothing destructive. Try it without a live chain:

node tools/fakesearcher.js arb-1 --scenario racing
node tools/fakesearcher.js arb-2 --scenario wedged
node tools/fakesearcher.js liq-1 --scenario bleeding
conductor --adapter mev-searcher

The validator-fleet adapter

The novel one: zero-instrumentation, chain-side observation. It queries the cluster, not the box β€” one batched getVoteAccounts + getEpochInfo (+ optional getBlockProduction / getClusterNodes / getBalance) poll per rpcUrl, never per-node β€” so delinquency, catchup, skip-rate, balance, and version-drift are learned without touching the validator. Config at <CONDUCTOR_DIR>/validators.json (default ~/.conductor): an array of { name, identityPubkey, votePubkey, cluster, rpcUrl, host?, control? }.

Signals: delinquent (critical) β†’ behind (failing catchup) β†’ degraded (skip rate) β†’ low-balance (identity near the vote-fee floor) β†’ version-drift (info) β†’ healthy (voting, caught up, producing). Sectioned in that order.

Safety is non-negotiable here. Control is observe-only by default β€” every capability (restart | catchup | topup-identity | drain) requires both a per-validator enable flag in control and a confirm token, and reaches the node via a control file it polls (default) or an arg-structured SSH exec. There is deliberately no hot identity-swap: swapping a validator identity while another copy may still be voting is the classic double-sign β†’ slashing / fund-loss footgun, so it is left out entirely (a future version would need a hard interlock proving the old identity is provably stopped first). broadcast is read-only β€” at most a non-mutating report, never a desk-wide restart.

Writing a new adapter

Drop a file at adapters/<name>.js exporting the contract below, and every surface works with --adapter <name> β€” no engine changes.

module.exports = {
  // REQUIRED
  discover(opts)          { /* β†’ array of trail handles (file paths, dirs, cursors) */ },
  parse(handle, opts)     { /* β†’ a normalized record (below), or null to drop it */ },

  // OPTIONAL
  liveness(handles, opts) { /* β†’ Set of handles live right now; else engine falls back to recency */ },
  status(record, { live, now }) { /* β†’ a status key string from record.statusInputs */ },
  project(baseRow)        { /* β†’ the public row shape for this domain (add domain fields) */ },
  statuses: [ { key, title, word, color } ],   // ordered vocabulary β†’ sections + sort order
  control: {                                    // opt-in command plane
    capabilities: ['pause', 'flatten', /* … */],
    send(target, command)  { /* one unit */ },
    broadcast(command)     { /* all units */ },
  },
};

The normalized record (the stable contract the engine depends on):

{
  id,                 // stable unique id for the unit
  shortId,            // display id
  label,              // friendly name (project / bot / agent)
  title,              // plain-language "what this unit is about"
  intent,             // its goal / mandate
  context,            // array of chips, e.g. ["feat-branch"] or ["Hyperliquid", "+2.3%"]
  recent: [ { actor, kind, summary, ts } ],  // ring-buffered recent events
  lastAction,         // one-line "doing now"
  lastActivityTs,     // ms epoch of true last activity
  statusInputs: { … } // adapter-specific signals the engine maps to a status
}

Notes: the engine groups records by id (freshest wins), applies liveness, computes status, sorts by your statuses order then recency, and runs project to produce the public row. The four worked examples are adapters/claude-code.js, adapters/fleet.js, adapters/mev-searcher.js, and adapters/validator-fleet.js. The two file-trail adapters share adapters/_filetrail.js (discover glob, streamed jsonl parse, tail-read liveness, control-file append); the validator adapter is chain-side and doesn't use it. Stream large trails (don't load them whole); bound any wide scan's concurrency. Zero runtime dependencies, Node β‰₯18.

The four-ingredient fit test

A domain is a Conductor adapter β€” rather than a rewrite β€” when it has all four. Naming them up front tells you what each hook becomes:

  1. A fleet of units with intent. Many semi-autonomous workers, each with a goal you can state in one line (a bot's mandate, a validator's "stay in consensus"). β†’ parse().title / intent.
  2. A trail that already exists. The unit emits its own activity β€” an append-only log, or a third party (the chain) reports on it β€” so observation is read-only and zero-instrumentation. β†’ discover() + parse().
  3. A liveness signal. Something that says "alive right now" distinct from "healthy": a heartbeat, a fresh feed event, presence in a consensus set. β†’ liveness().
  4. Supervise-by-exception status. A small ordered vocabulary that floats the one unit needing attention to the top (wedged, delinquent, feed-dead). β†’ statuses + status().

Control is the optional fifth ingredient β€” opt-in, structured (never shell-interpolated), with a confirm token on anything destructive and no destructive broadcast. Candidates that fit the recipe: DER/VPP dispatch, data-pipeline / ETL runs, CI fleets, crawler swarms, GPU training jobs.

License

MIT