Skip to content

panel — chamber-style brainstorming workspace

Convene N language models around a question, let them discuss via the filesystem, distil a shared deliverable. No Slack, no WebSocket. A workspace on disk, a read-only WebUI on top.


Contents

  1. What it is and why
  2. Concepts
  3. Typical workflows
  4. Installation
  5. CLI reference
  6. WebUI
  7. Integration with a participating LLM
  8. Troubleshooting
  9. Filesystem schema
  10. Future extensions

What it is and why

panel is a small tool to orchestrate ad-hoc multi-LLM panels. The usage pattern is that of a panel of experts you assemble for an evening:

"I have a stratospheric idea — go ahead: discuss it among yourselves and give me back a shared plan."

or

"Bug hunt is open on module X: each expert reads the code, has their say, and at the end I want a single report."

You are the convener (moderator: Roby). The experts are the language models available to you in separate terminals: Claude Code in one shell, opencode-DeepSeek in another, possibly opencode-mimo or opencode-m3 in others. Each LLM reads the same "room" and contributes in writing.

Why not Slack, why not a chat server

I could have built a small real-time server with WebSocket. I ruled that out for two practical reasons:

  1. Real-time push is not needed. The pattern is not continuous streaming: you launch a session, the LLMs take turns saying their piece, you re-read the transcript when convenient. Polling vs push, in this regime, is equivalent.
  2. Filesystem = free atomicity. With POSIX flock you get atomic append, ordered identity, and no risk of losing data if the server dies. The "server" is only a viewer: the conversation lives on disk even if the WebUI is off.

The result is a tool of about 250 lines total (bash + Python + JS) with three properties I care about: no critical backend process, every LLM speaks to the others via cat/bash (zero HTTP dependencies), and the history is already human-readable markdown.


Concepts

Concept What it is
Session A directory in ~/.panel/<slug>-<timestamp>/ holding one brainstorming session.
TOPIC.md The initial prompt (you write it with panel start). Frontmatter + markdown body.
TRANSCRIPT.md The append-only log of every message. One header line per message + markdown body.
DELIVERABLE.md The shared output that closes the session. You (or an LLM "rapporteur") write it.
Speaker A short alias (e.g. @claude, @ds, @mimo, @roby) identifying the writer.
Monotonic ID Every message has a sequential #N, assigned under lock. Used for delta-read.
PANEL_DIR Env var each participating terminal sets to the session it is attached to.
CURRENT File ~/.panel/CURRENT recalling the last session created (fallback when env missing).

Typical workflows

A — "I have a stratospheric idea, discuss and give me back a plan"

  1. Open the session (your terminal):
panel start "evaluate the monorepo migration from pip to uv"
# → ~/.panel/evaluate-the-monorepo-migration-...-20260609T1330Z

panel start prints the session path and saves it in ~/.panel/CURRENT.

  1. Write the initial prompt in TOPIC.md (with an editor, or by composing it via heredoc + replacing the file). The prompt should state: whom you are convening, what you are asking, what deliverable you want. Example:
# Migrating the monorepo to uv

## Convener
@roby

## Convening
@claude, @ds

## Question
We have been using pip + venv forever. uv promises 10× speedup,
native pyproject, deterministic lockfile. Discuss: is it worth it
now? What are the 3 main risks and how do we mitigate them?

## Expected deliverable
A 5-point mini-plan, with a "risks" section and a "next steps
verifiable in 2 hours" section.
  1. Launch the LLMs in their respective terminals (Claude Code in one shell, opencode-DS in another):
export PANEL_DIR=$(panel current)

Then in each terminal give them the minimal prompt:

"You are participating in the panel $PANEL_DIR. Read TOPIC.md and panel read, then write your opinion via panel say --as <your-alias>. You are @claude (or @ds)."

  1. Round loop (zero-coordination): each LLM reads the full transcript before speaking, and after speaking checks again to see if others added something. You step in when you feel it's time, by writing as @roby:
echo "@claude expand on the point about transitive lockfiles" | panel say --as roby
  1. Close the session with the consolidated deliverable. You can:

  2. synthesise it yourself (write it and do panel deliver < plan.md);

  3. or delegate it to a "rapporteur" LLM (e.g. @claude): "read the full transcript and write the final plan via panel deliver."

B — "Bug hunt is open, I want a report"

  1. panel start "audit module governance.js — bug hunt"
  2. TOPIC.md includes the file path and what to look for (race conditions? edge cases on empty inputs? implicit assumptions?).
  3. Each LLM reads the code, writes their findings as a list, tags the severity level in their own message.
  4. Subsequent rounds: LLMs contradict / confirm each other.
  5. panel deliver with a consolidated BUGS.md (severity + description
  6. reproduction + proposed fix).

C — Long asynchronous session

Sessions survive PC restarts: everything is on the filesystem. You can:

  • open a session on Monday, leave it open,
  • add a round on Wednesday,
  • close it on Friday with panel deliver.

panel sessions shows what is around and what is still open.


Installation

Prerequisites

  • Python ≥ 3.10 (for the dict | None type hints in the server).
  • Bash 4+ + flock (POSIX, native on Linux; on macOS: brew install util-linux).

Python dependencies

In the venv you usually use:

uv pip install -r tools/panel/requirements.txt

(or pip install if you don't use uv.)

Dependencies: fastapi, uvicorn[standard], sse-starlette, watchfiles, markdown.

ln -s "$(realpath migration/tools/panel/panel)" ~/.local/bin/panel

From here on panel works everywhere.

Environment variables

Env var Default Effect
PANEL_HOME ~/.panel Where sessions live
PANEL_DIR (unset) Per-terminal override pointing to a specific session

PANEL_DIR takes precedence over ~/.panel/CURRENT. Useful when you want different terminals to participate in different sessions simultaneously.


CLI reference

panel start "topic description"   create ~/.panel/<slug>-<ts>/, write TOPIC.md,
                                  update CURRENT, print the path
panel join     --as <alias>       register a participant in the active session
panel say      --as <alias>       read stdin and append to the transcript (id
                                  assigned under flock); auto-join on new alias
panel read     [--since=<id>]     print the full transcript, or only the
                                  messages after `#<id>`
panel new      --as <alias>       print the count of messages added after the
                                  alias's last message; exit 0 if any (count > 0),
                                  exit 1 if none. Cheap poll for "has anyone
                                  spoken after me?"
panel since-me --as <alias>       print the messages added after the alias's
                                  last message (full transcript if alias never
                                  spoke). Same as `panel read --since=<id>` but
                                  resolves the id for you.
panel wait     --as <alias>       suspensive poll: block until at least one new
            [--interval=<sec>]    message arrives (default poll every 3 s, no
                                  timeout — Ctrl+C / SIGTERM to abort), then
                                  print the delta and exit. Designed for LLM
                                  agents that can't natively background a task
                                  and notify on completion (opencode-DS,
                                  opencode-MiMo, …). Exit codes: 0 = new
                                  messages arrived; 2 = session closed
                                  (DELIVERABLE.md appeared); 3 = transcript
                                  missing (session deleted).
panel deliver                     read stdin, write DELIVERABLE.md, flag the
                                  TOPIC frontmatter as `status: closed`
panel sessions                    list sessions in PANEL_HOME
panel current                     print the active session (`~/.panel/CURRENT`)

Quick examples:

# create
panel start "experiment X"

# write a multi-line message
panel say --as claude <<'EOF'
My analysis:

1. Point one
2. Point two with `inline code`

See also [link](http://example.com).
EOF

# read only what is new since the last message you saw
panel read --since=5

# poll: is there anything I haven't seen yet (since my last message)?
if panel new --as claude > /dev/null; then
  panel since-me --as claude   # read only the delta
fi

# close
cat draft_plan.md | panel deliver

WebUI

cd migration/tools/panel
python server.py
# then open http://127.0.0.1:4848 in the browser

What it shows:

  • Left sidebar: every session in ~/.panel/, sorted by descending date. [open] or [closed] status for each. Click = select.
  • Main pane: TOPIC rendered as a box, list of participants, every message in the transcript with a per-speaker colour, and — when the session is closed — a Deliverable box at the bottom.
  • Live update: automatic refresh via Server-Sent Events. When an LLM does panel say, the main pane updates within ~100 ms without you reloading.

The WebUI is read-only by design: writing always goes through the CLI. This keeps the server stateless with respect to the conversation (it can crash and restart with no implications).

Bind: 127.0.0.1:4848. No auth, no token: designed for localhost only.


Integration with a participating LLM

To coordinate a panel well, each LLM must be instructed to follow 4 simple rules. The cleanest way is to put them in the universal AGENTS.md (or in the per-LLM RULES.md if you use opencode), so you don't have to repeat them at every session.

Rules to add (markdown snippet):

## Panel mode

If the environment variable `PANEL_DIR` is set, you are in a
brainstorming panel:

1. Before writing the first time, read `$PANEL_DIR/TOPIC.md` and
   `panel read`.
2. Write via `panel say --as <your-alias>` (the alias is specified in
   the prompt that invoked you; if missing, ask).
3. **After every `panel say`, immediately call
   `panel wait --as <your-alias>`.** This is a *blocking* call (no
   timeout) that suspends until at least one new message arrives from
   another participant, then prints only the delta and returns. Loop:
   *say → wait → process delta → say → wait → …*. The point of this
   pattern is to enable fluid multi-round panels without the convener
   nudging you to re-read the transcript: you stay in receive mode by
   default and only act when something genuinely new arrives. Stop
   when you decide you have nothing further to contribute, when you
   are asked to step out, or when `panel wait` returns exit code 2
   (session closed by Roby).
4. Alternatives to the blocking wait (use only when the suspensive
   pattern is wrong for the context — e.g. you want to do other work
   meanwhile and check back later): `panel new --as <you>` for a
   cheap non-blocking poll (exit 0 with count if new, 1 if none),
   `panel since-me --as <you>` for an explicit delta read.
5. Never close the session on your own initiative: `panel deliver` is
   up to Roby (or the rapporteur explicitly designated in the TOPIC).

The blocking panel wait pattern (rule 3) is the default for any agent that does not have native "background a task and notify on completion" semantics — typically opencode-DS, opencode-MiMo, and any LLM run from a shell that can only execute one command at a time. Agents that do have native backgrounding (e.g. Claude Code through its harness) can either use the same suspensive pattern, or background the wait and use the platform notification — both work.

Typical initial prompt (what you give the LLM the first time in a session):

You are participating in a brainstorming panel. Your alias is @claude. Read $PANEL_DIR/TOPIC.md for the question, and panel read for what has been said so far. Then reply via panel say --as claude. The expected deliverable is described in the TOPIC.


Troubleshooting

flock: command not found on macOS Install util-linux: brew install util-linux and add /opt/homebrew/opt/util-linux/bin to PATH.

Address already in use on port 4848 Another server.py instance is already running. pkill -f tools/panel/server.py or change the port at the bottom of server.py and restart.

The WebUI says "Session not found" Check that the server's PANEL_HOME matches the CLI's. If you set PANEL_HOME=/tmp/test for the CLI but the server runs with the default, they will see different directories.

panel say blocks forever Likely an orphan lock file (.lock under the session). flock should handle this, but to force-release: rm <session-dir>/.lock.

An LLM keeps not reading the TOPIC It probably doesn't know it has to: update AGENTS.md with the "Panel mode" section above, or repeat the instruction in the initial prompt.

Message IDs are not sequential Verify that the headers are formatted as ## [@speaker] <iso-ts> #<N> (no other # markers in the same header). The regex parser is precise but strict.


Filesystem schema

~/.panel/
  CURRENT                              path to the active session (1 line)
  <slug>-<YYYYMMDDTHHMMSSZ>/
    TOPIC.md
      ---
      schema: 1
      id: <slug>-<ts>
      created: <iso-ts>
      status: open                     becomes "closed" after `panel deliver`
      ---

      # <topic>

      <markdown body>

    TRANSCRIPT.md                      append-only
      ## [@<speaker>] <iso-ts> #<N>

      <markdown body>

      ## [@<other>] <iso-ts> #<N+1>

      <markdown body>

    participants.txt                   1 alias per line
    DELIVERABLE.md                     (present ↔ session closed)
    .lock                              flock file (atomic id+append)

Parsing conventions

A message header follows this exact regex:

^## \[@(<speaker>)\]\s+(<iso-ts>)\s+#(<N>)\s*$
  • <speaker>: alphanumeric + dash + underscore
  • <iso-ts>: ISO 8601 timestamp produced by date -Iseconds
  • <N>: monotonic integer, assigned by the CLI under flock

Everything between one header and the next is the message body in markdown.


Future extensions

Foundation in place but no code yet:

  • POST /api/say to write as @roby from the WebUI (today CLI-only).
  • Search in the transcript (server-side via grep, or client-side via fuse.js).
  • Multiple deliverables per session (e.g. plan + bug-list separated).
  • Schema v2: typing indicators, reaction emoji per message, replyTo.
  • Export markdown bundle (TOPIC + TRANSCRIPT + DELIVERABLE zipped) for long-term archive.
  • Desktop notifications when an LLM adds a message.
  • Auth token if you ever want to bind beyond localhost.

Known caveats

  • The bash CLI uses POSIX flock: native on Linux, optional on macOS.
  • watchfiles falls back to polling if inotify is unavailable (rare on standard desktop setups).
  • WebUI binds to 127.0.0.1, no auth — do not expose it on a network.
  • The CLI path is long: the symlink in ~/.local/bin/ makes daily use sustainable.
  • Messages are append-only: no edit / delete / nested reply. Design decision: for a brainstorming panel append-only is a feature, not a limit (the history is closed and verifiable). Future extensions can add replyTo as metadata without breaking append-only.