docs: design spec β shared remote Claude Code sessions fronted by an Eleven group (#1443)
* docs: design spec β shared remote Claude Code sessions fronted by an Eleven group Brainstormed 2026-08-04/05: @claude as a chat member, focus-routed prompts, one view per session via the views engine, board + ephemeral drill-in tail, scoped vs plain session flavors, bridge as a pure-outbound bot-model sidecar (lchat agents run). Design only β no implementation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: record Paul's decisions β agents naming, prompt-is-the-message, close-session-closes-worktree Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xxn758bWfMnEe9xZZuk5Mx ---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 file changed,
+247
−0
docs/superpowers/specs/2026-08-05-eleven-claude-agents-design.md+247 −0
diff --git a/docs/superpowers/specs/2026-08-05-eleven-claude-agents-design.md b/docs/superpowers/specs/2026-08-05-eleven-claude-agents-design.md| new file mode 100644 |
| index 0000000..e0c3af1 |
| --- /dev/null |
| +++ b/docs/superpowers/specs/2026-08-05-eleven-claude-agents-design.md |
| @@ -0,0 +1,247 @@ |
| +# π€ Shared remote Claude Code sessions β an Eleven group fronting a workstation |
| + |
| +**Date:** 2026-08-05 Β· **Status:** accepted β open questions decided by Paul |
| +2026-08-28 (see Β§ Decisions) Β· **Scope:** design only β no implementation yet |
| + |
| +## What this is |
| + |
| +A group in Eleven becomes a shared front-end to **Claude Code sessions running |
| +on a remote workstation** (the motivating box: docs-t1). Multiple people share |
| +the sessions: the chat on the left is where you steer β **@claude is an |
| +ordinary member of the chat** β and the right-hand pane is a live view of the |
| +remote sessions. Agent activity is translated into Eleven primitives (typed |
| +events, cards, views, action lines, notices) rather than mirrored as a |
| +terminal. |
| + |
| +The design uses almost entirely existing machinery β no new group type, no new |
| +E2EE exceptions, one new sidecar-shaped process and one new mini-app. |
| + |
| +``` |
| +ββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββ |
| +β Chat β agents app (Interface pane) β |
| +β β β |
| +β Paul: @claude fix the β βΈ fix-login π messenger β |
| +β flaky reminder test β running Β· turn 3 β |
| +β @claude [fix-login]: β β go test ./internal/bot β |
| +β Found it β the tickerβ¦ β βΈ ops-poke π₯ workstation β |
| +β Ana: @claude also checkβ¦ β waiting for approval β |
| +β β [Allow] [Deny] β |
| +ββββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββ |
| + the shipped Chat | Split | widget-bar layout, nothing new |
| +``` |
| + |
| +## Trust model (read this first) |
| + |
| +- **The E2EE rule holds untouched.** The server relays opaque ciphertext; |
| + every `session.*` event, prompt, approval, and view is encrypted under the |
| + thread key like any message. The server never learns what the sessions do, |
| + which repos exist, or that a box called docs-t1 is involved. |
| +- **The bridge is a bot-model process** (docs/bots.md): an E2EE member whose |
| + "device" is the workstation. It runs ON the box, beside the sessions, holds |
| + its member private key + the Anthropic access there, and is a **pure |
| + outbound client** β dials the instance and the Anthropic API, no inbound |
| + ports. Bot code never runs inside the lchat server process. |
| +- **Group membership is the consent moment.** Everyone in the group can steer |
| + sessions, which means everyone in the group has a lever on the box β |
| + filesystem, git credentials, whatever the bridge's unix user reaches. This |
| + is the operator-only dev-bot tier (docs/bots.md tier 4): chat input is |
| + untrusted input to a machine with authority. The bridge's config (below) |
| + enumerates exactly what that authority is; adding a member is granting it. |
| +- **Flat authority for v1.** Any member can prompt, approve, and interrupt. |
| + Roles (driver/observer) are layerable later as ordinary fold state |
| + (`agents.role` events) without schema changes. |
| + |
| +## The pieces |
| + |
| +### 1. The bridge β `lchat agents run` |
| + |
| +A sibling subcommand of `lchat bot run`, reusing the Go sidecar runtime |
| +(WebSocket client, key unwrap, reconnect, trigger plumbing β all existing). It |
| +runs on the workstation and: |
| + |
| +- joins the instance as the **@claude member** (one keypair, one roster row β |
| + the standard bot-member shape, not a charter persona); |
| +- owns the Claude Code sessions: spawns them **headless** (`claude -p |
| + --output-format stream-json`, resumable via `--resume <session-id>`; the |
| + Agent SDK is the same thing as a library), consumes the **structured event |
| + stream** β assistant text, tool_use blocks with full inputs, tool results, |
| + turn boundaries. **No terminal emulator**: nothing scrapes a TTY. The only |
| + terminal-ish work is stripping/basic-rendering ANSI colours out of Bash tool |
| + output (~50 lines, no cursor addressing exists in a `-p` session); |
| +- translates the stream into encrypted `session.*` events (vocabulary below) |
| + posted into the group thread, and reads members' prompt/approval/interrupt |
| + events back off the same thread; |
| +- serializes turns per session (a session is one-turn-at-a-time), queues |
| + prompts that arrive mid-turn, and **attributes prompts by speaker** β |
| + "Paul: fix the flaky test" β so the model knows it's talking to a room. |
| + |
| +Config, per group the bridge serves: |
| + |
| +``` |
| +groups: |
| + <thread-id>: |
| + roots: ["~/github.com/elevenmessenger/messenger"] # scoped sessions live here |
| + plain: false # whole-workstation sessions allowed? |
| +``` |
| + |
| +The config is the blast radius, written down in one place on the box. |
| + |
| +### 2. Sessions β two flavors, chosen at spawn |
| + |
| +- **Scoped** (default): bound to a directory β an allowed root, normally a |
| + **fresh git worktree under it** so parallel sessions don't trample each |
| + other. A scoped session gets that project's own `.claude/` settings, |
| + CLAUDE.md, hooks, and permission config exactly as a human dev would β the |
| + repo's existing guardrails govern the remote session with zero new |
| + machinery. |
| +- **Plain** (opt-in via `plain: true`): no pinning β cwd = home, the whole |
| + workstation at the bridge's unix user's reach. The "shared computer" mode |
| + for ops-ish, cross-repo work. Off by default because it hands every member |
| + the entire box. |
| + |
| +Flavor is **visible everywhere**: carried on `session.spawn`, badged on the |
| +session's view ("π messenger @ fix-login" vs "π₯ full workstation") and on its |
| +chat card. Nobody wonders which kind of hands are on the box. |
| + |
| +Birth is conversational: "@claude fix X" with zero live sessions spawns a |
| +scoped session in the group's default root; `/claude new <name>` names one; |
| +`/claude new --plain <name>` when the config allows. Session state persists on |
| +disk on the box (Claude Code's own session files), so a human on the |
| +workstation can `claude --resume <id>` from a real terminal after the bridge |
| +lets go β continuity is preserved; concurrent dual-driving is not pretended. |
| + |
| +### 3. Chat β @claude plus focus-based routing |
| + |
| +One bot member; the mental model is "a colleague in the chat who happens to be |
| +several sessions deep." |
| + |
| +- **Routing: the target is the active view.** Each member's client knows which |
| + session view they have focused in the right pane; a message to @claude |
| + carries `target: <session-id>` **inside the encrypted payload**. Focus is |
| + per-member β two people can steer two sessions from the same chat |
| + simultaneously. |
| +- **Routing is visible at send time.** When more than one session is live, the |
| + composer shows a chip β "β fix-login" β tappable to retarget without |
| + touching the pane. |
| +- **Replies pin the target.** Every @claude bubble carries a subtle session |
| + tag; replying to a bubble routes to that session regardless of pane focus β |
| + this is also the mobile/chat-only answer. The tag works the other way too: |
| + tapping it focuses that session's view. |
| +- **Degenerate cases collapse.** One live session: everything routes there, no |
| + chip. Zero sessions: @claude answers as the workstation ("no sessions |
| + running β want one?") and a prompt spawns one. |
| + |
| +Output posts as @claude with the session tag on each bubble β the feed reads |
| +as one assistant working several desks, which is the truth. |
| + |
| +### 4. The right pane β one app, one view per session |
| + |
| +- **One `agents` mini-app** (`web/static/apps/agents.js`), chat-placed |
| + (`places: ["chat"]`), added to the group like any app β one glyph in the |
| + widget bar, rendering in the standard Interface pane. The shipped |
| + Chat | Split | widget-bar toggle IS the "chat left, session right" layout; |
| + nothing new is built for it. |
| +- Its Interface is the **fleet board**: one card per session (name, flavor |
| + badge, status, current activity line, waiting-on-approval chips), folded |
| + from durable `session.*` events. |
| +- **Per-session status rides the views engine** (`views.js`, |
| + `view.create`/`view.update` from the bridge): one engine view per session. |
| + Views are declarative SCHEMA + DATA interpreted by **both web and the Swift |
| + twin engine**, so session boards render on iOS/Mac with no per-feature |
| + Swift. Chips on @claude's replies open the session's view; `/views` lists |
| + them. |
| +- **Fidelity is "board + drill-in" (option c).** Tap a session to focus it: |
| + the durable board plus a **live tail** β tool-call chips ("Ran `go test |
| + ./...`"), collapsed monospace output, file-edit lines, assistant prose β |
| + streamed as **ephemeral events** (relay-only frames, the existing |
| + primitive): live viewers see the chatter; nobody replays it; the encrypted |
| + log stores only what deserves permanence. The views engine cannot host the |
| + tail (views are durable documents; streaming through `view.update` would |
| + bloat the log) β the tail is the app's own render over the ephemeral |
| + stream, web-first. |
| + |
| +### 5. Event vocabulary |
| + |
| +Durable (thread log; the fold state every member converges on): |
| + |
| +| event | payload sketch | why durable | |
| +|---|---|---| |
| +| `session.spawn` | id, name, flavor, root/worktree, by | fleet board exists offline | |
| +| `session.status` | id, state (running/idle/waiting/dead), activity line | board truth | |
| +| `session.prompt` | id=target, text, by | the steering history IS the log | |
| +| `session.turn` | id, assistant text (final, per turn) | the conversation with the agent | |
| +| `session.result` | id, summary card (files changed, tests, links) | milestones as cards | |
| +| `session.approval` | id, request (tool, input digest), state, decided-by | audit trail of authority used | |
| +| `session.end` | id, reason, by | lifecycle honesty | |
| + |
| +Ephemeral (relay-only; live tail + presence): |
| + |
| +- `session.tool` β tool_use started/finished, truncated output chunks |
| +- `session.stream` β assistant text deltas while a turn is in flight |
| +- typing-style "session is thinking" indicators |
| + |
| +Streaming into *chat* has no partial-message primitive, so @claude's chat |
| +bubbles are **post-per-turn** (the durable `session.turn`), with the live |
| +delta visible in the drill-in tail instead β no post-then-edit churn in the |
| +feed. |
| + |
| +### 6. Approvals |
| + |
| +Claude Code's permission gate surfaces as a chip row β on the session's board |
| +and as a @claude chat card: "fix-login wants to run `git push` β [Allow] |
| +[Deny]". A tap emits an encrypted `session.approval` decision event; the |
| +bridge reads it and answers the gate. Flat authority: first decision wins, |
| +attributed in the event and shown in the feed ("Ana allowed `git push`"). |
| +Notices/push ride the existing E2EE notify path, so "waiting for approval" |
| +reaches pockets. |
| + |
| +### 7. Liveness |
| + |
| +Derived, never declared (the docs/bots.md pattern): the bridge heartbeats |
| +(ephemeral presence + a periodic durable status touch); a session whose bridge |
| +is gone must **read as dead/stale** in the board β "last seen 4m ago" β not |
| +linger as "running". A dead bridge silently eating prompts is the worst |
| +failure mode; staleness is always rendered from event age, client-side. |
| + |
| +## What the server gains |
| + |
| +Nothing. Every piece rides existing surfaces: bot membership, thread events, |
| +ephemeral frames, the views engine, the mini-app framework, notify/push. The |
| +bridge is a new subcommand of the existing binary; the app is one new file |
| +under `web/static/apps/`. |
| + |
| +## Hard parts, named |
| + |
| +- **The approval round-trip** β latency between the gate firing and a human |
| + tapping Allow; the session blocks meanwhile. Mitigations: push notices, |
| + per-session auto-allow lists (later; config-side, never chat-grantable). |
| +- **Log discipline** β a busy session emits a lot; the durable/ephemeral split |
| + above is the design's load-bearing wall. Anything high-churn must be |
| + ephemeral or it bloats every member's encrypted log forever. |
| +- **Prompt-injection containment** β session output is model output over |
| + untrusted repo/web content; it lands in chat as content, never as commands. |
| + The bridge only ever acts on typed events from members. |
| + |
| +## Deferred (deliberately) |
| + |
| +- **Driver/observer roles** β layer later as `agents.role` fold state. |
| +- **Fleet-across-groups overview** β a "you"-placed app aggregating every |
| + workstation group; needs nothing new from this design. |
| +- **Attaching to a human's interactive TUI session** β ruled out for v1 |
| + (tmux-scraping + keystroke injection fights the one-writer TUI; the honest |
| + handoff is `--resume` after the bridge lets go). |
| +- **Auto-allow permission profiles** per session flavor. |
| +- **Native drill-in tail** β boards are native-free via the views engine; |
| + the live tail ships web-first. |
| + |
| +## Decisions (Paul, 2026-08-28) |
| + |
| +1. **Naming: `agents` stands.** `lchat agents run`, `apps/agents.js`, |
| + `session.*` events, and the member reads as @claude. |
| +2. **The prompt IS the chat message.** One `session.prompt` event, two |
| + projections β the member's visible bubble in the feed and the bridge's |
| + steering input, tagged with the target. No separate `chat.msg`. |
| +3. **Closing a session closes its worktree.** Sessions are closable from |
| + within Eleven, and `session.end` is the cleanup moment: the bridge removes |
| + a scoped session's worktree then. The branch (any committed work) survives |
| + worktree removal as an ordinary git branch; only the checkout goes. |