Architecture
ai-coach is a ports-and-adapters (hexagonal) application. The core knows nothing about MCP, Ollama, Gemini, the file system or the terminal. Those sit behind ports the core defines, and one composition root wires them together.
Why each part is the way it is lives in the ADRs in docs/adr/. This page is the map; the ADRs are the reasons.
The system around it
┌───────────────────────── your machine ─────────────────────────┐
│ │
you ── terminal ──► coach CLI (this repo) ──stdio/MCP──► docs-mcp serve (kb-template) ──► Obsidian vault
│ │ ▲ │ (Markdown files)
│ └──HTTP, loopback only──► Ollama (Gemma) │
│ │ │
you ── Claude Code ──► .claude/skills (this repo) ──stdio/MCP───────┘ │
│ │
└────────────────────────────────────────────────────────────────┘
│
└──HTTPS, Tier 2 payload only──► Gemini (the judge)
- kb-template owns the vault. Its
docs-mcpserver is the only program that reads or writes vault files (ADR-0003). It keeps the PARA layout, converts PDFs into chapter notes, checks quotes against books, keeps the sparring history inCoach/sessions/, runs the planning model inPlans/, and serves the graph view on port 4747. Every write it offers is a preview until called withexecute. - The
coachCLI holds the learning rules, the conversations with Gemma and Gemini, and the confirm-before-write terminal flow. It startsnode <docsMcpBin> serve --vault <vault>as a child process for each command, talks to it over MCP on stdio, and stops it when the command ends. - Ollama runs Gemma locally. The CLI refuses any Ollama host that is not on loopback.
- The planning skills in
.claude/skills/are instructions for Claude Code, not code. They reach the same server through the MCP registration Claude Code loads (Claude Code starts its own copy), and never touch vault files directly (ADR-0029, ADR-0030).
Projects
driving side driven side
┌──────────────────────────────┐
terminal ──► cli ──► application ──► │ NotesRepository, SessionLog, │ ◄── adapter-vault-mcp (kb-template over MCP)
(composition root) │ │ VaultFiling, Library │
│ │ SparringPartner, StudyPartner│ ◄── adapter-ollama (local Gemma, loopback only)
│ │ StyleClassifier, │
│ │ SynthesisPartner │
▼ │ Judge │ ◄── adapter-gemini (Gemini)
domain │ PendingJudgements, Clock │ ◄── adapter-local-state (queue file, clock)
└──────────────────────────────┘
| Project | Tag | Contains | May import |
|---|---|---|---|
domain | type:domain | The sparring round aggregate, sparring rules, coaching styles, buzzword and verbatim checks, the review ladder, the judge's verdict rule, the Tier 2 payload invariant, study rounds, synthesis checks, domain errors | domain only; no npm package, no Node built-in |
application | type:application | Use cases (Review, Filing, Sparring, SparringConversation, Judging, Studying, Synthesizing), the ports they need, application errors, in-memory fakes for tests | domain; no npm package, no Node built-in |
adapter-vault-mcp | type:adapter, adapter:vault-mcp | McpVault and VaultClient: the vault and library ports over kb-template's MCP server | core; @modelcontextprotocol/sdk, zod, Node |
adapter-ollama | type:adapter, adapter:ollama | The four Gemma partners and OllamaClient: prompts, fencing, structured replies, the loopback-only guard | core; zod, Node |
adapter-gemini | type:adapter, adapter:gemini | GeminiJudge: the blind claim-extraction protocol, fallback model, boundary re-check of the payload | core; @google/genai, zod, Node |
adapter-local-state | type:adapter, adapter:local-state | FilePendingJudgements, SystemClock | core; zod, Node |
cli | type:app | Terminal I/O, config, commands, and composeCoach, the only place a port meets its adapter | everything |
Adapters never import each other, and nothing imports the cli. Each package's README says what it does in plain words:
| Package | In one line |
|---|---|
domain | The rules of learning: rounds, verdicts, the review schedule. No I/O at all. |
application | The things you can do (spar, judge, study, synthesize) and the ports they need. |
adapter-ollama | The local sparring partner, powered by Gemma. |
adapter-gemini | The online judge, powered by Gemini. |
adapter-vault-mcp | Talks to your vault through kb-template's server. |
adapter-local-state | Remembers which sessions are waiting for the judge, and what day it is. |
cli | The coach command you type. |
Ports and who provides them
| Port | File | Adapter |
|---|---|---|
NotesRepository, SessionLog, VaultFiling | application/…/ports/vault.ts | adapter-vault-mcp |
Library | application/…/ports/library.ts | adapter-vault-mcp |
SparringPartner, StudyPartner, StyleClassifier, Judge | application/…/ports/models.ts | adapter-ollama, adapter-gemini (Judge) |
SynthesisPartner | application/…/ports/library.ts | adapter-ollama |
PendingJudgements, Clock | application/…/ports/state.ts | adapter-local-state |
Rules
- Dependencies point inward. Ports are shaped by what the use cases need, not by a tool's API.
- Business rules live in the domain, in objects that guard their own invariants: a
SparringRoundwill not accept a challenge that quotes the note, andtier2Payload()rejects any field other thanchallengeandanswer(plus core assertions when allowed, ADR-0021). - Use cases orchestrate: they load through ports, let the domain decide, and save through ports.
- Every write to the vault is a preview first and an execute only after the user confirms (ADR-0009).
- Adapters translate technology failures into application errors (
UnavailableError,UnusableReplyError,VaultRefusedError); the CLI turns those into messages and exit codes. - The only data that may leave the machine is a
Tier2Payload. The Gemini adapter accepts nothing else and re-checks it. - Source code carries no comments (ADR-0011). Names and tests carry the meaning.
Enforcement
npm exec nx run-many -- -t lint runs @nx/enforce-module-boundaries with the tags above:
onlyDependOnLibsWithTagskeeps dependencies inward and stops adapters importing each other.allowedExternalImportsis a whitelist per tag: empty fordomainandapplication, and only its own SDK for each adapter.no-restricted-importsbans Node built-ins indomainandapplication, which the whitelist does not cover.- Circular project dependencies are errors.
Tests
| Level | Where | Against |
|---|---|---|
| Domain | domain | Plain functions and objects |
| Use cases | application | In-memory adapters in src/testing, including a check that a planted private line never reaches the judge |
| Adapters | adapter-ollama, adapter-gemini, adapter-local-state | A loopback HTTP server for Ollama, the real Gemini SDK with a fake client, a temporary directory |
| CLI | cli | A scripted terminal and stubbed use cases |
Tests that start the kb-template vault server (the MCP adapter and end-to-end CLI tests) are dropped for now (ADR-0010), so adapter-vault-mcp has no tests and passes with none.