Skip to content

Instantly share code, notes, and snippets.

@nazt
Created May 2, 2026 04:19
Show Gist options
  • Select an option

  • Save nazt/5816d93aac9b0d5e348f89655a66e63e to your computer and use it in GitHub Desktop.

Select an option

Save nazt/5816d93aac9b0d5e348f89655a66e63e to your computer and use it in GitHub Desktop.
maw-js: Multi-Agent CLI Orchestrator — guide, blog, smoke tests
#!/usr/bin/env bash
set -euo pipefail
echo ""
echo " maw engine test"
echo ""
# Check config.commands
echo " engines in config:"
cat ~/.config/maw/maw.config.json | grep -A5 '"commands"' | grep -v default | grep ':' | sed 's/[",]//g; s/^/ /'
echo ""
# Test buildCommand resolution (via --version as proxy)
echo " testing maw wake --codex on a dead pane..."
echo " (kill an agent first: tmux send-keys -t <pane> C-c C-c)"
echo " then run: maw wake <oracle> --codex"
echo ""
echo " or test swarm:"
echo " maw swarm --count 2"
echo " maw swarm codex claude"
echo ""
#!/usr/bin/env bash
set -euo pipefail
GREEN='\033[32m'
RED='\033[31m'
GRAY='\033[90m'
CYAN='\033[36m'
RESET='\033[0m'
pass=0
fail=0
total=0
run() {
local label="$1"; shift
total=$((total + 1))
printf " ${GRAY}%-40s${RESET} " "$label"
if output=$("$@" 2>&1); then
lines=$(echo "$output" | wc -l | tr -d ' ')
printf "${GREEN}✓${RESET} ${GRAY}(%s lines)${RESET}\n" "$lines"
pass=$((pass + 1))
else
code=$?
printf "${RED}✗${RESET} ${GRAY}(exit %d)${RESET}\n" "$code"
fail=$((fail + 1))
fi
}
echo ""
echo " ${CYAN}maw smoke test${RESET}"
echo ""
# Core
run "maw --version" maw --version
run "maw preflight" maw preflight
run "maw ls" maw ls
run "maw ls -v" maw ls -v
run "maw oracle ls --json" maw oracle ls --json
run "maw oracle search maw" maw oracle search maw
run "maw fleet ls" maw fleet ls
run "maw doctor --smoke" maw doctor --smoke
# Team (create → spawn → ls → shutdown → delete)
if [ "${1:-}" = "--team" ]; then
echo ""
echo " ${CYAN}team lifecycle${RESET}"
echo ""
run "team create" maw team create smoke-test --description "automated smoke"
sleep 1
run "team ls" maw team ls
run "team shutdown" maw team shutdown smoke-test
run "team delete" maw team delete smoke-test
fi
echo ""
icon="✓"
[ "$fail" -gt 0 ] && icon="⚠"
printf " ${icon} ${GREEN}%d${RESET} pass, ${RED}%d${RESET} fail / %d total\n\n" "$pass" "$fail" "$total"
[ "$fail" -eq 0 ]

maw team — Multi-Agent Orchestration Guide

Spawn, coordinate, and manage AI agents in tmux panes from one terminal.

Quick Start

# 1. Spawn 3 agents side-by-side
maw swarm --count 3

# 2. Send them work
maw tmux send %23 "review the auth module"
maw tmux send %24 "write tests for wake-cmd.ts"
maw tmux send %25 "run maw preflight and report"

# 3. Check on them
maw panes

# 4. Clean up
maw team shutdown swarm
maw cleanup

Architecture

┌─────────────────────────────────────────────────────────┐
│ tmux session: 01-mawjs                                  │
│                                                         │
│ ┌─────────────────┐ ┌─────────┐ ┌─────────┐ ┌────────┐ │
│ │                 │ │ scout   │ │ builder │ │ tester │ │
│ │   LEAD          │ │ (claude)│ │ (codex) │ │(claude)│ │
│ │   (you/oracle)  │ │         │ │         │ │        │ │
│ │                 │ │  %23    │ │  %24    │ │  %25   │ │
│ └─────────────────┘ └─────────┘ └─────────┘ └────────┘ │
└─────────────────────────────────────────────────────────┘
         ▲                ▲           ▲           ▲
         │                │           │           │
    maw team send    maw team peek  maw tmux send
    maw team broadcast ────────────────────────────>

Commands Reference

Team Lifecycle

maw team create <name> [--description <text>]
maw team spawn <name> <role> --prompt "instructions" --exec
maw team ls
maw team status
maw team shutdown <name>
maw team delete <name>

Communication

maw tmux send <pane-id> "command or message"    # direct to pane
maw team send <team> <agent> "message"          # via inbox
maw team broadcast <team> "message to all"      # fan-out
maw team hey <agent>                            # quick message
maw team peek <agent>                           # see their screen
maw team inbox <agent>                          # read their inbox

Swarm (Quick Multi-Agent)

maw swarm                           # 3 claude agents (default)
maw swarm --count 5                 # 5 agents
maw swarm codex codex claude        # mixed engines
maw swarm --tiled                   # equal-size layout

Multi-Engine Wake

maw wake <oracle>                   # default (claude)
maw wake <oracle> --codex           # use codex engine
maw wake <oracle> --engine gemini   # explicit engine flag
maw wake <oracle> -e aider          # short form

Engines configured in ~/.config/maw/maw.config.json:

{
  "commands": {
    "default": "claude --dangerously-skip-permissions --continue",
    "codex": "codex --search --dangerously-bypass-approvals-and-sandbox",
    "aider": "aider --yes-always"
  }
}

Layout & Panes

maw panes                          # list all panes across sessions
maw layout                         # rebalance (main-vertical)
maw layout --tiled                 # equal grid
maw zoom <agent>                   # toggle fullscreen
maw split <target>                 # split a pane
maw close <pane>                   # hide (don't kill)
maw open <pane>                    # restore hidden pane

Health & Recovery

maw preflight                      # version, plugins, dead agents, config
maw preflight --fix                # revive dead agents + prune symlinks
maw doctor --smoke                 # 7-check smoke test (~1s)
maw cleanup                        # kill zombie panes

Dead Agent Auto-Relaunch

When maw wake finds a pane with a dead agent (bare shell), it automatically re-sends the launch command instead of saying "already running":

$ maw wake esp32
⚡ 'esp32-oracle' in 07-esp32 — agent dead, re-launching...

Patterns

Pattern 1: Review Squad

maw team create review
maw team spawn review security --prompt "review for security issues" --exec
maw team spawn review perf --prompt "review for performance" --exec
maw team spawn review tests --prompt "check test coverage" --exec
# ... wait for results ...
maw team peek security
maw team peek perf
maw team peek tests
maw team shutdown review

Pattern 2: Mixed Engine Exploration

maw swarm claude codex aider
# Claude explores with tools, Codex with search, Aider with git
maw team broadcast swarm "analyze src/config/command.ts"

Pattern 3: Preflight Before Deploy

maw preflight --fix    # heal dead agents
maw doctor --smoke     # verify everything works
# then deploy

Building a Multi-Agent CLI Orchestrator in TypeScript

How maw-js manages AI agents across tmux panes with engine switching, dead-agent recovery, and team coordination.

The Problem

You have 10 oracle sessions running in tmux. Each oracle is a git repo with a Claude Code agent. Some agents crash. Some need Codex instead of Claude. You want to spawn 3 agents side-by-side to review a PR in parallel. And you want one command to check if everything is healthy.

Raw tmux can't do this. You need orchestration.

The Architecture

maw-js is a CLI that wraps tmux with:

  1. Fleet management — track which oracles are awake, sleeping, or dead
  2. Plugin system — 77 plugins loaded from ~/.maw/plugins/ via symlinks
  3. Multi-engine support — claude, codex, gemini, aider from one config
  4. Team orchestration — spawn, communicate, shutdown coordinated agents
  5. Health checks — preflight, smoke tests, dead-agent detection

The Command Pipeline

User types: maw wake esp32 --codex

1. CLI (src/cli.ts)
   → resolveTopAlias("wake") → direct-handler
   
2. top-aliases.ts
   → parseFlags(--codex) → detects engine shorthand from config.commands
   → cmdWake("esp32", { engine: "codex" })
   
3. wake-cmd.ts
   → resolves oracle name → finds repo path
   → checks tmux: session exists? window exists? agent alive?
   → if dead: re-sends command. if missing: creates window.
   → buildCommandInDir(windowName, path, "codex")
   
4. command.ts
   → engine="codex" → looks up config.commands["codex"]
   → returns "codex --search --dangerously-bypass-approvals-and-sandbox"
   
5. tmux
   → sends command to pane → agent starts

Engine Resolution

export function buildCommand(agentName: string, engine?: string): string {
  const config = loadConfig();
  
  if (engine && config.commands[engine]) {
    // Direct lookup — --engine codex → config.commands.codex
    return config.commands[engine];
  }
  
  // Pattern matching — "*-oracle" → specific command
  let cmd = config.commands.default || "claude";
  for (const [pattern, command] of Object.entries(config.commands)) {
    if (pattern === "default") continue;
    if (matchGlob(pattern, agentName)) { cmd = command; break; }
  }
  return cmd;
}

Config is a simple JSON map — add any engine with zero code changes:

{
  "commands": {
    "default": "claude --dangerously-skip-permissions --continue",
    "codex": "codex --search --dangerously-bypass-approvals-and-sandbox",
    "aider": "aider --yes-always",
    "shell": "zsh"
  }
}

Dead Agent Detection

The key insight: tmux list-panes tells you the window exists, but not whether the agent inside is alive. A pane running zsh looks identical to one running claude at the tmux level.

// Check what's actually running in the pane
const infos = await getPaneInfos([target]);
const info = infos[target];
const agentAlive = info && isAgentCommand(info.command);

if (!agentAlive) {
  // Agent exited — re-send the launch command
  await tmux.sendText(target, buildCommandInDir(window, path, engine));
}

isAgentCommand() matches known binaries (claude, codex, node) plus any binary from config.commands — so custom engines are auto-detected:

export function isAgentCommand(cmd: string): boolean {
  if (/claude|codex|node/i.test(cmd)) return true;
  // Also check config.commands for custom engine binaries
  const commands = loadConfig().commands || {};
  for (const v of Object.values(commands)) {
    const bin = v.split(/\s/)[0];
    if (bin && cmd.toLowerCase().includes(bin.toLowerCase())) return true;
  }
  return false;
}

Swarm: Quick Multi-Agent

maw swarm spawns N agents in split panes with one command:

for (let i = 0; i < agentList.length; i++) {
  const agentType = agentList[i]; // "claude", "codex", or raw command
  const fromConfig = configCommands[agentType]; // check config first
  const agentCmd = fromConfig || (KNOWN_AGENTS[agentType]?.cmd) || agentType;
  
  // Split pane and run
  await withPaneLock(async () => {
    paneId = await hostExec(
      `tmux split-window -h -P -F '#{pane_id}' '${agentCmd}; exec zsh'`
    );
  });
}

The withPaneLock mutex prevents race conditions when multiple panes spawn simultaneously — tmux's pane creation is not atomic.

Preflight: One Command Health Check

$ maw preflight

  maw preflight

  ✓ version: v26.5.2
  ✓ plugins: 77 loaded, 0 broken
  ✓ sessions: 9 (8 agents alive)
  ✗ dead agents: 1 pane with no agent
      ● thclaws-run:bash (bash)

    → maw preflight --fix   to revive dead agents
  ✓ config: node=m5, engines=[codex]

  ⚠ 4 pass, 1 fail (78ms)

--fix revives dead agents by re-sending buildCommand() to their panes.

Plugin Import Architecture

77 plugins live in ~/.maw/plugins/ as symlinks to the registry. They import maw-js internals via subpath exports:

// Before (broken after extraction):
import { cmdList } from "../../shared/comm";

// After (works everywhere):
import { cmdList } from "maw-js/commands/shared/comm";

42 subpath exports in package.json cover the full internal API surface. A lint script prevents regressions:

$ bash scripts/lint-imports.sh
✓ no escaping relative imports found

Safety Hook: tmux → maw

A pre-tool hook intercepts raw tmux commands and suggests maw equivalents:

$ tmux split-window -h
BLOCKED: Use 'maw split <target>' or 'maw swarm --count N'

This prevents state drift — maw tracks pane ownership, fleet config, and agent lifecycle. Raw tmux bypasses all of it.

Numbers

  • 77 plugins loaded via symlinks
  • 42 subpath exports for plugin imports
  • 334 imports migrated from relative to subpath
  • 7 smoke checks in ~1 second
  • 3 engines supported out of the box (claude, codex, aider)
  • ~80ms preflight check time
  • 0 manual intervention for ghost CalVer dates (auto-fix)

Stack

  • Runtime: Bun 1.3
  • Language: TypeScript
  • Session manager: tmux
  • Package manager: bun (global install)
  • CI: GitHub Actions + calver-release.yml
  • Versioning: CalVer v{YY}.{M}.{D}-alpha.{HMM}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment