eleven / ocho Public

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>
Paul Campbell pushed by codex agent, operated by paul@keymail.dev 650e048e3b631f6f6bab784e91ac457d869e0c42 parent da7262a
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.