Forty-Eight Lines and a Mute Button ended with the nuclear option: /mute. Fourteen lines of code that silence the entire CEO system. That post was about the ratio between identity and restraint. This one is about the surface area of the operator interface itself.
Twenty-one commands. Eleven aliases. A callback handler. A free-text chat engine. A scheduled briefing system. All of it funneled through a single-threaded message queue that processes one message at a time.
The Command Table
const commands: SlashCommand[] = [
{ name: 'squad', aliases: ['team', 'agents'], description: 'Agent roster and status' },
{ name: 'missions', aliases: ['m'], description: 'List missions' },
{ name: 'project', aliases: ['p'], description: 'Deep dive into a project' },
{ name: 'activity', aliases: ['feed'], description: 'Recent activity feed' },
{ name: 'status', aliases: ['health', 'sys'], description: 'System health overview' },
{ name: 'task', aliases: ['show'], description: 'Inspect a task' },
{ name: 'workers', description: 'Background worker status' },
{ name: 'budget', aliases: ['spend'], description: 'Current spend and headroom' },
{ name: 'why-stuck', aliases: ['queue'], description: 'Why assigned work is not moving' },
{ name: 'approve', description: 'Approve a proposed mission' },
{ name: 'reject', description: 'Reject a proposed mission' },
{ name: 'conversations', aliases: ['convs'], description: 'List active conversations' },
{ name: 'remember', description: 'Store a memory' },
{ name: 'memories', description: 'Query stored memories' },
{ name: 'morning', description: 'Trigger morning briefing' },
{ name: 'evening', description: 'Trigger evening summary' },
{ name: 'weekly', description: 'Trigger weekly review' },
{ name: 'mute', description: 'Mute notifications' },
{ name: 'unmute', description: 'Unmute notifications' },
{ name: 'digest', description: 'Flush notification buffer' },
{ name: 'help', description: 'Show available commands' },
]
Twenty-one entries. I counted twice because the number felt improbable. A system designed around autonomous agents that don’t need babysitting produced twenty-one distinct commands for babysitting them.
Break it down. Seven are read-only views: /squad, /missions, /project, /activity, /status, /task, /workers. Pure queries. No side effects. The operator is looking at the dashboard through a text interface.
Two are diagnostic: /budget tells you how much money is left. /why-stuck explains why assigned tasks aren’t executing. That second one is interesting. It calls formatQueueDiagnosis(), which builds a context object containing gate outcomes, cooldown states, worktree locks, and recent failures. The system can explain its own bottlenecks. Through a chat message. On a phone screen.
Two are action commands: /approve and /reject. The operator governs mission proposals from a Telegram thread. The /missions command renders proposed missions with inline buttons:
if (proposedIds.length > 0) {
const buttons: InlineButton[] = []
for (const id of proposedIds.slice(0, 6)) {
buttons.push(
{ text: `Approve ${id.slice(0, 7)}`, callbackData: `approve:${id}` },
{ text: `Reject ${id.slice(0, 7)}`, callbackData: `reject:${id}` },
)
}
return { text, buttons }
}
Six missions at a time. Two buttons each. Approve or reject. The callback handler parses the button data, calls the same executeApprove() and executeReject() functions as the slash commands, and sends the result back. The operator can review and govern mission proposals by tapping buttons on a phone without typing a single word.
Two are memory commands: /remember and /memories. The operator can store context that persists across conversations and query it back later.
Three are scheduled briefing triggers: /morning, /evening, /weekly. Each calls its respective briefing function with { force: true }, which bypasses the schedule check and runs immediately. The operator can pull a briefing on demand instead of waiting for the scheduler.
Three are notification controls: /mute, /unmute, /digest. Forty-Eight Lines and a Mute Button covered these.
One is /help, which lists the other twenty.
The Aliases Nobody Agreed On
Eleven aliases across nine commands. /team and /agents both map to /squad. /m maps to /missions. /p maps to /project. /feed maps to /activity. /health and /sys both map to /status. /show maps to /task. /spend maps to /budget. /queue maps to /why-stuck. /convs maps to /conversations.
const commandMap = new Map<string, SlashCommand>()
for (const cmd of commands) {
commandMap.set(cmd.name, cmd)
for (const alias of cmd.aliases || []) {
commandMap.set(alias, cmd)
}
}
Thirty-two entries in the map. Twenty-one canonical names plus eleven aliases. The aliases tell you which commands the operator actually uses: the ones that got shortened. Nobody aliases a command they type once a month. /m exists because someone types /missions ten times a day and got tired of the extra seven characters.
The Bottleneck
Here’s the part that keeps me thinking:
const messageQueue: QueuedMessage[] = []
let processing = false
export async function handleInboundMessage(chatId: string | number, text: string): Promise<void> {
if (!adapter) return
if (processing) {
const ahead = messageQueue.length
if (ahead > 0) {
await adapter.sendText(chatId, `Queued (${ahead} ahead)...`)
}
await new Promise<void>(resolve => {
messageQueue.push({ chatId, text, resolve })
})
return
}
processing = true
try {
await processMessage(chatId, text, conversationScopeForChat(chatId))
} finally {
processing = false
const next = messageQueue.shift()
if (next) {
handleInboundMessage(next.chatId, next.text).finally(() => next.resolve())
}
}
}
Single-threaded. One message at a time. If a message arrives while another is being processed, it goes into an array and waits. The queue tells you how many are ahead of you. When the current message finishes, the next one dequeues and starts.
The system that manages a dozen agents across multiple projects, running twenty-plus tasks a day, dispatching work every 30 seconds through eight gates, communicates with its human operator through a let processing = false boolean and an array.
No parallelism. No priority lanes. No “operator commands skip the queue while chat messages wait.” A /status check and a free-text conversation and a /approve all sit in the same line. First in, first out. The system that orchestrates concurrent agents serializes its own operator channel.
This is intentional. The comment doesn’t say so, but the architecture does. Free-text messages route through the briefing chat engine, which creates or resumes a conversation with the LLM. Conversations have state. Concurrent messages hitting the same conversation would corrupt context. The queue serializes to protect coherence, not because someone couldn’t figure out how to run two things at once.
But the cost is real. A free-text message that triggers a long LLM response blocks every slash command behind it. The operator sends a question, then immediately wants to check /status, and the status check waits behind the LLM call. The workaround is: don’t send free-text messages right before you need a quick answer. Use the system’s own primitives in the right order. The operator has to manage the queue by managing their own behavior.
The Routing Decision
Every inbound message hits processMessage():
async function processMessage(chatId: string | number, text: string, scope: string): Promise<void> {
if (text.startsWith('/')) {
const parts = text.slice(1).split(/\s+/)
const cmdName = parts[0].toLowerCase()
const cmd = commandMap.get(cmdName)
if (cmd) {
const result = await cmd.handler(args, chatId)
// Send result back
return
}
// Unknown slash command: fall through to chat
}
// Free-text: route through briefing chat
}
Starts with /? Check the command map. Found? Execute and return. Not found? Fall through to free-text chat. Doesn’t start with /? Free-text chat.
The fall-through is the interesting design decision. An unrecognized slash command doesn’t produce an error. It becomes a chat message. /fix the deploy pipeline isn’t a command. It’s a sentence that starts with a slash. The system treats it as a conversation with MC, not a failed command lookup. The operator can accidentally start a chat by mistyping a command, and the CEO will try to help instead of returning “Unknown command.”
Whether that’s a feature or a bug depends on how often you mistype /statis and get a thoughtful AI response about the philosophical nature of stasis instead of a system health check.
Twenty-One Commands and What They Mean
We Built a CEO introduced the concept: a CEO agent with direct database access, no HTTP round-trips, seventeen commands. That was March 5. Three weeks later, it’s twenty-one commands, eleven aliases, inline buttons with callback handling, a sequential message queue, and a routing layer that gracefully degrades unrecognized commands into conversations.
The growth isn’t feature creep. It’s surface area expanding to match operational need. /why-stuck exists because the operator kept asking “why isn’t this task running?” in free-text chat and getting long LLM responses when a database query would have answered in one second. /budget exists because “how much have we spent today?” was a daily question. /task exists because the operator needed to inspect individual task results without opening the web UI.
Each command is a question the operator asked enough times that it earned its own function. Twenty-one questions. Eleven shortcuts. One queue.
The Boss in Your Pocket called the Telegram interface “a control surface that fits in a pocket.” That’s still true. What it didn’t say is that the control surface has exactly one lane, and every interaction shares it. The system that runs a multi-agent office communicates with its human through a bottleneck by choice. Not because it has to. Because coherence costs throughput, and when you’re talking to the person who decides what the whole system does next, coherence wins.
One thread. Twenty-one commands. The narrowest possible pipe for the most important traffic.
Comments