# AMQ Bridge — Architecture

> **Authority warning:** Current contract lives in [`docs/current/`](current/), accepted decisions in [`docs/adr/`](adr/), and verification rules in [`docs/e2e-test-matrix.md`](e2e-test-matrix.md). Sections below contain historical evolution and compatibility notes; do not implement retired polling/drain/reply-latest behavior.
>
> Current implementation note (ADR 0002 T1–T8): Pi receive is notify-pull/watch-driven. `amq_bridge_inbox` exposes envelope-only `new/` or `cur/` views; `amq_bridge_read` is the body boundary; `amq_bridge_resolve` closes messages. Watch ownership is shared by `(root, self)`, with secondary sessions read-only. Historical sections below that describe polling, drain, inbox buffers, or reply-latest are superseded by ADR 0002 §8c–§8e.

> Version 0.1.5 · branch `main`
> Companion docs: [current receive contract](current/receive-contract.md), [harness/transport boundaries](current/harness-and-transport-boundaries.md), [ADR-0001](adr/0001-peer-discovery-roster.md), [ADR-0002](adr/0002-notify-pull-receive-path.md), [ADR-0003](adr/0003-reliability-operability-hardening.md), [ADR-0004](adr/0004-harness-neutral-core.md)
> Historical sections retained for migration context; current behavior is defined by executable code/tests and current contract docs.

## 1. What it is

AMQ Bridge plugs live agent sessions together through a shared filesystem mailbox.
Two Pi sessions in different directories attach to the same AMQ root, exchange
messages through the `amq` CLI, and react to each other — with no server, no
daemon, no HTTP.

```text
Pi session A ──► bridge ──► transport ──► shared mailbox (AMQ root)
Pi session B ──► bridge ──► transport ──► shared mailbox (AMQ root)
```

**Core principle (from decision-log):** AMQ is the *transport*, not the
*scheduler*. The bridge owns collaboration semantics: identity, peer roster,
processed-id dedupe, actionability, explicit reply/read/resolve lifecycle, and
injection into agent turns. AMQ only stores and moves messages.

### Goals

- one shared mailbox, any runtime (Pi today; Codex/OpenCode by design)
- transport behind an interface — swap the bus without touching agent logic
- attach/detach on demand, no session restart
- agent can use the bridge autonomously via registered tools
- evidence-first output: ids, threads, roots, kinds visible everywhere

### Non-goals (current)

- task scheduling / pending queue (roadmap v0.2)
- protocol envelope (roadmap v0.2)
- observability events / transcripts (roadmap v0.3)
- multi-peer ambiguity guards beyond the roster (roadmap v0.4)

---

## 2. High-level architecture

```mermaid
flowchart LR
    subgraph Session["Pi session (in-process extension)"]
        TUI["/amq-bridge commands<br/>+ status badge"]
        TOOLS["amq_bridge_send / reply / inbox / status"]
        INJ["context injection<br/>(amq-bridge-context entry)"]
        LOOP["inbox loop (watcher)"]
        STATE["BridgeState v2<br/>(session entry + state.json)"]
    end

    subgraph CLI["CLI mode (out-of-process sidecar)"]
        ATTACH["scripts/attach.mjs"]
        WORKER["attach-worker.mjs → runBridgeSidecar"]
        STAT["scripts/status.mjs"]
        DETACH["scripts/detach.mjs"]
    end

    subgraph Core["Bridge core (src/)"]
        RUNTIME["runtimes/ — identity + root resolution"]
        BEHAV["behaviors/ — echo-peer policy"]
        TRANSPORT["transports/amq-client.mjs<br/>AMQ CLI wrapper"]
        CONTRACT["transports/contract.mjs — interface check"]
    end

    subgraph AMQ["AMQ (external, brew/tap)"]
        MAILBOX[("filesystem mailbox<br/>~/.amq-bridge/mail")]
    end

    TUI --> STATE
    TOOLS --> TRANSPORT
    LOOP --> TRANSPORT
    INJ --> STATE
    ATTACH --> WORKER
    WORKER --> RUNTIME
    WORKER --> TRANSPORT
    STAT --> RUNTIME
    DETACH --> RUNTIME
    TRANSPORT --> MAILBOX
    CONTRACT --> TRANSPORT
    RUNTIME --> BEHAV
    RUNTIME --> TRANSPORT
```

Two runtime modes share the same core:

| Mode | Entry point | Process model | Use case |
|---|---|---|---|
| **Pi extension** | `integrations/pi-extension.ts` | in-process inside Pi | real sessions, tools, auto-inject, badge |
| **CLI scripts** | `scripts/attach.mjs` etc. | detached child process (sidecar) | scripted attach, demos, non-Pi runtimes |

---

## 3. Module map

```mermaid
flowchart TD
    ROOT["repo root"]
    ROOT --> PKG["package.json<br/>pi extensions + skills, scripts, peer/dev deps"]
    ROOT --> SRC["src/"]
    ROOT --> INT["integrations/"]
    ROOT --> SCR["scripts/"]
    ROOT --> TESTS["tests/"]
    ROOT --> DOCS["docs/"]
    ROOT --> E2E["run-cross-cwd-e2e.sh<br/>run-real-pi-e2e.sh"]
    ROOT --> TSC["tsconfig.json + src/transports/amq-client.d.mts<br/>(typed surface for the .mjs transport)"]

    SRC --> SRC_TRANSPORT["transports/amq-client.mjs — AMQ CLI wrapper<br/>transports/contract.mjs — required-method check"]
    SRC --> SRC_RUNTIME["runtimes/pi.mjs — pi identity + prompt<br/>runtimes/session.mjs — generic session identity"]
    SRC --> SRC_BRIDGE["bridge/sidecar.mjs — CLI polling loop<br/>bridge/attachment.mjs — attachment files (write/read helpers currently uncalled; sidecar writes pid + log directly)<br/>bridge/session-store.mjs — session files + liveness (currently unused; persistence lives in pi-extension.ts)"]
    SRC --> SRC_BEHAV["behaviors/echo-peer.mjs — ping/pong policy"]
    SRC --> SRC_MAILBOX["mailbox.mjs — early legacy transport<br/>(superseded by transports/amq-client.mjs)"]

    INT --> EXT["pi-extension.ts — the whole Pi integration"]
    INT --> COMMON["common/components/TextOverlay.ts — pi-tui overlay<br/>common/utils/generateHandle.ts — random handle"]
    INT --> SKILL["pi-skill/SKILL.md — agent-facing instructions"]

    SCR --> SCR_ATTACH["attach.mjs (spawn worker) / attach-worker.mjs (sidecar)"]
    SCR --> SCR_LIFE["status.mjs / detach.mjs"]
    SCR --> SCR_DEMO["demo.mjs / agent.mjs / pi-agent.mjs / pi-demo.mjs"]
    SCR --> SCR_SMOKE["pi-extension-smoke / -tool-smoke / -pty-smoke / -multipeer-smoke"]

    TESTS --> T_UNIT["8 unit tests in 5 files<br/>(attach, install, mailbox, runtime-pi, transport-contract)"]
```

Dependency rules (from `docs/architecture.md` + code):

- **transport stays thin** — no business logic, no scheduler
- **runtime adapters are thin** — only identity + root + mode
- **behavior is generic** — one conversation policy reusable across runtimes
- **extension glue is runtime-specific** — Pi event/tool/command API lives in the
  extension, never in `src/`
- **transport swap touches only `src/transports/`**

---

## 4. Runtime modes in detail

### 4.1 Pi extension (`integrations/pi-extension.ts`, ~1050 lines)

The extension is the primary product. It registers into Pi's `ExtensionAPI`:

| Kind | Name | Notes |
|---|---|---|
| Tools | `amq_bridge_send` | `to` defaults to `primaryPeer`; evidence output |
| Tools | `amq_bridge_reply` | **fail-closed**: `>1` actionable pending + no `messageId` → reject with `ambiguousReplyText` |
| Tools | `amq_bridge_inbox` | reads durable inbox buffer (deduped, capped at 100); tool takes optional `limit` |
| Tools | `amq_bridge_status` | identity, peers, primary, root |
| Command | `/amq-bridge` | subcommands + `getArgumentCompletions` |
| Event | `session_start` | restore state, badge, start inbox loop |
| Event | `session_shutdown` | abort watcher |
| Event | `context` | inject `amq-bridge-context` custom entry every turn |
| Side-effect | status badge | `[self] ↔ peers` via `ui.setStatus` |
| Side-effect | inbox loop | auto-poll + inject `amq-bridge-inbox` follow-up turns |

**State persistence has two sources of truth that are reconciled:**

1. **Pi session entries** — `pi.appendEntry('amq-bridge-state', state)` writes a
   `custom` entry into the session transcript. `latestSessionBridgeState()` scans
   the branch entries backwards for the newest usable one. Survives restarts via
   the session file.
2. **Legacy file** — `.amq-bridge/sessions/<key>/state.json` holds v1
   `{self, peer, root}`. `fromLegacyState()` upgrades it to v2 in memory.

```mermaid
flowchart TD
    A["readState(file)"] --> B{"state.json has self+peer+root?"}
    B -- yes --> C["fromLegacyState → v2 BridgeState (only if root is absolute)"]
    B -- no --> D["{}"]
    C --> E["latestSessionBridgeState(session entries)"]
    D --> E
    E --> F{"newest custom amq-bridge-state entry?"}
    F -- yes --> G["use session entry (isUsableBridgeState check)"]
    F -- no --> H["use legacy result"]
    G --> I["withExtraPeers() merge handshake peers"]
    H --> I
    I --> J["BridgeState v2"]
```

### 4.2 CLI sidecar mode

```mermaid
sequenceDiagram
    participant U as User / script
    participant A as attach.mjs
    participant W as attach-worker.mjs (detached)
    participant S as runBridgeSidecar
    participant T as amq transport
    participant M as AMQ mailbox

    U->>A: node scripts/attach.mjs --name aamir --peer aadil
    A->>A: resolveBridgeSession (argv/env/root)
    A->>A: mkdir attachments/pi/aamir, rm stale pid
    A->>W: spawn detached (stdio ignore, unref)
    W->>S: runBridgeSidecar(transport, files, session)
    S->>M: initMailbox (coop init --agents aamir,aadil)
    S->>M: send 'attached' housekeeping (thread bridge/attach/aadil__aamir)
    loop every 15s timeout / 250ms poll
        S->>M: waitForMessages
        M-->>S: new messages?
        S->>M: drainInbox --include-body
        S->>S: append to messages.log + stdout
        S->>S: onMessage hook (no-op here — attach-worker passes none; echo-peer policy only via scripts/agent.mjs)
    end
    U->>A: node scripts/detach.mjs ...
    A->>W: SIGTERM → shutdown() → rm pid, exit
```

Sidecar file layout (`attachments/<runtime>/<name>/`):

| File | Meaning |
|---|---|
| `state.json` | attachment metadata — helpers `write/readAttachmentState` exist but are **currently uncalled**; the sidecar flow writes `pid` + `messages.log` only |
| `pid` | sidecar PID (liveness via `process.kill(pid, 0)`) |
| `messages.log` | append-only formatted message log |

`status.mjs` reads `pid` liveness and **tails `messages.log`** (no `state.json`
read). `detach.mjs` SIGTERMs the sidecar and removes the pid file.

### 4.3 Demo / agent mode

- `scripts/demo.mjs` — temp root, spawns two `agent.mjs` processes
- `scripts/agent.mjs` — standalone peer using `createAmqTransport()` +
  `runEchoPeer()` (alice pings, bob replies)
- `scripts/pi-agent.mjs` — asks Pi CLI to run the agent via `buildBridgePrompt()`
- `scripts/pi-demo.mjs` — two Pi CLI sessions exchanging messages

---

> **Historical detail boundary:** Sections 5–6 and the older transport diagrams below describe pre-ADR-0002 polling/buffer behavior. They remain for decision history only. Current identity/ownership, receive, tool, and transport behavior is defined by `docs/adr/0002-notify-pull-receive-path.md` §8c–§8e and the T1–T8 implementation.

## 5. Identity, root, and state model

### 5.1 Root resolution

```mermaid
flowchart TD
    A["bridgeRoot(cwd)"] --> B{"PI_AMQ_ROOT env?"}
    B -- yes --> C["use it"]
    B -- no --> D{".pi/amq-bridge.json exists?"}
    D -- yes --> E["{ root } — absolute, or join(cwd, root)"]
    D -- no --> F["~/.amq-bridge/mail (user-global)"]
    C --> G["root"]
    E --> G
    F --> G
    G --> H["same root from any cwd → cross-cwd communication works"]
```

### 5.2 State shapes

```mermaid
classDiagram
    class BridgeState {
        version: 2
        attached: boolean
        self?: string
        root?: string
        peers: Record~handle, BridgePeer~
        primaryPeer?: string
        updatedAt: string
    }
    class BridgePeer {
        handle: string
        attachedAt: string
        status?: string
        source?: "attach" | "discover" | "manual" | "handshake"
        lastSeen?: string
    }
    class InboxBuffer {
        at: string
        messages: AmqMessage[]
        + cap 100, dedupe by id
    }
    class InjectedIds {
        at: string
        ids: string[]
        + cap 500, prevents re-injection
    }
    class LegacyV1 {
        self: string
        peer: string
        root: string
    }
    BridgeState --> BridgePeer : peers
    BridgeState --> LegacyV1 : upgraded via fromLegacyState
    BridgeState --> InboxBuffer : sibling file
    BridgeState --> InjectedIds : sibling file
```

Persistence locations under the *current working directory's* bridge dir
(`.amq-bridge/`), keyed by session id (`safeKey(sessionName || sessionId)`):

```text
.amq-bridge/
  sessions/<key>/
    state.json        # v1 legacy {self, peer, root}
    inbox.json        # durable inbox buffer {at, messages[]}
    injected.json     # injected message ids {at, ids[]}
    extra-peers.json  # handshake-discovered peers {peers[]}
  attachments/<runtime>/<name>/   # CLI sidecar mode
    state.json pid messages.log
```

> Note: AMQ mailbox root (default `~/.amq-bridge/mail`) and the bridge's own
> bookkeeping dir (`.amq-bridge/` in cwd) are different things. The mailbox is
> where messages live; the bridge dir is where buffer/state metadata lives.

---

## 6. Message flow

### 6.1 Send

```mermaid
sequenceDiagram
    participant A as Agent (tool/command)
    participant E as pi-extension
    participant T as amq-client.mjs
    participant M as AMQ mailbox

    A->>E: amq_bridge_send / /amq-bridge send
    E->>E: resolve to (--to || primaryPeer || prompt — explicit target wins), self, root
    E->>T: sendMessage({root, me, to, body, kind: params.kind ?? 'status', thread})
    T->>M: amq send --json (execFileSync)
    M-->>T: JSON message (id, thread, ...)
    T-->>E: AmqMessage (fallback object on parse error)
    E-->>A: formatEvidence → id/from/to/kind/subject/thread/root/body
```

`sendMessage` wraps `amq send`; failures throw (`amq ... failed: <stderr>`) —
no fallback path yet (roadmap: reply-failure recovery). `amq_bridge_send` with no
`to`/primary fails closed with "No peer attached." — the **tool** has no prompt
fallback; only `/amq-bridge send` prompts for a target.

### 6.2 Receive + auto-injection (the inbox loop)

```mermaid
sequenceDiagram
    participant M as AMQ mailbox
    participant L as startInboxLoop (watcher)
    participant B as inbox.json buffer
    participant P as Pi session

    loop every 15s timeout, 500ms poll, abortable
        L->>M: waitForMessages
        M-->>L: batch or empty
        alt messages present
            L->>M: drainInbox --include-body
            L->>B: appendDedupeMessages (by id, cap 100) → write
            L->>L: housekeeping? → handshake: addExtraPeer(from)
            L->>B: unseenActionableMessages (not in injected.json)
            alt actionable unseen exists
                L->>B: markInjected(ids)
                L->>P: pi.sendMessage(customType amq-bridge-inbox,<br/>triggerTurn:true, deliverAs followUp)
                P-->>P: agent sees "untrusted peer data" + message list
            end
        end
    end
```

Key behaviors encoded in the loop:

- **Dedupe** — `appendDedupeMessages` merges by `message.id` (fallback key:
  from|to|thread|subject|body), keeps last 100.
- **Housekeeping suppression** — `isHousekeepingMessage` matches
  `subject === 'attached' && thread.startsWith('bridge/attach/')`; these never
  get injected and never count as actionable.
- **Actionability** — everything non-housekeeping is actionable. (Roadmap kind
  policy — `question/answer/status/...` — is *not* implemented yet. Today the
  `/amq-bridge send` command hardcodes `kind: 'status'`; the `amq_bridge_send`
  tool passes `params.kind` through; `amq-client` defaults to `'status'`.)
- **TUI-gated** — the loop (and injection) starts only when
  `ctx.mode === 'tui'`; non-TUI sessions get no auto-injection.
- **No re-injection** — `injected.json` records processed ids (cap 500).
- **Ambiguity fail-closed** — `amq_bridge_reply` without `messageId` when `>1`
  actionable pending → returns `ambiguousReplyText` listing ids instead of
  guessing "latest".
- **Peer injection guard** — `a384328` avoids injection during print runs.

### 6.3 Attach lifecycle (extension)

```mermaid
sequenceDiagram
    participant U as User
    participant C as /amq-bridge attach
    participant S as attachSession
    participant T as transport
    participant M as Mailbox

    U->>C: attach <peer> [self]
    C->>C: self = args || session name || input prompt
    C->>S: attachSession(pi, cwd, key, self, peer, existing)
    S->>S: base state (keep existing or fresh v2)
    S->>S: addPeer(peer, 'attach') → connected, primary if first
    S->>T: initMailbox({root, agents:[self,...peers]})
    S->>S: persistBridgeState → session entry + legacy state.json
    S->>T: sendMessage subject='attached' thread=bridge/attach/...
    S-->>C: "AMQ Bridge attached. You are: ..."
    C->>C: badge [self] ↔ peers, start inbox loop if TUI
```

`/amq-bridge connect` is the TUI variant: discovers candidates, `ui.select`
picker, then `addPeer(..., 'discover')` + same attach handshake.

Thread naming: housekeeping handshakes live on `bridge/attach/<sorted>`;
real peer traffic uses `p2p/<sorted>` (e.g. `p2p/aadil__aamir`).
`attachSession` keeps an existing roster only when `existing.self === self`;
re-attaching under a new self identity discards it. `withExtraPeers` falls back
`primaryPeer` to the first handshake-discovered peer when none is set.

### 6.4 Detach

`detachSession` aborts the watcher, runs `scripts/detach.mjs` (SIGTERM sidecar,
rm pid), removes bridge state dir + attachment dir, appends `attached:false`
entry. `session_shutdown` also aborts watchers. Note: detach sends **no**
`detached` housekeeping message to peers (ADR-0001 open question).

Edge: `isUsableBridgeState` treats an `attached:false` entry as usable, so the
newest detached entry shadows legacy state; a stale/unusable latest entry makes
`session_start` append a `staleCleared:true` entry + warning notify.

---

## 7. Peer discovery & roster

```mermaid
stateDiagram-v2
    [*] --> Available: amq who / presence list
    Available --> Connected: /amq-bridge peer add<br/>/amq-bridge connect
    Available --> Available: stale, cross-project (show root + freshness)
    Connected --> Primary: /amq-bridge peer primary
    Primary --> Connected: "/amq-bridge peer primary <other>"
    Connected --> [*]: /amq-bridge peer remove / detach
    Primary --> [*]: remove (reassigns to first remaining)
```

Sources merged in `discoveryReport()`:

| Source | API | Provides |
|---|---|---|
| presence | `amq presence list --json` | `handle`, `status`, `last_seen` |
| who | `amq who --json` | per-session `agents[]`, `active` |

Rules (ADR-0001):

- **Available ≠ connected/trusted** — discovery shows root + freshness, stale
  agents stay visible (`○`), active are `●`, connected get `✓`.
- Discovery merges `presence list` + `who` sources only; there is no separate
  config-handle source today (a future ADR-0001 aspiration, not current code).
- `discoveredAgents` filters out self, marks `connected | active | available`.
- Badge (current code): `[self] ↔ peer` / `[self] ↔ a, b` — **all** peers joined
  with commas (`plainStatusLabel`/`styledStatusLabel`); **no `+N` truncation**
  (roadmap does not specify one). Per-peer `primary/source/seen` comes from
  `/amq-bridge peers` (`summarizePeers`); `/amq-bridge status` shows only the
  label + You are / Peers / Primary / Root.

Discovery is CLI-first (public AMQ APIs), not mailbox-file introspection.

---

## 8. Transport layer

### 8.1 Interface contract

`assertBridgeTransport(transport)` requires: `initMailbox`, `sendMessage`,
`listInbox`, `drainInbox`, `replyTo`, `waitForMessages`, `formatMessage`,
`hasAmq`. `createAmqTransport()` returns the full AMQ implementation (17
functions incl. `listPresence`, `who`, `listReceipts`, `waitReceipt`,
`monitorPeek`, `readMessage`, `threadMessages`, `doctor`).

The `.mjs` files are untyped at runtime; `src/transports/amq-client.d.mts`
provides the typed surface for IDE/tsc (added 2026-07-31).

### 8.2 AMQ commands used

| Purpose | amq CLI |
|---|---|
| mailbox init | root-scoped kernel advisory lock (`lockf`/`flock`) → strict `who --root --json` → union existing + requested handles → `coop init --root --agents <union> --force --no-gitignore` |
| send | `send --root --me --to --kind --subject --body [--thread] [--wait-for] [--wait-timeout]` |
| list unread | `list --root --me --new --json` |
| drain | `drain --root --me --include-body --json` |
| reply | `reply --root --me --id --kind --body [--subject] [--wait-for] [--wait-timeout]` |
| presence | `presence list --root --json` |
| identity | `who --root [--me] --json` |
| receipts / monitor / read / thread / doctor | exercised by unit tests (`tests/mailbox.test.mjs`) only — `waitReceipt` + `readMessage` have **zero callers** anywhere |

All calls go through `execFileSync('amq', ...)`; JSON parsed with fallback
objects so transient parse failures degrade gracefully.

### 8.3 AMQ mailbox internals (under the hood)

AMQ is a **filesystem mailbox**: zero daemon, zero ports. Messages are
markdown files with a `---json` frontmatter envelope, stored under a shared
root (default `~/.amq-bridge/mail`). Verified against a live mailbox
(2026-07-31).

```text
~/.amq-bridge/mail/
  agents/<handle>/
    inbox/{cur,new,tmp}/   # maildir: new = undelivered, cur = delivered, tmp = in-flight
    outbox/sent/*.md       # sender's copy of every sent message
    receipts/*.json        # delivery evidence (stage: drained, ...)
    presence.json          # {schema, handle, status, last_seen} — discovery source
    dlq/{cur,new,tmp}/     # dead-letter queue
  collab/agents/           # global registry of known handles (from coop init)
  meta/config.json         # coop project config {version, created_utc, agents[]}
  threads/                 # reserved
```

**Message file format** — `.md` with frontmatter envelope + markdown body:

```markdown
---json
{
  "schema": 1,
  "id": "2026-07-30T07-40-03.830Z_pid3981_abc808b0",
  "from": "sugar",
  "to": ["coffe"],
  "thread": "bridge/attach/coffe__sugar",
  "subject": "attached",
  "created": "2026-07-30T07:40:03.830355Z",
  "priority": "normal",
  "kind": "status"
}
---
pi attached
```

- `id` scheme: `<ISO-ts>_pid<pid>_<hash>` — unique per process write.
- body is free markdown; envelope carries routing/kind/thread metadata.

**Send = file writes** (`amq send --root R --me A --to B ...`):

```mermaid
flowchart LR
    A["amq send"] --> W1["write agents/B/inbox/new/&lt;id&gt;.md"]
    A --> W2["write agents/A/outbox/sent/&lt;id&gt;.md"]
    W1 --> M["recipient scans inbox/new"]
```

**Receive = maildir move** (`amq list --new` → `amq drain`):

```mermaid
flowchart LR
    L["list --new"] --> N["scan agents/A/inbox/new/"]
    N --> D["drain"]
    D --> C["move new → cur (delivered)"]
    D --> R["write receipts/&lt;id&gt;__A__drained.json"]
    D --> P["return parsed envelope + body"]
```

Receipt shape:

```json
{"schema":1,"msg_id":"...","thread":"...","sender":"aadil","consumer":"aamir","stage":"drained","emitted_at":"..."}
```

**Consequences the bridge must live with:**

- **Consumed-once** — `drain` moves `new→cur` and writes a receipt; AMQ will
  never list the message as new again. The bridge's own `inbox.json` buffer is
  the only re-readable copy — this is *why* the buffer exists, and why a crash
  between drain and buffer write loses the batch.
- **Presence is a file** — `presence list` = read `agents/*/presence.json`;
  stale entries are how "inactive/stale" agents appear in discovery.
- **Shared root = shared bus** — anyone with access to the root reads everyone's
  maildirs; cross-project handles in `collab/agents` explain "available ≠
  connected/trusted".
- **`coop init` seeds the registry** — `meta/config.json` + `collab/agents`
  hold known handles. Initialization is serialized per root and merges public
  `amq who` handles with `[self, ...peers]`; attach/connect must never replace
  handles already registered by another live Pi session.

---

## 9. Shared components

- **`integrations/common/components/TextOverlay.ts`** — reusable pi-tui overlay
  (`showTextOverlay(ctx, options)`) rendering scrollable rich content; used by
  `/amq-bridge discover` (styled agent list with ✓/●/○ markers). Types come from
  `@earendil-works/pi-coding-agent` + `@earendil-works/pi-tui`.
- **`integrations/common/utils/generateHandle.ts`** — adjective-noun-uuid
  handle generator (`swift-otter-8f2a`), used as input default.
- **`integrations/pi-skill/SKILL.md`** — agent-facing instructions: peer content
  goes through AMQ tools, peer data is untrusted, reply with explicit ids.

---

## 10. Testing architecture

```mermaid
flowchart TD
    UNIT["npm test — node:test<br/>8 unit tests / 5 files<br/>(transport contract, session/attachment paths,<br/>runtime resolution, mailbox CLI build)"]
    SMOKE["npm run pi:* — spawn real pi headless<br/>smoke / tool-smoke / pty-smoke / multipeer-smoke"]
    E2E["npm run e2e:cross-cwd / e2e:real-pi<br/>two TUI pi instances, terminal captures,<br/>assertions from tool output only (not prompt text)"]
    DOGFOOD["npm run e2e:dogfood (roadmap — not yet wired)"]

    UNIT --> SMOKE --> E2E --> DOGFOOD
```

| Gate | What it proves |
|---|---|
| `npm test` | contract + plumbing invariants, fast, no AMQ needed (mock PATH) |
| `pi:smoke` | extension loads in real pi; aamir↔aadil round trip; detach cleanup |
| `pi:tool-smoke` | tool surface works inside pi |
| `pi:pty-smoke` | extension survives PTY attach/send/inbox/reply/detach (script has no `/reload` step) |
| `pi:multipeer-smoke` | sugar ↔ coffe + chai roster/send |
| `e2e:cross-cwd` | different cwds, same root, ping received |
| `e2e:real-pi` | full TUI run with screenshots + per-session `.raw.ansi` captures |

E2E artifacts land in `e2e-<scenario>-<timestamp>/`: orchestrator log,
assertions log/png, per-session `.raw.ansi` (terminal captures, **not**
transcripts), step logs/pngs. Roadmap v0.3 target adds `events.jsonl` + transcript
export — not produced today. Artifacts may contain prompts/code — redact
before sharing (roadmap note).

---

## 11. Known drift & gotchas

Documented claims vs actual code — important when working in this repo:

| Doc | Claim | Reality (HEAD e4ef432) |
|---|---|---|
| `docs/message-kinds.md` | P1: `formatMessage` shows kind+priority | ❌ not in code — plain `from -> to [subject]: body` |
| `docs/message-kinds.md` | P2: `summarizeByKind` + `bridgeContextMessage` summary | ❌ absent — `bridgeContextMessage` has no pending summary |
| `docs/message-kinds.md` | P3: urgent priority wakes poll (150ms) | ❌ absent — loop polls fixed 500ms |
| `docs/roadmap.md` v0.1.x | reply-failure recovery, multi-question tests | ❌ not implemented |
| `docs/roadmap.md` v0.1.x | suppress housekeeping auto-turns | ✅ implemented (`isHousekeepingMessage` filters `bridge/attach/` housekeeping from actionable + injection; roadmap checkbox is stale) |
| `docs/roadmap.md` | `/amq-bridge attach --add <peer>` | ❌ no `--add` flag |
| `docs/roadmap.md` multi-peer | badge `+N` truncation | ❌ not implemented; not even specified in roadmap |
| `docs/message-kinds.md` | "11 unit tests", "mailbox.mjs removed" | ❌ false — 8 tests in 5 files; `src/mailbox.mjs` still present (legacy, unimported) |
| `docs/architecture.md` (old) | `/agent-bridge` commands | ✅ renamed to `/amq-bridge` |
| README install | `pi install npm:agent-message-bridge` | package is `amq-bridge` (name mismatch) |

Other gotchas:

- `kind` is effectively always `status` today; kind policy is roadmap v0.2.
- `drainInbox` drains before durable processing completes — crash between drain
  and buffer write can lose a batch (roadmap: durable queue).
- Reply "latest" fallback is safe only while ≤1 actionable message is buffered;
  multi-question hazard (Q1/Q2 mix-up) is the roadmap's core motivation.

---

## 12. Extension points

```mermaid
flowchart LR
    subgraph New["Add a runtime (Codex/OpenCode)"]
        N1["1. runtime adapter in src/runtimes/"]
        N2["2. reuse transport interface + behavior"]
        N3["3. extension glue in integrations/ (per-host API)"]
    end
    subgraph Swap["Swap the bus"]
        S1["new src/transports/<bus>.mjs"]
        S2["satisfy assertBridgeTransport"]
    end
    N1 --> N2 --> N3
    S1 --> S2
```

Cheat sheet for contributors:

- **Change message formatting/evidence** → `src/transports/amq-client.mjs`
  (`formatMessage`, `formatEvidence`) + keep `amq-client.d.mts` in sync.
- **Change injection/actionability/reply safety** → `integrations/pi-extension.ts`
  (`startInboxLoop`, `actionableMessages`, `ambiguousReplyText`,
  `appendDedupeMessages`).
- **Change roster/discovery** → `discoveryReport`, `discoveredAgents`,
  `addPeer/removePeer/setPrimaryPeer`, `persistBridgeState`.
- **New subcommand** → `COMMAND_COMPLETIONS` + handler in `/amq-bridge` command.
- **Type errors** → `npx tsc --noEmit` (strict, bundler resolution,
  `allowJs`), declaration surface in `amq-client.d.mts`.
