bridge
A local web board driven entirely by shell commands. The agent feeds cards, events, and
messages; the human watches live, replies in context, and moves cards. Agent-agnostic:
any agent with shell access can drive it.
- Server (
server.js): node built-ins only, zero deps. State persists in
~/.bridge/boards/<name>.json; archived cards append to <name>.archive.jsonl.
- CLI (
bridge-axi): the agent's whole interface. Never talk HTTP directly; run
bridge-axi with no args for full usage. All commands take --port (default 4777)
and --board (default default).
- UI (
ui/): vanilla ES modules, no build step. Chat on the left (full height),
board on the right; collapses to Chat/Board tabs on phones. Card detail shows
attributes + markdown body + event timeline; the "💬 talk" button switches the chat
window into that card's thread. Notification bell, drag&drop, long-press move menu,
filters, label registry, optional TTS voice.
Core model
- Columns are owned state, ordered, set via
bridge-axi columns (idempotent). A card
sits in exactly one column; every move is a deliberate act that records a timeline event
with its actor. Automated feeders must never move cards.
- Cards:
{id, title, column, labels, attributes, body, events, thread}.
attributes: generic key/value pairs (URLs render as links). Well-known keys the UI
understands: type (sets the tile emoji: plan 🧠, implementation 🔥,
investigation 🕵️♂️; unknown types get a neutral marker), emoji (explicit override),
owner (colored, groupable, click-to-filter), prs (list of
{url, state: open|merged|closed} — state-colored chips), artifacts (list of
{uri, label} — resources hung on the card).
body: markdown, the card's CURRENT state — rewrite it as work evolves.
events: append-only timestamped timeline. Level 2 = timeline only; level 1 = also a
notification. kind is an open token: a board can register its own kinds map with
bridge-axi kinds <file.json|-> ({"<kind>": {"emoji": "…", "level": 1|2}},
idempotent replace like columns; bare bridge-axi kinds prints the effective map).
Built-ins ship for the bridge's own operations, overridable by the registered map:
created 🐣 2, moved 🔁 2, handoff 👀 1, landed 🏁 1, killed 🪦 2,
resurrected 🧟 1, question 🙋 1. On append, an explicit --level wins, else the
kind's level from the effective map, else 2; a kind in neither map is stored as-is
(opaque token, no emoji).
labels: USER-owned (edited in the UI, managed registry with colors). Agents never
set or rewrite them.
- Unified event stream: board-level events + every card's events, one global sequence.
The notification queue is the level-1 slice; suppressed level-2 events expand inline in
the bell dropdown. Per-user read state persists server-side in the board file.
- Kill = archive:
bridge-axi archive <id> snapshots the card to the append-only
archive file and removes it from the board. No destructive delete; bridge-axi archived
lists recent kills. The board event is typed by reason: merged → landed 🏁 (level 1),
killed → killed 🪦 (level 2 — the human's own act, no bell). An archived card can be
restored: bridge-axi restore <id> brings the most recent snapshot back in full (body,
events, thread, frozen column) with a loud level-1 resurrected 🧟 event; the archive
record stays, so the board is truth for liveness — an
archive record never by itself means the card is off the board. A card restored into a
column that has since been removed from the frame won't render until moved.
Agent loop
bridge-axi open — start the server if needed (idempotent), print the URL once.
Binds 127.0.0.1 by default (localhost-only). To reach the board from other devices,
the user sets "host" in ~/.bridge/config.json (e.g. a VPN/tailnet interface IP) —
machine-private config, never baked into commands or docs; --host overrides for one
run. A non-loopback bind also listens on 127.0.0.1 so local CLI calls keep working.
- Feed reality:
create new cards, patch bodies/attributes as state evolves, event
for timeline signals (level 1 only for things the human must see), move for real
state transitions (an agent move defaults to a handoff that notifies; pass
--kind moved for a quiet reshuffle), archive when work is dead or landed,
restore to resurrect an archived card, say to talk.
- Keep
bridge-axi poll running as a tracked background task. It BLOCKS until human
input, prints JSON lines, and exits; handle each line, reply with
bridge-axi say <target> --text-file <f>, then bridge-axi ack <seq> (highest seq
handled), re-run poll. Lines carry kind:
message — human message; target is chat or card:<id>.
card-created — the human made a card in the UI (target names it). Awareness
only: creating a card is not a demand — a card with an empty thread owes nothing.
Act when the human speaks in its thread or main chat.
card-moved — the human moved a card (from/column fields): a handoff or a
handback, act accordingly.
- Always answer feedback with
say to the same target — the UI shows "agent is working…"
until the reply lands, and flips to an amber "may be stuck" warning if no reply comes
within ~3 minutes (BRIDGE_AWAITING_STALE_SECS on the server overrides).
Rules:
- Message/body text goes via
--text-file/--body-file or stdin — never interpolated
into a shell command.
- Delivery is at-least-once: feedback counts as delivered only when
acked, never by
being polled. An unacked line is re-offered by every poll — so a poller killed
mid-handling loses nothing, and a repeated line (same seq) just means the previous
handling never acked; dedupe by seq. Ack only what you have actually handled.
- Cards are for units of work, not for questions — ask questions in a thread (
say) plus
a level-1 question event on the card the question belongs to.
Notifications and read state
The bell is a derived view of everything the human hasn't seen yet — one read-state
mechanism, two scopes: level-1 events (signal emojis) UNION unseen agent thread replies
(kind reply; no seq of their own, so they ride the per-thread read marker and show the
💡 fallback emoji — the "· N events ·" gap dividers skip them). Opening a card clears its
unread (events and replies alike); "mark all" marks the event items read AND advances the
thread read markers of cards with unseen replies, so it also clears those cards' unread.
Read state lives in the board JSON per user (default user user), so it survives reloads
and devices. Thread unread badges (chat bubbles, tiles) use the same per-thread read
markers, also server-side.
Voice
~/.bridge/config.json {"voices": ["Some Voice", "Another"]} — case-insensitive
substring filter for the UI's TTS voice dropdown; absent/empty = full list. Manage with
bridge-axi config voices "a,b" / config show. The 🔊 toggle persists per browser and
speaks new agent messages (emojis stripped).
Migration
node migrate-v1.js <v1-board.json> [out.json] converts a v1 board doc (columns/cards
with summary/detail_md/badges/links, threads, chat, labels) to the v2 model. It never
writes in place and never touches a server: stop the server, convert, move the file into
~/.bridge/boards/ yourself.
See README.md for the HTTP API reference.
1---2name: bridge3description: Run a live "agent board" — a local web UI where a human follows an AI agent's work as a kanban of cards with timelines and notifications, and talks to the agent in context (unified chat with per-card threads). Triggers when the user wants a live board/dashboard of agent work, wants to "open the bridge", or an agent needs a visual command surface for a human to follow along and steer.4---56# bridge78A local web board driven entirely by shell commands. The agent feeds cards, events, and9messages; the human watches live, replies in context, and moves cards. Agent-agnostic:10any agent with shell access can drive it.1112- **Server** (`server.js`): node built-ins only, zero deps. State persists in13 `~/.bridge/boards/<name>.json`; archived cards append to `<name>.archive.jsonl`.14- **CLI** (`bridge-axi`): the agent's whole interface. Never talk HTTP directly; run15 `bridge-axi` with no args for full usage. All commands take `--port` (default 4777)16 and `--board` (default `default`).17- **UI** (`ui/`): vanilla ES modules, no build step. Chat on the left (full height),18 board on the right; collapses to Chat/Board tabs on phones. Card detail shows19 attributes + markdown body + event timeline; the "💬 talk" button switches the chat20 window into that card's thread. Notification bell, drag&drop, long-press move menu,21 filters, label registry, optional TTS voice.2223## Core model2425- **Columns are owned state**, ordered, set via `bridge-axi columns` (idempotent). A card26 sits in exactly one column; every move is a deliberate act that records a timeline event27 with its actor. Automated feeders must never move cards.28- **Cards**: `{id, title, column, labels, attributes, body, events, thread}`.29 - `attributes`: generic key/value pairs (URLs render as links). Well-known keys the UI30 understands: `type` (sets the tile emoji: plan 🧠, implementation 🔥,31 investigation 🕵️♂️; unknown types get a neutral marker), `emoji` (explicit override),32 `owner` (colored, groupable, click-to-filter), `prs` (list of33 `{url, state: open|merged|closed}` — state-colored chips), `artifacts` (list of34 `{uri, label}` — resources hung on the card).35 - `body`: markdown, the card's CURRENT state — rewrite it as work evolves.36 - `events`: append-only timestamped timeline. Level 2 = timeline only; level 1 = also a37 notification. `kind` is an open token: a board can register its own kinds map with38 `bridge-axi kinds <file.json|->` (`{"<kind>": {"emoji": "…", "level": 1|2}}`,39 idempotent replace like `columns`; bare `bridge-axi kinds` prints the effective map).40 Built-ins ship for the bridge's own operations, overridable by the registered map:41 `created` 🐣 2, `moved` 🔁 2, `handoff` 👀 1, `landed` 🏁 1, `killed` 🪦 2,42 `resurrected` 🧟 1, `question` 🙋 1. On append, an explicit `--level` wins, else the43 kind's level from the effective map, else 2; a kind in neither map is stored as-is44 (opaque token, no emoji).45 - `labels`: USER-owned (edited in the UI, managed registry with colors). Agents never46 set or rewrite them.47- **Unified event stream**: board-level events + every card's events, one global sequence.48 The notification queue is the level-1 slice; suppressed level-2 events expand inline in49 the bell dropdown. Per-user read state persists server-side in the board file.50- **Kill = archive**: `bridge-axi archive <id>` snapshots the card to the append-only51 archive file and removes it from the board. No destructive delete; `bridge-axi archived`52 lists recent kills. The board event is typed by reason: `merged` → `landed` 🏁 (level 1),53 `killed` → `killed` 🪦 (level 2 — the human's own act, no bell). An archived card can be54 restored: `bridge-axi restore <id>` brings the most recent snapshot back in full (body,55 events, thread, frozen column) with a loud level-1 `resurrected` 🧟 event; the archive56 record stays, so **the board is truth for liveness** — an57 archive record never by itself means the card is off the board. A card restored into a58 column that has since been removed from the frame won't render until moved.5960## Agent loop61621. `bridge-axi open` — start the server if needed (idempotent), print the URL once.63 Binds `127.0.0.1` by default (localhost-only). To reach the board from other devices,64 the user sets `"host"` in `~/.bridge/config.json` (e.g. a VPN/tailnet interface IP) —65 machine-private config, never baked into commands or docs; `--host` overrides for one66 run. A non-loopback bind also listens on `127.0.0.1` so local CLI calls keep working.672. Feed reality: `create` new cards, `patch` bodies/attributes as state evolves, `event`68 for timeline signals (level 1 only for things the human must see), `move` for real69 state transitions (an agent move defaults to a `handoff` that notifies; pass70 `--kind moved` for a quiet reshuffle), `archive` when work is dead or landed,71 `restore` to resurrect an archived card, `say` to talk.723. Keep `bridge-axi poll` running as a tracked background task. It BLOCKS until human73 input, prints JSON lines, and exits; handle each line, reply with74 `bridge-axi say <target> --text-file <f>`, then `bridge-axi ack <seq>` (highest seq75 handled), re-run poll. Lines carry `kind`:76 - `message` — human message; `target` is `chat` or `card:<id>`.77 - `card-created` — the human made a card in the UI (`target` names it). Awareness78 only: creating a card is not a demand — a card with an empty thread owes nothing.79 Act when the human speaks in its thread or main chat.80 - `card-moved` — the human moved a card (`from`/`column` fields): a handoff or a81 handback, act accordingly.824. Always answer feedback with `say` to the same target — the UI shows "agent is working…"83 until the reply lands, and flips to an amber "may be stuck" warning if no reply comes84 within ~3 minutes (`BRIDGE_AWAITING_STALE_SECS` on the server overrides).8586Rules:87- Message/body text goes via `--text-file`/`--body-file` or stdin — never interpolated88 into a shell command.89- Delivery is at-least-once: feedback counts as delivered only when `ack`ed, never by90 being polled. An unacked line is re-offered by every poll — so a poller killed91 mid-handling loses nothing, and a repeated line (same `seq`) just means the previous92 handling never acked; dedupe by `seq`. Ack only what you have actually handled.93- Cards are for units of work, not for questions — ask questions in a thread (`say`) plus94 a level-1 `question` event on the card the question belongs to.9596## Notifications and read state9798The bell is a derived view of everything the human hasn't seen yet — one read-state99mechanism, two scopes: level-1 events (signal emojis) UNION unseen agent thread replies100(kind `reply`; no seq of their own, so they ride the per-thread read marker and show the101💡 fallback emoji — the "· N events ·" gap dividers skip them). Opening a card clears its102unread (events and replies alike); "mark all" marks the event items read AND advances the103thread read markers of cards with unseen replies, so it also clears those cards' unread.104Read state lives in the board JSON per user (default user `user`), so it survives reloads105and devices. Thread unread badges (chat bubbles, tiles) use the same per-thread read106markers, also server-side.107108## Voice109110`~/.bridge/config.json` `{"voices": ["Some Voice", "Another"]}` — case-insensitive111substring filter for the UI's TTS voice dropdown; absent/empty = full list. Manage with112`bridge-axi config voices "a,b"` / `config show`. The 🔊 toggle persists per browser and113speaks new agent messages (emojis stripped).114115## Migration116117`node migrate-v1.js <v1-board.json> [out.json]` converts a v1 board doc (columns/cards118with summary/detail_md/badges/links, threads, chat, labels) to the v2 model. It never119writes in place and never touches a server: stop the server, convert, move the file into120`~/.bridge/boards/` yourself.121122See `README.md` for the HTTP API reference.