Proudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/
Ephemeral Board of AI Advisors that only convene when you need them.
Put one question to a panel of independent models, in parallel, and read where they disagree.
$ roundtable "Is replacing our message queue with direct calls a mistake?"
==========================================================================
Skeptic (some-vendor/some-model) 2.1s
==========================================================================
...
9/9 lanes answered in 51.2s
transcript: ~/.local/share/flatline-roundtable/transcripts/20260825-163840.jsonOne model asked one question gives you one prior. Ask eight models from eight different training lineages and the disagreement is the product: where independently-trained models land in the same place, that convergence is evidence; where they split, that is where your thinking is underdetermined.
A panel of one model wearing eight hats gives you neither — shared priors produce convergence that looks like corroboration and is not.
This replaced a design where each advisor was a long-lived agent process with its own identity, message-bus membership, and a tool it had to call to publish its answer. Everything that went wrong with that was transport, not models:
- An abandoned run kept billing for 54 hours — a backgrounded agent has no reason to stop.
- A nine-advisor review delivered four answers. The other five produced correct responses that died in log files, because those models never called the publish tool. The harness logged the text and discarded it.
- Advisor names that resolved ambiguously notified nobody, silently.
None of that is possible here:
- No long-lived processes. A lane is one HTTP request or one short-lived subprocess. There is nothing left running to bill.
- No delivery step. The answer arrives in the response or the lane failed loudly. Nothing can "succeed" into a place nobody reads.
- Partial delivery is a failure. If any active lane is silent, the exit code is non-zero. A quiet advisor is the bug, not a rounding error.
Requires Python 3 with PyYAML, and pass if
any lane needs an API key.
$ git clone https://github.com/CryptoJones/FlatlineRoundtable
$ cd FlatlineRoundtable
$ mkdir -p ~/.config/flatline-roundtable
$ cp FlatlineRoundtable.yaml.example ~/.config/flatline-roundtable/FlatlineRoundtable.yaml
$ $EDITOR ~/.config/flatline-roundtable/FlatlineRoundtable.yaml
$ ./roundtable --list # shows the roster; makes no network callsOptionally ./install.sh to expose it as a Claude Code skill.
No key ever appears in a config file, in this repo, or in a process list.
- A lane names a
passentry (key_entry: openrouter/agent/skeptic), never a key. - Keys are read once, before fan-out, and held in memory only. Never in
argv, which is world-readable viaps. - Keys never reach the transcript or
--jsonoutput. - A missing entry fails that lane loudly and names the fix. It never silently falls back to an unauthenticated call.
- Your real config lives at
~/.config/flatline-roundtable/, outside the repo, because.gitignoreis not a security boundary —git add -f, stashes, and editor backups all defeat it.
harness decides cost, not model.
An http lane bills per token. cli and acp lanes drive a vendor's own binary
against an existing subscription and cost nothing extra.
Rewriting a cli lane as an http lane pointed at the same vendor's API
silently moves it onto metered credits. So does leaving an API key in your
shell: these CLIs prefer an env key over the OAuth session. FlatlineRoundtable
scrubs ANTHROPIC_API_KEY, OPENAI_API_KEY and friends from every CLI lane's
environment for that reason.
Prices come from the gateway's own table (cached for a day), so they do not go
stale the way a hand-maintained number does; price_per_mtok overrides it for
vendors with no price endpoint. --max-spend and budget_usd are checked
before dispatch against a worst-case estimate, so an overrun is prevented
rather than reported. Under a budget, an http lane whose price cannot be
resolved is refused outright rather than estimated at $0 — a guard that
cannot see a lane cannot bind on it. cli/acp lanes, :free models and an
explicit price_per_mtok: 0 are free, not unknown.
roundtable --each "the brief" # each lane its own process (do this)
roundtable --lanes Skeptic "..." # one lane
roundtable --panel "the brief" # all lanes, one shared deadline
cat brief.md | roundtable - # long briefs on stdin
roundtable --lanes Skeptic,Chair "..." # a subset
roundtable --list # roster + route; no network calls
roundtable --json # structured output
roundtable --config PATH
roundtable --max-spend 0.50 # refuses BEFORE dispatch if the estimate exceeds it
roundtable --panel --diff # report only where the lanes disagree (not with --each)
roundtable --no-transcript
roundtable --each --revise latest:12 # optional second round — see below--diff asks lanes to report AGREED / SPLIT / LONE CLAIMS across the others,
because reading N full answers does not scale and disagreement is the product.
It needs every answer in one process, so it pairs with --panel; --each --diff is refused up front rather than silently dropping the flag. To get the
independent-process run and a comparison, run --each and synthesize from
the transcripts afterwards.
It is opt-in and it is not neutral — a synthesizer is one model with its own
priors deciding what counts as a disagreement, so the raw answers still go to
the transcript. That is also why it uses two readers by default, picked from
different vendors: with a single reading you cannot tell a real split from that
model's taste in splits. Where the two place the same lane differently,
roundtable says so under THE READERS DISAGREE — that is the reading doing the
work rather than the evidence, and a signal to go read that lane's raw answer.
The comparison is mechanical on purpose. Handing it to a third model would just
move the problem one level up. Set synthesizers: 1 to go back to a single
reader, or name a specific one with synthesizer: <lane>. It prefers free
lanes, so the convenience does not quietly cost money.
After a run, any lanes that turn out not to be independent are named:
LANES THAT ARE NOT INDEPENDENT
Twin1, Twin2 — answered from claude / haiku
Agreement between these is an echo, not convergence.
Two lanes collide when a gateway served them the same model, or when they
declare the same lineage:. It is never fatal — a deliberate duplicate is a
legitimate thing to want — but it must not be silent, because it is invisible in
the answers themselves and it falsifies the one claim the tool makes.
Two config files ship with the repo. FlatlineRoundtable.yaml.example is the
annotated template — every option, with the reasoning for each. examples/full-roster.yaml
is a real thirteen-lane roster, kept because a tuned config is mostly numbers
you had to measure, and those are worth reading before you pick your own.
Every run writes a transcript to
~/.local/share/flatline-roundtable/transcripts/, because answers that exist
only in a terminal scrollback are answers waiting to be lost.
Round 1 is blind by construction: a lane cannot see the others because their
answers do not exist yet in its process. --revise is the one deliberate
exception. It replays a finished run's transcript(s), handing every lane
the locked round-1 answers — its own marked YOURS, the rest anonymised as
PANELIST A/B/C — and instructions to open with HOLD or REVISE and to move
only for a reason it can state. Anonymised, because "the Anthropic lane said
so" is exactly the deference the instructions forbid.
roundtable --each "the brief" # round 1, blind, 12 lanes
roundtable --each --revise latest:12 # round 2: the whole prior run
roundtable --each --revise latest:12 "focus on the cost claims" # extra focus
roundtable --lanes Chair --revise ~/.local/share/.../20260901-*.json--each writes one transcript per lane, which is why --revise takes
latest:N and comma-separated paths and merges them; it refuses to mix
transcripts whose briefs differ, because that is only ever an accident. The
report ends with who held and who moved, under a banner that says the thing
that matters:
ROUND 2 — lanes saw the round-1 answers. Agreement here is persuasion,
not independent convergence.
Treat round-2 convergence accordingly. Round 1 tells you where independent
models land; round 2 tells you which positions survive contact with the
others' arguments. Both are useful; only the first is evidence of
independence. A round-2 transcript records round, its parent transcripts,
and the alias map, so the anonymity is auditable after the fact. --revise latest:12 on a round-2 run produces round 3; nothing caps it, but each round
is another full panel spend, and the returns fall fast.
(--converse is an alias; the two are the same flag.)
--revise shows every lane the others' answers once. --discuss lets them
talk. The panel shares one context — the brief, the anonymised opening
positions, and every turn said so far, in order — and takes turns in it, one
lane speaking at a time. Each turn is one ordinary request with that shared
context as the prompt, so the process holds a single request in flight, a
cli lane's agent runtime exists only for its turn, and nothing outlives the
run. That is the whole point: a standing fleet with a resident runtime per
lane and a message bus between them can do this too, and is far too heavy to
keep around for it.
roundtable --each "the brief" # round 1, blind
roundtable --discuss latest:12 # the same 12 lanes discuss it
roundtable --discuss latest:12 --discuss-rounds 3 "focus on the cost claims"
cat brief.md | roundtable --discuss new - # start cold, no blind round
roundtable --lanes HAL9000,SHODAN,Cortana --discuss latest:12 # a subsetSeed it from a blind round and round 1 keeps its epistemic claim; new
starts cold and its transcript is round 1 — a dependent one. Each pass
gives every lane one turn (the opener rotates each pass, so no lane always
anchors or always gets the last word), and after the last pass each lane
writes a closing that opens with HOLD or REVISE. The report tallies the
closings under the same banner --revise uses, because the same caveat
applies: agreement after a discussion is persuasion, not independent
convergence.
The thread is O(N²) in prompt tokens — turn k re-sends the k−1 turns before
it — so a turn is capped at ~300 words and --discuss-rounds defaults to 2.
The budget gate prices the longest packet times the requests per lane
(rounds + 1), and deadline_seconds bounds one turn rather than the run.
Every turn is appended to the transcript as soon as it returns, so a run that
dies at turn 17 leaves 16 turns on disk. The closings are saved as results,
which makes a discussion transcript a valid parent for --revise, for --diff
synthesis, or for another --discuss.
--discuss refuses --each, --panel and --revise alongside it (there is
nothing to fan out — each turn is built from the ones before it), and refuses
fewer than two lanes.
Use it when lanes split on a judgment — severity, reachability, which fix
is right — and the split is what you need settled. When the only dissent
rests on a checkable fact, check the fact; a discussion costs
rounds × lanes requests and can manufacture evidence, a fact check costs one
command.
What a turn can see. --discuss drives the lanes itself, through the same
http / cli / acp harnesses as round 1. An http or acp turn sees the
shared packet and nothing else — no tools, no repo — so anything the panel
needs to check must already be in the openings or in the focus text (quote
contiguous excerpts, mark elisions). A cli turn has whatever its vendor CLI
normally has. If round 1 was a tool round driven by skill/toolpanel.py, its
per-lane answer files are not transcripts; fold them into one with
skill/toolpanel-seed.py and seed from that:
skill/toolpanel-seed.py brief.md seed.json answers/*.md
roundtable --lanes HAL9000,SHODAN,Cortana --discuss seed.json "the split to settle"What the transcript holds. brief (the round-1 brief), results (the
closings), discussion (every turn: turn, pass, alias, lane, answer,
seconds, cost), aliases (letter → lane, so the anonymity is auditable),
parent (the seed transcripts, [] for new), round, mode: discuss.
$ python3 -m unittest discover -s tests132 tests against a stub HTTP server and fake CLI binaries — no vendor is contacted, nothing is spent, no credential is needed. They cover the behaviours that silently cost money or leak processes: the env scrub, the process-group kill, the missing-secret abort, and partial delivery failing the run.
- Three harnesses:
http(any OpenAI-compatible endpoint),cli(one-shot subscription CLIs), andacp(JSON-RPC-over-stdio agents, driven for exactly one turn). Anacplane'sconfig_options(thought level, mode, model — whatever the agent lists underconfigOptions) are applied withsession/set_config_optionaftersession/newand before the prompt, because the agent's own CLI flags do not reach them; a rejected or unhonoured value fails the lane rather than answering at the wrong setting. - Tool access is a property of the harness, not the panel.
httpandacplanes see the brief and nothing else — put the material in it.clilanes run the vendor's CLI with its normal tools and may read, run, and write in the directory they are launched from; commit first, and expect them to investigate. A lane keeps its tools until it cheats (reads another lane's answer, edits the code under review, games the question); then it loses them. - Lanes never see each other's answers. That is the point; it is also why they cannot build on one another.
- A weak lane is worse than an absent one — it still gets counted. Prune the
roster on quality, and record known defects in each lane's
notes.