Skip to content

Repository files navigation

Veche banner

Veche

Вече — bounded multi-agent deliberation as an MCP server.

veche is a Model Context Protocol (MCP) server that lets an orchestrator agent (Claude Code, a custom agent, your IDE) convene committee meetings between LLM agents and read back a structured transcript. The protocol is self-extinguishing: every member replies in rounds, anyone can opt out with the literal token <PASS/>, and the meeting terminates the moment everyone passes in the same round (or hits max_rounds).

The name comes from the medieval Slavic вече — a popular assembly that deliberated by acclamation and dispersed when the matter was settled. That maps almost directly onto rounds + <PASS/> until silence.

v1 ships two symmetric CLI-backed adapters:

  • Codex via codex exec (OpenAI)
  • Claude Code via claude -p (Anthropic) — with a recursion guard that prevents a child Claude Code from re-entering this same server

The orchestrator may itself be Claude Code, enabling Claude Code ↔ another Claude Code + Codex committees. Architecture and behaviour are fully specified in spec/ using a C4-inspired four-level model; code follows the spec and changes are made to the spec first.


Why

LLMs collapse into a single line of reasoning on hard problems. A standing MCP tool for pulling a second opinion from a different model — or a differently-prompted instance of the same model — broadens the search, surfaces blind spots, and produces more balanced decisions. The committee protocol drives the discussion to a stable point without the orchestrator hand-rolling turn-taking.

Install

npm install -g veche
veche install        # writes SKILL.md to ~/.claude/skills/veche/ and ~/.codex/skills/veche/
                     # plus optional skill UI metadata under agents/openai.yaml
                     # plus claude mcp add / codex mcp add for the stdio MCP server

veche install --for=hermes   # opt-in install into Hermes Agent (~/.hermes/skills/veche/
                             # plus `hermes mcp add veche --command node --args …`)

Restart your Claude Code / Codex / Hermes Agent session after installing. The skill is invoked as /veche <question>.

If you cloned the repo:

npm install
npm run build
node dist/bin/veche.js install

Prerequisites

Required for
Node.js ≥ 20.11 running the server
codex on PATH Codex participants
claude on PATH Claude Code participants and as an MCP host
CODEX_API_KEY or codex login Codex auth (either path works)
claude login Claude Code auth

Quick start

As a Claude Code skill

After veche install, open Claude Code and type:

/veche should we use argparse or click for a new Python CLI?

The skill first asks for launch options: whether to include a Human Participant, whether to use the default round budget or enter --rounds N, and whether to customize roles for this launch. If custom roles are enabled, it shows the default codex / claude role metadata and system prompt, then lets you override role description, weight, and system prompt without writing a persistent profile.

After that, the skill drives the full lifecycle: start_meetingsend_message → poll get_response until the Job terminates → end_meeting → render the transcript grouped by member with a synthesis paragraph. --rounds N, --human, --no-human, --custom-roles, and --default-roles preselect the corresponding launch answers.

As a generic MCP server

veche install registers the server under Claude Code and Codex by default, and under Hermes Agent when invoked with --for=hermes. To wire it into a different MCP host, use whichever invocation that host supports — the binary is just stdio. A representative mcp.json snippet:

{
  "mcpServers": {
    "veche": {
      "command": "node",
      "args": ["/abs/path/to/dist/bin/veche-server.js"],
      "env": { "VECHE_LOG_LEVEL": "info" }
    }
  }
}

Programmatic embedding (Node)

import { bootstrap } from 'veche';

const { mcp, shutdown } = await bootstrap();
await mcp.connect(); // stdio — point an MCP client at the process
// ...
await shutdown();

Committee protocol

One send_message = one Job. Each Job executes up to maxRounds (default 8, capped by VECHE_MAX_ROUNDS_CAP, default 16) rounds:

  1. Round 0 — the facilitator's message is appended to the transcript.
  2. Round N (N ≥ 1) — every active model member is dispatched in parallel. Each member sees every other member's prior messages since its own last turn. A member replies with either new substantive content (speech) or the literal token <PASS/> (pass) to opt out of the current round.
  3. If an enabled Human Participant is present, the Job enters waiting_for_human after the model round. The human can agree with a target at strength 1..3, skip, or steer the next model round.
  4. The discussion terminates when:
    • every active model member emits <PASS/> in the same round → all-passed
    • roundNumber >= maxRoundsmax-rounds
    • all members dropped out → no-active-members
    • an external cancel_job fired → cancelled

If a member's adapter fails irrecoverably, that member is dropped and the discussion continues. The drop is recorded in the transcript as a system message so remaining members can acknowledge the missing voice.

Full rules: spec/partitions/committee-protocol.md.

MCP tool surface

Tool Purpose
start_meeting Create a Meeting. Opens an adapter Session per member.
send_message Post a facilitator message; returns jobId + cursor. Non-blocking.
get_response Poll a Job: status + transcript delta since a cursor. Supports waitMs.
list_meetings Enumerate meetings. Filter by status / time range.
get_transcript Read a transcript outside of a Job-polling loop.
end_meeting Close a meeting. cancelRunningJob terminates an in-flight Job first.
cancel_job Abort a running Job. Graceful first (30s), forced after.
submit_human_turn Submit human agree, skip, or steer feedback for a pending request.
set_human_participation Enable or disable a Human Participant for future pauses.
submit_synthesis Store the facilitator's final synthesis for a terminal Job.

Schemas and per-tool behaviour: spec/partitions/meeting.md.

CLI: veche

veche is a separate read-only binary that reads $VECHE_HOME directly via FileMeetingStore. It can run alongside the MCP server safely — both processes share the same data.

veche list                           # active meetings (default)
veche list --status all --limit 20   # widen the filter
veche list --format json | jq .

veche show <meetingId>                          # text + ANSI on a TTY
veche show <meetingId> --format markdown
veche show <meetingId> --format json > transcript.json
veche show <meetingId> --format html --out report.html
veche show <meetingId> --format html --open     # tmp file + browser
veche show <meetingId> --raw                    # include round.* and job.* events
veche show <meetingId> --home /alt/store        # override $VECHE_HOME

veche watch                          # local web viewer (live SPA + SSE)
veche watch --port 7654 --no-open    # explicit port, skip auto-open

veche install                        # register skill + MCP server (both hosts)
veche install --for=claude-code      # only one host
veche install --dry-run              # print actions without performing them

The HTML report (show --format html) is a single self-contained file (no remote assets, no <script>, no <link rel="stylesheet">) with a chat-bubble layout per round and colors derived deterministically from participantId.

The live web viewer (watch) starts a loopback HTTP server with a single-page app and two Server-Sent Events channels: one for the meeting list (sidebar updates as new meetings appear / change status), one per selected meeting (transcript updates as messages are appended). Cross-process safe — the watch server polls the on-disk store at 750 ms cadence, so MCP-server-driven updates surface within ~1 s without restarting anything. Speech bubbles render the same Markdown subset as show --format html. The viewer remains loopback-only by default and has a narrow write surface for Human Turn submit/toggle controls only.

Exit codes: 0 ok · 2 store unavailable / write failed · 3 meeting not found · 64 usage error · 1 unhandled internal error.

Full CLI specs: spec/partitions/meeting.md (list / show), Partition: web-viewer (watch), Partition: install (install).

Configuration

Environment

Variable Default Purpose
VECHE_HOME ~/.veche Root for FileStore data and the user config.
VECHE_STORE file file or memory.
VECHE_LOG_LEVEL info trace/debug/info/warn/error.
VECHE_MAX_ROUNDS_CAP 16 Hard clamp for max_rounds.
VECHE_E2E Set to 1 to enable opt-in real-CLI E2E tests.
CODEX_API_KEY Optional; Codex also reads ~/.codex/auth.json.
CODEX_BIN codex Override the Codex binary.
CLAUDE_BIN claude Override the Claude Code binary.

Profiles

Named participant profiles live in $VECHE_HOME/config.json (see examples/config.json.example):

{
  "version": 1,
  "profiles": [
    {
      "name": "codex-senior",
      "adapter": "codex-cli",
      "model": "gpt-5-codex",
      "systemPrompt": "You are a senior engineer. Give concise opinions.",
      "discussionRole": {
        "name": "senior engineer",
        "description": "Independent model participant focused on implementation tradeoffs.",
        "weight": 1
      },
      "workdir": null,
      "extraFlags": ["--skip-git-repo-check"],
      "env": {}
    }
  ]
}

start_meeting.members[] accepts either { profile: "codex-senior" } or ad-hoc overrides.

Recursion guard (Claude Code)

Every claude -p invocation is launched with --strict-mcp-config --mcp-config '{"mcpServers":{}}' plus --disallowedTools=Bash,Edit,Write,NotebookEdit by default. The child Claude Code cannot inherit the parent's MCP configuration, so it cannot re-enter this server and spawn more children. Participants that need extended isolation can add --bare via extraFlags (which additionally requires ANTHROPIC_API_KEY).

Architecture

Hexagonal + Vertical Slices. Every feature lives under src/features/<feature>/ with its own domain/, application/, ports/, and (for features that own ports) adapters/.

src/
├── features/
│   ├── meeting/                 # 7 MCP tools, domain entities, JobRunner
│   ├── committee-protocol/      # round algorithm, pass signal, terminate/drop
│   ├── agent-integration/       # AgentAdapterPort + codex-cli, claude-code-cli, fake
│   └── persistence/             # MeetingStorePort + in-memory, file (JSONL)
├── adapters/inbound/
│   ├── mcp/                     # MCP server (stdio), tool registration, zod schemas
│   ├── cli/                     # veche CLI (list, show, watch, install) + renderers
│   └── web/                     # watch server: HTTP, SSE, SPA
├── shared/markdown/             # escape-then-transform Markdown converter (used by html + watch)
├── infra/                       # DI composition root, StructuredLogger, SystemClock, UuidIdGen
├── shared/                      # branded ids, DomainError, Clock/IdGen/Logger ports
└── bin/
    ├── veche-server.ts          # stdio MCP entrypoint
    └── veche.ts                 # human-operator CLI entrypoint

Full specification: spec/spec.md (index) → spec/partitions/ for the per-feature spec files.

Development

npm run typecheck      # strict tsc across src/
npm run lint           # biome check
npm run lint:fix       # biome check --write
npm test               # vitest — unit + MCP stdio smoke + web SSE integration
npm run build          # emit dist/

End-to-end against real CLIs (opt-in)

Gated on VECHE_E2E=1 to avoid token spend on normal runs:

VECHE_E2E=1 npx vitest run src/e2e/claude-code.e2e.test.ts   # ~4s
VECHE_E2E=1 npx vitest run src/e2e/codex.e2e.test.ts         # ~6s
VECHE_E2E=1 npx vitest run src/e2e/committee.e2e.test.ts     # ~25s

The committee test runs a real 3-round discussion between codex exec and claude -p through the full pipeline (start_meetingsend_messageDiscussionRunner → adapters → get_response).

Troubleshooting

  • claude-runtime: Not logged in · Please run /login — run claude login on the host. Do not set --bare in member extraFlags unless you also provide ANTHROPIC_API_KEY.
  • codex-generic on every turn — check codex login status or set CODEX_API_KEY.
  • MeetingBusy — an earlier Job is still running. get_response to let it finish, or cancel_job to abort.
  • Session ID <uuid> is already in use (direct claude -p test) — don't hand-craft a stable UUID across runs; the adapter generates a fresh one per session.
  • veche show prints meeting <id> not found — the meeting lives under a different $VECHE_HOME. Pass --home <path> or export the env var. Exit code 3 signals this.
  • veche show --format html --open does not open a browser on Linuxxdg-open must be on PATH. The file is still written to $TMPDIR; the path is printed to stderr.
  • veche watch doesn't show new meetings until restart — fixed in current builds via cross-process MeetingStorePort.refresh(). If you observe it on an old build, rebuild and restart veche watch.
  • Empty transcript on get_response — make sure you're passing cursor back unchanged each poll; advancing it is the server's job.
  • mcp__veche__start_meeting not available after veche install — restart the host session. Claude Code / Codex only loads MCP servers at startup.

License

MIT — see LICENSE.

About

MCP server for bounded multi-agent committee meetings — Codex + Claude Code deliberate in rounds

Resources

Stars

Watchers

Forks

Releases

Contributors

Languages