Skip to main content

Aegis

The programmable multi-agent meta-harness.

Drives Claude Code, Gemini CLI, and OpenCode in one terminal, gives them six primitives for working together, and lets you orchestrate them with deterministic Python workflows and scheduled jobs.

CI Docs PyPI Python License

┌ 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…                                                      │
└───────────────────────────────────────────────────────────────────────┘

What aegis is

Meta-harness. Most agentic frameworks (CrewAI, LangGraph, AutoGen, the long list) talk directly to LLM providers — they replace your coding agent and reimplement tool use, permissions, sandboxing, terminal integration. Aegis sits above your existing coding agents and drives them over their structured protocols — stream-json for Claude Code, the Agent Client Protocol (ACP) for Gemini CLI and OpenCode, with a clean driver seam for whatever lands next. The harness keeps owning tool use, model selection, MCP hosting, sandboxing. Aegis owns the layer above — tabs, routing, delegation, persistence — the things a single-conversation CLI was never built to do.

Multi-agent. Six composable coordination primitives, all wired into one MCP plane every spawned agent sees. Inbox for fire-and-forget context handoff. Queue for spawn-a-worker-on- demand dispatch. Canvas for shared markdown blackboards. Terminal for live shared PTYs. Groups for broadcast-and- gather across a committee. Workflow for deterministic Python orchestration. Mix providers freely — a Claude tab hands off to a Gemini tab; an OpenCode worker drops its result in your inbox; three agents co-author a canvas; a workflow drives all of them in lockstep.

Programmable. The substrate is scriptable from the outside and the inside. Outside: @workflow-decorated Python functions that compose the six primitives into deterministic procedures (TDD loops, branch reviews, spec-to-plan-to-implementation pipelines), cron- scheduled or invoked on demand. Inside: spawned agents can mutate .aegis.yaml themselves through MCP — declare a new specialised agent profile, register a queue, drop a plugin dir, and dispatch work to it in the same session, no restart. The substrate extends from inside.

Six primitives for agent coordination

— the multi-agent pillar.

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, six wake patterns.

→ Inbox — send context to a peer

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 review

⏳ Queue — spawn a worker on demand

Enqueue 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…

▦ Canvas — collaborate on a shared document

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…

▮ Terminal — share a live shell

Spawn a PTY-backed shell that any agent (or Alex) can run commands on, send raw keystrokes to, and subscribe to. Command boundaries are detected from OSC 133 shell-integration markers; every finalized command lands in an append-only JSONL ledger and wakes subscribers through the same ✉ channel.

# PM
aegis_term_spawn(name="build", from_handle="pm")
aegis_term_subscribe(name="build", from_handle="pm")

# builder (after a handoff)
rec = aegis_term_run(name="build", cmd="pytest -q",
                     from_handle="builder")
# → PM's transcript:
#   ✉ from term:build · 14:03:25Z
#     $ pytest -q  · run by agent:builder
#     exit 0 · 4.20s
#     ──
#     6 passed in 4.18s

▣ Groups — broadcast and gather

Form a named committee of agents that share one inbox-fanout channel and one in-flight broadcast slot. Send one structured four-field question — objective, output_format, tool_guidance, boundaries — collect N parallel answers, reduce them into a single result. Use when the same question has multiple useful perspectives, or when you want to race providers and keep the fastest.

# spawn three reviewers with different lenses
aegis_group_spawn_mixed(name="audit", from_handle="pm",
    profiles=["sec_reviewer", "style_reviewer", "logic_reviewer"])

aegis_group_broadcast(name="audit",
    objective="audit PR #214 (branch feat/rate-limit)",
    output_format="bullet list, each item severity-tagged (high/med/low)",
    tool_guidance="prefer Read + Grep; avoid Bash and Edit",
    boundaries="report only — no patches, no commits")

# collect every reply, keyed by reviewer
result = aegis_group_wait_all(name="audit",
                              timeout=300,
                              reducer="join_by_handle")
# → result.reduced = {"sec_reviewer": "…", "style_reviewer": "…",
#                     "logic_reviewer": "…"}

Switching to aegis_group_wait_any returns on the first reply and (by default) sends a passive cancel envelope to the losers — useful when the cheapest acceptable answer wins. Built-in reducers: concat, join_by_handle, last_wins, majority_vote; custom reducers register one function. Groups also have YAML presets in .aegis.yaml (groups.presets.<name>.profiles: […]) and a dedicated TUI tab with Members / Current broadcast / Recent broadcasts panels.

Reach for it when: multi-lens code audit, fastest-answer racing, cross-provider consensus, generate-and-pick (N candidates → one), role-persona panels (PM / eng / UX react to the same proposal). Full walk-through in docs/groups.md.

⟳ Workflow — deterministic Python orchestration

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.

What else is in the box

  • Multi-tab TUI. Generated alliterating handles (lucid-knuth, wry-hopper) for agents, purpose names (build, db) for terminals. 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. Live ctx Nk (P%) segment shows the current turn's true input against the model's context window — Opus 4.x at 1M, Sonnet/Haiku at 200k, Gemini at 1M. 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. F4 expands into a full-screen modal with QUEUES / IN-FLIGHT / QUEUED / RECENT bands and a live assistant-text tail.
  • Fleet dashboard. F10 shows every session as a card: what it finished, what it is doing now, its last tool events, plan, repo and cost, with a band of fleet-wide counts on top. Queue workers stay on screen for a minute after they close. aegis dash opens it in a second terminal.
  • File browser + viewer. Ctrl+O opens a FileBrowserTab — a persistent tab, not a modal, so several can coexist. It lists files newest-first over a background watchdog index (the common case is open whatever the agent just touched, which no filename-first picker serves), with a typeahead filter that narrows without disturbing that order and a DirectoryTree sidebar on the right under F3. Pick a file and the same tab becomes a full editor — syntax-highlighted read-only view by default, e toggles edit mode, Ctrl+S saves, p previews Markdown, Escape with unsaved edits prompts to discard; b or Escape returns to the list, on the file you were just reading. Agents can drop you into the view via the aegis_view_file MCP tool, and Ctrl+click on a backtick-wrapped filename in any agent response opens it directly. Ctrl+click on a Read / Write / Edit block opens the file that call touched — an Edit lands on the line the edit began.
  • Config panel. F2 opens the live .aegis.yaml editor inside the TUI — see agents/queues at a glance, add an agent through a validated modal. Same edit helpers back the scriptable aegis config CLI verbs and a parallel MCP surface (aegis_config_add_agent, …_add_queue, …_add_plugin_dir, …_set_schedule_enabled, plus removes and reads), so agents can extend the substrate from inside — declare a queue and enqueue to it within one session, no restart. Panel, CLI, and MCP all route through the same comment-preserving atomic-write path.
  • Session persistence. aegis reopens the last workspace by default — agent tabs, terminal tabs, profiles, order, with each underlying session genuinely resumed (model memory intact). A tab is restored by resuming its harness conversation, so one that never took a turn has no id to resume and does not come back. aegis --clean opts out.
  • Workflow catalog. aegis.workflows ships four ready-to-use seeds (brainstorm_to_spec, execute_plan, review_branch, tdd_cycle); importing them registers. Engine offers ask_human, explicit checkpoint + durable resume, bash_predicate retry loops, and parallel fan-out.
  • A daemon, and clients. aegis boots no brain: a detached aegis server holds it and every view, and a terminal attaches over a unix socket. Sessions outlive the terminal, several terminals can watch one brain, and Ctrl+Q detaches. aegis ls and aegis kill manage daemons across roots. Add a web: token to drive the same backend from the same TUI in any browser, one view per tab (aegis web).
  • MCP plane. Every spawned agent is injected with the aegis MCP server: orientation (aegis_meta), session listing, handoff, queue dispatch, canvas ops, terminal ops, group broadcast/gather, workflow invocation. One consistent surface across providers. With --strict-mcp-config, aegis is the only MCP server the spawned agent sees.
  • Driver parity. The canonical event surface unifies what every substrate publishes: semantic tool kinds (📖 read / ✏️ edit / ⌬ execute / 🔎 search / ✻ think / 🌐 fetch) with locations + raw inputs, file diffs for edits, plan blocks for TodoWrite / AgentPlanUpdate, mid-turn cost / mode / title telemetry, and end-of-turn stop_reason / cost_usd / per-model attribution. Same render code paths for Claude, Gemini, and OpenCode; opencode's per-token thought stream coalesces by message_id into one block per assistant message.

Plugins — extend aegis without forking

Plugins are how third-party code adds behavior to a session. Three composable primitive shapes, auto-imported from disk, installable from anywhere over gh: registry URLs:

  • @hook(event) — fires on harness lifecycle events. pre_turn is the mutator (prepend system text, rewrite the user message, block the turn); post_turn / session_start / session_end are observers. Composes deterministically; timeout-wrapped per call; JSONL-logged.
  • @tool — first-class FastMCP tools the spawned agent can call. Schema is auto-generated from type hints + docstring. Reserved names (every built-in aegis_*) are guarded at registration.
  • @workflow — orchestrated procedures driven by the WorkflowEngine (delegate, spawn, send/drain, bash, groups). CLI-, MCP-, or scheduler-invoked.

A plugin is one directory: a plugin.toml manifest, the Python module(s) holding decorated functions, and optional _install.py / _uninstall.py for setup and teardown. The loader recurses and auto-imports *.py, skipping anything starting with _ — so install scripts coexist with runtime code without colliding.

Install lifecycle:

aegis plugin install <name> --from gh:owner/repo#plugins/<name>
aegis plugin list / show / update / search / uninstall

--from accepts gh:owner/repo[@ref][#path] (HTTPS git archive fetch) or file:///abs/path (local copy). Without --from, the default registry is gh:apiad/aegis#plugins/. Installs roll back on failure; .aegis/plugins.lock records the resolved source + timestamp.

Two canonical plugins (in this repo)

skill-system — Claude-Code-style skill selection on any harness. A pre_turn hook injects a numbered menu parsed from .aegis/skills/*.md; a load_skill(name) @tool pulls the body on demand. ~100 lines of Python.

aegis plugin install skill-system --from gh:apiad/aegis#plugins/skill-system

memory-system — Hermes-inspired persistent memory with periodic dreaming. Per-project .aegis/memory/ holds a user-edited SOUL.md + USER.md, a MEMORY.md index over typed entries (user / feedback / fact / reference), and a dreams/ log. The pre_turn hook injects the persona bundle on turn 0 and top-K relevant teasers on later turns. Five memory_* @tools let the agent read/write/search. The dream @workflow runs a three-stage consolidate-plus-synthesize pass over the last week of session transcripts; install asks once whether to schedule it daily at 03:00. Exercises every substrate primitive (@hook, @tool, @workflow) end-to-end.

aegis plugin install memory-system --from gh:apiad/aegis#plugins/memory-system

Full protocol reference: Plugins documentation.

Execution hosts — run an agent on another machine

Give aegis an SSH destination and any agent can run its harness over there, while its tab, transcript and peer identity stay here:

hosts:
  vps:
    ssh: vps.apiad.net
    cwd: /home/apiad/Workspace
/spawn main@vps               # or Ctrl+N → host tier
/spawn main@vps:/srv/app      # …in a specific tree

The point is not remote access — it is that the agent's Bash, Read, Edit and Grep act on that machine's filesystem, natively, instead of one ssh invocation per command. One SSH ControlMaster is shared by every session on a host, and a reverse tunnel carries the MCP plane back, so the remote agent is an ordinary peer:

# from a local agent — the new peer runs on the VPS
aegis_spawn(agent="main", prompt="Audit the nginx config",
            from_handle="lucid-knuth", host="vps")
# → it can aegis_handoff back to you, join your canvas, and shows up
#   in aegis_list_sessions carrying host="vps"

Paths become host-scoped, because /srv/app/x.py on two machines is two different files: Ctrl+click on a remote pane copies vps:/srv/app/x.py rather than opening the local one, and file claims don't collide across hosts. If the link drops, the pane says so instead of looking idle, and /reconnect rebuilds the harness in the same tab with its history.

Not durable by design — for "keep working while the laptop sleeps", run aegis server on the box and reach it with aegis web in a browser, or ssh in and aegis attach.

→ Execution hosts

Remote plane — cross-machine handoff

aegis server can expose a second HTTP plane — distinct from the loopback MCP plane, bound wherever you want it reachable — that other aegis server instances POST into. One agent on one machine can hand a long task off to another without leaving the substrate:

aegis_enqueue(
    queue="implementation",
    payload="Implement the design at <path> with TDD…",
    from_handle="lucid-knuth",
    target="builder",           # ← new — routes to a remote aegis
)
# → {task_id: "01J…", target: "builder",
#    callback_note: "no wire return channel in v1; completion behavior
#                    is whatever the receiving serve is configured to do"}

Configuration lives in .aegis.yaml. Outbound — the peers this serve can call:

remotes:
  builder:
    url: http://100.64.0.5:8556
    # token: "<optional bearer>"

Inbound — opt-in receive side; default off:

remote_plane:
  bind: 100.64.0.5:8556         # the address to listen on
  accept_tokens: []             # optional bearer-token allowlist
  accept_from: []               # optional source-IP allowlist

Bind the plane wherever it should be reachable from, and only from — typically a private overlay network (Tailscale/Headscale/WireGuard/ VPN) so the network itself is the trust anchor. Bearer-token and source-IP gates compose with AND on top. By default the call is fire-and-forget: the receiving serve runs the worker under its own config and whatever it does on completion (commit and push, message through its own bridge, write to a shared folder, nothing) is up to it.

Since v0.8.0, aegis_enqueue(target=…, callback=True) opts in to a wire callback that delivers the remote worker's final message back to the originating agent's inbox as a normal ✉ from queue:<peer>:<name> envelope (symmetric peers config required — both sides define each other in remotes:). A small /remote/v1/schedule/* control plane (with aegis_schedule_* MCP tools and aegis schedule push --to <peer> / --remote <peer> CLI verbs) lets one serve push schedules into a peer and inspect or remove them remotely — useful for self-scheduling future work or managing a fleet from one host. Full surface, error model, and patterns in docs/remote.md.

Per-queue budgets

— the programmable pillar.

Declare rolling USD or output-token ceilings on any queue. The substrate enforces them at enqueue time — if admitting the task would push the queue over any configured ceiling, the enqueue is rejected immediately with a structured error that names the blocking constraint and gives an ETA for when the queue will unblock.

# .aegis.py
queues = {
    "impl": {
        "agent": "opus",
        "max_parallel": 2,
        "budgets": [
            {"usd": 1.00,             "window": "1h"},
            {"usd": 10.00,            "window": "24h"},
            {"output_tokens": 500000, "window": "1h"},   # runaway belt
            {"usd": 50.00,            "window": "7d"},
        ],
    },
    "fast": {
        "agent": "haiku-fast",
        "max_parallel": 4,
        # no budgets: key → no caps; behaves as before
    },
}

All-must-allow: a task is admitted only if every budget entry is under its ceiling. When rejected, the error names every blocking constraint, how much was spent vs the limit, and an unblock_at ETA.

Budget state is visible via aegis budget list/show (with --remote <peer> for cross-host), the aegis_budget_status MCP tool, and the read-only GET /remote/v1/budget endpoints on the remote plane. No alerts — observability is pull-only; the rejection at enqueue time is the only loud signal.

See docs/budget.md for the full model.

Scheduled workflows

— the programmable pillar.

Aegis runs a cron-style scheduler alongside QueueManager and the inbox router. Schedules are declared in .aegis.yaml and can be split into drop-in overlays under .aegis/schedules/<name>.yaml. Each entry names a workflow (built-in or registered), a trigger (cron or fire_at), a lifecycle (forever / once / {fires: N} / {until: <iso>}), and an overlap policy (skip / queue / kill).

# .aegis.yaml
schedules:
  morning-briefing:
    workflow: prompt
    cron: "0 6 * * *"
    timezone: America/Havana
    args: { agent: default, message: "Write today's briefing." }
  ci-watch:
    workflow: enqueue
    cron: "*/5 * * * *"
    lifecycle: forever
    on_overlap: skip
    args: { queue: ci, payload: "Check CI status and report failures." }

Two workflows ship in-tree: prompt (one-shot agent message) and enqueue (scheduler → queue handoff).

The substrate writes a JSONL audit log per schedule under .aegis/state/schedules/<name>.jsonl plus a derived schedules.snapshot.json for dashboards. On boot it replays each log to rebuild fire counts and closes any dangling fire_requested record as failed:interrupted. Editing .aegis.yaml or any overlay file hot-swaps the schedule table without a restart — entries that didn't change keep their state.

aegis schedule list                # current schedules + next fire
aegis schedule show morning-briefing
aegis schedule run morning-briefing   # force-fire once
aegis schedule disable morning-briefing  # comment-preserving YAML edit
aegis schedule logs morning-briefing -n 50

Install

pip install aegis-harness        # or: uv pip install aegis-harness

Requires Python 3.13+ and at least one of: claude, gemini, or opencode on your PATH, signed-in.

Quickstart

aegis          # full-screen TUI — first-class UI for local dev
aegis web      # the same TUI in a browser, one view per tab

aegis in a terminal and aegis web in a browser are two screens on the same daemon. aegis web is its own process: it ensures a token and a daemon, prints a login URL, and serves each browser tab its own view of the same TUI, rendered by xterm.js. Sessions, queues and monitors live in the daemon, so every screen shows the same ones.

With no .aegis.yaml in the directory, aegis drops you straight into the TUI ConfigPanel — press a to add your first agent and save. The scriptable equivalent:

aegis config agent add main --provider claude-code \
                            --model opus --effort high
aegis                       # start the TUI normally

.aegis.yaml is declarative YAML — edit it by hand or use aegis config verbs (every section reachable: agents, queues, default-agent, plugin-dir). Mid-session, reach the ConfigPanel via F2.

Keys

Key Action
Enter Send
!cmd Shell escape — run cmd locally, inject its output as your message
/cmd Slash command — aegis runs it directly (/help lists them); never reaches the agent
/loop <instruction> Repeat an instruction every turn until the agent calls aegis_loop_stop or the cap (default 20) is hit; /loop stop or Esc cancels
/fork [prompt] Branch this conversation into a new tab — a worker that already knows. Refused mid-turn; the parent is untouched. ~$1
/btw <question> A side note that doesn't cost a conversation — answered from this pane's recent window, never written to the log. Legal mid-turn; Esc cancels
/recap Where this session stands — a task / outcome / next block over the last eight turns, at the level of what is being solved rather than which files changed. The automatic version, the outcome with the task under it, is drawn after any turn that moved something or needs you. Never written to the session log
@handle <question> Ask an idle peer, from where you're standing. The answer is a real turn in their transcript and a transient block in yours; --cc also lands it in yours
/usage quota Live subscription utilisation for every provider you hold credentials for (Claude, OpenCode Go) — every window, with reset countdowns. The status bar carries the short form (⧗ cc 5h 9% · wk 65% │ oc 5h 0% · wk 0% · mo 14%) always, since quota is an account property and the point is to know which rail to launch on. aegis usage quota prints the same from a shell
Ctrl+T / Ctrl+N New tab (default agent) / new tab (pick agent)
Ctrl+E New terminal tab (term:<name>)
Ctrl+W Close tab (last → quit)
Ctrl+1..9 / Ctrl+Tab / Ctrl+←→ Switch tabs
Ctrl+Shift+←→ Move the active tab along the bar (or drag it with the mouse)
Ctrl+K Toggle terminal-tab input between run and raw mode
F4 Open / close the queue dashboard
F10 Open / close the fleet dashboard: every session as a card
Ctrl+D Detach: leave the daemon and its agents running
Ctrl+Q Quit: detach, and stop the daemon if no other client is attached and a client started it
Ctrl+R Session history — reopen a prior session (jump / resume / fresh)
Ctrl+O New file browser tab — recency list, filter, tree sidebar, editor
F2 Open the ConfigPanel — edit agents/queues/etc. live
Escape Interrupt the active turn (or dismiss a modal)
Alt+↑↓ / Ctrl+↑↓ / Alt+End Read back over the transcript: one line, one message, or straight to the live tail
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.

Configuration

.aegis.yaml is declarative YAML. Author it interactively (the TUI ConfigPanel — boot-into-panel when no config exists, F2 mid-session) or with the scriptable CLI (aegis config agent add, aegis config queue add, aegis config default-agent, …). Shape:

default_agent: default

agents:
  default:
    provider: claude-code
    model: opus
    effort: high
    permission: auto
  reviewer:
    provider: claude-code
    model: sonnet
    permission: read
  fast:
    provider: gemini
    model: gemini-3-flash-preview
    permission: full
  oss:
    provider: opencode
    model: opencode/kimi-k2.6
    permission: full

queues:
  review:
    agent: reviewer
    max_parallel: 2
  fast:
    agent: fast
    max_parallel: 4

Full reference: Configuration.

The daemon, headless and web

aegis is a client. aegis server is the daemon it attaches to, and running it in a terminal yourself is how you read its output when it will not start. aegis attach [--view N] attaches this terminal to a root's daemon, and aegis dash does the same with the F10 fleet dashboard already open, for a second monitor. aegis ls lists daemons across roots and aegis kill stops one. A daemon keeps the code it booted with, and reaps itself after 30 minutes with no views and no sessions; know-how/the-daemon.md covers both.

aegis server binds no web port. Browsers reach the daemon through aegis web, a separate process that holds the port and the token:

# .aegis.yaml
web:
  bind: 127.0.0.1          # front with a reverse proxy for remote access
  port: 8899               # omit to reuse the last port, else pick a free one
  # token: "…"             # or set AEGIS_WEB_TOKEN (env wins) — keeps it out of git

aegis web prints http://127.0.0.1:8899/?t=<token> once. Opening it trades the token for a cookie, so the token does not stay in the address bar. If the daemon restarts, open tabs say so and reconnect to their views. Remote terminals use ssh: ssh host and then aegis.

Embed aegis in your own program

aegis.embed() boots the whole brain — SessionManager, queues, inboxes, canvas, terminals, schedules and the MCP plane — in your process, at a root you name:

import aegis

async with aegis.embed("/path/to/project") as ae:
    handle = await ae.manager.spawn("impl", opening_prompt="ship the thing")
    ...          # ae.queues, ae.roots, ae.mcp.server

It runs in your event loop (it never calls asyncio.run) and installs no signal handlers, so the host keeps its own shutdown. Several instances coexist in one process, each rooted at a different project, with disjoint state and no config cross-talk — which works because aegis resolves paths against three explicit roots (config_root, state_root, harness_cwd) rather than against the process working directory. That directory belongs to the host.

See know-how/embedding-aegis.md and the API reference.

When something goes wrong: aegis logs

Session transcripts record what each agent said. .aegis/state/aegis.log records what aegis itself did — and the crashes it caught on its way down, which would otherwise be a traceback printed onto a screen that is already being torn down.

aegis logs --crashes    # crash banners and their tracebacks
aegis logs -f           # follow

Crash entries carry the tab roster alongside the traceback, and unwrap exceptions that arrive inside a Textual WorkerFailed. Details in Usage.

Troubleshooting: flicker / tearing in the TUI

If the cursor, the working spinner, and scrolling all flicker or stutter as if two frame rates are fighting — and it starts or stops when you plug in a monitor, close the laptop lid, or wake from sleep — this is not an aegis bug. aegis sets no custom render options, and Textual wraps each frame in the synchronized-output (DEC mode 2026) protocol, which makes it atomic on terminals that implement the protocol. VTE terminals do not; rows that duplicate for a frame while scrolling are a different problem, covered in Known issues. The flicker is one layer lower: your terminal emulator's GPU renderer pacing against a display whose refresh rate just changed. It affects every TUI (btop, htop, …), not just aegis, and the fix is a terminal/GPU setting.

Mitigations by terminal:

  • GTK4/VTE terminals — Ptyxis, GNOME Console, Black Box, GNOME Terminal (GTK4). GTK removed its old GL renderer in 4.18; on older iGPUs (e.g. Intel HD 5xx / Skylake) the newer GPU renderers desync against the Wayland frame clock after a display change. Force the software renderer:

    mkdir -p ~/.config/environment.d
    echo 'GSK_RENDERER=cairo' > ~/.config/environment.d/50-gsk-renderer.conf
    # log out and back in — this must apply to the terminal's own GTK process,
    # not the shell inside it. Reversible: delete the file + re-login.
    

    Costs negligible CPU for a terminal. GSK_RENDERER=gl is a middle ground that keeps GPU accel. Refs: GNOME Discourse, GTK docs.

  • kitty. Set sync_to_monitor no in kitty.conf — it stops tying redraws to the monitor refresh (the source of mixed-refresh tearing / CPU spikes). Ref: kitty #3154.

  • WezTerm. Try front_end = "Software", or pin max_fps to your lowest monitor's refresh rate — WezTerm has known jitter/adaptive-sync issues on mismatched-refresh multi-monitor setups. Refs: max_fps, wezterm #2450, #7611.

  • Alacritty. No built-in vsync toggle; use the Mesa env knob vblank_mode=0 alacritty (or 0/3 to taste) to change GL sync behaviour. Ref: alacritty #1739.

  • Any terminal. Keeping the aegis window on a single monitor, or setting your displays to a matching refresh rate, avoids the mismatch that triggers it.

Docs

Full documentation: https://apiad.github.io/aegis/

Status

Beta. Personal-infrastructure-grade, evolves fast. Expect change before 1.0. See the roadmap for what's next.

License

MIT — see LICENSE.

Metadata

Release files for aegis-harness 0.40.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for aegis-harness 0.40.1
File Size Uploaded
aegis_harness-0.40.1.tar.gz 913.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for aegis-harness 0.40.1
File Interpreter ABI Platform
aegis_harness-0.40.1-py3-none-any.whl Python 3 none any Details

Total release size: 1.9 MB

Release files / aegis_harness-0.40.1.tar.gz

Download URL aegis_harness-0.40.1.tar.gz
Size 913.5 kB
Tags Source
SHA-256 checksum
How to use checksums
69ba122a74a4bda4ae4740da29d20342bedd04ad57a0e10a9a44e856135422bc
BLAKE2b-256 checksum
How to use checksums
467a8138355a715da5aa7a9e0463c6128b9c6403b7c962949288f1887ddecaa9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.21 {"installer":{"name":"uv","version":"0.12.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / aegis_harness-0.40.1-py3-none-any.whl

Download URL aegis_harness-0.40.1-py3-none-any.whl
Size 976.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f6bedbd64e5997511fc7d690b3d8fb50ad418326c0dd7322608f6885284b777b
BLAKE2b-256 checksum
How to use checksums
e275aa90a3d19efaf2ac708744a1fc5ef1f096d59bf0cb06559aa0cfc3db418f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.21 {"installer":{"name":"uv","version":"0.12.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

2.3.0

2 release files

2.2.0

2 release files

2.1.0

2 release files

2.0.1

2 release files

2.0.0

2 release files

This release

0.40.1 This release

2 release files

0.40.0

2 release files

0.39.0

2 release files

0.38.0

2 release files

0.37.0

2 release files

0.36.0

2 release files

0.35.0

2 release files

0.34.0

2 release files

0.33.0

2 release files

0.28.1

2 release files

0.28.0

2 release files

0.27.0

2 release files

0.26.0

2 release files

0.25.0

2 release files

0.24.1

2 release files

0.24.0

2 release files

0.23.0

2 release files

0.22.0

2 release files

0.21.0

2 release files

0.20.0

2 release files

0.19.0

2 release files

0.18.0

2 release files

0.17.0

2 release files

0.16.0

2 release files

0.15.1

2 release files

0.14.0

2 release files

0.12.0

2 release files

0.11.0

2 release files

0.10.0

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page