No description
Find a file
2026-10-15 14:27:53 +02:00
commands lobby defaults fix + member revoke (API/UI/tool) 2026-10-15 14:27:53 +02:00
examples preamble identity + budgets + canonical project + truthful send + EN sweep 2026-10-14 19:10:45 +02:00
packages lobby defaults fix + member revoke (API/UI/tool) 2026-10-15 14:27:53 +02:00
plugins lobby defaults fix + member revoke (API/UI/tool) 2026-10-15 14:27:53 +02:00
scripts sticky session dir + identity 3-tier (local>system>env) + loud no-identity 2026-10-15 14:16:49 +02:00
.env.example all-EN sweep + plugdep fix err_5dcd5b98 2026-10-14 08:57:11 +02:00
.gitignore gate bridge-scoped + stato .opencomm/ (plugin, core, bridge, docs, installer) 2026-10-13 21:27:26 +02:00
ARCHITECTURE.md lobby defaults fix + member revoke (API/UI/tool) 2026-10-15 14:27:53 +02:00
bun.lock green suite 263 + bridge-only gate + global docs 2026-10-14 09:15:32 +02:00
installer.sh sticky session dir + identity 3-tier (local>system>env) + loud no-identity 2026-10-15 14:16:49 +02:00
LICENSE preamble identity + budgets + canonical project + truthful send + EN sweep 2026-10-14 19:10:45 +02:00
LLM_INSTALL.md dry-run default false (live), lockdown opt-in 2026-10-15 14:01:52 +02:00
opencode.json.example prompts+slack: tool descriptions, agent instructions, excerpt(), transport-slack mock (128 tests green) 2026-10-06 22:46:24 +02:00
package.json green suite 263 + bridge-only gate + global docs 2026-10-14 09:15:32 +02:00
README.md lobby defaults fix + member revoke (API/UI/tool) 2026-10-15 14:27:53 +02:00
ROADMAP.md preamble identity + budgets + canonical project + truthful send + EN sweep 2026-10-14 19:10:45 +02:00
SKILL.md preamble identity + budgets + canonical project + truthful send + EN sweep 2026-10-14 19:10:45 +02:00
SPEC.md discord: restore as zero-hosting option, companion default via OPENCOMM_TRANSPORT (168 tests green) 2026-10-07 11:21:07 +02:00
tsconfig.base.json scaffold root 2026-10-06 22:19:09 +02:00

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 the MessageSink interface.
  • 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 explicit opencomm_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 👍 means abort() + 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 (--companion starts it detached); all projects talk to the same http://127.0.0.1:7070.
  • Opt-in stays per project: each project joins with /opencomm connect <room> (persists .opencomm/connection.json with enabled: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 .env only (600, gitignored). Never in the repo, never in chat after setup — if exposed, rotate immediately.
  • Discord allowlist + DRY_RUN are fail-closed: empty allowlist means zero sends, only blocked-not-allowlisted logs. Live smoke only on explicitly authorized targets.
  • Default-deny permissions (opencode.json.example, cf. ARCHITECTURE.md §2.1): * is ask; --auto/--yolo are forbidden in prod.
  • Localhost first: companion and opencode serve bind 127.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.)