The meta-harness. Drive Claude Code, Gemini CLI, and OpenCode side by side from one calm terminal — and make them collaborate.
┌ aegis · 3 agents · ~/code/aegis ─────────────────────────────────────┐
│ ● 1 lucid-knuth ·opus· ● 2 wry-hopper ·gemini· ● 3 brisk-curie * │
│ │
│ › explain the retry path in worker.py │
│ │
│ ⠹ Thinking… (3.2s) │
│ ⏺ Read(worker.py) │
│ └ ok │
│ The retry path lives in _run_turn at line 142 … │
│ │
│ ⏺ aegis_handoff(target=wry-hopper) │
│ └ delivered to wry-hopper │
│ │
│ queues: tests ●1/2 ○0 ✓3 ✗0 last: brisk-curie │
│ lucid-knuth ·opus· opus·full ↑128k (94% cached) ↓1k │
│ ───────────────────────────────────────────────────────────────────── │
│ › ask something… │
└───────────────────────────────────────────────────────────────────────┘
Most agentic frameworks (CrewAI, LangGraph, AutoGen, the whole long list) talk directly to LLM providers — they replace your coding agent and reimplement tool use, permissions, sandboxing, terminal integration. Aegis takes the opposite path:
- It sits above your existing coding agents and drives them over
their structured protocols —
stream-jsonfor Claude Code, the Agent Client Protocol (ACP) for Gemini CLI and OpenCode, and a clean driver seam for whatever lands next. - It doesn't reimplement the agent. Tool use, sandboxing, MCP hosting, model selection — that's the harness's job. Aegis's job is the layer above: tabs, routing, delegation, persistence, the things a single-conversation CLI was never built to do.
- It makes them collaborate. Four composable coordination primitives mean a Claude tab can hand off to a Gemini tab, dispatch an OpenCode worker, subscribe to a shared canvas, or kick off a deterministic Python workflow that drives all three.
The harness wars are over. You probably already have your favorite (or two, or three). Aegis lets you keep them — and run them as a team.
Each primitive has one verb and lands the same way in the receiving
agent's transcript: as a ✉ block with a sender tag, timestamp, and a
short body preview. One delivery channel, four wake patterns.
Any agent can hand off to any other live agent. Fire-and-forget; the recipient gets a normal user-message turn tagged with the sender's handle. Use when you want a specific peer to pick up where you left off.
aegis_handoff(target_handle="reviewer", from_handle="impl",
context="PR ready at branch feat/x — please review")
# → reviewer's transcript:
# ✉ from agent:impl · 17:42:03Z
# PR ready at branch feat/x — please reviewEnqueue a task to a named queue and the substrate spawns a fresh agent
of the queue's configured profile, runs the payload as its opening
turn, and (with callback=true) delivers the worker's final result
back to your inbox. Producer keeps working between enqueue and
callback. Generalizes delegation: parallelism, max-in-flight caps,
restart safety, all built in.
aegis_enqueue(queue="review", payload="…full self-contained prompt…",
from_handle="impl", callback=True)
# → {task_id: 01HK…, queued_position: 1}
# …minutes later, in impl's transcript:
# ✉ from queue:review · task#01HK… · ok · 17:46:11Z
# PR looks clean. Two nits flagged in the diff comments…Open a shared markdown file. Multiple agents read it, write sections of it, subscribe to it. Each write wakes every other subscriber with a diff-aware notification. The classical blackboard pattern — terminal- native, MCP-driven, file-backed (you can grep it, commit it, open it in your editor).
# PM
aegis_canvas_open(name="report-q3", file="vault/reports/q3.md",
from_handle="pm")
aegis_canvas_subscribe(name="report-q3", from_handle="pm")
# Researcher (in another tab, after a handoff)
aegis_canvas_write_section(name="report-q3", section="data",
content="Q3 numbers came in stronger…",
from_handle="researcher")
# → PM's transcript:
# ✉ from canvas:report-q3 · 20:30:00Z
# section "data" · written by agent:researcher (+18 / -3 lines)
# ──
# Q3 numbers came in stronger than projected…When the dance has to be reliable — TDD loops, bug triage, multi-step plans, anything where retries with feedback matter — wrap it in a workflow. Plain Python at the top of the stack. Calls agents, runs bash predicates, retries with feedback, captures structured output.
@workflow("tdd-cycle")
async def tdd_cycle(engine, *, feature: str) -> str:
impl = await engine.spawn("implementer")
await engine.send(impl, f"Write a failing test for: {feature}")
await engine.bash_predicate(
f"pytest tests/ -k {feature} 2>&1 | grep -E 'FAIL|ERROR'",
retry_with="The test should fail because the feature isn't built yet")
await engine.send(impl, "Now implement it.")
await engine.bash_predicate(
f"pytest tests/ -k {feature}",
retry_with="Tests are still failing. Output:\n{stdout}")
reviewer = await engine.spawn("reviewer")
return await engine.send(reviewer, "Final review of branch.")Triggered by any agent: aegis_run_workflow(name="tdd-cycle", kwargs={"feature": "rate_limit"}). Workflows sit at the top of the
stack — they span agents, they own the loop, they're the right tool
when the spec is "follow this exact procedure" rather than "figure
it out."
The aegis.workflows package ships four seed workflows registered on
import: brainstorm_to_spec (Q/A → spec doc), execute_plan (parse
plan → dispatch implementer per task with durable resume),
review_branch (parallel reviewer fan-out → report), and tdd_cycle
(predicate-driven TDD loop). See docs/workflows.md.
- Multi-tab TUI. Generated alliterating handles (
lucid-knuth,wry-hopper). State dots, sticky*, terminal bell when a backgrounded agent finishes. Click any block to copy it. - Honest metrics. True input (incl. cache) with cached %, output, tool calls, per-turn and per-session wall-clock. Provisional while streaming, exact at turn end. No log scraping anywhere.
- Queue dashboard. Always-on one-line strip above the status bar
shows live per-queue depth and the most recent in-flight worker.
Ctrl+Dexpands into a full-screen modal withQUEUES / IN-FLIGHT / QUEUED / RECENTbands and a live assistant-text tail. - Session persistence.
aegisreopens the last workspace by default — same tabs, same profiles, same order, with each underlying agent session genuinely resumed (model memory intact).aegis --cleanopts out. - Headless + Telegram.
aegis serveruns the SessionManager + MCP plane without a TUI. Add a Telegram token to drive the team from your phone. - MCP plane. Every spawned agent is injected with the aegis MCP
server: orientation (
aegis_meta), session listing, handoff, queue dispatch, canvas ops, workflow invocation. One consistent surface across providers. With--strict-mcp-config, aegis is the only MCP server the spawned agent sees.
pip install aegis-harness # or: uv pip install aegis-harnessRequires Python 3.13+ and at least one of: claude, gemini, or
opencode on your PATH, signed-in.
aegis init # interactive wizard — detects installed CLIs, writes .aegis.py
aegis # full-screen TUIThe wizard finds whichever agent CLIs you have installed and walks you
through picking a model, permission mode, and optional queues. The
generated .aegis.py is plain Python — edit it freely afterward.
| Key | Action |
|---|---|
Enter |
Send |
Ctrl+T / Ctrl+N |
New tab (default agent) / new tab (pick agent) |
Ctrl+W |
Close tab (last → quit) |
Ctrl+1..9 / Ctrl+Tab / Ctrl+←→ |
Switch tabs |
Ctrl+D |
Open / close the queue dashboard |
Escape |
Interrupt the active turn (or dismiss a modal) |
Click on a block |
Copy that message / tool result to clipboard |
Ctrl+Q |
Quit |
A backgrounded tab that finishes shows a * and rings the bell.
.aegis.py is plain Python. The wizard writes one for you; here's the
shape:
from aegis import Agent, ClaudeCode, GeminiCLI, OpenCode
agents = {
"default": Agent(provider=ClaudeCode(model="opus", effort="high",
permission="auto")),
"reviewer": Agent(provider=ClaudeCode(model="sonnet",
permission="read")),
"fast": Agent(provider=GeminiCLI(model="gemini-3-flash-preview",
permission="full")),
"oss": Agent(provider=OpenCode(model="opencode/kimi-k2.6",
permission="full")),
}
default_agent = "default"
queues = {
"review": {"agent": "reviewer", "max_parallel": 2},
"fast": {"agent": "fast", "max_parallel": 4},
}Full reference: Configuration.
aegis serve runs the SessionManager and MCP plane without the TUI; add
a Telegram token to drive it from your phone:
# .aegis.py
telegram_token = "…" # or set AEGIS_TELEGRAM_TOKEN
telegram_chat_id = 123456 # the single allowed chatRouting inside the chat:
/new [agent]— spawn a new session/close [handle]— close a session/interrupt— interrupt the active turn/<handle> text…— one-shot to a specific session- bare text — sent to the active session
A systemd unit template lives at scripts/aegis-serve.service.
Full documentation: https://apiad.github.io/aegis/
- Install
- Usage
- Configuration
- Drivers — Claude / Gemini / OpenCode
- Queues — inter-agent delegation
- Canvas — shared markdown blackboard
- Workflows — Python orchestration
- MCP plane — the tool surface
- Architecture
- API reference
Beta. Personal-infrastructure-grade, evolves fast. Expect change before 1.0. See the roadmap for what's next.
MIT — see LICENSE.