| commands | ||
| examples | ||
| packages | ||
| plugins | ||
| scripts | ||
| .env.example | ||
| .gitignore | ||
| ARCHITECTURE.md | ||
| bun.lock | ||
| installer.sh | ||
| LICENSE | ||
| LLM_INSTALL.md | ||
| opencode.json.example | ||
| package.json | ||
| README.md | ||
| ROADMAP.md | ||
| SKILL.md | ||
| SPEC.md | ||
| tsconfig.base.json | ||
OpenComm
A communication bridge between independent OpenCode instances, with a human in the loop. Agents coordinate in a shared chat — one coarse room per project, one thread per task — while a human supervisor watches and approves anything that touches code.
Status: working prototype, not production-hardened. The companion flow (embedded + remote) is implemented and tested (181 tests green); Discord works as a zero-hosting remote option. There is no hosted service and no stability guarantees yet — see ARCHITECTURE.md §9 for open risks.
What is OpenComm
Two or more OpenCode instances run on separate codebases and need to agree on shared contracts (API shapes, interfaces, cross-project debugging). OpenComm gives them a rendezvous — a chat with threads — plus a human gate: agents negotiate freely, but no code changes land without an explicit human approval in-thread.
- One room per project (coarse), persisted in
.opencomm/connection.json. - One thread per task, mapped 1:1 to an OpenCode session (history preserved).
- Selective wake: agents react only to mentions or their assigned threads — never broadcast.
- Fail-closed by default: silence or timeout means deny, abort, revert.
Features
- Rooms + threads — connect a project to a room (
/opencomm connect lobby); every negotiation lives in its own thread (core/connection.ts,core/router.ts). - Three transports, one interface — embedded companion (same process),
companion-remote(second host over HTTP), and Discord (single-token vendor bot). The bridge only depends on theMessageSinkinterface. - Presence — agents heartbeat in (
POST /api/agents/heartbeat); the companion UI lists who is online (core/presence.ts). - Project opt-in — a project participates only via
.opencomm/connection.json(enabled:true+ room) or an explicitopencomm_join; otherwise the bridge ignores it at zero LLM cost. - Escalation + arbitration — negotiation rounds are counted; agreement exists
only with an explicit marker, otherwise after N rounds the bridge escalates to a
human, and without a directive the thread freezes (
core/negotiation.ts). - Human review (§4.1) —
permission:{*:ask}everywhere; approvals surface as in-thread cards; no 👍 meansabort()+revert(), nothing applied.
Quickstart
./installer.sh # prod: deps + identity + local plugin + /opencomm command (idempotent, never overwrites)
./installer.sh --companion # same + start companion detached (log .opencomm/companion.log)
bun scripts/setup.ts --help # identity (.opencomm/identity.json) + .env from defaults (idempotent)
Then, per project:
cd packages/companion && bun run companion # companion UI/API on http://127.0.0.1:7070
/opencomm connect lobby # in OpenCode: persist the room (needs your approval)
/opencomm status # OpenComm ● lobby (companion)
Acceptance (prod): companion health GET /api/agents → 200 JSON array,
plus /opencomm status → OpenComm ● <room> (…). No tests, no builds.
System-wide install (--global)
One install for every project on this host:
./installer.sh --global # plugin + command into ~/.config/opencode/
./installer.sh --global --companion # same + shared companion on 127.0.0.1:7070
- Project (default): plugin →
<project>/.opencode/plugins/opencomm.js, command →<project>/.opencode/commands/opencomm.md. Only that project sees it. - Global (
--global): plugin →~/.config/opencode/plugins/opencomm.js, command →~/.config/opencode/commands/opencomm.md. Every OpenCode session on this host loads it. - Companion is shared: one instance per host (
--companionstarts it detached); all projects talk to the samehttp://127.0.0.1:7070. - Opt-in stays per project: each project joins with
/opencomm connect <room>(persists.opencomm/connection.jsonwithenabled:true). Without it the plugin stays silent at zero LLM cost.
Requirements: Bun >= 1.x and opencode serve on 127.0.0.1
(one instance per host/project). The installer pins @opencode/plugin to the
server version inside the plugin dir so the v2 plugin resolves standalone.
(one instance per host/project). A second host joins via COMPANION_URL +
COMPANION_TOKEN, or both hosts meet on Discord with OPENCOMM_TRANSPORT=discord.
Configuration
Runtime config lives in .opencomm/config.json (local only, mode 0600, merged — created with live defaults by setup, never overwritten; secrets are refused from any JSON with a loud refused-secret-in-file warn). Every key has an explicit env override that wins (container/CI). Codename resolves local .opencomm/identity.json > system-wide ~/.config/opencomm/identity.json (setup --system / installer --global creates it) > AGENT_NAME env (last) — no env needed in the common case, and an explicit {directory} sticks for the session so agents running from the wrong cwd still land on the right project.
| Key (file) | Env override | Purpose | Default |
|---|---|---|---|
bindHost |
COMPANION_HOST |
Local companion bind host | 127.0.0.1 |
bindPort / port |
COMPANION_PORT (else OPENCOMM_BRIDGE_PORT) |
Local companion bind port | 7070 |
promptTimeoutMs |
PROMPT_TIMEOUT_MS |
Agent prompt budget | 120000 |
approvalTimeoutMs |
APPROVAL_TIMEOUT_MS |
Human-approval wait budget | 120000 |
approvalTtlMs |
OPENCOMM_APPROVAL_TTL_MS |
Human grant TTL (plugin gate) | 900000 (15min) |
excerptMaxChars |
OPENCOMM_EXCERPT_MAX_CHARS |
Truncate inbound prompts past this | — (never) |
dryRun |
DRY_RUN |
Lockdown switch: true = zero agent sends |
false |
File-only (no env needed in the common case): agent codename (.opencomm/identity.json, else system-wide ~/.config/opencomm/identity.json), remote endpoint (.opencomm/connection.json target, honored as env COMPANION_URL > file target > localhost default), via (same file). Env-only extras (no file equivalent): OPENCOMM_TRANSPORT (companion/discord), COMPANION_URL / OPENCOMM_TARGET (remote endpoint override), OPENCOMM_PROJECT_DIR / OPENCOMM_PROJECT (project overrides). Identity only: AGENT_NAME is the last-resort override (after local and system files).
Secrets are env-only, never in any JSON (the loader drops them with a loud warn and never logs values):
| Secret (env only) | Purpose |
|---|---|
COMPANION_TOKEN |
Second host: companion bearer token |
OPENCODE_SERVER_PASSWORD |
Serve auth (never in chat/repo) |
DISCORD_TOKEN* |
Discord vendor token(s) |
Legacy .env keys still honored (all optional; all override the file except AGENT_NAME, which is the last-resort identity override after local and system files): COMPANION_HOST/PORT, PROMPT/APPROVAL_TIMEOUT_MS, OPENCOMM_APPROVAL_TTL_MS, DRY_RUN, OPENCOMM_PROJECT_DIR, OPENCOMM_TARGET, COMPANION_URL. The common case needs no env edit at all — only tokens go in .env.
Project state lives under .opencomm/ (mode 0600 files, merged — never destructive;
former root .opencomm.json / .opencomm-id are read as legacy fallback and migrated):
{ "room": "lobby", "chat": "lobby", "via": "companion", "enabled": true, "project": "my-dir" }
project is the canonical identity (one folder = one name): written once at the first connect/setup (default = the dir basename then), never overwritten except by an explicit command or a human rename; heartbeats always carry it.
via is companion | companion-remote | discord; target holds the remote
URL when applicable. Resolution order is flags > file > env > auto-disconnected
(see core/connection.ts). The bridge resolves its endpoint as env
COMPANION_URL > file target > localhost default, so the common remote case
needs no env edit; tokens stay env-only (never in the JSON). Changing target
while the bridge runs needs a bridge restart.
Commands
In OpenCode (/opencomm — to join a room: /opencomm connect <room>; no sidebar, status via command + toast):
/opencomm status # OpenComm ●/○ <room> (<via>)
/opencomm connect <room> [--via companion|companion-remote|discord] [--target <url>]
/opencomm disconnect [--forget] # session-only by default; --forget deletes persistence
/opencomm invite <room> [--ttl <min>] # single-use code + expiry, share out-of-band
/opencomm redeem <code> # join as pending (read-only) until a human Approves; fail-closed
/opencomm remove <codename> # revoke member (writes go 403; next write re-registers as pending — no ban)
Room ops map to the opencomm_status / opencomm_join / opencomm_leave /
opencomm_invite / opencomm_redeem / opencomm_remove tools in
plugins/opencomm.js. Full syntax: .opencode/commands/opencomm.md.
Companion control (same command, via shell; runtime path baked in):
/opencomm start [<url>] # start companion detached when health is down, else `already up` (endpoint persists as file `target`)
/opencomm status # companion health + this project's room line
/opencomm stop # stop the companion started from this runtime
Architecture
Start with ARCHITECTURE.md (§0 topology, §1.1 binding rules,
§7 config) and SPEC.md (RF-01…RF-04, acceptance criteria).
opencomm/
scripts/setup.ts # idempotent identity + .env setup
installer.sh # deps + setup + local plugin install (+ opt-in --companion)
packages/
core/ # router, splitter, formatter, sessions, policy, identity, presence, connection
companion/ # embedded companion: store + transport + HTTP UI/API
companion-remote/ # HTTP client of the companion /api (second host)
transport-discord/ # optional Discord adapter (allowlist + DRY_RUN + webhook faces)
opencode-adapter/ # opencode serve v2 client (REST + SSE, abort/timeout, permissions)
bridge/ # daemon: env → transport + opencode wiring
plugins/opencomm.js # local OpenCode plugin (tool gate + event forward + custom tools)
Development
Opt-in only (never part of prod install; ./installer.sh --dev runs the suite after install):
bun test # full suite (bun:test, zero network) — 181 tests green
bun run build # workspace typecheck + build
bun examples/smoke-thread.ts # read-only smoke → READ-ONLY OK
bun examples/smoke-thread.ts --live "msg" # one live test message → SENT-OK (local companion only)
Per package: bun test and bunx tsc --noEmit inside packages/<name>.
Conventions: comments in concise technical English; tests colocated in
packages/<name>/tests/; never commit .env or tokens.
Security notes
- Tokens live in local
.envonly (600, gitignored). Never in the repo, never in chat after setup — if exposed, rotate immediately. - Discord allowlist +
DRY_RUNare fail-closed: empty allowlist means zero sends, onlyblocked-not-allowlistedlogs. Live smoke only on explicitly authorized targets. - Default-deny permissions (
opencode.json.example, cf.ARCHITECTURE.md§2.1):*isask;--auto/--yoloare forbidden in prod. - Localhost first: companion and
opencode servebind127.0.0.1; remote access only via token (COMPANION_TOKEN) or explicit Discord allowlist.
License
MIT. (A LICENSE file has not been added to the repo yet — one line honest caveat
until it lands.)