Website: botference.com · If botference is useful to you, consider sponsoring its development.
Note
This README was AI-generated. I (Angadh) have skimmed and supervised its creation.
Multi-LLM planning where you and the AIs collaborate in a council — a
free-form group chat where the bots can also hand each other the floor and
hash things out directly, with you steering. The result is an
implementation-plan.md and checkpoint.md you can take into any workflow.
The primary contribution of this repository is plan mode — a multi-LLM planning session where Claude and Codex collaborate in real time. This is the part that works well and is ready for use.
Warning
This is vibe-coded. Use at your own risk. Research-plan mode and build mode
are deeply experimental — under active development, will change without
notice, and not recommended for general use. If you are here to get things
done, use botference plan and take the resulting plan into your own
workflow.
Ink TUI — projects panel (left), full-width council panel (right), input field and status line at the bottom. This is the primary interface and default planner UI:
The Projects panel lists Inbox plus every folder under projects/. The
controller sends the recent resumable chats for every project — up to
BOTFERENCE_PANEL_SESSION_LIMIT (default 100; 0 means no limit at all), a
bound on the size of a snapshot that is broadcast after every turn rather than
on how many chats you are allowed to see; the web sidebar scrolls a long list.
The Ink panel expands the active project (the web sidebar lets you expand any
of them). Pane controls in Ink:
| Key | Action |
|---|---|
Tab / Shift+Tab |
Toggle focus between Council and Projects (when visible) |
↑ / ↓ |
Navigate the Projects list while it has focus |
| type | Filter projects and chats by title while the panel has focus (Esc clears; / still starts a command) |
Enter |
Open the highlighted project, switch to Inbox, or resume the highlighted chat |
Ctrl+P |
Toggle the Projects panel's visibility |
Chats under the active project are sorted newest-first and show a compact
age (5m, 3h, 2d); the chat you're currently in is marked ▸ … · open.
Selecting Inbox runs /project clear; selecting a project header runs
/project open <id>; selecting a session row runs /resume <session-id>.
You can switch sessions mid-chat — the current session is persisted on every
turn, so you can /resume <its-id> to come back later. /resume <id> reaches
any saved chat, including one filed under a different project; opening it
makes that chat's project the active one.
The Ink TUI is the only frontend. First use after clone: cd ink-ui && npm install. It supports app-level pane-clamped text selection, multiline input
(Shift+Enter), streaming, and activity status. Use --claude to skip Codex
and run a solo Claude session (no TUI, just the Claude CLI).
Project-local runtime behavior and the long-term packaging direction are
documented in docs/project-model.md and
docs/distribution-roadmap.md.
Default planning behavior in project-local mode is intentionally lazy: plan
and research-plan do not scan the whole project up front, and vault-style
projects keep writes confined to Botference-owned paths unless you explicitly
expand them in botference/project.json.
Botference is built around one metaphor: the council — an open room where you and the AIs all talk, like a three-person group chat. You steer the conversation, ask questions, push back, and direct who speaks; the bots can also hand each other the floor and debate directly (budgeted, and you can always interrupt) until they bring you a coherent proposal instead of conflicting opinions. When they agree on who should write the plan, the lead is set automatically.
- Claude Code CLI installed and authenticated
- Codex CLI installed and authenticated
- Python 3 available on your
PATH - Node.js + npm for the default Ink UI (
plan/research-plan)
If you cloned this repository and want to try Botference immediately, run it from the repo root with the local launcher:
./botference plan # Ink TUI (default)
./botference plan --claude # Solo Claude
./botference plan --claude-interactive # Experimental: mirror interactive Claude through tmux
./botference research-plan # Structured planning in Ink (experimental)
./botference --help
man docs/man/botference.1 # Man page (launcher + in-session commands)Do not run botference init in the Botference source repo. This checkout
uses the legacy self-hosted layout (work/, build/, archive/) rather than
a top-level botference/ state directory.
Before the first planning run from a fresh clone:
cd ink-ui && npm installFrom a target project root:
botference init # Create project-local botference/ state (and register it with Discuss)
botference site # Just the Discuss half, for a repo already initialised
botference init --project-dir=spaceship # Or create botference-spaceship/ instead
botference plan # Council: you + Claude + Codex (Ink default)
botference --project-dir=spaceship plan # Use botference-spaceship/
botference plan --claude # Solo Claude (no Codex)
botference plan --claude-interactive # Experimental interactive-Claude tmux transport
# Building (experimental)
botference build # Interactive build loop
botference -p build # Headless build loop
botference -p build 10 # Headless, max 10 iterations
botference -p build --parallel # Phase-level parallelism
botference --help # Full usage + supported modelsAfter botference init, Botference stores its state inside ./botference/ in
that target project. This is the right workflow for brownfield or greenfield
projects outside the Botference engine repo.
If you use --project-dir=<slug>, pass the same option on later Botference
commands for that project, or set BOTFERENCE_PROJECT_DIR_NAME=<slug> in your
shell/project environment. Slugs may contain letters, numbers, hyphens, and
underscores. Bare slugs are prefixed, so --project-dir=spaceship resolves to
botference-spaceship/; explicit names like botference-spaceship are accepted
as-is.
If you move the Botference framework itself to a new directory later, target
projects do not need to be re-initialized. Their local ./botference/ state
stays valid. Just make sure the launcher points at the new framework path, for
example:
export BOTFERENCE_HOME=/new/path/to/botference-mainThere is no extra “run it once from the framework repo” step. The framework
path is resolved at launch time from BOTFERENCE_HOME or from the botference
script you actually invoked.
Botference ships a Google-Docs-style review interface for LaTeX,
Markdown and Jupyter Book sources (a repo with _config.yml + _toc.yml:
chapters as .ipynb notebooks and/or MyST .md, in _toc.yml order;
each notebook is reviewed and edited cell by cell through a plain-text
view of its cells, and MyST directives render approximately): the paper
renders as commentable HTML, margin
comments become bot turns when you tag @claude/@codex/@all, bots
reply in threads and post accept/reject suggestion cards, and accepted
changes are applied to the sources deterministically with separate
Apply / Commit / Revert steps. Multi-human collaboration works through
git (per-user comment files) or a password-protected tunnel
(--hosted).
One command from your document repo:
botference review # set up (first run) + build + serve; agents auto-detected
botference review --share # hosted + tunnel: one URL + password to send around
botference review [dir] [--share] [--hosted] [--port N] [--no-agents]
[--upgrade | --upgrade-only]The first run copies the engine into <dir>/review/, auto-detects the
document configuration (master file, sections, bib, figures — echoed so
you can correct review/review.config.json if it guessed wrong), adds
the gitignore block, builds the site, and prints next steps. Subsequent
runs rebuild only when sources changed, then serve. Agents are on by
default when the machine can run them: the launcher checks for python3
plus a claude/codex CLI on PATH and prints agents: on (claude, codex detected) — or serves read-and-comment only with an explanation
when they're missing. --no-agents forces the bridge off, --agents
forces it on (clear error if impossible), --hosted adds
REVIEW_PASSWORD auth for a shared URL (an in-page gate asking for a
name and the password together, no browser popup; tools can still send
basic auth with any username), and --upgrade refreshes the engine
files without touching your config, comments, or suggestions.
--upgrade-only does that same refresh, rebuilds the site with the new
assets and exits without serving — safe to run against a paper that is
already up, which is how the review hub keeps hosted papers current. Set
REVIEW_OWNER_PASSWORD too and that second password signs the owner
in from any device — phone included: whatever name is typed, the gate
issues the owner's handle and owner token, while the guest password
keeps working unchanged.
Two ways to bring in collaborators:
- Live URL (
--share): serves hosted mode behind a cloudflared quick tunnel — respectsREVIEW_PASSWORDor generates one — and printsshare this: <url> password: <pw>. Guests give their name at the gate, then read, comment and suggest; their agent summons queue for you to release — or, if you grant a named guest agent access in the People panel, go straight through within a daily cap they can see. Apply, commit, revert and model switching stay yours alone. Ctrl-C stops server and tunnel together. Withoutcloudflaredit prints an install hint and keeps serving locally. For a stable URL across sessions, configure a named tunnel (docs link in the man page). - Git sync: commit
review/and.gitignore; collaborators clone and either install botference and run the identicalbotference review(agents auto-detected — without the CLIs they still read and comment) or use plainnode review/server.mjs, then commit their comments back withnode review/submit.mjs --push.
One store of comments, when you also use Discuss. A visitor without
the Discuss extension comments in the review page's own margin, and by
default that comment stays there — invisible to your drawer, to the
bots, to send review and to the Obsidian export. Add one line to
review/review.config.json:
"discuss": { "companion": "http://127.0.0.1:4189" }and every margin comment becomes a Discuss thread on that page, under
the name of whoever wrote it and anchored to the same words; anything
said back — a bot's answer, your reply — comes home to the margin they
wrote in. A comment about the document as a whole lands in that page's
chat rather than pretending to have an anchor. The two servers talk over
the loopback only, so nothing about your tunnel or your guests' sign-in
changes. Add "base": "https://<your paper's hostname>" if the paper is
read at more than one address and you want them treated as one page.
Without that block nothing changes — a clone with no companion, a
collaborator's checkout and a static site/ opened from disk all keep
the commenting they have today.
Hosting several papers? frontends/review/hub.mjs is a stable front
door for all of them behind ONE named cloudflared tunnel: a gated
portal on the hub hostname lists each visitor exactly the papers their
login opens (validated against each paper's own gate — the hub stores
no passwords) or that declare them as a collaborator, and every paper
gets its own hostname transparently proxied to its local server (run
with --hosted; no tunnel of its own). When a paper's server is down
its hostname serves a friendly "work from the git repo" page instead
of an error. On the machine itself, localhost is the owner: no login,
every paper listed. Config in ~/.botference/review-hub.json (see the
header of hub.mjs), re-read live — no restarts.
The owner portal runs the whole estate, so the config stops being something you hand-edit:
- Auto-discovery. Set
"workspace"(e.g."~/MySiteFromObsidianVault/botference") and every directory under<workspace>/projects/becomes a review candidate — scaffolded once it hasreview/review.config.json, not set up yet otherwise. The owner sees all of them, running or not, merged with the explicitpapersentries; an explicit entry always wins a collision. - On/off toggles. Turning a paper on scaffolds it if it was never
set up, picks a free port from
"portRange", runscloudflared tunnel route dns review <slug>.<domain>, and starts it hosted as a managed service with a generated guest password. If the DNS route fails the paper still comes up and the portal shows you the exact command to run. Turning it off stops that paper's service by its ledger entry, from the paper's own directory — never a pattern kill. - Wake-on-request. Ask for a paper whose server is down and, if you are the owner, it starts behind a self-refreshing "starting…" page. Guests keep getting the offline page; starting a paper is never a guest's decision.
- Passwordless owner devices. A new browser can ask to be trusted;
the hub pops a macOS dialog on the machine, and Approve hands that
browser a year-long signed cookie that makes it the owner on the hub
and the paper subdomains.
REVIEW_HUB_PASSWORDstill works as the password route from any device. - Private by default. A newly enabled paper gets a generated guest
password and an empty
collaboratorslist, so it is invisible and unreachable to everyone but you until you declare who may see it. Passwords live in~/.botference/review-paper-secrets.json(0600), never in the config. - Project files. Every discovered project also gets an owner-only
files view at
/p/<slug>/files/served by the hub itself — no review scaffolding, no paper server, no DNS record — so a plot or HTML report your bots produced is viewable at the portal with zero setup. Dotfiles (.git,.botference, …) and traversal are refused, and a project's own HTML is sandboxed to an opaque origin.
What you get in the browser: comment or suggest on any block —
paragraphs, figures, headings, list items, quotes, captions, table
cells and the title. A suggestion (yours or a bot's) renders inline as
tracked changes in its author's colour and flows through accept → ⚡
Apply → ✓ Commit. Tagging @claude/@codex/@all in a comment is the
only way to summon an agent. A top-right cluster shows who is here
(coarse, in-memory, never logged to disk, desktop only), and the owner
gets a bottom task console for document-level instructions plus a
Settings panel with live context occupancy, session usage and the
model switcher.
- Engine:
frontends/review/(document-agnostic; copied into each project'sreview/at setup). - Bot-driven setup also works: ask your bots to "set up paper review in " — the `paper-review` skill (`.claude/skills/paper-review/SKILL.md`) has the full contract; `design.md` next to it records the architecture.
- Requires
pandoc. Serving by hand still works:node review/server.mjs --chatfrom the project, then open the printed URL. - Shell completion: zsh — add
fpath=($BOTFERENCE_HOME/completions $fpath)beforecompinit; bash —source $BOTFERENCE_HOME/completions/botference.bash.
The planning council also runs as a chat web app (claude.ai-shaped:
sidebar where every project expands to its recent chats — tap any chat to
open it, no "make this the active project" step; the chat's project becomes
the active context on open — streaming transcript, slash commands
with autocomplete, choice/permission cards inline, image attachments —
attach button/paste/drag-drop on desktop, camera or photo library on
iOS — plus clickable links and tap-to-copy password chips). When Claude
spawns subagents (the Task tool), an inline progress lane in the
turn shows one live row per subagent — status dot, label, elapsed clock,
and latest tool activity — collapsing to a summary when each finishes.
Bot messages render markdown: fenced code, inline code, links, bold —
and tables (a sign-off sheet arrives as a real table, horizontally
scrollable on a phone, not a wall of pipes). An agents panel shows
each agent's condition for the open chat: a context gauge (whole
percents and compact tokens, e.g. 43% · 86k / 200k, with the 50%
auto-relay threshold ticked), live activity (current tool + target
while it works), the model picker, per-agent relay buttons plus
relay both, the auto-relay toggle, relay provenance ("memory reset
12m ago · self handoff" — when that agent's session memory last
restarted and which tier authored the handoff), and session facts
(project, mode, lead, route). On wide desktop it is a right-hand rail;
on phones and narrow windows it slides in from the RIGHT via its own
header toggle (the left hamburger drawer stays purely projects and
chats), so context usage is always readable on mobile. The open chat is
reflected in the URL as #/chat/<session-id>, so the
address bar is a shareable per-chat link; opening it reopens that chat
(an unknown id falls back to the current one). The server runs one
agent bridge per open chat, so browser tabs on different chats are
truly concurrent sessions behind the same tunnel — message away in one
tab while another chat's turn runs; tabs on the same chat share one
live stream. There is no limit on how many chats you open: past
COUNCIL_MAX_CHATS live bridges (default 4) the least-recently-used
idle, unwatched chat is parked (its process exits and it respawns from
disk when reopened), and when every chat is busy or watched the new one
opens anyway.
At the top of that panel sits tasks — the checklist from the newest
message that has one, whoever wrote it, kept in view while the chat
scrolls past it. The bots re-issue their task list as the plan moves; a
revised list replaces what the panel shows, so there is one list there
and it is the current one. Its checkboxes are the same ticks as the
message's (tick in either place and both follow; they survive reloads and
replays), a quiet line says who it came from and how much is done, and
↑ source scrolls the transcript back to the message it came from. The
section collapses, and folds away entirely in a chat that has no list.
Ticks are remembered per message, so a revised list starts from the
[x] marks the bots wrote into it — nothing guesses which old item
became which new one.
The agents panel also carries billing: per agent, whether its CLI
runs on the subscription it is logged into or on an API key
(auto / subscription / API key, the same three modes and the same
storage Discuss uses — see
API keys
below; a key pasted in either product works in both, while the mode is
each product's own). The switch works from anywhere you can sign in;
keys can only be added from the machine the server runs on, so
through the tunnel the fields are gone and the panel says where to add
one instead. A billing change applies to agents started from then on — a
chat already running keeps what it started with, because a process's
environment is fixed when it starts and the server will not kill a live
turn to answer a settings click.
The sidebar drives housekeeping without leaving the browser, and every affordance sends the same slash command the TUI takes — one code path:
- Every project header is a chevron toggle — active, inactive or archived — and lists that project's recent chats, scrolling once there are many. Tapping a chat opens it and makes its project the active context; there is no separate "activate this project" step. The project you land in auto-expands, and a project you collapse by hand stays collapsed.
- New is a split control —
+ Newwithchat/projectstacked beside it.chatruns/new;projectopens an inline title field and sends/project create <title>. - Each chat row — in a project block and in the Recent shortlist at the
top — has a ⋯ menu offering three different-sized ways out of
a list, safest first: Remove from project (
/project unfile <id>— the chat is untouched, only its filing goes, so it lands in Inbox), Archive (/archive <id>— it leaves every listing, every byte survives) and Delete… (/delete <id>, whose confirmation is the controller's own choice card in the transcript, so nothing is confirmed twice). - Each project block offers ▸ contents — a read-only look at what is in
the project: its chats, and a shallow listing of
projects/<id>/with sizes. The folder is fetched when you open the panel, not carried in the per-turn snapshot. - ⇪ publish to GitHub… (
/project github <id>) makes the project folder a git repo and pushes it to a new private GitHub repo through your ownghlogin. Confirm-gated in the transcript, never one click; the resulting URL is remembered and shown under the contents panel. - Each project block offers ⊘ archive project
(
/project archive <id>); archived projects collapse into an Archived section at the bottom of the sidebar, closed by default, where ↩ unarchive project brings one back. - Every project block, live or archived, ends with a red 🗑 delete
project… (
/project delete <id>). It removes the folder and every chat filed in it, so the controller asks first in the transcript, naming the folder and the chat count.
botference plan --web # serve locally, open the printed URL
botference plan --share # + password gate + cloudflared tunnel: one
# https URL + password — open it on your phone
botference plan --share --no-auth # skip the password gate (open URL).
# ⚠ anyone with the URL can drive your agents,
# which read/write files on this machine — URLs
# leak. Prefer the default password.The Ink TUI stays the default botference plan. The web server spawns
its own bridge session — do not open the same session in the TUI and
the browser at once (a second web server per workspace is refused via
.botference/council-web.lock). --port N picks the port
(default 4187); COUNCIL_PASSWORD fixes the password (generated per
session otherwise). Engine: frontends/council/.
For a stable share URL across sessions (both plan --share and
review --share), create a named cloudflared tunnel once and point
botference at it:
cloudflared tunnel login
cloudflared tunnel create <your-tunnel-name>
cloudflared tunnel route dns <your-tunnel-name> council.example.com
export BOTFERENCE_TUNNEL=<your-tunnel-name>
export BOTFERENCE_TUNNEL_URL=https://council.example.com # printed as the share URLWithout BOTFERENCE_TUNNEL, --share uses a quick tunnel with a
random trycloudflare.com URL.
Every message box — in the council web page and in the Discuss drawer
(page chat, comment threads, the library) — has a mic button. Tap it
to start recording, tap it again to stop (Esc throws the recording
away). The recording is turned into text on your Mac by whisper (the
Homebrew whisper-cpp package), and the words land in the box where
your cursor was, for you to edit before sending — nothing is sent on its
own. The speech-to-text model lives in ~/.botference/models; if there
is none there, Superwhisper's copy is used when it is installed. Nothing
leaves the machine. The button only shows when the Mac has a model.
It works from a phone over the tunnel, because the tunnel address is
HTTPS and browsers only allow the microphone on HTTPS pages (and on
localhost). On a plain-http address the button stays hidden. The
first tap asks for microphone permission; on an iPhone, Safari may ask
again in later visits.
The web annotator: discuss any page on the open web with Claude and
Codex. (botference plugin is the same command — the product was
renamed, the plumbing was not.)
You clone the whole repo either way — it is one script and no build — but nothing below the extension is needed to use it. Three steps:
git clone https://github.com/angadhn/botference && cd botference
./botference discussthen brave://extensions (or chrome://extensions) → Developer mode →
Load unpacked → frontends/plugin/extension. Highlight a passage on any
article and leave a comment. Done — the terminal council is a separate
thing you never have to run.
Two optional extras: ./botference discuss --install-autostart (macOS)
makes the companion start at every login so there is no terminal to
remember, and --install-tunnel gives it a permanent URL for your
phone. Bot replies need the claude and/or codex CLI installed and
logged in — without either, highlights, comments and Obsidian export
all still work.
The review-doc experience, injected into any static article page on the
open web: a browser extension (Chromium/Brave, frontends/plugin/extension)
plus a local companion server. Highlight text on a blog post or article,
leave a comment, and — if the comment (or any later reply) mentions
@claude, @codex, or @all — the bots answer inline in the same
thread, streaming into a right-side drawer. Threads without bots stay
private notes. Every page exports to one Obsidian note (quote +
comment pairs, plus the page chat); bot conversations persist as
council chats under the Plugin pages project, titled by the
article's own headline — except on a page your own council wrote, where
the chat is that project's own (see "the pages your council writes",
below).
botference discuss # serve the companion on 127.0.0.1:4189
botference discuss --service # same, detached (stop: botference service stop plugin-web)
botference discuss --share # hosted + cloudflared tunnel: invite collaborators
botference discuss [--port N] [--no-agents] [--hosted] [--here]
botference discuss --install-autostart # macOS: run it at every login
botference discuss --uninstall-autostart # …and undo that
botference discuss --install-tunnel # macOS + your own domain: one permanent URLSet-and-forget (macOS). --install-autostart, run from the
workspace you want the companion attached to, writes
~/Library/LaunchAgents/com.botference.plugin-web.plist and loads it
immediately: the companion is then running after every login, restarts
if it dies, and appends its output to
.botference/logs/plugin-autostart.log (check it with launchctl list | grep com.botference.plugin-web). Any --port/--no-agents you pass
alongside is baked in, and the PATH it gets still finds node,
python3, and your agent CLIs. Running one by hand still wins — it
holds the workspace lock, and the autostart copy takes over within
~10s of your Ctrl-C. --uninstall-autostart removes the job and the
plist.
One-time extension install: brave://extensions → Developer mode →
Load unpacked → frontends/plugin/extension. The drawer has two tabs —
Comments (threads anchored to highlights) and Page chat (one
conversation about the whole page) — and remembers the last tab you
used per site. Run the companion from your main workspace so bot chats
land in that workspace's council. Vault location and export folder live
in .botference/plugin/config.json (defaults to your vault's
Web Clippings/). Static article pages only by design: anchors are
quote-plus-context and degrade to an "orphaned" badge rather than ever
losing a comment. (Google Docs is the deliberate exception: a site
adapter reads the doc's text and margin comments with your own session
— Page chat only, since Docs paints to a canvas.)
Bubbles: the comment beside its highlight. Opening a 420px panel to
read one comment is a big gesture for a small question, so there is an
alternate view. In the drawer's gear panel, set comments: bubbles
(per browser, off by default — panel is exactly what the drawer has
always done). With it on and the panel shut, clicking a highlight opens
a small card beside the mark, joined to it by a drawn curve with an
arrowhead on the highlight. Open one beside every passage you care
about — they all stay up, and the one you last touched comes to the
front. Clicking a highlight again closes that bubble, the ✕ closes one,
Esc closes the front one and Shift+Esc closes them all; a click
elsewhere on the page leaves them alone. Past three messages a thread
squashes to its newest card with the earlier ones as a counted lip you
click to fan open. You can reply from one — the same box, the same
@claude/@codex pills, the same thread, and a bot's answer streams
into a new card without moving you off what you were reading. Each
bubble belongs to its passage rather than to your screen: scroll the
highlight out of view and the bubble goes with it, scroll back and it
is where you left it. Drag it by its header to move it and by its
bottom-right corner to resize it; both are remembered per thread for
the session, and double-clicking the corner resets the size. The
sidebar button hands a thread to the panel. Works the same on a PDF.
Starting a new comment from a selection still opens the panel.
PDFs are pages too. A PDF on the web normally opens in the
browser's own viewer, which no extension can reach into, so Discuss
brings its own (Mozilla PDF.js, vendored — no CDN, nothing fetched):
navigate to an address ending in .pdf and it opens in Discuss
instead, with real selectable text, highlights, comments, @claude,
Obsidian export and phone reading exactly as on an article. Quotes
carry their page number — the export writes > — p. 12 under the
blockquote — and the record is filed under the PDF's own URL, so the
same paper read on another machine is the same page. A PDF served
without .pdf in its address is not intercepted; click the toolbar
button on it and it opens in Discuss anyway. A scan says so in one
quiet line and offers no highlighting, because there is no OCR here.
Turn the whole thing off (or back on) on the extension's options page.
And the PDFs on your own disk. Open a file:// PDF and it opens in
Discuss too, with everything above. It needs one switch, once, which no
extension is allowed to set for you: brave://extensions → Botference
Discuss → Details → "Allow access to file URLs". The options page says
so if it is off, and so does the viewer.
A file on your disk is filed by what it is rather than where it is: Discuss identifies it by a hash of its words. So move it, rename it after reading it, annotate it in Acrobat, re-save it, keep a second copy on another Mac — same page, same comments, every time. Genuinely revise the document and it honestly becomes a new page; the old one keeps its comments under the old name. And the same paper read from the web and from your disk are two pages, because one is identified by its address and the other by its text. (A scan has no words to hash and is filed by its bytes instead.) The Obsidian note names the file it came from, since a hash tells you nothing.
The file itself never leaves your machine — nothing is uploaded, copied or stored anywhere. What your phone reads is the text, exactly as for any PDF.
The comments already in the file — and the ones you write back. A
manuscript that has been round a supervisor arrives with comments in it:
Acrobat highlights, Preview sticky notes, replies to replies. Discuss
reads them. The drawer says "This PDF carries 7 comments" and offers
to import them; press the button and each becomes an ordinary thread —
on the same words, on the same page, under the name of whoever wrote it,
at the moment they wrote it. From there they take replies, resolve,
@claude, send review and export like anything else. Nothing is
imported until you ask, nothing is imported twice however often you
reopen the paper, and a comment somebody edits in Acrobat comes back as
a new one rather than overwriting the thread your bots have answered in.
Going the other way, the export chooser on a PDF offers an annotated
copy: every thread on the page, the bots' replies included, written
into a copy of the file as standard highlights with the conversation in
the popup, and downloaded as <name> (discussed).pdf. Send it to
somebody who has only Acrobat and they can read the whole discussion.
Your original is never touched, and the annotated copy is still the same
document to Discuss — same page, same chat — because annotations are not
words.
The bots can see the page, not just read it. A figure is not text: it
is in no extract, so for a long time a question about a plot got a bot
paraphrasing its caption back, or saying plainly that it could not see
it. Now, when you comment on a page of a PDF, Discuss renders that page
to an image beside the page's text and the turn hands the file to the
agents to open — and they really do open it (claude with its Read tool,
codex with view_image). So "what does this figure actually show?" on
a caption is a question about the figure. Tables and equations come
through the same way, and a scanned page — which has no words at all —
is finally something they can read.
It costs about a tenth of a second when you save a comment, the same
page is never sent twice, and a page that could not be drawn (the
document is not open in a tab, or the comment came from your phone)
makes the bots say so rather than guess. The pictures live beside the
page's text under .botference/plugin/ and go when the page does.
Striking a passage through. On a PDF the selection pill has two tools
rather than one — Adobe's pair. The first is the comment button you know.
The second strikes the passage through: a thin red line through the
middle of the words, the way Acrobat draws it, with the words left
perfectly readable underneath. It needs no note — the line has already
said "this should come out", so Send files it with the box empty — but
type one if you want to say why. From there it is an ordinary thread:
reply, @claude, resolve, and the line turns amber and then sage on the
page as the thread moves along, exactly as a highlight's wash does.
Strikeouts travel both ways through the file: a /StrikeOut already in
the manuscript arrives struck, and the annotated copy writes yours back
out as real strikeout annotations in red, which Acrobat and Preview draw
— so whoever you send it to sees the deletion you meant. The reading
room quotes a struck passage struck, and the Obsidian note wraps it in
~~ with suggested deletion beside the page number.
This is PDFs only, on purpose: a strikeout is a suggested edit to a draft, and a news article is not a draft. The pill on an ordinary page is the single comment button it has always been.
A discussion that ends in several changes. Argue about a passage and the bots may end an answer with up to three proposals at once — three chips, three Strike it buttons, each one you can take or leave. Every one you take becomes its own strikeout in your name, and the discussion grows a quiet line saying what it produced — struck through here — 3 changes — with a number for each that jumps to it. And when your highlight was short of the words the change actually needs, the bot names the full passage itself and the chip shows you exactly what will be struck before you press anything; you are never sent back to highlight it again. A named passage has to be on the page you are reading, found there exactly once, and clear of any marking already there — otherwise no button appears and the chip says why.
And the pages your council writes. When a council chat builds an HTML
artifact inside its own project —
…/botference/projects/spaceship-engineering/index.html — opening that
file gives you the drawer with no setup at all. Discuss recognises it by
the folder above it (a project.json beside work/ and projects/,
with the file inside projects/<id>/); the header says which project it
belongs to. The first time a new council folder turns up, the drawer
asks once whether it is yours and keeps the answer.
The chat behind such a page is the real chat of that project, in your real workspace, beside everything else the project has ever said — not a plugin chat. So Page chat becomes the project's chat archive: the bar names the chat you are standing in, the list behind it is every chat the project has, and opening one renders its recent history and lets you carry it on. "+ new" starts a fresh one, filed in the same place.
This is the one local file identified by where it is rather than what it is — deliberately, because a project rebuilds its artifact in place and a hash would throw your comments away at every build. On these pages — and only these — the bots may also edit the files in that one project's folder when you ask them to, and nothing outside it; when a turn changes the file you are reading, the tab reloads itself. Deleting the page never deletes the project's chat.
Discuss annotates nothing else on your disk: a local page that is neither a PDF nor a project artifact is left completely alone.
File a page in a project. The other half of that: an ordinary page —
the arXiv PDF of somebody's second draft, say — can be filed under one
or more council projects, and then every turn on it carries what those
projects already know: their chat titles, their TASKS.md, their files,
and the actual words of their two most recent conversations. That is the
point of it. The bots reading draft two arrive knowing what was said about
draft one, instead of you retyping last round's objections into this
round's margin.
Filing is a read, not a move. The page stays where it is, keeps its own
chat, and gains no permission to write anything anywhere; unfiling is one
click and leaves no trace. The folder button in the drawer header lists your
projects with a peek at each one's recent chats and files, and ticks the ones
this page is already in. And on a page filed nowhere, the bots are told the
project names and may suggest one — "this looks like it belongs in
Adriana's paper" — which the drawer draws as a chip with a File it
button on it. Bots never file anything; you press the button. And where
nothing you keep fits, they may say so and name a project you do not have
yet — Start a project "Doc Fest 2026" for this page? — which makes the
folder, its PROJECT.md and its portfolio row when you press it, and files
the page in it. (+ new project at the bottom of the same folder-button
list does it by hand.)
Start over without losing your comments. Also beside send review, on every page: + new chat. It sets the conversation aside and starts a fresh one — useful when a bot has settled on a misreading of the page and keeps restating it — while your highlights and comments stay exactly where they are. It asks once first. Nothing is thrown away: the chat you left is filed under archive ▾ next to the button, the last ten of them by name and date, and opening one brings it back. (On a project artifact page the archive bar at the top of Page chat is already this control, with your council's own chats behind it, so there is no second button.)
Carry a conversation into the council. Also on the dock, on every page
that has a chat or comments: continue in council ↗. A page chat lives with
Discuss and never shows up in your council; this button copies it — the whole
chat, every margin comment with the passage it is on, and the page's title and
address at the top — into your council as a new chat of your own, and opens
the council on it in a new tab. Keep going there as you would in any council
chat; both bots pick up the whole conversation on your first message. The page
keeps its chat and comments and no bot is asked anything. The copy is filed in
the project the page is filed in (if it is in exactly one), otherwise it waits
in the council's Inbox for you to file it. Pressing it again with nothing new
said opens the same council chat; with new messages it makes a fresh copy. No
browser handy? botference discuss continue <address or a few words of the title> does the same from a terminal and prints the link (--list to find a
page, --dry-run to see what would be copied, --project <id> / --inbox to
choose where it goes).
Make a page out of the one you marked up. Beside send review, on any page filed in a project, there is make artifact: say in one line what you want — a planner of what to watch and when, with costs — and one bot writes a single self-contained web page into that project's folder, reading your margin comments, the chat about the page, and the page itself. It appears as a link under the review row. Because it lives in the project folder it is an ordinary project page from then on: open it, comment on it, send a review, and the bots edit it in place — and it carries a line saying which page it was made from, with the way back.
And publish it to your own website. One more button on such a page:
publish copies that one file into your site's repo, commits it and
pushes, and your host rebuilds. It asks first, and the question names the
address — publish to https://angadh.com/lff/? This makes the page
public — because that is what you are agreeing to. Where it goes is one
block in .botference/plugin/config.json (there is no settings screen):
"publish": { "repo": "/Users/you/sites/yoursite", "dir": "lff",
"url": "https://angadh.com/lff/", "branch": "main", "push": true }Until that is there the button is disabled and says so. Pick a dir your
site's _config.yml does not exclude: — if your site folder is also a
council root it excludes projects/ and work/, and a file published into
one of those would be committed and never built. Your page goes over
exactly as it is — no front matter, no template, no rewriting — and
nothing else in that repository is ever written: not _config.yml, not your
own uncommitted work, which stays uncommitted and out of the commit. Only
that one file is committed, nothing is rebased or forced, and if the push
fails you are told the page is committed and given git's own words for why.
"push": false commits and leaves the pushing to you. (Putting it on a
subdomain like lff.angadh.com is three settings in your DNS and host and no
code at all — the exact steps are in frontends/plugin/SPEC.md, "publish to
the reader's site".)
Or ask for a site of its own, and the bots build it. On a project page
you can say put this at lff.angadh.com and they do the whole thing
themselves, in <council root>/sites/lff/: a git repo, a private GitHub
repo, a Netlify site, a deploy — and then they tell you the one step that is
yours, which is a CNAME record at your DNS provider (the page is already live
at the host's own address meanwhile). When it is finished the companion
notices and adds it to publish in config.json by itself, so the publish
button can point at it from then on; a target already there is never
overwritten. sites_domain says what the sites hang under, and is worked out
from your existing publish target when you do not set it. This is a narrow
allowance and it is enforced in code, not asked for in a prompt: writes only
under sites/, and only on a project lane; commands only git, four shapes
of gh and five of netlify, decided on the parsed command line. --public
is refused, force pushes and repo delete and sites:delete and
netlify env:* are refused, and anything naming your own blog — its folder,
its GitHub name, its Netlify site id — is refused outright. An ordinary web
page's chat can still write nothing at all.
Message formatting. Every message renders markdown — yours and the
bots' alike — with links, code, and tickable - [ ] checklists. LaTeX
maths renders too: $…$ inline and $$…$$ on its own line for display
(\(…\) / \[…\] also work), typeset by a vendored KaTeX with no
network call of any kind. Prices are safe — "costs $5 and $10" stays
prose — and a formula KaTeX cannot parse falls back to its own source
rather than swallowing the message. Obsidian gets the raw $…$ source,
which it typesets itself. Long threads fold in the middle, keeping the
opening message and the latest replies behind a "Show N earlier
replies" line.
The workspace is sticky: after the first run, botference discuss from
any directory reuses the recorded workspace (--here picks the current
one instead). Put the launcher on PATH once (ln -s "$PWD/botference" /usr/local/bin/botference from the repo) and it runs from any terminal.
Collaborators (--share): hosted mode behind a password gate plus a
cloudflared tunnel — guests sign in with a name, see the same threads,
and comment under their own handle; their @-mentions are refused
politely unless granted in .botference/plugin/grants.json
({"<handle>": {"agents": true, "daily_cap": 5}}, re-read live).
Guests without the extension get a server-rendered reading room at
/pages. Once several people have written on a page, the Comments tab
grows a row of pills — one per person, bots included, All first —
and pressing one narrows the pane to that person's threads (open list,
"ready for review" and the resolved archive alike). The reading room has
the same filter as links, so a margin narrowed to one person is
something you can send. Remote collaborators with the extension point it at your
tunnel from its options page (URL, password, display name). Owner-only
stays owner-only: export, page deletion, model/effort/verbosity, relay,
interrupt.
A turn on one page no longer holds up another. Ask something on an article, then go and send a whole margin review on a draft in another tab: both are answered, side by side. What stays strictly in order is what has to — a page's own chat is one conversation, so your next message there waits for the answer to your last one, and everything belonging to one project takes its turn, because those pages share a folder the bots may write in and the record of what changed in it. A review round is one page's conversation across many threads, so it is still answered comment by comment, in page order, and its progress strip still counts straight while something else is being answered elsewhere.
The waiting line says which wait it is: queued behind this
conversation… when your own last message is still being answered,
queued behind another chat… when every agent is busy with somebody
else's page. bridge_pool in .botference/plugin/config.json sets how
many run at once (default 3; 1 is the old one-at-a-time behaviour),
and spare agents are retired after bridge_idle_ms of quiet (default 15
minutes) so an afternoon of reading does not leave processes lying
around. The web council has had the same arrangement for a while —
one bridge per open chat, idle ones parked past COUNCIL_MAX_CHATS.
You write a post in markdown, run the site locally (jekyll serve), and
read the finished page in the browser. Comment on a paragraph there and
ask for it to be tightened, and the bots edit the markdown the page was
built from — not the rendered HTML, which is a photocopy the next build
throws away. Jekyll rebuilds by itself, the tab reloads, and you are
looking at the result. Ask for a picture in a spot and it goes into
assets/ with the right path in the post; ask for an existing one to be
cropped and they use whatever image tools the machine has.
The bots place pictures themselves — they never hand you a file to copy.
A finished picture goes straight into the page's image folder; the scripts,
previews and drafts behind it go in a scratch folder inside your repo,
.botference/plugin/artifacts/ (the companion adds .botference/ to your
.gitignore if it is not there). Nothing for your site is written into the
Botference folder. If a bot proposes a picture that it left in scratch,
the companion copies it into place when the turn ends (never over a file
you already have) and says so.
Tell it about your site by running one command in its folder:
cd ~/sites/yourblog
botference site # or `botference init`, which does this tooThat writes .botference/site.json in the repo (the kind is detected; the
address it guesses, http://localhost:4000 for Jekyll, is printed — fix it
with --serve <origin>), makes sure .botference/ is in your
.gitignore, and adds the folder to ~/.botference/sites.json, the list the
companion reads when it starts. A running companion is told straight away.
Running the command in the folder is your answer to is this your site?, so
the drawer does not ask again. botference sites lists what is in scope
(and whether the companion sees it); botference sites --remove <dir> takes
one out. The drawer's header names the project folder of a page in scope.
The older way still works, and wins for an address both name: declare the
site by hand in the companion's own .botference/plugin/config.json,
"blog_sites": [{ "serve_origin": "http://localhost:4000",
"root": "/Users/you/sites/yourblog", "kind": "jekyll" }]and the first page of that site you open asks — once — is this your site?,
exactly as it asks about a council folder and for the same reason: yes is
what lets the bots write in there. Either way, it then works out which file each
address came from by reading your repo: front-matter permalink
first, then the permalink templates in your _config.yml, then the
filename conventions, then the slug. Posts, drafts, pages and collections
all resolve; if two files share a slug it says so instead of guessing,
and if it cannot tell which markdown renders at an address it says that
too and leaves everything read-only. The drawer names the file it is
about to edit, so you always know what is about to change — and a rewrite
nobody commented on still opens its own comment thread, as on any
document.
Not every page of a site is built from markdown, and those work too: a
hand-written HTML page, a scrollytelling piece or an SVG figure sitting in
your repo is its own source, so opening it straight off the disk
(file:///…/yourblog/assets/…/page.html) — or at the address the local
server copies it through to — attaches Discuss to the file itself, and a
comment there changes that file.
It will never publish your site. Nothing here commits, pushes, branches or tags anything in that repository, there is no publish button on a blog page, and the bots are told not to run git in there (the CLI refuses it as well). Putting the site live stays yours, by your own route. (The publish button above is a different thing in a different place: it is yours, not the bots'; it acts on a project page rather than on your drafts; it writes one new file into the folder you named and never touches what is already in the repo. No bot can reach it.) That is a rule in the code rather than a setting — it belongs to the kind of folder, so a config restored onto another computer brings it along and there is nothing to switch off. (Paper review is the deliberate opposite and still commits and pushes: a paper under review is a shared working copy; your blog is your published name.)
The same works for a Jupyter Book: open the built book in the browser — what your readers see — highlight, comment, chat, and the bots propose changes to the notebook or MyST file the page was built from. You accept or refuse each one in the drawer; an accepted change rewrites exactly one cell of the notebook (outputs, metadata and formatting are left byte for byte as they were), and a proposal that would span two cells, or touch a code cell's fence, is refused on its card rather than guessed at. There is no separate review page — it is your real book.
Serve the build locally. Either of these, from the book's root:
python -m http.server 8000 --directory _build/html # after `jupyter-book build .`or, if you would rather the book rebuild itself on every save,
sphinx-autobuild (pip install sphinx-autobuild) — which needs a Sphinx
conf.py, and jupyter-book config sphinx . writes one into the book's
root, so that is a new file in your repo:
jupyter-book config sphinx . && sphinx-autobuild . _build/html --port 8000then put the book in scope — once, from its root — and open it:
botference site # or `botference init`; --serve 8000 if you serve elsewhereA _toc.yml beside _config.yml is recognised as a book, the address is
guessed as http://localhost:8000 (printed, so you can correct it), and if
jupyter-book is on your PATH its absolute path becomes the rebuild
command (--rebuild <cmd> to choose another, --no-rebuild for none).
By hand instead, in the companion's .botference/plugin/config.json — this
still works, and a hand-written row wins for its address:
"blog_sites": [{ "serve_origin": "http://localhost:8000",
"root": "/Users/you/SpacecraftDynamics", "kind": "jupyterbook",
"rebuild": "/Users/you/opt/anaconda3/bin/jupyter-book build ." }]Addresses are mapped by reading _toc.yml: a chapter
orbital-mechanics/Lecture3/Lecture3 is the page
/orbital-mechanics/Lecture3/Lecture3.html, the root: entry is also /
and index.html, and a notebook or markdown file that is not in the toc
is found at its own path. A notebook and a markdown file with the same
name side by side are reported as ambiguous, not picked between.
Rebuilding. Jekyll rebuilds itself; jupyter-book does not. The
optional rebuild is the command you would type yourself, and after an
accepted change (or a picture the bots replaced) the companion runs it in
the book's root — once for a burst of changes, one build at a time, with
its output in the companion's log — and the tab reloads when it has
finished. Give the builder by its absolute path: the companion may not be
running with your shell's PATH. A rebuild command may not mention git
or gh. Without one, nothing rebuilds for you: run jupyter-book build .
after accepting, then reload (or serve with sphinx-autobuild, which
rebuilds on save — leave rebuild out then). If your _config.yml says
execute_notebooks: force, every build re-runs every notebook, so a
rebuild takes as long as that does.
The book's style (_config.yml, _static/) and every other chapter are
left alone unless you ask for a change there in so many words, and the
bots never run git in the book either. Redrawn figures go beside the
chapter, in its own image folder; the drawing scripts and previews go in
the book's .botference/plugin/artifacts/, as for a blog.
Everything else in Discuss is a record of having understood something once. Beside every comment's ✓ there is now a small ?, and the selection pill on any page carries one too: this is interesting, make a question of it. One click is the whole of your part in it — a bot writes a short question about the idea (multiple choice, or true/false, or fill-in-the-blank where the material suits it), and it lands in the vault. There is no card to approve, no format to pick, no difficulty to set: you choose what is worth remembering and nothing else. The bots may offer as well, rarely, when a conversation has shown you have not got something — a File it / No chip under the reply, filing nothing until you press it.
Then open /quiz in the reading room — from the pages list, or from
the door in the drawer's header that says how many are due. It works on a
phone, which is the point of it living there: one question at a time,
longest-overdue first, one tap to answer. Right and it comes back later;
wrong and it comes back before you stand up, with the explanation, the
passage it was made from, and a link to the conversation that produced
it. The rescheduling is Anki's SM-2, so a question you keep getting right
disappears for months.
Every card links home because a bot wrote it, and bots are wrong
sometimes: this card seems wrong takes it out of the rotation. One
bank, not decks — a card remembers its page, the council project that
page is filed under and its tags, and the quiz's chips narrow to a topic
and show which topics you keep missing. The whole page works with
JavaScript switched off. The vault is .botference/plugin/questions.json
and it is owner-only end to end.
Every page you annotate is one conversation. The library is the other
kind: one conversation about all of them at once. Open the pages list in
the drawer (the stacked-pages button) and it sits above the list, with a
composer that says Ask about everything you've read — "what have I been
sceptical about lately?", "did anything I read contradict the piece on
night trains?", "pull together everything I highlighted about pricing".
The bots answer by reading your archive off disk — the page records
in .botference/plugin/pages/*.json, your quotes, your comments, their
own earlier replies — so they cite the page and the passage each claim
comes from rather than guessing from memory. They can only read: writing
files is refused here exactly as it is everywhere else in Discuss. It is
an ordinary conversation in every other respect — mention @claude,
@codex or @all, export it to Obsidian as a "Library" note, clear it
and start again — and it is on your phone at /pages too.
Keeping the list usable. The archive grows, so the list underneath
that conversation filters: quiet chips for what a page IS — All ·
Articles · PDFs · Docs, drawn only for the kinds you actually have —
and a rail of your own tags beside them, both combinable and both
remembered per browser. Every row can be renamed (a paper called
2601.01234v2.pdf becomes Kolmogorov flows everywhere: the list, your
phone, the Obsidian note's title and file name, and the council chat
behind it, renamed on that page's next message) and tagged (click
#, type, complete against tags you have used before; a tag chip
filters the list, and the tags ride into the note's frontmatter beside
botference-discuss, which is what makes Obsidian's own search find
them). Renaming and tagging are the owner's: a guest on a shared
companion sees the list and its filters, and no editing at all.
A conversation about a page full of numbers turns into code very
quickly — "plot that", "check the arithmetic", "what does the
distribution look like". So any fenced ```python block in any
message, yours or a bot's, carries a quiet Run button. Press it and
the snippet runs, and what it printed appears under the block: stdout in
a mono block, stderr marked as stderr, an exit line only when something
went wrong, and any matplotlib figures as inline thumbnails — click
one and it fills the window, because a plot at the width of a 420px
column is a picture of a plot. plt.show() needs no changing: the
backend is headless and every open figure is saved for you.
The result is stored on the message itself, so it is still there after a
refetch, a reload, or in another tab, and it goes into the Obsidian note
under the code fence when you export — with the figures copied into
<vault>/<export folder>/attachments/ as ordinary markdown images.
Running a block again replaces its last result; deleting the message,
the thread or the page deletes everything those runs left on disk. Runs
are killed after 30 seconds (there is a stop button meanwhile) and each
stream is cut at 64KB with a marker saying so.
What this is, exactly. The code runs on this Mac, as you, with your
files, your keys on disk and your network — there is no sandbox and
nothing here pretends otherwise; the button's own tooltip says "Runs
this code on this Mac as you". Treat a block written by a bot the way
you would treat pasting a stranger's script into your terminal, because
that is the same act: the page you are annotating is untrusted text, and
it can try to talk the bots into writing something worth running.
Nothing runs on its own — a block only ever runs when you press the
button — and the code that runs is the code stored in the message you
can see, never anything a request carried. It is owner-only end to end:
on a shared companion a guest never sees the button, /run refuses
them, and the figures are behind the same gate.
If you would rather not have it at all, set "run_python": false in
.botference/plugin/config.json — the button disappears and the
endpoint refuses.
Once the companion has a public address (below), Discuss is a
bookmark — discuss.botference.com. Two things make that worth doing.
You are the owner there, not a guest. Signing in from a phone uses
the same identity as your review docs — botference has one owner
credential, not one per tool. If your browser is already an approved
device for the review hub, it is the owner at the annotator too, with
nothing typed: the hub scopes its device cookie to the parent domain
and the companion verifies it with the hub's own secret. Otherwise type
the owner password — the one in
~/.botference/review-paper-secrets.json, which the hub hands to every
paper server, and which --install-tunnel prints for you. Either way
you get every owner right remotely: export, delete, the agent controls,
and @claude/@codex with no grant needed. Your machine on
127.0.0.1 stays the owner with no password at all, exactly as before.
Sessions last 30 days and renew themselves as you use them, so the gate
should be a once-a-year event. /signout ends one.
The article comes with it. When you annotate a page, the extension
sends the companion a clean copy of the prose, and /a/<page> serves
that copy back with your highlights painted where you made them — tap
one to open its thread, select text to start a new one, ask the bots,
export to Obsidian. It is the review-doc experience for any article you
read. New highlights made on the phone are ordinary threads: the phone
runs the extension's own anchoring code, so a passage you mark on the
train is highlighted in the page itself next time you open it on the
Mac.
The snapshot is sanitized on arrival — rebuilt from an allowlist, so no
scripts, no iframes, no event handlers, no javascript: links survive —
and served under a strict CSP that can run nothing the page did not
itself nonce. Images are kept (an article without them reads poorly)
and load from their original https sources. Pages annotated before this
existed have no copy yet: their article view says so and points at the
comments, and opening the page once on the Mac captures it. Annotating
a brand-new URL from the phone is not built — the phone can only mark
up pages your Mac has seen.
--share is a conversation — a random trycloudflare.com URL that dies
with the terminal. If you own a domain whose DNS lives on Cloudflare,
--install-tunnel gives the annotations an address you can bookmark on
your phone instead:
brew install cloudflared && cloudflared tunnel login # once, ever
botference discuss --install-tunnelIt prints two credentials at the end: yours (the shared botference
owner password — full owner rights from anywhere) and the
collaborators' guest password. It creates (or reuses) a named
tunnel botference-plugin, routes discuss.botference.com to it (and
keeps plugin.botference.com, the address from before the rename,
routed and served by the same companion so old bookmarks still land,
plus memorizer.botference.com — see below), writes
~/.cloudflared/botference-plugin.yml (one ingress rule to
http://127.0.0.1:4189), installs a second LaunchAgent
com.botference.plugin-tunnel that keeps the tunnel up at every login,
and switches the companion's own LaunchAgent to --hosted so the
password gate is what strangers meet. The password is generated once —
four words and a number, typeable from a phone — and kept in
~/.botference/plugin-password (0600); it is never written into a
plist, because launchd starts the launcher and the launcher reads the
file. The final URL and password are printed at the end. Set
BOTFERENCE_PLUGIN_HOSTNAME (the legacy one with
BOTFERENCE_PLUGIN_LEGACY_HOSTNAME, the vault's with
BOTFERENCE_PLUGIN_MEMORY_HOSTNAME — either empty to drop it — and the
tunnel name with BOTFERENCE_PLUGIN_TUNNEL) if your domain is not this
one.
The questions you filed, asked back on a schedule, at an address of
their own: memorizer.botference.com — where / is the quiz.
Same companion, same tunnel, same sign-in (an approved phone is the
owner there with nothing typed), owner-only end to end, and scriptless,
so it works on a train with JavaScript off. One question at a time; the
options are form posts; a wrong answer shows the right one, why, and
the passage it was made from, with a link into the conversation that
produced it. Every card carries a quiet trace back to its source, and
a card you decide is not worth keeping can be discarded (gone) or
flagged as seems wrong (parked, for a bot to rewrite later).
The reading room stays at discuss.botference.com; anything but the
quiz on the vault's hostname goes home rather than serving the archive
twice. In the drawer, a Memorize tab does the near version of the
same thing — the questions from the page you are reading (or from a
project it is filed in), reviewed in the column beside it, through the
same endpoints and the same schedule. A comment thread that has
produced questions says so, quietly, with the way to them.
No ports are opened: the tunnel dials out from your machine. This
machine stays the unauthenticated owner on http://127.0.0.1:4189, and
everything arriving through the tunnel is a guest — the companion tells
them apart by the Host header and the CF-*/X-Forwarded-* headers
Cloudflare stamps on, since cloudflared's own hop is loopback too.
--uninstall-tunnel stops and removes the tunnel LaunchAgent and puts
the companion back to plain localhost mode. It deliberately leaves the
Cloudflare tunnel and the DNS record alone (re-installing is then one
command) and prints the cloudflared tunnel delete you would run to
remove them from your account as well.
Discuss and the web council both drive the claude and codex CLIs, so
by default they run on whatever those are logged into and there is
nothing to configure. If you would rather bill an API key, paste one per
agent — in the extension's options page, or in the council's agents
panel. There is one store of keys for both: paste a key once and
every botference frontend on that machine can use it. The billing mode
is per product, so the council can sit on your subscription while
Discuss burns credit. Keys are stored on that machine in
~/.botference/discuss-keys.json (mode 0600) and are write-only over
the API: the page can save one and remove one, and the only thing it
can ever read back is set or unset. They can only be set from the
machine the server runs on — a request through the tunnel is refused
even when it is you, because a key has no business crossing a network to
reach the CLIs running next to it.
Each agent has a billing mode — in Discuss the drawer's gear menu, in the web council the agents panel:
| mode | what happens |
|---|---|
auto (default) |
a saved key is used; no key means your subscription — the same rule Claude Code itself applies |
subscription |
never the key, even though it stays saved |
key |
always the saved key |
Anything that means "not a key" removes the variable from the bridge's
environment rather than blanking it, and clears the other auth sources
alongside it (ANTHROPIC_AUTH_TOKEN, the Bedrock/Vertex/Foundry
switches), because an empty key and a leftover token are both ways to
end up billing something you did not choose. Changes apply at the next
bridge start. Discuss restarts an idle bridge for you and lets a busy one
finish its turn first; the council leaves running chats alone entirely and
says so ("applies to agents started from now on"), because in a council a
running bridge is a chat someone has open.
The two CLIs disagree, and botference does not pretend otherwise.
Claude Code prefers a key over your subscription whenever one is set —
documented, and in the non-interactive mode the bridge uses there is not
even a prompt. Codex is the other way round: its stored ChatGPT login
wins, and a key in the environment is only consulted when you are not
logged in. So for Codex a saved key is a fallback, not an override; to
make it authoritative, run codex login --with-api-key yourself. (There
is a config key that forces it, and it silently deletes your
~/.codex/auth.json when it switches — botference never sends it.)
Discuss sends one anonymous event a day so I can tell whether anyone is using it, which is the only thing deciding whether it keeps getting built. This is the entire payload:
{
"client_id": "9f2c…",
"non_personalized_ads": true,
"events": [
{ "name": "discuss_alive",
"params": { "app_version": "1.0.0", "engagement_time_msec": "1" } }
]
}client_id is 16 random bytes generated once and kept in
.botference/plugin/.beacon. It is not derived from your machine, your
name, your workspace or anything else — delete that file and you are a
new install. There is no URL, no page title, no annotation text, no
handle, no username, no workspace path, no location, and no counts of
anything. It fires at most once per day, on companion start, and a
failed send waits until tomorrow rather than retrying.
Turn it off completely:
BOTFERENCE_NO_TELEMETRY=1 # env, honoured by the LaunchAgent tooor put "telemetry": false in .botference/plugin/config.json. Both are
checked before anything touches the network. Builds with no analytics
secret compiled in — which is what a fresh clone is — never send
anything at all.
Layout and design failures produce no errors and no warnings — an agent
that only reads code and logs will ship a page that "works" but looks
broken. botference see closes that loop with zero new dependencies:
it renders a page in headless system Chrome and writes one PNG per
viewport (defaults: 390x844 phone + 1440x900 desktop — the phone shot
is real mobile layout, since responsive CSS responds to the viewport),
printing each path for the agent to read back with its image tooling.
botference see fitlog # a RUNNING service by name — port discovered live
botference see :4123 fitlog # bare localhost port
botference see charts/hero.html # a local file (chart, report, mockup) — no server needed
botference see https://example.com home --viewport 500x900Tell your bots: after any UI change, botference see <service> and
look before declaring done. Pair it with deterministic invariants for
charts and figures (assert data coverage, containment, label collisions
on the generated SVG) — vision discovers a failure class once, the
invariant test locks it out forever.
Agent sandboxes cannot launch Chrome (seatbelt kills it). The same
command still works there: when the local render fails wholesale,
see hands the exact request to the see-broker — start it once
with botference service start see-broker -- botference see --serve —
which watches every registered workspace's .botference/see/ spool
from outside the sandbox, renders, and answers through the filesystem.
Same command, same wrote: output, no sandbox loosened.
Anything started in a shell — including by an agent inside a botference
session — dies when that shell's process group is torn down at the end
of the turn. botference service is the sanctioned, auditable way to
leave a server or tunnel running:
botference service start <name> -- <command…> # detached; survives the shell
botference service list # EVERY service in every registered ledger, from anywhere
botference service logs <name> [-n N] # tail the service log
botference service stop <name> # TERM the group, KILL after 5s
botference service stop --allstart fully detaches the command (own session and process group,
stdin from /dev/null, stdout+stderr appended to the service log with
simple ~5MB rotation), records {name, pid, pgid, command, started, cwd, log} in the per-workspace ledger .botference/services.json
(atomic writes), and refuses duplicate running names; stale entries
with dead pids are reaped on every invocation. Names are [a-z0-9-],
max 32 chars. Ledger and logs live under <workspace>/.botference/
(gitignored by convention — botference review adds the ignore block
for document repos).
The share flows compose with it — this is what to ask your bots for when you want a live review or council link stood up mid-session:
botference review --share --service # review share (server + tunnel) as service 'review-share'
botference plan --share --service # council share as service 'council-share'Both print the usual share this: <url> password: <pw> line (parsed
from the service log with a bounded wait) and then return control, so
an agent can stand up a share on request and end its turn with the
tunnel still up. Re-running while the service is up reprints its last
share line. Stop with botference service stop review-share (resp.
council-share).
In a project initialized with botference init, the local policy lives in
botference/project.json.
With a custom state directory, the policy file moves with it. For example,
botference init --project-dir=spaceship creates
botference-spaceship/project.json.
Workspace-style repos can also put policy at the project root as
project.json. This is useful for portfolio workspaces where the models should
be able to clone or edit under top-level folders such as projects/ without
creating a separate botference/ state directory.
Default write scope:
{
"write_roots": {
"plan": ["botference"],
"build": ["botference"]
}
}This means:
planandresearch-planmay write anywhere underbotference/buildmay write anywhere underbotference/- the rest of the project stays read-only by default
- anything outside declared
write_rootsis blocked at runtime and by post-run audit
In practice, this gives you two common workflows:
- greenfield work happens naturally inside
botference/, where planning can create notes, exports, scratch files, and plan artifacts without touching the project tree yet - brownfield work keeps the existing project read-only by default; actual
project-file edits happen only if you explicitly widen
write_roots, typically forbuild, or if you make the edits manually yourself
If you want to opt into another Botference-owned writable area later, add it explicitly. For example:
{
"write_roots": {
"plan": ["botference"],
"build": ["botference", "assets/generated"]
}
}If you want a narrower boundary, reduce the roots instead, for example
"build": ["botference/build"].
If the whole workspace is intentionally writable, use ".":
{
"write_roots": {
"plan": ["."],
"build": ["."]
}
}Rendered artifacts have a stricter definition of done than ordinary text or
code. For HTML, plots, charts, PDFs, LaTeX files that produce PDFs, web UI,
generated images, and inline document figures, agents must follow
specs/visual-verification.md:
- use Changed, Generated, Structurally checked, Visually verified, and User-review needed precisely
- do not say "done", "fixed", "ready", or "verified" for visual work unless the rendered output was inspected
- default to static SVG/PNG figures inside prose documents; keep interactive Plotly/D3/Chart.js views as standalone linked pages unless browser-verified
- batch likely visual fixes before asking the user to reload
Botference also enforces this after each agent turn. If Claude or Codex changes
or generates a rendered artifact and the turn does not include a matching render
check, Botference adds a system-level Visual verification gate warning and
marks the artifact as User-review needed. For .tex edits, the generated
PDF is the rendered artifact: a compile step alone is not enough; the PDF must
also be inspected with view_pdf_page, a screenshot, or an equivalent visual
check.
Verification actions are marked in the tool stack as [verify] and rendered
with a distinct bright accent in the Ink UI, so visual checks are easier to
spot while a turn is running or after it completes.
Botference includes a deterministic HTML visual gate:
python3 "$BOTFERENCE_HOME/tools/cli.py" visual_check_html '{"html_file":"botference/projects/spaceshipengineering/index.html"}'The tool renders the page at desktop, tablet, and mobile widths, saves
screenshots plus report.json under $BOTFERENCE_WORK_DIR/visual-checks/,
and reports layout failures such as horizontal overflow, clipped text,
overlapping visible text, console errors, and page errors. It is intentionally
usable from plan and research-plan mode through Bash, so agents can inspect
their own generated HTML without another model turn.
Install the browser dependency once when you want automatic visual checks:
python3 -m pip install playwright
python3 -m playwright install chromiumIf Playwright is unavailable, agents must report User-review needed and tell you which artifact to reload.
For a fresh clone, install Ink's Node dependencies once before using the default planner UI:
cd ink-ui && npm installAPI keys: If your terminal already has Claude Code and Codex configured via subscription accounts (e.g. Claude Max, OpenAI Plus), Botference works out of the box with no extra setup. In build mode, subscription users run through the MCP fallback path (
fallback_agent_mcp.py→claude -p).If you provide an API key, build mode uses the direct agent runner (
botference_agent.py) instead, which owns the tool-calling loop and gives finer control over retries, context tracking, and token accounting. Copy the example env file and add your keys:cp .env.example .env # Then edit .env with your ANTHROPIC_API_KEY and/or OPENAI_API_KEYBotference will auto-load keys from
.envwhen they are not already in your environment. Do not commit.envto version control.Warning If
OPENAI_API_KEYis present in.envor your shell environment, Botference will prefer API-key auth for Codex and re-runcodex login --with-api-keyon startup. That overrides a subscription/device login for the local Codex CLI. If you want Codex to use your subscription login instead, removeOPENAI_API_KEYbefore launching Botference.
Type freely to send a message. By default your first message goes to both models; after that, messages are sticky to whoever you last addressed.
| Input | Effect |
|---|---|
@all <msg> |
Send to both Claude and Codex |
@claude <msg> |
Send to Claude only |
@codex <msg> |
Send to Codex only |
<msg> |
Auto-routed (first message → @all, then sticky to last target) |
Messages in the council panel are labelled by speaker:
- Claude — Claude's responses
- Codex — Codex's responses
- You — your messages
- System — the framework talking to you, not an LLM. Covers:
- Session lifecycle (starting, relaying, or tearing down a model)
- Mode changes (draft started, draft complete)
- Errors and warnings
- Command feedback (lead set, usage info)
- Deterministic file-write feedback (
implementation-plan.md, reviewer comments,checkpoint.md)
Tool activity in the council is shown as a short folded summary under the model response rather than a raw command/output transcript. Deliberate code excerpts still render as code blocks.
The council behaves like a real group chat: you send a message, the addressed model(s) reply, and a bot's reply can hand the floor to the other bot, who replies immediately, recursively, until someone hands the floor back to you. Every bot-to-bot thread is seeded by one of your messages — the bots never start talking while the room is idle — but within a thread they exchange as many turns as the budget allows without any prompting from you.
Mechanics:
- Every bot reply ends with a small JSON footer (stripped from the display)
declaring
status(continuing/converged/blocked),next(@claude/@codex/@user), and a one-line summary.next: "@codex"gives Codex the floor;next: "@user"— or no footer and no mention — returns it to you. A prose@mentionof the other bot works as a fallback if a model forgets the footer. - Budgets, not hard stops. A bot-to-bot thread gets 6 bot turns and ~8K output tokens; each turn prompt shows the countdown so the models pace themselves. On exhaustion the controller grants one automatic extension (+3 turns / +4K tokens), then forces the floor back to you with the last footer summary. The thread pauses rather than dies — reply (even just "continue") and they pick up with a fresh budget.
- You always preempt. Typing while Claude is speaking steers its current turn in place (see Steering); anything else you type pauses the thread at the next turn boundary and the floor is yours.
- Conciseness is enforced by measurement. If a bot's turn exceeds ~400 output tokens, its next prompt carries a cap nudge.
- Writer consensus sets the lead. The footer has an optional
writer: "@claude"|"@codex"field. When both bots vote for the same writer, the lead is set automatically (a manual/leadalways wins).
Turn-based behavior is the degenerate case of free-form: a reply with no handoff (no footer, no mention) simply returns the floor to you.
| Command | What it does |
|---|---|
/lead @claude|@codex |
Manually set which model writes the plan. You can also use /lead auto to let the bots' writer consensus decide. |
/draft [rounds] |
Update the project-local implementation-plan.md via the lead model, with optional AI review rounds. Defaults to 2; /draft 0 writes the plan with no AI review, /draft 1 does one review/revise cycle, and so on. Review rounds run in the council like free-form turns: the reviewer's footer can end rounds early (converged — sign-off, no revision needed) or pause the draft and hand the floor to you (blocked), and typing mid-draft pauses at the next round boundary. Reviewer comments are saved beside the plan in the Botference state directory. |
/finalize |
Lead-only finalization. The lead addresses all active reviewer comment files, rewrites the project-local implementation-plan.md if needed, creates checkpoint.md, and archives reviewer comments under the Botference archive directory. |
/fresh @claude|@codex|@both |
Restart a bot with no memory of this chat. For a session that has gone wrong — a bot stuck refusing an ordinary topic, or answering a question three turns old. Nothing is carried over: it sees only what you send next, so say the context again. The other bot keeps its memory and is told. In the browser plugin, type it into any chat box or use the fresh button beside relay in the agents panel. Separately and automatically: when Claude Code reports that the running model's safeguards declined a message (Fable 5.1 does this on some ordinary technical topics; the error names the model), this chat's Claude switches to the next model in safeguard_fallback in context-budgets.json — Opus 5.5, then Opus 5 — and retries the same message, with a note in the chat. |
/relay @claude|@codex|@both |
Tear down a model's session, generate a structured handoff, and restart that model immediately in the current botference process. Useful when context is getting long. @both (alias @all, /relay-both) resets both agents at once, token-efficiently: the agent with the most context headroom authors one shared handoff, both fresh sessions bootstrap from it, and the restarts run in parallel. Falls back to the free mechanical handoff when both agents are too degraded to author one. |
/autorelay [on|off] |
Toggle auto-relay. When a model's context occupancy crosses 50% of its window, botference relays it automatically (same handoff machinery as /relay) before its next turn — never mid-turn or mid free-form thread. On by default; the preference is per-user (~/.botference/settings.json) and persists across chats and projects. In the web council, a toggle in the agents panel mirrors this. No argument flips the current state. |
/new-project <title> |
Create a project and file this chat under it. Every /project verb is also a command of its own, which reads better as a command than as a menu: /open-project <id>, /assign-project, /unfile-project, /clear-project, /current-project, /project-contents, /project-github, /archive-project, /unarchive-project, /delete-project, /project-from-chat, /activate-build. A bare /project <words> that matches no project now says so loudly and shows the /new-project line to create it. |
/projects |
List project folders under projects/ and show the active project marker, status, priority, chat count, and next action when known. |
/project [open <id>|clear|current|create <title>|create-from-chat] |
Set, clear, show, or create the current project context. The status bar and Projects panel show the selected project; Inbox means no project is selected. |
/project assign [<session-id-prefix>] <project-id> |
File a chat under a project. Filing this chat moves the active context with it, because being filed into a project and then left looking at another project's plan files would be nonsense. A chat remembers its own project: once filed, saving it while some other project is open never moves it. Filing another saved chat rewrites that chat's session JSON (and the project index) while you stay where you are; if that chat is open in another botference process, that process wins on its next save. |
/project archive <id> / /project unarchive <id> |
Tuck a project away, or bring it back. Nothing moves on disk and nothing is deleted: only the status field in projects/portfolio.json flips to archived. Archived projects sort last everywhere and collapse into the closed Archived section at the bottom of the web sidebar; archiving the active project drops the room back to Inbox. |
/project delete <id> (alias /delete-project) |
Delete a project and every chat filed in it, for good. A confirm names the project, how many chats go with it and the folder (projects/<id>/) that will be removed; nothing happens until you pick "Delete project and N chats". The folder, its projects/portfolio.json row, its session-index.json rows and each filed chat's session file are all removed. If you are in one of those chats you land in a fresh Inbox chat. Without a picker (headless), only the exact project id is accepted. In the web sidebar, "🗑 delete project…" in the project block (live or archived) sends this. Chats already archived with /archive are not touched. |
/adopt [<id-prefix>] |
Continue a pre-existing native Claude Code chat inside the council. Opens a picker of recent claude sessions for the current folder; the adopted chat becomes the room's Claude session (full native memory), Claude writes a handoff into the shared transcript, and Codex joins from that brief. Run it from a fresh chat in the folder where the original conversation happened. Under --claude-interactive, the tmux pane launches as claude --resume <that chat> — botference becomes the steering layer over the real, attachable Claude Code session (watch it with tmux attach -rt botference-claude-…). |
/new [title] / /new --project <id> [title] / /new --inbox [title] |
Start a fresh chat in place — the current chat is saved and stays resumable. With no flag the new chat inherits the project you are standing in. --project <id> files it under that project from birth (and opens that project's files), --inbox keeps it deliberately unfiled even while a project is open. A chat's filing is written once, here or by /file; later saves never re-file it based on whatever project happens to be open. |
/file [<project-id>] |
File the current chat under a project, which becomes the active project (same as /project open, plus the filing). With no args, opens the arrow-key project picker (including "create a new project from this chat"). Alias: /add-to-project. |
/delete [<id-prefix>] |
Delete a saved chat. With no args, opens a picker of recent chats; either way a confirm step follows. Deleting the chat you're in rolls into a fresh /new. The project index entry is cleaned up too. |
/archive [<id-prefix>|list] |
Archive a saved chat: its session JSON moves from work/sessions/ to archive/sessions/ (BOTFERENCE_ARCHIVE_DIR), so it leaves every listing while staying byte-for-byte on disk. No confirm step — it is reversible. With no args, opens a picker of recent chats; /archive list shows what is already archived. Archiving the chat you're in saves it first, then rolls into a fresh /new. |
/unarchive [<id-prefix>] |
Move an archived chat back into the active listing (picker with no args). It never overwrites a live chat with the same id — if one exists, the archived copy is left untouched and nothing is lost. |
/resume [latest|<number>|<title>|<session-id-prefix>] |
Restore a previously saved planning session. With a project selected, project-associated and project-local sessions are shown first, while old unassigned sessions remain visible. You can resume mid-chat — the current session is persisted on every turn, so the chat you're leaving stays recoverable via /resume <its-id>. Selecting a session row in the Ink Projects panel runs this command for you. |
/rename <name> (or /rename auto) |
Name this chat. Without a name, a chat gets a short title (two to four words) written by a small model after its first exchange; /rename auto asks for a fresh one. The header, the chat list and the browser tab show it. |
/permissions |
Show the current planner write roots and any runtime grants approved for this session. |
/status |
Show context usage, lead, mode, and session state. |
/notify [on|off] |
Toggle the desktop notification posted when the bots finish (see Desktop notifications). No argument flips the current state; the preference is per-user (~/.botference/settings.json) and persists across chats and projects. |
/agents [on|off] |
Grant or revoke the Claude participant's subagent (Task) tool. Off by default in every chat: Claude is instructed to suggest subagents when a task would benefit and wait for your approval — and the gate is enforced at the tool level (the CLI is simply not given the Task tool until you grant it), not by prompt alone. The grant persists with the chat across /resume and resets on /new. Codex has no subagent facility; not available under --claude-interactive. |
/verify [on|off] |
When the bots mark the thread converged, run one more turn before the floor comes back to you: the bot that did not write the last claim is handed the claims and the sources the room has — and not the discussion — and reports each claim confirmed (with the supporting line), contradicted (with the contradicting line), or not checkable. On by default, per chat; skipped when the final message is too short to be a claim, when the room has no source, or when the other bot is not in the room. See Adversarial review. |
/allow-host [<domain>|remove <domain>] |
Grant the bots' sandbox network access to a site — or take one away with remove, defaults included (the built-in list covers GitHub, Wikipedia and the public planetary-science archives: HiRISE, USGS Astrogeology, PDS imaging, NASA images). Grants persist per workspace (persists per workspace in .botference/allowed-hosts.json, applies from their next turn — no restart). The sandbox blocks all other hosts by design; the bots are instructed to ask you for this instead of working around a blocked fetch. Bare /allow-host lists current grants. |
/watch <url> [question] |
Have Gemini watch a YouTube video and post what it saw into the chat, so Claude and Codex (who cannot take video) can read it on their next turn. See YouTube videos. |
/parallel <prompt> |
Both bots answer at once, neither seeing the other's reply. The word may sit anywhere in the prompt (what do you make of this? /parallel). Each bot has the whole chat up to your prompt; both replies land in the shared history and each sees the other's on its next turn. No bot-to-bot thread follows — the point is two independent readings. A YouTube link is still watched by Gemini first. See Summoned build agents and /parallel. |
/lasso <words> |
Search everything you have already read and said — the pages you have annotated in the browser, your past council chats, and any folders you have named — and offer the matches. Nothing is attached until you say so. /lasso <path> attaches a file of your own directly; /lasso attach <n> (or all) takes one of the offers; /lasso detach <n> takes one off; bare /lasso lists what this chat is carrying. See Lasso. |
/help |
Show the command reference. In the browser — the council page and the plugin's Discuss drawer — /help (or just help) opens a small popup instead: each command with one line saying what it does, and nothing is sent to the bots. Close it with Esc, a click outside, or ×. The council page also has a ? button in the header that opens the same popup. |
/quit |
Exit without writing files. |
Typical workflow: discuss (the bots hand each other the floor and
converge on a writer) → /draft [rounds] → iterate with human comments as
needed → /finalize.
Messages typed during a Claude turn are steered into that turn, exactly
like typing in Claude Code itself: the message is injected into the running
session and Claude reads it after its current tool call, adjusting course
without losing in-flight work. Steered messages show as (↪@claude) in the
council and land in the shared transcript, so Codex sees them at its next
turn too. This works on both transports — the programmatic adapter injects
over stdin; under --claude-interactive the text is pasted into the live
tmux pane, where Claude Code's own queued-message handling takes over.
Not everything steers: messages @-addressed to a bot that isn't currently
speaking, slash commands, messages with image attachments, and anything
typed during a Codex turn take the normal queue and run as the next
turn (codex exec accepts no input once launched). During bot-to-bot
free-form threads the same rule applies — steer Claude mid-turn, and
anything queued pauses the thread at the next turn boundary as before.
Council turns can run for minutes, so botference pings you when it's your
turn again: after a turn (or bot-to-bot thread) that ran at least ~5 seconds
finishes, and whenever a bot is blocked waiting on a write-permission
prompt. Notifications are on by default; /notify off disables them.
There is no daemon or OS integration — botference emits a standard terminal escape sequence and your terminal posts the native notification, the same mechanism Claude Code and Codex use. Ghostty, iTerm2, WezTerm, kitty, foot, and Windows Terminal all support it (Ghostty and WezTerm get a titled notification via OSC 777; others get OSC 9), and it passes through tmux. Most terminals only show the notification while the window is unfocused — if you're already looking at the council, nothing pops up. Interrupting a turn with Esc also suppresses the ping, since you're clearly at the keyboard.
Three ways to get files to the bots (all support several at once):
- Drag files from Finder into the terminal — the paths are parsed
(including escaped spaces in screenshot names, quotes, and
file://URLs) and become[image N]attachments. PDFs, spreadsheets (.xlsx/.xls/.csv), Word documents (.docx/.doc) and text files (.md/.txt/.json) work the same way and attach as[file N]. - Finder Cmd+C → Cmd+V — copied files paste as paths and attach the same way.
- Ctrl+V — attaches a raw image from the clipboard (a screenshot taken with Cmd+Shift+Ctrl+4, or a browser "Copy Image"). Terminals only deliver text through normal paste, so raw image data has its own key.
Only paths that actually exist become attachments; a bad path stays visible as text, and any attachment that can't be found at send time is reported in the room instead of silently dropped. Claude views attached images and reads attached PDFs with its Read tool; Codex receives the file path (its CLI cannot view image content mid-session, and reads PDFs only as well as its own tooling allows).
Neither Claude nor Codex can watch video. Gemini can, so botference has it watch for them: paste a public YouTube link in a message and the video is watched before the message reaches the bots, with the written report — title and channel, a summary, timestamped sections, the key claims and numbers, quotes word for word, and what is on screen that the narration does not say — appended to what they receive. It is labelled as a witness report, not an instruction, because it describes a page nobody in the room can check.
- Set the key once: put a Google AI Studio key in
~/.botference/gemini-key(chmod 600it) or setGEMINI_API_KEY. With no key, a YouTube link is noted once per chat and nothing is watched. - You see what Gemini saw. Every report is posted as its own message from
gemini— the header says what was watched, how long it took and by which model — folded after a few lines in the web UI with a "show more". The bots get the same report in their own copy of the turn. /watch <url> [question]watches on demand, posts the report, and then hands the room to both bots to discuss it — no "so, thoughts?" needed. With a question, Gemini answers it first and the summary follows.- A bot can ask. Ending a reply with a line of its own reading
watch: <youtube-url>,watch: <youtube-url> — <question>orask gemini: <question>(about the video most recently watched here) has the video watched or the question put; the line stays in the reply. A question gets the asking bot — only that bot — the floor straight back with the answer in front of it, once; and each bot may ask at most two questions per turn, so a video cannot become a conversation the user is not in. - A working light. While a watch is running, a Gemini mark spins beside the Claude and Codex marks in the web UI's header, shows the outcome for a moment, and then goes — it is a tool the room used, not a third participant.
- Up to three links per message are watched automatically, reports are cached
for 30 days (a re-sent link costs nothing), and
BOTFERENCE_VIDEO_WATCH=offturns the automatic watching off while leaving/watchworking.
Only public videos work — private, unlisted and age-restricted ones are refused by the API, and that refusal is reported in the room rather than swallowed. Google's free tier allows about eight hours of YouTube a day.
The thing that would settle the question is often something you have already
read or already said — a chat from July, a paper you marked up in the browser
in August, a PDF sitting in ~/Downloads. None of it is in the conversation
in front of you, and until now the only way in was to remember a file path.
/lasso <words> searches all of it and shows you the matches. Nothing is
attached until you press a button (or type /lasso attach <n>), and an
attachment is a file the bots read on demand: every later turn names it by
its path with one sentence about what it is, and the bots open it when it
matters. A forty-page transcript pasted into every turn would bury the turn.
- What is searched. Pages you have annotated with the browser companion
(their titles, your highlights and comments, and the saved text of the page),
every chat in the councils you have vouched for, and any folder you list in
the companion's
config.jsonaslasso_folders(e.g.["~/Downloads"]— PDFs, markdown, text and HTML, by filename and their first couple of thousand characters; PDF text needspdftotext, from poppler). - Ranking is deliberately simple: a word in the title counts three, in a highlight or comment two, in the body one; matching every word beats matching most of them; recency breaks ties. The index is built lazily and cached against file times — there is no daemon and nothing to start.
- Without the browser companion running,
/lassoin the council falls back to searching this council's own chats, and says so on the card. /lasso ~/papers/kalman.pdfattaches that file directly — a path is not a search. The file is copied into the workspace (with its extracted text beside it where a PDF allows), so tidying your Downloads later does not break the chat. Your original is never moved or changed, and/lasso detachonly ever deletes botference's own copy.- The bots can ask. A reply may end with a line of its own reading
lasso: <what to look for>. The search runs and you get the matches; nothing is read and nothing is attached by the asking. Same discipline asfile-in:andwatch:— a bot may offer, and only your click does anything.
In the browser, the same thing lives in the companion's drawer: type /lasso
in any composer and the matches appear above it as chips, with what is attached
shown as small chips with a ✕ from then on. Typing a bare / opens the same
menu the @ does, with every command the drawer takes (/lasso, /help) and
one line saying what each does, so it is not a command you have to already
know. The @claude, @codex and @all rows carry a line too. The terminal
/help, the council popup and these menus all read one table in the
controller (COMMAND_HELP in core/botference.py), so they cannot disagree. One line above the chips says what happened — lasso · 3 matches for "fat tails", lasso · fetched "Rockets, part 1" (angadh.com), or
lasso · nothing matched "…" — try other words, or paste a link or a file path.
/lasso https://…in the drawer fetches the page rather than searching for the words of the address. The companion reads it on your machine, saves what it says, and offers you one chip to attach; a site that refuses says so ("could not fetch: HTTP 403 — open it in the browser with the plugin once and lasso it by title") instead of coming back empty. A link you have already annotated comes back as your copy of it, with your comments on it.- A link you paste into an ordinary message is fetched for the bots
automatically — up to three per message, skipping any host they can already
reach — so they read it from a file. There is no
/allow-hostin the drawer and the bots are told not to ask for one: they name the host that was refused and stop.
The bots in a chat talk with you and shape the work. They do not build the deliverable. When one of them decides it has enough, it says so and ends its reply with a brief on a line of its own:
summon: build the landing page from the outline above — one HTML file,
dark theme, save it under projects/acta/artifacts/
Botference starts a fresh build agent with that brief and the room's last
turns, in the project folder. By default it is Claude Code on Opus 5.5 at
high effort; change it under builder in context-budgets.json, where a
for.codex entry can send Codex's summons to a different builder (say GPT-6
Sol at xhigh) while Claude's go to Opus. Codex can summon because botference
does the summoning, not the bot.
What you see, in the council page, the browser plugin and the terminal: a card indented under the message that summoned it, in the style of a reply on a thread —
codex I have enough. summon: build the landing page …
↳ agent · Claude Opus 5.5 (high) · summoned by Codex · working… ⏱ 0:42
— which fills in when the agent finishes: done in 3:10, its report (what it
built, where, what it left out), an artifact: link, and the tool log folded
away. The report enters the shared history in the agent's own name, and the
summoner is woken once to tell you what to open. The other bot sees the
report like any other message, attributed to the agent, not the summoner.
Hardware: a build agent runs blender outside its sandbox so it can use the GPU
(unsandboxed_commands under builder in context-budgets.json); everything else
it does stays sandboxed.
Rules: one summon per bot per user turn; a build runs until it is done (no
time cap by default — set timeout_s under builder in context-budgets.json
if you want one) and reports as failed if it dies; a summon inside the
wake-up is not acted on, so no bot can chain builds without you. A bot that
writes an HTML page, plot or PDF in the chat anyway gets a visible
⚠ built in-chat, not delegated stamp; the file is not touched. An artifact
an agent builds is reviewed by the other bot from the summoner, as in
Adversarial review.
/parallel is for the other half of the problem: with an ordinary @all
turn Claude answers first and Codex sees that answer before writing its own,
so it reacts rather than answers. Put /parallel anywhere in a prompt and both
bots take it at the same time, each with the whole chat up to your prompt and
not the other's reply. Both replies land in the shared history; each bot sees
the other's on its next turn. No bot-to-bot thread follows a parallel turn. A
YouTube link is still watched by Gemini first, as on every turn.
Two agents reasoning from the same context agree on wrong facts, and the longer they discuss something the stickier the shared premise gets. So asking the other one "do you agree?" is not a check — it is the same reasoning read twice. A check is the claim held against the source, and where the claim is mechanically checkable there is no model in the loop at all.
Four places that rule is enforced:
1. Quotes are checked, by a machine. Any passage a bot puts in quotation
marks that runs to six words or more is looked up in the page's own text —
against the page number it named, on a PDF — and the answer is a stamp on
the message: ✓ quote checked, or ⚠ quote not found in the page with the
quote in its tooltip. A done — this passage now reads: "…" line is checked
the same way against the file as it stands now. Nothing is blocked and
nothing is altered; a reply with nothing checkable in it carries no stamp at
all, because a stamp on everything would say nothing about anything. The bots
are told this, so the honest alternative — paraphrase without quotation marks —
is obviously available. (Discuss drawer and bubbles; frontends/plugin/checks.mjs.)
2. A verification turn at convergence. See /verify in the command table:
when the bots mark the thread converged, the one that did not write the last
claim gets a turn carrying the claims, the sources the room has, and nothing of
the discussion — and reports each claim confirmed with the supporting line,
contradicted with the contradicting line, or not checkable. Its reply is badged
verification in the chat so it cannot be mistaken for another opinion.
3. botference review-build — an independent reader for a commit. The agent
that wrote the code wrote the commit message and the tests too. This spawns a
headless Claude Code that has seen none of the conversation and gives it the
diff, the message and the test files, with a sceptical brief: list every claim
the message makes and mark each verified or unverified by reading the code;
find tests that assert the implementation rather than the behaviour; run the
suites itself; report under 300 words with VERDICT: accept | accept with notes | reject. Output goes to stdout and to .botference/reviews/<sha>.md.
botference review-build # HEAD~1..HEAD
botference review-build main..HEAD
botference review-build --dry-run # print the brief, spawn nothingIt is deliberately not a git hook: a check nobody chose to run is a check nobody reads.
4. The artifact reviewer is never the drafter. /make-artifact records which
bot wrote the file, and a send review round on that artifact page routes its
per-comment turns to the other bot, saying so in the round's preamble. A
thread you have addressed to one bot yourself still stays that bot's — the rule
gives a round a default, it does not take the room away from you.
If the TUI ever dies, the next launch prints a "previous run appears to have crashed" notice pointing at the evidence:
<cwd>/.botference/ink-crash.log— UI (Node) exceptions and bridge deaths, with stack traces<cwd>/.botference/crash-reports/— fatal V8 reports (e.g. out-of-memory aborts, which no in-process handler can catch; enabled via--report-on-fatalerror)<work>/sessions/crash.log— controller/bridge (Python) exceptions<cwd>/.botference/run-ledger.jsonl— every launch and exit with the real exit code; a start with no matching end means a hard kill. Abnormal runs are counted in the startup notice even when they left no other trace.<cwd>/.botference/flight.jsonl— the flight recorder: a heartbeat every 15s with memory usage (flagging >85% heap pressure) and the last activity, so a run that dies with no exception still leaves a story in its final breadcrumbs.
The launcher also unconditionally restores terminal modes after the TUI
exits, so even a hard crash (OOM, kill -9) can no longer leave your
shell spraying mouse escape sequences.
Plan-mode participants discover repo-local skills at session start. Add Codex
skills under .agents/skills/<skill-name>/SKILL.md and Claude skills under
.claude/skills/<skill-name>/SKILL.md. When Claude or Codex sees a matching
request, it is instructed to read the relevant SKILL.md before responding.
For shared behavior, keep the same name and description frontmatter in both
directories. The built-in skills follow this layout: grill-me (stress-test a
plan with hard questions), tufte-viz, and review-doc — when a bot produces
a document that exists to be read and reacted to (like an implementation
plan), it can render it as a self-contained HTML page with Google-Docs-style
margin commenting and an "Export feedback" button, and feed your feedback file
back into the next revision.
Botference can treat projects/ as a lightweight portfolio of durable work
containers. This is separate from a single chat session: a project can have many
resumable planning sessions, imported work artifacts, and future build/delegation
threads.
In plan mode the TUI is laid out as Projects | Council. The left
Projects panel is persistent: it shows Inbox, discovered projects, the active
project marker, and the active project's resumable chats. New controller
sessions start in Inbox; Botference does not create a new project until you
ask it to.
Browsing a project never requires activating it. The controller's project
panel carries each project's recent chats, so the web sidebar expands any
project and opens any chat directly; the chat you open decides the active
project (/resume <id> behaves the same way from the TUI).
On the first message of an Inbox chat, Botference shows an arrow-key picker asking where the chat should live: existing projects whose title or next-action matches your message, "Create a new project from this chat", or "Stay in Inbox" (Esc dismisses). Picking a project sets it as the active context and files the chat there. Nothing is ever filed automatically.
Run /projects in plan mode to list directories under projects/. A directory
appears even without metadata; Botference infers its title from, in order:
PROJECT.mdfirst# HeadingREADME.mdfirst# Heading- the directory slug, title-cased
Use /project open <id> to select a project:
/projects
/project open spaceship-engineering
/resume
The status bar will show the selected project. /project clear returns to the
default Inbox context, and /project current prints the active project root.
Create a project from plan mode with:
/project create My New Project
Botference creates projects/my-new-project/PROJECT.md, adds a stable
projects/portfolio.json entry, sets the new project active, and persists the
current session with that project_id. If the slug already exists, creation is
refused and the existing project is left untouched.
Use /project create-from-chat to create a project from the current session
title, or from the first user message if the session has not been renamed. This
is deterministic; it does not call a model.
PROJECT.md is the human-readable project card. New project cards include
placeholders for Status, Priority, Cadence, Why This Matters, Desired Outcome,
and Next Action. Optional portfolio metadata lives in projects/portfolio.json;
use it for status, priority, cadence, desired outcome, and next action. Optional
session associations live in projects/session-index.json, which lets old chats
in work/sessions/ show up under a project without moving the session files.
Project-local sessions under projects/<id>/sessions/*.json are also supported.
/relay is now eager. When you relay @claude or @codex, botference
generates the handoff, tears down that model's old session, and immediately
starts a fresh session in the same running controller.
- Successful relays keep only a timestamped history copy under the project-local Botference handoff history directory.
- Fresh
./botference planlaunches do not auto-load persisted handoff notes. - The live
handoff-claude.mdandhandoff-codex.mdfiles are failure-only artifacts used to preserve a retry payload if the immediate restart fails.
Auto-relay. By default, botference watches each model's context occupancy
and relays it automatically once it crosses 50% of its context window,
using the same handoff machinery as /relay. The relay is always deferred to a
safe boundary — it never fires mid-turn or inside a free-form bot-to-bot thread;
it lands before that model's next turn (or immediately after the current thread
ends). It re-arms only after occupancy falls back below the threshold, so a
single crossing triggers exactly one relay. Disable it with /autorelay off
(TUI) or the Auto-relay toggle in the web council agents panel; the setting persists
per-user, and the pending flag survives restarts alongside the session.
Plan-mode sessions are snapshotted under work/sessions/ after each turn by
default. Project-local sessions under projects/<id>/sessions/ can also be
listed and restored when that project is selected. A small sidecar file at
work/sessions/.metadata-index.json caches per-session metadata (mtime,
project_id, transcript length) so the Projects panel can render counts
quickly without re-parsing every session JSON on every refresh — it's
maintained automatically and is safe to delete (it'll be rebuilt on next
launch). Each snapshot includes:
- the shared transcript
- council panel history
- route, lead, mode, and status state
- the active
project_id, when the session belongs to a project - Claude
session_idand Codexthread_idfor native CLI resume
Use /resume in any ./botference plan session to list saved sessions,
then /resume latest, /resume <number>, /resume <title>, or
/resume <session-id-prefix> to restore one. Resume works mid-chat — the
current session is persisted on every turn, so the chat you're leaving stays
on disk and can be re-resumed by id. If a project is selected with
/project open <id>, /resume lists that project's local or indexed sessions
first and leaves unassigned legacy sessions visible underneath. In the Ink
Projects panel you can also press Enter on a chat row to do the same thing.
Use
/rename <name> during a session to set a durable title; otherwise Botference
derives a title from the first user message or task.
When a resumed session includes project_id, Botference restores that active
project context and updates the status line and Projects panel.
What the models actually remember after a resume: Botference stores each
model's native CLI session id, so on your next message a resumed model
continues its own CLI session (claude --resume <session_id> / the Codex
thread) — full private context, including its earlier reasoning and tool
output, not a replay. If a model's native session is missing (resumed on
another machine, or the CLI purged it), that model is restarted fresh with
the shared transcript backfilled into its initial prompt — it re-reads the
whole conversation but loses its private context. The tell: a
Starting claude session… / Starting codex session… system message means
that model was bootstrapped fresh with backfill; no such message means it
picked up its native session where it left off.
Unhandled plan-mode crashes are appended to work/sessions/crash.log.
If you also run with debug panes, the model stream logs remain:
build/logs/debug-claude.logbuild/logs/debug-codex.log
By default, Botference plan mode still talks to Claude through the structured programmatic Claude Code path. To opt into a screen-scraped interactive Claude Code session instead, run:
botference plan --claude-interactive
# or
BOTFERENCE_CLAUDE_TRANSPORT=tmux botference planThis starts or reuses a dedicated tmux session running interactive claude,
pastes prompts into it with tmux load-buffer / paste-buffer, captures the
pane with tmux capture-pane, and mirrors newly detected Claude output back
into the Council pane. Codex remains on the existing structured adapter path.
Limitations: this is a best-effort mirror, not a structured Claude stream. Idle
detection is heuristic, Claude tool activity is not available with the same
JSON structure as the default transport, and terminal wrapping can make prompt
echo removal imperfect. Logs are written to debug-claude-tmux.log under the
current Botference run/log directory, or .botference/logs/ when no run
directory is available.
To inspect or recover the underlying Claude session:
tmux ls | grep botference-claude
tmux attach -t <session-name>
tmux kill-session -t <session-name> # only when you want to clean it upBotference does not kill the tmux Claude session on normal exit, so the interactive Claude context can continue across Botference restarts.
Plan mode still starts with the write roots from botference/project.json (or
the default Botference work directory in legacy layouts). If a model wants to
edit somewhere else, it must first request a runtime grant for the narrowest
directory it needs.
In the Ink UI, this appears as an allow/deny prompt. Choosing Allow once
expands the planner write roots for the rest of the current session and records
that grant in the resumable session snapshot. Choosing Deny keeps the current
roots unchanged and the model must continue without writing there.
The TUI shows the projects panel (left, toggleable) and the full-width council panel, with a text input field at the bottom.
- Arrow keys do not move between panels. Use the mouse to scroll within each panel.
- Ink text selection: app-level mouse selection is the default Ink
behavior. Dragging inside a pane selects text from that pane only, keeps mouse
scrolling enabled, highlights the selected range, and copies the selected
plain text on release. On local macOS this uses
pbcopy; in tmux it also triestmux load-bufferand an OSC 52 passthrough; otherwise it writes OSC 52 as a best-effort fallback. Copy diagnostics are appended to.botference/ink.logunlessBOTFERENCE_INK_LOG=0is set. SetBOTFERENCE_INK_LOG=/path/to/logto put those diagnostics elsewhere. - Ink activity line: while a turn is running, the status line shows a
small animated glyph and action phrase. It uses whimsical fallback verbs
such as
Prestidigitating..., but switches to concrete activity when bridge events expose it, for exampleCodex is reading README.md...orClaude is responding.... - Native terminal selection: press Ctrl+Y to enter mouse selection mode, drag-select with the mouse/trackpad, and copy using your terminal's normal shortcut (for example Cmd+C on macOS). Press Ctrl+Y again or Esc to return to Botference mouse scrolling. Native selection can span panels; it does not clamp copied text to the active pane.
- Shift+Enter inserts a newline.
- Esc interrupts the current in-flight turn. It no longer clears the input buffer.
- When a protected write is requested, use Left/Right or Tab to switch
between
Allow onceandDeny, then press Enter. - The Ink text field can be glitchy when resizing the terminal window — if it gets stuck, try narrowing and re-widening the window.
Ink intentionally keeps Botference's existing renderer and bridge protocol rather than vendoring the larger CC custom renderer. The app-level selection code keeps a pane-local representation of rendered lines for hit-testing, clamps drags to the starting pane, and borrows the CC renderer's terminal lessons for clipboard routing and exit cleanup. A full screen-buffer renderer remains deferred unless the lighter pane-line layer proves insufficient.
The status line fields:
| Field | Meaning |
|---|---|
| Mode | Current session state: public (normal chat), draft (lead writing), review (other model reviewing) |
| Lead | Which model will write the plan when you /draft or /finalize. Set manually with /lead or auto-set when the bots agree on a writer. |
| Route | Where your next message goes (@all, @claude, or @codex) |
| Claude / Codex | Context usage as percentage of the model's window |
| Observe | Debug observation mode (off by default) |
Note: @claude in the input field is who you're talking to — the Lead in
the status bar is who will write the plan. These are independent.
Warning
Claude reports a point-in-time occupancy snapshot. Codex has no native occupancy signal, so Botference estimates it: tool-free turns give an exact reading (the thread re-sends the full conversation per API call), while tool-heavy turns get an approximation that the next tool-free turn corrects. Expect the Codex figure to be steady but occasionally a turn behind.
Botference has two main modes: planning and building.
There are two options for planning:
Plan mode (./botference plan) — Freeform planning room. Multi-agent mode
(Claude + Codex TUI) is the default; use --claude for solo Claude. No
structured prompts or system instructions are injected.
Plan mode keeps the project tree read-only, but the models may write inside the
Botference work directory (botference/ in project-local mode, work/ in the
self-hosted layout). /draft [rounds] still drives the main plan-writing flow
and updates:
- the project-local
implementation-plan.md - reviewer comment files beside the plan in the Botference state directory
Then /finalize updates:
- the project-local
implementation-plan.md - the project-local
checkpoint.md
and archives active reviewer comments under the Botference archive directory. Nothing else in your repo is touched by this draft/finalize workflow.
Research-plan mode (./botference research-plan) — prompts/plan.md and .claude/agents/plan.md,
following the multi-step planning workflow. Multi-agent mode by default; use
--claude for solo Claude.
A chat session with no system prompts to seed the conversation:
./botference plan # Freeform planning (Claude + Codex)
./botference plan --claude # Freeform planning (solo Claude)A structured session guided by the plan agent and prompt templates:
./botference research-plan # Structured planning (Claude + Codex)
./botference research-plan --claude # Structured planning (solo Claude)Build mode uses the Ralph Loop: a managed iteration cycle that executes the
plan one task at a time. Each iteration picks the next unchecked task from
implementation-plan.md, runs the appropriate agent, and updates
checkpoint.md before yielding.
The build system has two agent runners that serve the same role through different mechanisms:
-
botference_agent.py(primary) — A direct API agent runner that loads per-agent tool registries and runs its own tool-calling loop. Requires anANTHROPIC_API_KEY. This follows ghuntley's coding agent architecture: colocated tool definitions and handlers, registered per-agent, with the agent runner owning the full loop. -
fallback_agent_mcp.py(fallback) — Exposes the same per-agent tool registry as an MCP server, soclaude -pcan call botference's tools natively. Used automatically when no API key is present (OAuth/Max plan users). Same tools, same boundaries, different execution substrate.
Botference detects which path to use at runtime: if an Anthropic API key is set, the primary agent runner handles the task directly; otherwise, it falls back to the MCP path through the Claude CLI.
The ghuntley philosophy — that a coding agent with the right tools is the core unit of work — originally targets software engineering. Botference extends this to research agents (scout, deep-reader, critic, paper-writer, etc.) by giving each agent its own scoped tool registry. For most people building software, the coder agent alone may be sufficient. Whether that holds for research workflows is an open question this project is exploring.
Botference includes context-aware session management that monitors token usage and yields before exhausting the context window. The thresholds are:
- 20% of the window for 1M-token models (Opus 4.6, Sonnet 4.6)
- 45% of the window for 200K-272K models (Haiku, GPT-5.4, o3, o4-mini)
Launches an interactive Claude session that builds using the instructions in
prompts/build.md:
./botference build # Interactive build loopRuns non-interactively (suitable for CI or unattended execution):
./botference -p build # Non-interactive build loop
./botference -p build 10 # Max 10 iterationsBotference supports serial (default), parallel, and orchestrated architectures. Parallel and orchestrated modes are experimental:
./botference -p build --serial # One task at a time (default)
./botference -p build --parallel # Phase-level parallelism
./botference -p build --orchestrated # AI-driven dispatchbotference/
├── botference # Entry point (shell script)
├── work/ # Active thread state (checkpoint, plan, inbox)
├── build/ # Generated outputs, logs, runtime (gitignored)
├── archive/ # Archived completed threads
├── core/ # Python modules (controller, Ink bridge, adapters, agent runner)
├── ink-ui/ # Ink (Node.js/React) terminal UI
├── prompts/ # Dispatcher prompts for plan and build modes
├── .claude/agents/ # Agent definitions (plan, coder, orchestrator, etc.)
├── .claude/skills/ # Claude Code repo-local skills
├── .agents/skills/ # Codex repo-local skills
├── lib/ # Shell libraries (config, detection, monitoring, post-run)
├── tools/ # Python tool implementations (MCP server, file ops, search, etc.)
├── scripts/ # Utility scripts (archive, evaluation, usage extraction)
├── specs/ # Specifications and design documents
├── templates/ # Blank templates for checkpoint and plan files
└── tests/ # Test suite
work/— Legacy active thread state for the self-hosted repo layout. In project-local mode these files live underbotference/.build/— Generated and runtime artifacts:AI-generated-outputs/,logs/,run/. Fully gitignored.archive/— Legacy archive path for the self-hosted repo layout. In project-local mode archives live underbotference/archive/.archive/sessions/holds chats put away with/archive; move a file back towork/sessions/(or run/unarchive <id-prefix>) to restore one.
The header badge is generated from docs/badges/loc.json, which is refreshed by
.github/workflows/update-loc-badge.yml on pushes to main.
It measures tracked source lines in the repo's own code, not docs or runtime
artifacts. The counter includes shell, Python, and TypeScript/JavaScript source
files that are tracked by git, and excludes generated or vendored paths such as
build/, archive/, work/, docs/, templates/, specs/, ink-ui/dist/,
and ink-ui/node_modules/.
If you want to refresh it locally, run:
python3 scripts/update_loc_badge.py| Variable | Purpose |
|---|---|
BOTFERENCE_HOME |
Path to this framework (auto-detected) |
ANTHROPIC_MODEL |
Global model override (default: claude-fable-5-1) |
OPENAI_MODEL |
Codex participant model (default: gpt-6-astra; gpt-6-sol/gpt-6-luna added 2026-09-23; gpt-5.6-sol the previous default; gpt-5.6-terra/gpt-5.6-luna for cheaper/faster, gpt-5.5 still supported) |
OPENAI_REASONING_EFFORT |
Codex participant reasoning effort for planner sessions (default: medium; low/medium/high/xhigh/max, plus ultra on Sol and Astra) |
ANTHROPIC_API_KEY |
API key for Claude models (only if not using subscription) |
OPENAI_API_KEY |
API key for OpenAI models. If set in .env or your shell, Botference prefers API-key auth for Codex and will override local subscription login on startup. |
BOTFERENCE_CLAUDE_TRANSPORT |
Claude plan-mode transport: programmatic (default) or experimental tmux interactive mirror |
BOTFERENCE_CLI_TIMEOUT |
Timeout in seconds for both CLI adapters unless a model-specific override is set |
BOTFERENCE_CLAUDE_TIMEOUT |
Timeout in seconds for Claude CLI turns (default: 3600) |
BOTFERENCE_CLAUDE_TMUX_TIMEOUT |
Timeout in seconds for experimental interactive Claude tmux turns |
BOTFERENCE_CLAUDE_TMUX_POLL_SECONDS |
Poll interval for tmux pane capture in interactive Claude mode |
BOTFERENCE_CLAUDE_TMUX_IDLE_SECONDS |
Idle grace period before Botference treats the tmux Claude turn as complete |
BOTFERENCE_CODEX_TIMEOUT |
Timeout in seconds for Codex CLI turns (default: 3600) |


