Skip to main content

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-mcp server 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 in Coach/sessions/, runs the planning model in Plans/, and serves the graph view on port 4747. Every write it offers is a preview until called with execute.
  • The coach CLI holds the learning rules, the conversations with Gemma and Gemini, and the confirm-before-write terminal flow. It starts node <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)
└──────────────────────────────┘
ProjectTagContainsMay import
domaintype:domainThe 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 errorsdomain only; no npm package, no Node built-in
applicationtype:applicationUse cases (Review, Filing, Sparring, SparringConversation, Judging, Studying, Synthesizing), the ports they need, application errors, in-memory fakes for testsdomain; no npm package, no Node built-in
adapter-vault-mcptype:adapter, adapter:vault-mcpMcpVault and VaultClient: the vault and library ports over kb-template's MCP servercore; @modelcontextprotocol/sdk, zod, Node
adapter-ollamatype:adapter, adapter:ollamaThe four Gemma partners and OllamaClient: prompts, fencing, structured replies, the loopback-only guardcore; zod, Node
adapter-geminitype:adapter, adapter:geminiGeminiJudge: the blind claim-extraction protocol, fallback model, boundary re-check of the payloadcore; @google/genai, zod, Node
adapter-local-statetype:adapter, adapter:local-stateFilePendingJudgements, SystemClockcore; zod, Node
clitype:appTerminal I/O, config, commands, and composeCoach, the only place a port meets its adaptereverything

Adapters never import each other, and nothing imports the cli. Each package's README says what it does in plain words:

PackageIn one line
domainThe rules of learning: rounds, verdicts, the review schedule. No I/O at all.
applicationThe things you can do (spar, judge, study, synthesize) and the ports they need.
adapter-ollamaThe local sparring partner, powered by Gemma.
adapter-geminiThe online judge, powered by Gemini.
adapter-vault-mcpTalks to your vault through kb-template's server.
adapter-local-stateRemembers which sessions are waiting for the judge, and what day it is.
cliThe coach command you type.

Ports and who provides them​

PortFileAdapter
NotesRepository, SessionLog, VaultFilingapplication/…/ports/vault.tsadapter-vault-mcp
Libraryapplication/…/ports/library.tsadapter-vault-mcp
SparringPartner, StudyPartner, StyleClassifier, Judgeapplication/…/ports/models.tsadapter-ollama, adapter-gemini (Judge)
SynthesisPartnerapplication/…/ports/library.tsadapter-ollama
PendingJudgements, Clockapplication/…/ports/state.tsadapter-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 SparringRound will not accept a challenge that quotes the note, and tier2Payload() rejects any field other than challenge and answer (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:

  • onlyDependOnLibsWithTags keeps dependencies inward and stops adapters importing each other.
  • allowedExternalImports is a whitelist per tag: empty for domain and application, and only its own SDK for each adapter.
  • no-restricted-imports bans Node built-ins in domain and application, which the whitelist does not cover.
  • Circular project dependencies are errors.

Tests​

LevelWhereAgainst
DomaindomainPlain functions and objects
Use casesapplicationIn-memory adapters in src/testing, including a check that a planted private line never reaches the judge
Adaptersadapter-ollama, adapter-gemini, adapter-local-stateA loopback HTTP server for Ollama, the real Gemini SDK with a fake client, a temporary directory
CLIcliA 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.