This document is the map for new contributors. It explains what each module owns, how a turn flows through the system, and where the sharp edges are.
For user-facing docs, see README.md. For release / contribution mechanics, see CONTRIBUTING.md.
┌─────────────────────────────────────────────────────────────────────┐
│ CLI entrypoint (src/cli/index.ts) │
│ parseCliArgs → buildRuntime → runRepl │
└──────────┬──────────────────────────────────────────────────────────┘
│
┌──────▼──────┐ ┌─────────────────┐ ┌──────────────────┐
│ Runtime │◀──────│ commands.ts │ │ coordination- │
│ (runtime.ts)│ │ (slash cmds) │ │ view.ts │
└──┬───────────┘ └─────────────────┘ └────────▲─────────┘
│ │
│ each user turn │ ArenaEvents
▼ │
┌──────────────┐ ┌──────────────────┐ │
│ turn.ts │──────▶│ ClashEngine │───────────┘
│ (solo/team) │ │ (src/core/ │
│ │ │ clash-engine/) │
└──┬───────────┘ └──────┬───────────┘
│ │
│ │ tool calls
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ team-cache │ │ SandboxHandle │
│ (SHA256-key) │ │ docker | shuru | │
└──────────────┘ │ local │
└──────────────────┘
v1.3: The external @jackchen_me/open-multi-agent dependency has been
replaced by ClashEngine, a purpose-built orchestrator owned by ClashCode.
All inter-agent communication flows through the SignalBus event system,
enabling real-time TUI visualization of every agent action.
ClashEngine lives in src/core/clash-engine/ and is the single orchestration
layer for both team collaboration and structured debate.
┌────────────────────────────────────────────────────┐
│ ClashEngine (clash-engine.ts) │
│ Public facade: executeAgent, executeSquad, │
│ executeClashDebate, onEvent, registerTool │
├────────────────────────────────────────────────────┤
│ Arena (execution-context.ts) │
│ Owns: model, provider, apiKey, SignalBus, │
│ ToolVault. No globals — one Arena per session. │
├────────┬───────────────┬───────────────────────────┤
│ Signal │ ToolVault │ LLM Client │
│ Bus │ (tool- │ (llm-client.ts) │
│ (event │ vault.ts) │ Raw fetch to OpenAI- │
│ -bus. │ Built-in + │ compatible endpoints. │
│ ts) │ sandbox │ No external SDK. │
│ │ tools. │ │
├────────┴───────────────┴───────────────────────────┤
│ Runners │
│ ForgeRunner (team-runner.ts) — 3-role squad │
│ ClashRunner (debate-runner.ts) — 6-persona debate │
└────────────────────────────────────────────────────┘
| File | LOC | Responsibility |
|---|---|---|
types.ts |
~150 | All type definitions: AgentSpec, SquadBlueprint, ArenaEvent, ToolDef |
event-bus.ts |
~90 | SignalBus: typed pub/sub with legacy bridge for backward compat |
execution-context.ts |
~100 | Arena: resolved config, provider mapping, env key lookup |
agent-registry.ts |
~75 | CODER/REVIEWER/CONSENSUS presets, defaultSquadBlueprint |
tool-vault.ts |
~170 | ToolVault registry, defineTool, built-in tools (bash, file ops, grep) |
llm-client.ts |
~120 | callModel: raw fetch to OpenAI-compatible chat/completions |
team-runner.ts |
~140 | runSpark (single agent loop), runSquad (multi-agent + coordinator) |
debate-runner.ts |
~180 | runClashDebate: 5-phase debate with rich event emission |
clash-engine.ts |
~100 | Public facade class |
Every meaningful action emits a rich ArenaEvent through the SignalBus:
| Event Kind | Emitted When |
|---|---|
spark_ignited |
Agent starts processing |
spark_completed |
Agent finishes (includes token usage) |
tool_invoked |
Tool call initiated |
tool_resolved |
Tool call completed (includes elapsed time) |
round_opened |
Debate round begins |
convergence_probed |
Convergence score computed |
phase_shifted |
Workflow phase transition |
fault_recovered |
Error handled gracefully |
session_sealed |
Run complete |
The SignalBus includes a legacy bridge that auto-converts ArenaEvents
to the old ViewEvent format consumed by coordination-view.ts, ensuring
backward compatibility with the existing TUI.
The Arena owns all runtime state (model, API key, tools, signal bus). There are no module-level singletons in the engine. This makes concurrent sessions safe and testing trivial — construct a fresh ClashEngine per test.
| File | Lines | Responsibility |
|---|---|---|
index.ts |
~100 | Entrypoint. init subcommand short-circuit; otherwise parse → bootstrap → build → repl. |
bootstrap.ts |
~170 | Arg parsing, log-level, telemetry exporters, global error handlers, --help / --version. |
runtime.ts |
~160 | Builds the Runtime aggregate: settings, session store, team cache, ClashEngine, sandbox handle. |
repl.ts |
~145 | Readline loop, paste detection, slash-command routing, graceful shutdown. |
turn.ts |
~230 | Per-turn execution: solo vs team, cache probe, diagnostics panel. |
commands.ts |
~710 | Slash-command registry (declarative COMMAND_REGISTRY map + handlers). |
coordination-view.ts |
~555 | Live mission-control view rendered to stderr. TTY-aware fallback. |
completer.ts |
~120 | Readline tab-completion. |
model-select.ts |
~135 | /model menu — fetches model list from provider API. |
init-interactive.ts |
~220 | clashcode init --interactive wizard. |
doctor.ts |
~185 | /doctor diagnostics. |
ui.ts |
~110 | ANSI colours, banner, box, error/success/info helpers. |
| Path | Responsibility |
|---|---|
core/clash-engine/ |
ClashEngine orchestrator — see section 2 above. |
orchestrator/index.ts |
createOrchestrator adapter: creates a ClashEngine, registers sandbox tools. |
orchestrator/tools.ts |
SandboxHandle lifecycle, sandbox tool definitions (sandbox_exec/write/read). |
consensus/index.ts |
Convergence scoring heuristics, LexicalConvergenceScorer, formatDebateReport. |
consensus/personas.ts |
Built-in personas, pluggable registries. |
consensus/types.ts |
Debate types, ConvergenceScorer and PersonaRegistry interfaces. |
consensus/store.ts |
DebateStore — persistent debate history. |
config/index.ts |
Settings load/save, migration, DEFAULT_SETTINGS. |
config/keychain.ts |
keytar-backed API-key storage. |
state/index.ts |
SessionStore — session persistence. |
state/team-cache.ts |
TeamCache — keyed on (goal + agents + model) hash; TTL-based pruning. |
sandbox/backend.ts |
SandboxBackend interface, validateSandboxPath. |
sandbox/factory.ts |
createSandboxBackend, resolveBackend — picks backend from config. |
sandbox/backends/ |
Docker, Shuru (microVM), Local backends. |
user types "refactor the auth module"
│
▼
repl.ts handleLine(trimmed)
│ ├─ starts with '/'? → commands.ts handleCommand() → return
│ └─ else → turn.ts executeTurn()
▼
turn.ts executeTurn(runtime, msg)
│ 1. store.append(session, { role:'user', content:msg })
│ 2. showCoordinationView(msg, viewAgents)
│ 3. if teamMode → runTeamTurn
│ else → runSoloTurn
│ 4. freezeCoordinationView()
│ 5. print summary + response
│ 6. store.append(session, { role:'assistant', content:output })
│ 7. store.addTokens(session, in, out)
▼
runTeamTurn:
├─ TeamCache.key(msg, agents, model) → cache.get() → HIT? return
├─ engine.executeSquad(blueprint, msg, coordinatorModel?)
│ └─ Runs each agent via runSpark(), then coordinator synthesis
├─ extract coordinator.output as final answer
├─ if diagnostics: printDiagnosticsPanel()
└─ if cacheWorkerOutputs: teamCache.set(...)
Key invariants:
- The coordination view is the only thing writing to stderr during a turn. Agent progress flows through ArenaEvents → SignalBus → legacy bridge → feedEvent.
- Cache hits record 0 tokens (fair accounting).
- Team names are UUID-suffixed. Concurrent turns are safe.
- No coordinator-model override hack. ClashEngine's
executeSquadaccepts an explicitcoordinatorModelparameter.
src/orchestrator/tools.ts uses SandboxHandle — a class that owns
one lazy-created backend instance. The legacy configureSandbox() singleton
API still works for backward compatibility.
Direct ANSI cursor control on stderr. Fine in a real terminal; breaks
under prompt_toolkit-style stdout patching. If embedding clashcode,
call disableTUI() or set CLASHCODE_NO_TUI=1.
- Write a handler in
src/cli/commands.tsreturningCommandResult. - Add one line to
COMMAND_REGISTRY. - Update autocomplete arguments in
src/cli/completer.tsif it takes args.
Three ways:
- Built-in: Add to
BUILT_IN_PERSONASinsrc/consensus/personas.ts. - Config file: Create
.clashcode/personas.jsonwith an array ofPersonaobjects; load withConfigPersonaRegistry. - Custom registry: Implement
PersonaRegistryinterface and pass toClashEngineconstructor viadebateOptions.personaRegistry.
Implement ConvergenceScorer (see src/consensus/types.ts) and pass it
to ClashEngine constructor via debateOptions.scorer.
- Implement
SandboxBackendinterface fromsrc/sandbox/backend.ts. - Register in
src/sandbox/factory.tsunder a newSandboxBackendName. - Add a doctor check in
src/cli/doctor.ts.
Add to the provider maps in src/core/clash-engine/execution-context.ts
(UPSTREAM_MAP, BASE_URLS, ENV_KEY_MAP).
Call engine.registerTool(toolDef) to add tools available to agents.
The defineTool() helper converts Zod schemas to JSON Schema automatically.
- Unit (
test/*.test.ts) — fast, in-process, no network. ~20 files, ~250+ tests. - Integration (
test/integration/*.integration.test.ts) — exercises real sandbox backends. - Snapshot (
test/consensus-report.snapshot.test.ts) — pins debate report format.
Coverage gate: 90% lines / 85% branches.
~/.clashcode/settings.json ─┐
env XAI_API_KEY ├──▶ resolveApiKey ──▶ ClashEngine (Arena)
OS keychain (keytar) ─┘
env CLASHCODE_LOG_LEVEL ─▶ bootstrap.applyLogLevel ─▶ logger ─▶ stderr
--log-level <l> ─┘
env CLASHCODE_OTEL_ENDPOINT ─▶ enableOtelExport ─▶ in-proc metrics
env CLASHCODE_SENTRY_DSN ─▶ enableSentry ─▶ error capture
| Option | Default | Description |
|---|---|---|
noNewPrivileges |
true |
Blocks setuid/setgid bit elevation |
readOnlyRootfs |
false |
Read-only container root |
seccompProfile |
undefined |
Path to seccomp JSON, or 'builtin' |
apparmorProfile |
undefined |
AppArmor profile name |
capAdd |
undefined |
Drops ALL capabilities and only adds these back |
Shuru (microVM) provides stronger isolation than Docker by running in a true VM boundary.
The ConvergenceScorer interface is designed for this. An embeddings-based
implementation would call an embedding API, compute cosine similarity, and
map to the 0-100 scale.
ClashEngine's SignalBus can be extended with a content_delta event kind
to stream agent output token-by-token to the TUI.
The Arena-per-session model is compatible with distributing agents across processes. Each process would get its own Arena with an independent SandboxHandle.
The ToolVault and slash-command registry are both extensible. A plugin
interface could load tools and commands from .clashcode/plugins/.