diff --git a/Architecture.md b/Architecture.md
new file mode 100644
index 0000000..64b3339
--- /dev/null
+++ b/Architecture.md
@@ -0,0 +1,209 @@
+# Architecture
+
+`claude-bridge` connects a **primary** Claude Code session (Opus 4.8, on your Pro/Max
+subscription) to one or more **secondary** Claude Code **workers** running a *different*
+model. Two channels carry traffic between them:
+
+1. **Synchronous** — primary delegates a task and waits for the reply → **AgentAPI** (HTTP
+ over terminal emulation). This is the main channel.
+2. **Asynchronous / duplex** — either side drops a message for the other to pick up when
+ idle → a **message broker** polled by a `Stop`-hook long-poll. This is the optional
+ layer for detached progress reports and out-of-band questions.
+
+A third piece, **herdr**, is *not* a transport — it is the human-facing ops/observability
+layer (persistence, state sidebar, a readiness gate).
+
+## Components
+
+```mermaid
+flowchart TB
+ subgraph prim["PRIMARY — subscription (env CLEAN)"]
+ OPUS["Claude Code · Opus 4.8
leads, reviews, merges"]
+ end
+
+ subgraph work["SECONDARY worker(s) — off-subscription"]
+ AA["agentapi server
HTTP :3284"]
+ WCC["claude (worker)
ANTHROPIC_BASE_URL set"]
+ HOOK["Stop-hook
long-poll client"]
+ AA -->|"launches / drives PTY"| WCC
+ WCC --- HOOK
+ end
+
+ BROKER["Broker
Redis Streams / NATS JetStream
inbox-primary · inbox-worker"]
+ MODEL["ollama.ltms.dev /v1
or GX10 vLLM
(worker model)"]
+ HERDR["herdr server
panes · state · socket API
(ops layer, optional)"]
+
+ OPUS -->|"POST /message (sync)"| AA
+ AA -.->|"SSE /events (reply)"| OPUS
+ OPUS -.->|"write / long-poll (async)"| BROKER
+ HOOK -.->|"long-poll / write (async)"| BROKER
+ WCC -->|"inference"| MODEL
+ HERDR -.->|"send-keys · agent wait
pane read"| WCC
+
+ classDef sub fill:#2b6cb0,stroke:#1a365d,color:#ffffff;
+ classDef pick fill:#2f855a,stroke:#22543d,color:#ffffff;
+ classDef warn fill:#b7791f,stroke:#7b341e,color:#ffffff;
+ class OPUS sub
+ class AA,WCC pick
+ class BROKER,HERDR warn
+```
+
+*Figure: solid arrows = synchronous AgentAPI channel; dotted = the optional async broker
+and the herdr ops plane. The primary's env stays clean; only the worker sets
+`ANTHROPIC_BASE_URL`.*
+
+## Subscription boundary (the non-negotiable)
+
+The whole design exists to keep the **primary** on Pro/Max while the **worker** runs a
+cheaper/local model — without a policy-violating proxy on the primary.
+
+- The **primary** `claude` process **never** sets `ANTHROPIC_BASE_URL`. It talks to the
+ worker over **HTTP** (AgentAPI) or a **broker**, never by re-pointing its own endpoint.
+- Only the **worker's** `claude` process launches with
+ `ANTHROPIC_BASE_URL=https://ollama.ltms.dev` (+ `ANTHROPIC_AUTH_TOKEN` bearer) or a GX10
+ vLLM URL. Because it is a *separate process*, model selection is just per-process env —
+ which also sidesteps Claude Code's lack of per-subagent provider routing (the worker is
+ not a subagent).
+
+> **Rule:** anything that sets `ANTHROPIC_BASE_URL` is, by definition, the worker. If you
+> ever feel tempted to set it on the primary, stop — that is the subscription line.
+
+## Channel 1 — AgentAPI (synchronous, the main path)
+
+[`coder/agentapi`](https://github.com/coder/agentapi) wraps the worker's `claude` **CLI**
+as an HTTP server (`POST /message`, `GET /messages`, `GET /events` SSE, `GET /status`),
+driving the CLI's terminal underneath. One session per server.
+
+```mermaid
+sequenceDiagram
+ participant P as "Primary (Opus)"
+ participant A as "agentapi server"
+ participant W as "Worker claude (other model)"
+
+ P->>A: "POST /message (task, full context inlined)"
+ A->>W: "inject into running session"
+ activate W
+ W-->>A: "streamed tokens"
+ A-->>P: "GET /events (SSE): assistant reply"
+ deactivate W
+ Note over P: "review diff / result, merge"
+ alt worker is blocked
+ W-->>A: "status = blocked (needs input)"
+ A-->>P: "GET /status: blocked"
+ P->>A: "POST /message (answer inlined)"
+ end
+```
+
+*Figure: the primary delegates and waits; replies stream back parsed over SSE. A blocked
+worker surfaces on `/status`, and the primary re-sends with the answer inlined
+(return-and-reinvoke) — the same pattern `crush-bridge` uses.*
+
+Why AgentAPI over raw `tmux send-keys`: it gives a clean request/reply contract and
+**parsed** replies (SSE), instead of scraping a terminal buffer. See [[Approaches]] for the
+full transport comparison.
+
+## Channel 2 — broker + Stop-hook long-poll (async, duplex)
+
+For traffic that has **no caller waiting on a connection** — a long-running worker posting
+progress, an out-of-band question, another agent or a webhook injecting work — use a
+broker. The mechanism is symmetric, so it carries **both** directions: each side
+long-polls its **own** inbox stream and writes to the **other's**.
+
+```mermaid
+sequenceDiagram
+ participant P as "Primary (Opus)"
+ participant B as "Broker (Redis / NATS)"
+ participant H as "Worker Stop-hook"
+ participant W as "Worker claude"
+
+ W->>H: "turn ends → Stop fires"
+ H->>B: "long-poll inbox-worker (BRPOPLPUSH, ≤30s)"
+ alt message queued
+ B-->>H: "payload"
+ H-->>W: "{decision: block, reason: [payload]} → new turn"
+ else timeout
+ B-->>H: "(nothing)"
+ H-->>W: "allow stop → idle"
+ end
+ Note over W,B: "worker writes result/question → inbox-primary"
+ W->>B: "XADD inbox-primary (result / question)"
+ P->>B: "Stop-hook long-poll inbox-primary (ON IDLE ONLY)"
+ B-->>P: "payload → primary resumes"
+```
+
+*Figure: a `Stop` hook long-polls the broker and injects any message as the block
+`reason`, forcing another turn; on timeout the session idles. Reversing it (worker →
+primary) is the same hook on the primary's inbox.*
+
+### Guardrails (mandatory for the async layer)
+
+| Risk | Mitigation |
+|------|------------|
+| **Primary quota burn** | The primary must **never** perpetual-poll. Use the `Stop`-hook variant (fires only at a natural idle boundary) or pull on-demand. Free busy-polling is for the off-subscription **worker** only. |
+| **Cross-agent ping-pong** | A→B→A→B can loop forever. `stop_hook_active` guards single-agent re-entry but **not** cross-agent. Carry a round/turn budget or a `no-reply-needed` sentinel in the message envelope. |
+| **Lost / double-processed messages** | Use a broker with **ack + visibility timeout + consumer groups** (Redis Streams `XACK`, NATS JetStream). A crash mid-turn re-delivers instead of dropping. |
+| **Context growth** | A perpetual worker's context window fills up. Cap idle cycles / tokens, then **stop and respawn fresh with state on the filesystem** (Ralph-loop). Perpetual != one infinite session. |
+
+## herdr — ops / observability plane (optional)
+
+[herdr](https://github.com/ogulcancelik/herdr) is a Rust **agent multiplexer** ("tmux for
+agents"): a persistent headless server + attachable TUI, each agent in a real PTY pane, a
+sidebar rolling every agent up to 🔴blocked / 🟡working / 🔵done / 🟢idle, all scriptable
+over a Unix-socket JSON API.
+
+It is **not** the transport and has **no** Claude-specific inject API — sending input is
+the generic `pane send-text` / `send-keys` / `run` PTY primitive, and its Claude state
+detection is **screen-manifest scraping**, not hooks. What it adds:
+
+- **`herdr agent wait --status idle|blocked`** — a readiness/blocked gate. An external
+ driver can `wait agent-status blocked → pane run ""` instead of guessing when a
+ pane is ready. This is the fragility that raw `send-keys` lacks.
+- **Persistent server, detach/reattach over SSH** — good for a long-lived off-subscription
+ worker on a remote box you occasionally eyeball.
+- **Multiplexing + state sidebar** — pays off once there is a *herd* of mixed workers.
+
+**Positioning:** AgentAPI (or the broker) is the **machine transport**; herdr is the
+**human ops layer**. You can run AgentAPI-wrapped or broker-driven workers *inside* herdr
+panes and use `agent wait` as the readiness gate. Caveats: replies from herdr are scraped
+buffer text (not parsed like AgentAPI's SSE), and it is a very young, single-developer
+project — don't put a core transport on it.
+
+## Deployment shape (target)
+
+```mermaid
+flowchart LR
+ subgraph mac["Your machine"]
+ OPUS["Primary Opus
(Claude Code)"]
+ end
+ subgraph host["Worker host (off-subscription)"]
+ direction TB
+ HS["herdr server (optional)"]
+ AA2["agentapi server"]
+ W2["worker claude"]
+ HS -.-> AA2 --> W2
+ end
+ BR["Broker"]
+ ML["ollama.ltms.dev / GX10 vLLM"]
+ OPUS -->|"HTTP"| AA2
+ OPUS -.->|"async"| BR
+ W2 -.-> BR
+ W2 --> ML
+ classDef sub fill:#2b6cb0,stroke:#1a365d,color:#ffffff;
+ class OPUS sub
+```
+
+*Figure: the worker (agentapi + optional herdr) lives on an off-subscription host near the
+model; the primary reaches it over HTTP (sync) and the broker (async).*
+
+## Related pages
+
+- **[[Approaches]]** — why AgentAPI, and the full transport comparison (SDK, Stop-hook, tmux)
+- **[[Setup]]** — running the worker agentapi server pointed at `ollama.ltms.dev`
+- **[[Operations]]** — health, restart, model swaps, troubleshooting
+
+## Sources
+
+- [coder/agentapi](https://github.com/coder/agentapi)
+- [herdr — GitHub](https://github.com/ogulcancelik/herdr) · [docs](https://herdr.dev/docs/) · [agents / state detection](https://herdr.dev/docs/agents/) · [CLI reference](https://herdr.dev/docs/cli-reference/)
+- [Agent Room — Stop-hook async collaboration](https://dev.to/agent-room/how-a-claude-code-stop-hook-unlocks-async-multi-agent-collaboration-no-polling-required-2e0e)
+- [Hooks reference — Claude Code Docs](https://code.claude.com/docs/en/hooks)