Polycode is a multi-model consensus coding assistant TUI. It queries multiple LLMs in parallel and synthesizes their responses into a single answer via a designated primary model. Built in Go with Bubble Tea.
go build ./cmd/polycode/ # Build the binary
go test ./... -count=1 # Run all tests
go build ./... # Verify all packages compileNo special environment variables needed for building. Tests use mock providers — no API keys required.
cmd/polycode/ → CLI entry point (Cobra), app wiring, setup wizard
internal/
config/ → YAML config loading/validation/saving (~/.config/polycode/config.yaml)
provider/ → Provider interface + adapters (Anthropic, OpenAI, Gemini, OpenAI-compatible)
consensus/ → Fan-out dispatcher, consensus engine, pipeline orchestration, truncation
tokens/ → Token tracking, model limits registry, litellm metadata fetcher
auth/ → Keyring storage, file fallback, OAuth device flow
action/ → Tool execution, safety guardrails, project context for system prompts
tui/ → Bubble Tea TUI (model, update, view, splash, settings, wizard, mcp_wizard)
mcp/ → MCP client — server connections, tool/resource/prompt discovery, JSON-RPC transport (stdio + HTTP)
openspec/ → OpenSpec change artifacts (proposals, designs, specs, tasks)
- Provider interface: All LLM adapters implement
provider.Provider(ID, Query, Authenticate, Validate). Query returns<-chan StreamChunkfor streaming. - Bubble Tea architecture: TUI uses Elm pattern — Model struct, Update handles messages, View renders. View modes:
viewChat,viewSettings,viewAddProvider,viewEditProvider. - Fan-out pipeline:
consensus.Pipeline.Run()dispatches to all providers, collects responses, synthesizes via primary model. Three phases: dispatch → collect → synthesize. - Config is the source of truth: All provider setup flows through
config.Config. TUI settings and YAML both write to the same config file. - Token tracking:
tokens.TokenTrackeraccumulates per-provider usage.tokens.MetadataStorefetches model limits from litellm's JSON database with local cache + TTL. - MCP client:
mcp.MCPClientmanages connections to external MCP servers. Tool names are prefixedmcp_{serverName}_{toolName}and resolved via a lookup map (toolIndex) to avoid underscore-parsing ambiguity. Supports stdio (subprocess) and HTTP transports. A single multiplexed reader goroutine per stdio connection routes responses by request ID and dispatches notifications (e.g.tools/list_changed). Config changes apply at runtime viaReconfigure()without restart. - MCP Registry:
mcp.RegistryClientqueriesregistry.modelcontextprotocol.io/v0/serversfor server discovery. Results are cached in-memory with 15-minute TTL.ToMCPServerConfig()maps registry metadata to config (npm→npx, pip/pypi→uvx, oci→docker, remote→URL). The wizard browse step and/mcp searchboth use the registry, falling back to the hardcodedPopularMCPServerslist when offline.
- Standard Go project layout (
cmd/,internal/) - No external HTTP framework — all API calls use
net/http+bufiofor SSE parsing - Provider adapters handle their own streaming SSE parsing (no shared SSE library)
- Errors are wrapped with
fmt.Errorf("context: %w", err)pattern - Thread safety via
sync.Mutex/sync.RWMutexwhere needed (TokenTracker, conversationState) - Config validation happens in
Config.Validate()— enforces exactly one primary provider
All pipeline → TUI communication uses typed messages sent via program.Send():
QueryStartMsg/QueryDoneMsg— query lifecycleProviderChunkMsg— streaming chunks from individual providersConsensusChunkMsg— streaming consensus outputTokenUpdateMsg— token usage snapshot after each turnConfigChangedMsg— triggers registry/pipeline rebuild + MCP reconfigureTestResultMsg— provider connection test resultMCPStatusMsg— MCP server connection status updateMCPTestResultMsg— MCP server connection test resultMCPToolsChangedMsg— dynamic tool refresh notificationMCPCallCountMsg— MCP tool call count updateMCPDashboardDataMsg— full dashboard data (servers, tools, stats)
- After adding a new provider type: implement the
Providerinterface, add toregistry.go'snewProvider()switch, add to the wizard's type list inwizard.go - After adding new config fields: update the struct + YAML tags, add validation in
Validate(), update the setup wizard if applicable - After changing the TUI: keep view mode dispatch in
View(), key routing inUpdate()via mode-specific handler functions (updateChat,updateSettings,updateWizard) - After changing consensus logic: update the integration tests in
consensus/integration_test.go - After adding/modifying tools: update
AllTools()and/orReadOnlyTools()intools.go, add executor dispatch inexecutor.go, updateToolUsageHints()inproject_context.go. Read-only tools go in both sets; mutating tools go inAllTools()only and requiree.confirm(). - After modifying MCP client: tool metadata changes go in
discoverTools(). New config fields need: YAML tags inMCPServerConfig, validation inValidate(), change detection inmcpConfigChanged(), and handling inReconfigure(). New MCP methods route throughsendRequest()(auto-logged when debug enabled). Test with thenewTestClientFullmock pattern inclient_test.go. The MCP wizard lives intui/mcp_wizard.go; wizard test usesTestConnection()with staged config.
AllTools() (primary model — 10 tools): file_read, file_write, file_edit, file_delete, file_rename, shell_exec, list_directory, grep_search, find_files, file_info
ReadOnlyTools() (fan-out providers — 5 tools): file_read, file_info, list_directory, grep_search, find_files
MCP tools (discovered at runtime): Prefixed mcp_{server}_{tool}, resolved via MCPClient.ResolveToolCall(). All MCP tools go to the primary model; read-only MCP tools (server read_only: true or tool readOnlyHint annotation) also go to fan-out. MCP tools route through the confirmation gate unless the server is marked read-only.