context-attachment-manager
Codex and Claude Code plugin (works in Claude Desktop): choose which past images go to the model with your next message. Unchecked images become placeholders, the conversation goes on, and requests st
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證A table of AI models that debates your question and writes the decision. Desktop app on your coding-agent CLIs, and a Slack app on your own OpenRouter key.
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗One question. Several AI members that disagree on purpose — each one a coding-agent CLI you already pay for (Claude Code, Codex, Gemini CLI, Grok Build). They sit at a table and talk it through in short turns — opening statements, then real back-and-forth, then a forced convergence — and a chair writes the consortium: one decision document with the rationale, a plan, and the dissent kept in.
Local-first desktop app (Electron). No accounts, no server: the app spawns the CLIs that are already signed in on your machine. An optional fifth seat, the Deep-agent, runs on an API key instead.
Consortium for Slack is the same table in a Slack thread, for any workspace: consortium-yqs2.onrender.com (press Add to Slack). It's built and maintained by Julien (@jwuthri-cloud). The server is a free Render web service, deployed from this repo's main by render.yaml; its data (installs, settings, encrypted keys) lives in a free Neon Postgres. Each workspace brings its own OpenRouter key and picks its models. How it works and how to run your own: Hosted.
The desktop app, in real meetings.
Convene. Write the question, attach a project if it's about code, and seat the table: each seat is a persona on a CLI or the Deep-agent.
The meeting room. Members speak in turns; each badge shows the seat's role and the model actually running it. The conversation and the clerk's notes sit on the right.
A live turn. While a member prepares, the room shows what it's doing (reading files, searching, using a tool) and who speaks next. You can add context or steer at any time.
The whiteboard. The clerk keeps what's agreed, disputed and still open, with the turns each note comes from.
The decision. The chair writes the consortium: recommendation, rationale, plan and the dissent, next to who agreed with what.
npm install
Sign in to the CLIs you want at the table (each one's own flow — the app never touches credentials):
| CLI | Install | Sign in |
|---|---|---|
| Claude Code | curl -fsSL https://claude.ai/install.sh | sh | claude → /login |
| Codex | npm i -g @openai/codex | codex login |
| Grok Build | see x.ai | grok login |
| Gemini CLI | npm i -g @google/gemini-cli | gemini → Google login |
Any seat whose CLI is not signed in simply reports the CLI's own error on the table.
npm run dev
This launches the real Electron desktop app. It uses the same React/Three.js interface as the browser preview, with the preload IPC bridge connected to the installed agent CLIs, folder picker, and saved sessions. Opening the Vite URL in a browser instead uses scripted demo responses.
If an already-running development window still shows an older interface, finish the current discussion, then use View → Reload (⌘R on macOS) and reopen the saved session. To create an updated standalone macOS app, run npm run build:mac.
Convene — write the question, optionally attach a project folder (members then inspect it read-only before they argue), pick seats (persona · CLI · model), a chair, and a turn budget (20 / 30 / 50 turns across all members). "Check" runs a one-word prompt through a seat's CLI to prove it answers.
Chamber — a meeting room with real-time 3D agent mascots. Provider colors and clothing identify Anthropic, OpenAI, Google, or xAI; each cap displays the seat’s role and the suit badge shows the actual model, including automatically selected CLI defaults, without thinking or context-budget annotations. Drag a character or use arrow keys to rotate it; double-click or press Home to reset. Use gallery view or spotlight a participant with its expand button. Speaker view follows the active member unless pinned. Conversation and Decision tabs hold the live transcript and clerk’s notes. While a seat prepares, the room shows connection, preparation, reading/searching, tool use, and response states from CLI events, plus elapsed time and the next seat. Raw reasoning and tool logs stay out of these status indicators. A quiet agent is labelled honestly when no update arrives. Send context to interrupt the active response immediately, or end the session. Choose “Read the decision” when the chair finishes.
The mascots are procedural Three.js meshes rendered locally, with blinking, idle motion, and speaking gestures. They do not use image-generated portraits or remote assets. Reduced-motion settings suppress animation; offscreen/hidden views skip rendering. A text fallback appears when WebGL is unavailable.
Consortium — the chair's document, with the agreement matrix and the panel. Saved as markdown to <project>/consortium/<question>-<date>.md (or the app's data folder when no project is attached).
Members only ever see each other's persona ("The Skeptic"), never which model sits behind a seat. The turn budget drives the phases: the first cycle is openings, then free debate until 60% of the budget, then converge (only the open points, concede where persuaded, no new topics), and the last cycle is final statements. Every turn ends with a small vote block (position, confidence, who it still disagrees with); a full cycle with no disagreements left ends the conversation early. Each member keeps its own CLI session across turns, so it remembers everything it has heard and is only sent the new turns.
Setup, meeting history, the live room, and the decision document share the charcoal/lavender theme and the same logo. App icons can be regenerated with python3 scripts/generate-icons.py (Pillow).
Model names come from CLI response metadata, the matching Codex rollout, or Grok’s default-model catalogue. They are saved with each seat; older sessions without recorded model metadata display “Model not recorded.” “Check connections” also resolves the current default into the setup field.
Model-metadata, saved-state and Deep-agent tests: npx tsx --tsconfig tsconfig.web.json --test scripts/*.test.ts. Reopened sessions left running by an app restart display as stopped unless their engine is still active.
The meeting engine also runs headless, as an HTTP server: no window, no Electron. Use it to ask the table a question from a script, another app or an agent.
POST a question and the seats you want.One meeting runs at a time per server. The CLIs bill your subscriptions as usual; Deep-agent seats bill their API key.
npm run api
It listens on http://127.0.0.1:4317 (this machine only) and prints a token. Send it on every request as Authorization: Bearer <token>. The token is kept in the data folder as api-token; set CONSORTIUM_API_TOKEN to choose your own, and CONSORTIUM_PORT / CONSORTIUM_HOST to move it. Keys for the Deep-agent and its web search come from the server's environment (OPENROUTER_API_KEY, OPENAI_API_KEY, …): only the app can open the keys saved in its Settings.
{
"task": "One repo or two for web and API?",
"members": [
{ "cli": "claude" },
{ "cli": "codex" },
{ "cli": "harness", "model": "openrouter:z-ai/glm-5.3" }
],
"projectPath": "/path/to/repo"
}
Each member needs only cli: claude, codex, gemini, grok, or harness for the Deep-agent (which also needs a model). Optional per member: model (defaults match the app), persona (strategist, engineer, researcher, designer, skeptic, customer; filled in otherwise) and id (m1, m2, …). Optional for the meeting: chairId (the first member otherwise), maxTurns (default 30), projectPath, attachments (file paths on the server), board ({"cli","model"} for the whiteboard writer; the chair's otherwise).
Wait. POST /meetings?wait=1 holds the request until the decision is written (usually one to ten minutes, depending on seats and turns) and answers with it:
curl -X POST "http://127.0.0.1:4317/meetings?wait=1" \
-H "Authorization: Bearer $TOKEN" -H "content-type: application/json" -d @question.json
{
"id": "d5608ecd-…",
"status": "done",
"decision": "# Consortium: One Repo for a 3-Person Startup\n\n## Recommendation\nUse one repository with separate web and api directories… [T1, T2]\n\n## Trade-offs\n…",
"path": "~/Library/Application Support/consortium-ai/consortium/a-3-person-startup-….md"
}
status is done, or stopped / failed if it didn't get there.
Stream. POST /meetings answers at once with {"id"}. Then GET /meetings/:id/events streams the meeting live as server-sent events, word by word, and closes when it ends. Connecting late (or again) replays the meeting from its first event, then continues live.
curl -N "http://127.0.0.1:4317/meetings/$ID/events" -H "Authorization: Bearer $TOKEN"
event: turn-start
data: {"sessionId":"8028…","type":"turn-start","memberId":"m1","turn":1,"phase":"open"}
event: delta
data: {"sessionId":"8028…","type":"delta","memberId":"m1","turn":1,"text":"We shouldn't write automated tests before launch. For a three-person startup, survival depends"}
event: turn-done
data: {"sessionId":"8028…","type":"turn-done","turn":{"n":1,"speaker":"m1","label":"The Strategist","text":"We shouldn't write automated tests before launch…","vote":{"position":"Skip automated testing before launch…","confidence":0.85,"agrees_with":[],"disagreements":[]}}}
event: consortium
data: {"sessionId":"8028…","type":"consortium","markdown":"# Consortium: Pre-Launch Testing…","path":"…/consortium/should-a-3-person-startup-….md"}
The events, in the order you'll meet them:
| Event | Carries |
|---|---|
turn-start | Who speaks next, the turn number and the phase (open, debate, converge, decide) |
member-model | The model a seat actually runs |
activity | What the speaker is doing: thinking, reading, searching, tool, reviewing, responding |
trace | One tool call ({"kind":"tool","name","detail"}) or a piece of reasoning, as the speaker works |
delta | The next piece of the speaker's text. Raw: the vote block at the end of a turn arrives here too |
turn-done | The finished turn: clean text, the parsed vote, the tool trace, the cost |
turn-error | A seat failed this turn (the meeting carries on) |
human | Something you said through /messages, as it enters the conversation |
shape, whiteboard, board-status | The clerk's notes and the live whiteboard: agreed, disputed, open questions, actions |
usage | Tokens and cost so far, per seat |
chair-start, then delta with turn: 0 | The chair writing the decision |
consortium | The decision document (Markdown) and where it was saved: the last event |
stopped / failed | The meeting ended without a decision: the last event |
You can also look at a meeting any time: GET /meetings/:id returns the whole session (turns, votes, traces, whiteboard, usage) and GET /meetings/:id/decision the decision as Markdown.
| Route | What it does |
|---|---|
GET /health | Detected CLIs, which keys are set, the running meeting |
POST /meetings | Start a meeting: {"id"} at once, or the decision with ?wait=1. 409 while another runs, unless ?replace=1 |
GET /meetings/:id/events | The meeting as a live stream (above) |
GET /meetings/:id | The whole session |
GET /meetings/:id/decision | The decision, as Markdown (404 until it's written) |
POST /meetings/:id/messages | Speak in the meeting: {"text"} to everyone, {"kind":"focus","text"} to steer it, {"kind":"ask","targetId":"m2","text"} to a member, who answers next |
POST /meetings/:id/pause, /unpause | Hold the meeting between turns, and let it go on |
POST /meetings/:id/conclude | Skip to the chair's decision |
POST /meetings/:id/stop, /resume | Stop it; pick a stopped meeting up where it left off |
GET /meetings, DELETE /meetings/:id | The library |
GET /models/:cli, POST /check ({"cli","model"}) | Model suggestions for a seat; a one-word connection test |
The same meeting in a Slack thread. The live whiteboard is a canvas the channel can read, titled with the question and rewritten as the clerk revises it (at most every 5 seconds): the recommendation, the moderator's guidance, agreed, disputed, open questions and actions, each citing its turns, the board's visuals as tables, and a footer with the phase, turn and cost. The thread's first message keeps the gist (the recommendation and how many notes each column holds) with Open whiteboard, Pause, Conclude and Stop buttons; on a plan without canvases it carries the whole board instead. Each member speaks in the thread under its persona's name, streaming as it answers, with its vote underneath. When the table decides, the decision is posted as its own canvas (or attached as a .md file if canvases are off) and linked from the first message. It uses Socket Mode, so it runs on the Mac where the CLIs are signed in, with no public URL.
Meetings run side by side, one thread each, so several people can convene at once: every thread steers only its own meeting, and its buttons act on it alone. Up to maxMeetings (default 5) run at once; past that, /consortium answers, only to you, how many are running.
slack-manifest.yml.connections:write scope. That's SLACK_APP_TOKEN (xapp-…).SLACK_BOT_TOKEN (xoxb-…)./invite @Consortium.slack.json in the data folder (~/Library/Application Support/consortium-ai/ on a Mac):{
"allow": ["U012ABCDEF"],
"presets": {
"default": { "members": [{ "cli": "claude" }, { "cli": "codex" }], "maxTurns": 12 },
"repo": {
"members": [
{ "cli": "claude" },
{ "cli": "codex" },
{ "cli": "gemini", "persona": "skeptic" }
],
"projectPath": "/Users/you/code/app"
}
},
"channels": { "C0123456789": "repo" },
"maxMeetings": 5
}
allow is the Slack user ids (profile → ⋯ → Copy member ID) that may start meetings, steer them and press the buttons; everyone else is refused, and an empty list refuses everyone. "allow": ["*"] lets anyone in a channel the bot is in. maxMeetings caps the meetings running at once: each one runs its own CLIs on this Mac. A preset is a question without its task. channels gives a channel its preset. A project folder only ever comes from a preset, never from what someone types. The file is re-read on every command, so edits apply without a restart.
SLACK_BOT_TOKEN=xoxb-… SLACK_APP_TOKEN=xapp-… npm run slack
/consortium Should we ship a CSV export? or @Consortium Should we ship a CSV export? starts a meeting with the channel's preset (or default); /consortium <preset>: … names one.focus: … sets the focus, and skeptic: … (any persona's name) asks that member, who answers next.The same Slack app for anyone: they press Add to Slack, connect their own OpenRouter account in the app's Home tab, and the table runs on their key. One server holds many workspaces; each one's meetings, settings, key and transcripts are its own.
What changes from the single-workspace bridge:
fetch_url refuses local and private addresses)./consortium alone (or New question in the Home tab) opens a form: the question, the channel, a repository and up to 10 files. Nobody types a repository's name: once GitHub is connected, Consortium picks the repository your question names ("why does backend retry webhooks twice?" picks connectly-backend: a name's words count, not its owner's prefix or words like api and service), else the channel's last one, and you confirm it. /consortium <question> opens the form with the question filled in and that repository selected, so it's one click on Start; an @mention gets a message only you see, with the same repository picked, a No repository choice and Start. Without GitHub, both start at once. A repository Consortium can't read gets a plain answer, the closest names it can read, and where an admin adds it. Files attached to an @mention count too, and files posted in a running meeting's thread reach the table with the reply. Text and code become files the seats read with their read-only tools (listed in the opening brief); a PDF becomes its text, through OpenRouter's PDF parser (its free engine); an image goes, as an image, to the seats whose model takes images (OpenRouter's catalogue says which), and a vision model on the workspace's key describes it once for the seats and the clerk that don't see. The meeting's root message shows what it reads and what was left out. Limits: 10 files, 20 MB each, images 5 MB each (15 MB together go to the seats as images; past that, the description), repositories up to 300 MB on GitHub.The mode is picked at start: with SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, SLACK_SIGNING_SECRET and DATABASE_URL set, npm run slack (or the Docker image) serves the hosted app over HTTP; without them it's the Socket Mode bridge above, unchanged.
| Variable | What |
|---|---|
SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, SLACK_SIGNING_SECRET | Basic Information → App Credentials of the public app |
DATABASE_URL | postgres://… (Railway and Fly give you one), or sqlite:/data/consortium.db for one file on a volume |
CONSORTIUM_SECRET | 32+ random characters (openssl rand -base64 32). Encrypts keys and tokens and signs links. Keep it: changing it makes the saved keys unreadable (admins re-add them) |
PUBLIC_URL | Where Slack and browsers reach the server, like https://consortium.example.com (on Render, defaults to RENDER_EXTERNAL_URL) |
PORT | Default 3000 |
CONSORTIUM_DATA | Transcripts and the agents' memory, one folder per workspace. /data in the image: mount a volume there to keep them across deploys |
GITHUB_APP_ID, GITHUB_APP_SLUG, GITHUB_APP_PRIVATE_KEY, GITHUB_APP_CLIENT_ID, GITHUB_APP_CLIENT_SECRET | The GitHub App the table reads repositories through (below). Optional: without all five, repositories are hidden and everything else works |
Routes: / (the landing page with Add to Slack), /slack/install and /slack/oauth_redirect (Slack OAuth), /slack/events (events, interactivity and the slash command, signature-checked), /openrouter/connect and /openrouter/callback/:state (OpenRouter PKCE), /github/setup and /github/created (creating the GitHub App), /github/connect and /github/callback (a workspace connecting it), /mcp/connect and /mcp/callback (signing in to an MCP server), /privacy, /health.
files:readReading attached files takes the files:read scope, which the manifest now asks for. In the Slack app's settings, add it (or paste the updated slack-manifest.hosted.yml), then reinstall in each workspace: a workspace installed before has no files:read, so its Home tab shows Reinstall to enable files (a link to /slack/install), and a question with files gets the same link instead of a meeting. Everything else keeps working until then.
One GitHub App, owned by you, that any workspace installs on its own GitHub account or organization. It asks for Contents: read and Metadata: read, nothing else, and no webhooks.
PUBLIC_URL/github/setup (for Render, https://consortium-yqs2.onrender.com/github/setup) and press Create it on GitHub. GitHub shows the app it's about to create from the manifest; pick another name there if it's taken, and create it.GITHUB_APP_ID, GITHUB_APP_SLUG, GITHUB_APP_CLIENT_ID, GITHUB_APP_CLIENT_SECRET, GITHUB_APP_PRIVATE_KEY. Set them on the server (Render: the service → Environment; paste the private key as is, line breaks included, or on one line with \n). The service restarts with them.How it stays private: GitHub's installation_id can be forged, so an installation counts only if the person who signed in to GitHub can reach it (the client id and secret are for that one check; their GitHub token is used once and never kept). Each meeting mints an installation token for that one repository, shallow-clones it (the last 50 commits of the default branch, blobs over 1 MB in older commits left out) into a temporary folder with the token passed to git as an HTTP header through the environment (never in the command line, .git/config or the remote URL), gives the seats their read-only file and git tools on it, and deletes it when the meeting ends. Anyone who can see an installation's repositories on GitHub can connect it to their workspace, so install it on the repositories you mean to share.
Seats can call the tools of remote MCP servers the workspace connects: a seat checking a Datadog monitor or a Linear issue while it argues. Remote servers only (streamable HTTP over https); nothing is run on the Consortium server.
Connect one (admins, Home tab → Tools):
PUBLIC_URL), and Notion's sign-in is for the person's own pages, so connect it with an account that sees what the table should read.Authorization: Bearer …, one per line). With no headers, it's a server you sign in to: its menu in Tools has Authorize. Each server's menu also has Test, Allowed tools and Disconnect (asked to confirm first). Addresses on a local or private network are refused.Read-only by default. A seat gets only the tools the server marks read-only (readOnlyHint). An admin can allow more per server (Allowed tools), and should allow only ones that just read. Which seats use which server is in The table → Edit (each seat's Tools; all of them by default, including servers connected later).
Limits: 8 tool calls per seat per turn and 60 per meeting, results cut at 8,000 characters, 30 seconds per call, 15 seconds to connect; a server that doesn't answer is left out of the meeting, not fatal. Each seat's message ends with the tools it called and a Tool calls button: each call in order, its arguments as sent, how long it took, how much came back and the start of it (or the error), secrets hidden; kept until the app is uninstalled. The meeting's root message lists the servers and how many tools each gave.
Test (admins, next to each connected server) connects, lists its tools and runs one cheap read: Datadog searches all logs of the last 15 minutes (*, one at most), Linear lists three issues, other servers only list. It shows what came back or the exact error, so a bad key, site or permission shows at once. Datadog empty while its Log Explorer isn't? The site must be the one you sign in at (US1 is datadoghq.com), and the application key's owner needs Logs Read Data and Logs Read Index Data.
Privacy. A connection is the workspace's, not a person's: everyone who can convene a meeting there uses the access of whoever connected it. Credentials (OAuth tokens, pasted headers) are encrypted at rest like the OpenRouter key, bound to the workspace and the server, opened only to start a meeting, and never logged, shown or put in a transcript. Tool results go to the seats' models on OpenRouter and into the meeting's transcript; the seats are told those results are untrusted data, not instructions. Disconnect deletes the credentials (revoke the grant on the provider's side too, if you like).
Render: New → Blueprint → this repo. render.yaml creates the web service (Docker, Free plan: it sleeps after 15 idle minutes, so the first Slack request after a nap can time out; Starter avoids that) and generates CONSORTIUM_SECRET; it asks for the three Slack values and DATABASE_URL, a Postgres connection string from outside Render (a free Neon database is enough). PUBLIC_URL is optional there: the server uses Render's own RENDER_EXTERNAL_URL.
Railway: New Project → Deploy from GitHub repo (it builds the Dockerfile) → add a Postgres database (it sets DATABASE_URL; reference it in the service) → set the variables above → Settings → Networking → Generate Domain, and use it as PUBLIC_URL. To keep transcripts, add a volume at /data (the image runs as node; if the volume isn't writable, set RAILWAY_RUN_UID=0).
Fly.io:
fly launch --no-deploy # picks up the Dockerfile; internal port 3000
fly postgres create && fly postgres attach <db-app> # sets DATABASE_URL
fly volumes create data --size 1 && echo -e '[mounts]\n source = "data"\n destination = "/data"' >> fly.toml
fly secrets set SLACK_CLIENT_ID=… SLACK_CLIENT_SECRET=… SLACK_SIGNING_SECRET=… CONSORTIUM_SECRET="$(openssl rand -base64 32)" PUBLIC_URL=https://<app>.fly.dev
fly deploy
Keep one instance: the PKCE flows in progress and the meeting counts are held in memory.
slack-manifest.hosted.yml, replace consortium.example.com with your domain (four places), then at api.slack.com/apps: Create New App → From a manifest, pick a workspace of yours (it's the app's home; anyone can install it later), paste it. Slack checks the events URL, so deploy first.PUBLIC_URL/slack/install), not Slack's own sharable URL: an install has to start at /slack/install, which sets the OAuth state cookie the redirect checks. Listing in the Slack Marketplace is a separate review: it needs a real privacy policy (the /privacy page is a placeholder to replace), support contact and the app's icon.Run it from a checkout instead of Docker with the same variables: npm run slack. Locally, Slack needs a public HTTPS URL to reach you (a tunnel like cloudflared tunnel --url http://localhost:3000, then that URL as PUBLIC_URL and in the manifest).
Don't run it with Bolt's debug logging in production: it logs request bodies, and a pasted key travels in one.
Every CLI is driven in its headless JSONL mode with its own session resumed each round, so a member remembers its earlier turns and only receives the others' new arguments:
| CLI | Command | Resume | Project attached | No project |
|---|---|---|---|---|
| claude | claude -p … --output-format stream-json | --resume <id> | --tools Read,Glob,Grep | --tools "" |
| codex | codex exec … --json -s read-only | codex exec resume <id> | -C <dir> | temp dir |
| grok | grok -p … --output-format streaming-json --permission-mode plan | -r <id> | --cwd <dir> | --tools "" |
| gemini | gemini -p … --output-format stream-json --approval-mode plan | --resume <id> | --include-directories <dir> | — |
Adapters live in src/main/cli.ts (one args + one parse per CLI). The debate engine is src/main/debate.ts, the prompts src/main/prompts.ts. The renderer never touches Node; it receives DebateEvents over IPC (src/shared/types.ts).
Engine check without the UI (real CLIs, two members, ten turns; add SMOKE_PROJECT=/path/to/repo to attach a project):
npx tsx scripts/smoke.ts claude,codex 10
Vote-parser self-check (no CLIs):
npx tsx scripts/check.ts
Open the renderer at the Vite URL in a normal browser (no Electron) and it runs on a scripted mock debate — handy for UI work.
npm run build:mac
Our own member: a Deep Agents agent (planning, read-only file tools, subagents) that runs inside the app on an API key. Pick it under Connection, then a provider and a model. Its tools:
ls, read_file, glob and grep on the attached project and files, read-only.git runs read-only commands in the project (log, show, diff, blame, status, shortlog, ls-files, grep, rev-parse, describe, for-each-ref; branch and tag only list). No shell: options that write, run programs or read outside the repo are refused.web_search (OpenRouter's web search on your OpenRouter key, about $0.007 a search, not counted in the usage meter) and fetch_url (reads a public page; local and private addresses are refused).write_todos and task.Secrets stay out of reach: .env files, private keys and credential files can't be read or matched by search, so a web page can't talk the agent into sending them out.
| Provider | Model examples | Key |
|---|---|---|
| OpenRouter | z-ai/glm-5.3, anthropic/claude-opus-5.5, any | OPENROUTER_API_KEY |
| OpenAI | gpt-6-sol | OPENAI_API_KEY |
| Anthropic | claude-opus-5-5 | ANTHROPIC_API_KEY |
gemini-3.8-flash | GEMINI_API_KEY | |
| xAI | grok-4.7 | XAI_API_KEY |
Add the key under Settings (top right of the lobby), or from the seat's Add key link: it is encrypted with the macOS keychain into the app's data folder and only ever sent to that provider. The environment variable works too. Cost is the catalogue list price from OpenRouter, so it shows as ≈. Each seat's memory lives in harness/<thread>.json in the app's data folder (harness is the seat's internal id).
Turn on Read turns aloud in the lobby and each seat speaks its final answer in its own ElevenLabs voice (elevenlabs/eleven-v4-turbo through OpenRouter's speech API, so it uses the OpenRouter key from Settings or OPENROUTER_API_KEY). Pick a voice per seat; two seats never share one. Only the answer is read: thinking, tool activity, the vote block, markdown and [T3] citations stay silent. The chair's decision is not read.
OpenRouter returns a clip only once it is fully synthesized (no streaming), so the engine covers the gap two ways:
resources/voices/. Regenerate them after changing FILLERS in src/shared/voices.ts: npx tsx scripts/voices.ts (needs OPENROUTER_API_KEY; existing files are kept).The next seat thinks while the previous one is still talking, and the audio never falls more than one turn behind the text. The room highlights whoever is audibly talking; the seat already writing its next answer shows its working state. Speed cycles 1×, 1.25×, 1.5× and 2× without changing the voice's pitch (ElevenLabs v4 has no speed setting, so it is playback speed). Mute silences playback and lets the meeting run at text pace. Cost is the ElevenLabs list price per character, about $0.02 per thousand characters through OpenRouter. If the key is missing or a request fails, the room says "Voice failed" and the meeting carries on in text. Engine check with voices: SMOKE_VOICES=1 npx tsx scripts/smoke.ts claude,codex 4.
Unsigned, unnotarized. When launched from Finder the app asks a login shell for the CLI paths, so nvm / ~/.local/bin / ~/.grok/bin installs are found.
claude binary with their own subscription; shipping this to other people means each of them signs in to the CLIs themselves — the app must never collect or route anyone's credentials.--skip-trust; the app only ever runs it in the attached project or an empty scratch directory.Open Whiteboard in the meeting toolbar for a dedicated view of the current direction, shared ground, disagreements, questions, and proposed actions. Notes link back to their conversation turns. The Electron main process runs a separate moderator/whiteboard writer, configured in the lobby. The default is Claude Haiku through Claude Code, falling back to the chair’s connection when Claude Code is not detected. Its selected connection must be installed and signed in (or have an API key for a Deep-agent writer). The browser preview uses illustrative demo updates.
The conversation strip updates as speech arrives. The writer coalesces new text into short batches (normally at least eight seconds between starts, with turn-end/manual refreshes requesting an immediate update). Model generation adds latency; only one writer runs at a time, and speakers never wait for it. A failed update keeps the previous board. Snapshots are saved with the session; the timestamp and turn number show their coverage. The writer stops when the meeting ends, so the last board can trail the final transcript; the chair's decision is the final synthesis.
The whiteboard now includes a Visual canvas alongside All notes. Its writer can maintain up to three complementary artifacts: connected flow/system diagrams, alternatives tables, and step sequences. Visuals use the same conversation sources as the notes; proposals, shared ground, and open questions remain visibly distinct. Existing sessions without visuals still open normally.
Each agent and the chair receives the current structured visual in its prompt. Members can propose a drawing or describe a correction in their contribution; the writer updates it on the next successful synthesis. Selecting a node, comparison criterion, or plan step exposes its context and sources. Discuss this prepares an editable host message containing the visual revision and a readable snapshot, so the room can address the exact drawing even if it has since changed. Send it to everyone or choose an agent from the host controls.
Use Expand whiteboard and diagram zoom for detail. Freeze view holds a local revision without pausing the meeting; Return to live catches up. Rendering uses validated data and native SVG/table elements, not model-generated executable markup. Malformed visual updates keep the last successful board. Complex diagrams still arrive in batches and depend on model latency; this is a generated visual workspace, not a freehand drawing editor.
The whiteboard is the default meeting view, with a compact participant strip and the conversation alongside it. Layout choice is remembered on this device. Questions appear first in the canvas notes; Answer this question opens an inline editor. The displayed board stays still while you answer, and the submitted message carries the exact question and revision. Delivery is shown on the card. Other notes have Discuss this actions, and changed notes are labelled Updated on the next board revision.
The existing board writer also acts as a moderator. It watches for drift, repeated arguments, unanswered host input, and unresolved disagreements, then publishes a short visible redirect and optionally suggests the next speaker. The engine applies fresh guidance at turn boundaries after every seat has had an opening, with a fairness limit to prevent a seat being starved. Human targeting always wins. Refocus the room sends an immediate host instruction to return to the original question while preserving earlier constraints.
Moderation is asynchronous and shares the board writer’s latency and connection. A failed update leaves the last board intact; stale or provisional guidance cannot change speaker order. Host interruption is handled locally and does not wait for the moderator or the cancelled provider to finish. Browser preview responses remain scripted; engine regression tests use controlled providers to verify cancellation, targeted routing, chair corrections, and stale audio suppression.
anthropic/claude-sonnet-5.5openai/gpt-6.1-solgoogle/gemini-3.8-flashCONSORTIUM_SECRET, bound to the workspace), as are the installations' bot tokens; a key is decrypted only to start that workspace's meeting and is handed to its seats per run, never through the environment. It is never logged, shown or saved with a transcript.