AttentionChecker, so teaching it a new one is a single file.One keyboard. All your agents.
A terminal cockpit for running several AI coding agents at once — and knowing, without checking, which one needs you.
The problem
Five agents, five terminal tabs, no idea which one is stuck
You're running five AI agents across five repos. Each lives in its own terminal tab. When one finishes, you don't notice for twenty minutes. When one asks for permission, you're in another window. When one errors out, it sits there burning tokens on nothing.
The bottleneck stops being the agents and starts being you — specifically, your attention moving between windows. Every tab you aren't looking at is either idle, blocked on a question, or quietly failing, and you can't tell which without going to look.
How it works
Every agent gets a real terminal
Each agent runs in its own PTY with full VT100 emulation — not a scraped log, an actual terminal. Anything that works in your shell works here, which is why the agents don't need to know they're being supervised.
Every 500 ms an attention detector evaluates each session and assigns it one of six states. That verdict is what drives the sidebar, so the interface answers one question at a glance: where am I needed?
The interesting part
Two layers, because one wasn't honest enough
Detecting “this agent needs a human” is harder than it sounds. Pattern matching is fast and free but brittle; an LLM is accurate but slow and metered. So it does both, in that order.
The escalation matters more than either layer. Heuristics handle the overwhelming majority for nothing; the model is spent only on genuine ambiguity, which keeps a always-watching supervisor from costing more than the agents it watches.
Remote control
The same cockpit, from your phone
The point of watching agents is being told when they need you — which is worthless if you have to be at the desk to hear it. So the whole thing bridges to Telegram: every project gets its own Forum Topic, and that topic becomes a two-way channel to the terminal sitting at home.
You get a message when a state actually changes — a permission request, a question, an error, or an agent going quiet with an answer. Not a stream of terminal output. The bridge is driven by the same attention detector as the sidebar, so it inherits the thing that took longest to get right: only speak when there's something to say.
Both of the agent's interactive wizards come through as buttons. A permission request arrives with its three real outcomes — allow once, allow for the whole session, deny — and a question arrives with its options numbered, one button each. Tapping one selects that option and confirms it. You can also just type a new prompt into the topic, and images the agent produces get forwarded up.
A tap has to end up as keystrokes in a real terminal — there is no API on the other side.
That last hop is where the work is. There is no API on the far end; the agent is a terminal program, so “approve” has to become actual bytes typed into a PTY. And they aren't the same bytes for every agent CLI — some show a text prompt, others a selection dialog needing arrow keys, so the keystrokes come from the same per-agent adapter that powers detection.
Then you wait. Writing the text and the Enter together loses the input, because the agent's own TUI hasn't finished processing the first part — so each adapter declares a SubmitDelay, and the bridge writes, pauses, and only then sends the carriage return.
The bot token isn't kept in the config file either — the config stores the name of the environment variable to read it from.
Extending it
What it takes to support another agent CLI
An agent here is one running instance in one project — that's what gets a PTY and a row in the sidebar. The agent software is the CLI behind it: Claude Code, OpenCode, and so on. Supporting another CLI widens what you can run instances of, and everything specific to one lives behind a single interface, in a file that registers itself. Six methods are mandatory:
type AgentAdapter interface {
Type() config.AgentType
Command(repoPath string, opts LaunchOptions) *exec.Cmd
BootstrapFiles() []BootstrapFile
ApproveKeystroke() []byte // approve this request
ApproveSessionKeystroke() []byte // approve for the session, or nil
DenyKeystroke() []byte
}
Everything else is opt-in. Implement SubmitDelay if that CLI's TUI needs a pause before Enter; ChromeLayout if it draws borders that must be skipped when capturing scrollback; QuestionResponder if its question dialog needs arrow keys rather than a number; AttentionChecker to teach the heuristics what that CLI's “I'm waiting for you” looks like. Skip any of them and you get sane defaults.
Say you wanted to add Codex CLI. That is the entire adapter:
type codexAdapter struct{}
func init() { Register(&codexAdapter{}) }
func (a *codexAdapter) Type() config.AgentType { return config.AgentCodex }
func (a *codexAdapter) Command(repoPath string, opts LaunchOptions) *exec.Cmd {
args := []string{}
if opts.Prompt != "" {
args = append(args, opts.Prompt)
}
cmd := exec.Command("codex", args...)
cmd.Dir = repoPath
return cmd
}
// Codex uses y/n prompts.
func (a *codexAdapter) ApproveKeystroke() []byte { return []byte("y\n") }
func (a *codexAdapter) DenyKeystroke() []byte { return []byte("n\n") }
func (a *codexAdapter) ApproveSessionKeystroke() []byte { return nil }
func (a *codexAdapter) BootstrapFiles() []BootstrapFile { return nil }
Note how much the interface absorbs. Codex answers permissions with a literal y, while OpenCode shows a selection dialog where approving means Enter and approving-for-the-session means an arrow key first. Same button in Telegram, same call in the supervisor, entirely different bytes.
The adapter registers itself, so nothing has to import it — declare the type in config alongside the others and a project can run it. One file, two lines.
What's left is the part worth doing well: AttentionChecker, which teaches the detector what this CLI looks like when it's thinking, and what it looks like when it's waiting for you. That's the knowledge each agent CLI actually contributes, and it's why the interface puts it in the same place as everything else.
Try it
One binary
go install github.com/wilfoa/openconductor/cmd/openconductor@latest
openconductor # launch the TUI
openconductor telegram setup # notifications off-machine
Config lives at ~/.openconductor/config.yaml, or press a in the sidebar to add projects interactively. Pre-built binaries for Linux and macOS are on the releases page.
One honest note
A supervisor is only worth it past a certain number of agents. With one or two, you already know what they're doing and this is overhead. It starts paying at three, and by five the alternative is a tab you forgot about spending money on an error from twenty minutes ago.