Metadata-Version: 2.5
Name: tyamka
Version: 0.6.0
Summary: Zero-token memory for your coding agent, plus a local-first knowledge base it can read
Project-URL: Homepage, https://github.com/tyamka/tyamka
Project-URL: Repository, https://github.com/tyamka/tyamka
Project-URL: Changelog, https://github.com/tyamka/tyamka/blob/main/CHANGELOG.md
Project-URL: Documentation, https://github.com/tyamka/tyamka/blob/main/docs/GUIDE-en.md
Project-URL: Issues, https://github.com/tyamka/tyamka/issues
Author: dafuct
License-Expression: MIT
License-File: LICENSE
Keywords: agent-memory,citations,claude-code,embeddings,knowledge-base,mcp,rag,sqlite
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Documentation
Classifier: Topic :: Text Processing :: Indexing
Classifier: Typing :: Typed
Requires-Python: >=3.13
Requires-Dist: aiosql>=13.3
Requires-Dist: aiosqlite>=0.21
Requires-Dist: anthropic>=0.75
Requires-Dist: fastembed>=0.8.0
Requires-Dist: fastmcp>=3.4
Requires-Dist: httpx>=0.28
Requires-Dist: jinja2>=3.1
Requires-Dist: pydantic>=2.10
Requires-Dist: pymupdf>=1.25
Requires-Dist: rich>=13.9
Requires-Dist: sentence-transformers>=3.4
Requires-Dist: sqlite-vec>=0.1.6
Requires-Dist: tenacity>=9.0
Requires-Dist: trafilatura>=2.0
Requires-Dist: typer>=0.15
Description-Content-Type: text/markdown

# tyamka

[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
![Claude Code plugin](https://img.shields.io/badge/Claude%20Code-plugin-8A63D2)
![Python](https://img.shields.io/badge/Python-3.13%2B-3776AB)

**Zero-token memory for your Claude Code agent** — plus a local-first knowledge base it can read.

tyamka gives your coding agent a memory that costs **no LLM tokens**: it records *why* your code changed, then feeds the relevant history back into your next prompt automatically — all on local embeddings, in one SQLite file you own. It's also a full knowledge-base compiler: research topics with parallel agents, compile cited, confidence-scored articles, and query them with hybrid retrieval.

*Example — what the why-guardrail hands your agent before it edits a file, on zero LLM calls:*

```text
$ tyamka why tyamka/ops/recall.py
● tyamka/ops/recall.py — 3 recorded decisions (newest first)

  2d ago · refactor
    Embed the prompt once, then gate candidates against their stored vectors —
    recall must stay under the 15s UserPromptSubmit hook timeout.

  23d ago · decision
    Gate at cosine 0.42 — measured, not guessed: on paraphrase-multilingual-
    MiniLM-L12-v2 it keeps 13/18 relevant prompts and admits 2/24 noise ones.

  zero LLM calls · pure SQL + one local embedding
```

- **Zero tokens by default** — capture, recall, and context injection run on local embeddings and make **no LLM calls**. You spend tokens only when you explicitly research, compile, or summarize.
- **Local-first** — one SQLite file (WAL, FTS5 full-text + `sqlite-vec` vectors). No server, no cloud state, no lock-in — your `~/wiki` is the whole system of record.
- **Provenance + injection-aware** — every finding and citation traces back to an immutable raw source, and all ingested text is sealed as untrusted `<source_data>` before it ever reaches a model (on the way *out* to an agent's context, too).
- **Two thin surfaces, one core** — a `rich` Typer CLI and a `fastmcp` MCP server are both thin wrappers over one shared service layer. Nothing is implemented twice.

```text
/plugin marketplace add tyamka/tyamka
/plugin install tyamka@tyamka
/tyamka:init
```

**Full walkthrough →** [docs/GUIDE-en.md](docs/GUIDE-en.md) · **Architecture →** [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) · **How it works →** [#how-it-works](#how-it-works) · **Agent memory in depth →** [#agent-memory-that-costs-no-tokens](#agent-memory-that-costs-no-tokens)

---

## Install as a Claude Code plugin

The repository doubles as a [Claude Code](https://claude.com/claude-code) plugin — `/tyamka:*` slash commands plus MCP tools, running on your **Claude subscription** (no API credits) or an Anthropic API key.

```text
/plugin marketplace add tyamka/tyamka
/plugin install tyamka@tyamka
```

At install you choose the LLM backend (`subscription` or `api`). Install the `tyamka` CLI yourself — no hook does it for you (a few minutes on first run, including a one-off local embedding-model download), and re-run the same command after pulling an update:

```bash
uv tool install --force --reinstall --no-cache <this repository's directory>
```

Until it is on PATH every hook no-ops behind its `command -v tyamka` guard. Then:

```text
/tyamka:init             # create a knowledge base for this project
/tyamka:research "..."   # gather knowledge (or /tyamka:ingest <url|pdf|file>)
/tyamka:compile          # synthesize cited articles
/tyamka:query "..."      # cited answers
```

The **Viewer UI** starts itself in the background on session start — a local, read-only web view
over every wiki on your machine. Open **http://127.0.0.1:8080** once a session has begun (the very
first run builds it once — see [Viewer UI](#viewer-ui-viewer)). Needs `java` on your PATH (`java`
+ `npm` for that first build); it silently does nothing if they're absent.

Requires [`uv`](https://docs.astral.sh/uv/) plus either a logged-in `claude` CLI (subscription) or an `ANTHROPIC_API_KEY` (API backend). Full setup, commands, and caveats: **[docs/PLUGIN.md](docs/PLUGIN.md)**. An end-to-end walkthrough — the canonical loop, worked scenarios (including a wiki *inside* a code repo), and every command with its token cost: **[docs/GUIDE-en.md](docs/GUIDE-en.md)**.

The rest of this README covers running tyamka **from source** as a standalone CLI + MCP server.

---

## Releasing

Tagging is the whole release. `.github/workflows/release.yml` fires on a `v*` tag, runs the full
gate (`pytest`, `ruff`, `mypy --strict`) plus a check that the tag and the package version agree,
verifies the built wheel actually carries its runtime data files, publishes to PyPI through
**Trusted Publishing** — no API token exists in the repository or in any secret — and opens a
GitHub Release whose body is that version's `CHANGELOG.md` section.

```bash
# bump the version in all four places, update CHANGELOG.md, then:
git tag -a v0.6.1 -m "0.6.1 — ..." && git push origin v0.6.1
```

A PyPI version is immutable, which is why the gate runs *before* the upload rather than after.

---

## Requirements

- **Python 3.13+**
- [**uv**](https://docs.astral.sh/uv/) for dependency management
- **One LLM backend**: either an **Anthropic API key** (`[llm] backend = "api"`) or a logged-in `claude` CLI (`backend = "subscription"` — no API credits). See [Choosing an LLM backend](#choosing-an-llm-backend).
- A **Voyage AI key** is optional (hosted embeddings; the default runs a local embedding model with no key). Capture, recall, and `--extract` need **no** LLM backend at all — only the embedder.

## Install

```bash
uv sync
```

This installs everything, including a local embedding model (`sentence-transformers`) and a cross-encoder reranker that are downloaded on first use.

## Migrating from wikiforge

tyamka was called **wikiforge** through 0.5.0. Nothing about your data changes — the rename touches
names on disk only, and no wiki is rewritten:

```bash
uv tool install --force --reinstall --no-cache <this repository's directory>
tyamka migrate            # .wikiforge/ -> .tyamka/, and ~/.config/wikiforge -> ~/.config/tyamka
```

Then update any `hooks.json` you wired by hand: every `wiki …` becomes `tyamka …`, and the guard
becomes `command -v tyamka`. The plugin's `hooks/hooks.json.example` already reads that way.

`tyamka migrate` is free, offline, and idempotent — it never opens the database, so it works on a
wiki this build could not otherwise read. If **both** `.wikiforge/` and `.tyamka/` exist it refuses
and moves nothing: two directories can hold two different dev logs, and picking a winner silently
would lose one.

Until 0.7.0 you can skip it. The old names still resolve, read-only and one release only:
`.wikiforge/` is found when `.tyamka/` is absent, `$WIKIFORGE_HOME` and `$WIKIFORGE_BACKEND` are
honoured after their `TYAMKA_*` spellings, `~/.config/wikiforge/peers.toml` is read when the new
registry does not exist (registering any peer writes the new one), and the Viewer scans for both
layouts. The `wiki` command also still works — it prints one deprecation line on stderr and
delegates to `tyamka`.

---

## Configure secrets (environment only)

Secrets are **never** stored in the config file — they are read from the environment:

```bash
export ANTHROPIC_API_KEY=sk-ant-...      # required for [llm] backend = "api" only
export VOYAGE_API_KEY=pa-...             # optional — enables hosted embeddings
export TYAMKA_HOME=~/wiki             # optional — default wiki location (else ~/wiki)
```

With `backend = "subscription"` no key is needed — tyamka shells out to your logged-in `claude` CLI instead.

## First run

```bash
# 1. Create a wiki (config.toml, SQLite DB, topics/ dir)
uv run tyamka init "My Wiki" --home ~/wiki

# 2a. Grow it autonomously — research agents fan out, gather, and normalize findings
uv run tyamka research "Rust async runtimes" --new-topic

# 2b. …or add sources by hand (URL / PDF / text file)
uv run tyamka ingest https://tokio.rs/blog/2020-04-preemption

# 2c. …or attach an internal/private source to a topic — compiles with NO web search
uv run tyamka ingest ./docs/internal-auth-design.md --topic "our-auth-design" --new-topic
#     (already ingested? bind it later:  uv run tyamka attach <source-id> our-auth-design)

# 3. Compile gathered evidence into cited, confidence-scored articles
uv run tyamka compile

# 4. Ask questions over the compiled knowledge (answers cite their sources)
uv run tyamka query "How does cooperative scheduling work in async Rust?"

# 5. Explore
uv run tyamka related rust-async-runtimes
uv run tyamka stats
uv run tyamka export site --out ./site   # static HTML you can open in a browser
```

> **Which wiki a command talks to.** Every command accepts `--home <dir>` and every command resolves
> it the same way, in this order: `--home` → the **main repository's** `.tyamka/` if it exists →
> the current directory's `.tyamka/` → `$TYAMKA_HOME` → `~/wiki`. So inside a project with a
> `.tyamka/`, `tyamka compile` and `tyamka query` read exactly the wiki that `capture`, `recall` and
> `why` write to; outside one, everything lands in `~/wiki` as before. Only `--home` overrides a
> project wiki — `TYAMKA_HOME` sets your default, not a project's.

---

## Project identity (`project.toml` is committed; `wiki.db` is not)

`tyamka init` mints a `project_id` (a UUID) and records the normalized git remote as
`remote_key`, storing both in the database **and** in `<home>/project.toml`:

```gitignore
.tyamka/*
!.tyamka/project.toml
```

Those two lines are the point. Before 0.6 a wiki's identity was its path on disk, which makes two
clones of one repository — a laptop and a desktop — two unrelated projects. The database beside the
file is a local cache and stays ignored; the identity is what has to travel with the code, so a
fresh clone adopts the project it was cloned into rather than minting a second one. Commit
`project.toml`; ignoring `.tyamka/` wholesale silently re-creates the problem.

`remote_key` is derived, not authoritative — `git@github.com:you/x.git` and
`https://github.com/you/x` both normalize to `github.com/you/x`, which is what lets two clones be
recognised as one project even if each minted its own `project_id` first. A repository with no
`origin` (`~/wiki`, a scratch repo) simply has no `remote_key`; that is normal, not an error. It is
re-read on `tyamka init` and `tyamka stats`, so a repository that moves to a new host or org catches
up without a special command. `tyamka stats` prints both, as does the MCP `get_stats` tool.

---

## Exporting the wiki (`tyamka sync`)

`tyamka sync push --dry-run --out push.ndjson` writes every row this wiki has not yet sent as
NDJSON — one `kind`-tagged record per line. There is **no server**: without `--dry-run` the command
exits 2 and says so. What exists today is the payload and its schema.

```bash
tyamka sync push --dry-run --out /tmp/push.ndjson      # the delta since the last watermark
tyamka sync push --dry-run --out /tmp/all.ndjson --since-zero   # everything — a portable backup
tyamka sync schema                                      # the JSON Schema a server must accept
```

Nothing rowid-shaped is in the file. Rows are addressed by what identifies them on any machine —
`content_hash`, `topics.slug`, `(topic_slug, version)` — because a SQLite rowid is meaningless
anywhere else. The exceptions are named `local_rowid` / `local_session_rowid` and belong to the
ledger tables, which have no natural key at all; those are only ever meaningful paired with the
`device` in the header.

`--dry-run` never advances `sync_state`. Nothing has acknowledged these rows, and a watermark moved
past rows no server received is data silently never sent. Dev-event bodies are redacted a second
time on the way out: capture redacts on the way in, and this is the moment text leaves the machine.

---

## How it works

```
ingest / research  →  raw_sources (immutable)  →  chunks (FTS5 + vector index)
                                   │
                                   ▼
                          compile  →  articles (versioned, cited, confidence-scored)
                                   │                     │
                                   ▼                     ▼
                       topic_links (graph)          query (hybrid retrieval + RAG)
```

- **Ingestion** canonicalizes URLs (strip tracking params, normalize host/scheme), extracts clean text (`trafilatura` for HTML, `pymupdf` for PDF), and dedups by `sha256` content hash. Re-ingesting the same content updates provenance, never the immutable text.
- **Research** fans out persona agents (academic, technical, applied, news, contrarian, …) in waves using `asyncio.TaskGroup`, with a per-session USD budget and resumability. Each agent web-searches, stores its finding as an immutable source, and normalizes it into the schema.
- **Attaching** binds an ingested source directly to a topic (`tyamka ingest --topic` / `tyamka attach`) so it compiles into that topic's article with **no web search and no LLM spend** — the bridge for internal or private material (a project's own dev log, an internal design doc) that public research would never surface. Research findings and directly-attached sources feed the same compiler; a topic's article can rest on either or both.
- **Compilation** synthesizes a topic's evidence — researched findings and directly-attached sources alike — into a Markdown article with inline citations, detects conflicts between sources, and computes a **confidence score in code** (source count, diversity, recency, evidence strength, minus a conflict penalty). Compilation is incremental — an unchanged content digest is skipped unless `--full`. It makes no web-search calls: it synthesizes only over the sources already tied to the topic.
- **Retrieval** merges FTS5 BM25 and `sqlite-vec` KNN rankings via Reciprocal Rank Fusion. `--scope` (`all` / `articles` / `devlog`) picks what's searched — everything, by default, at any depth. `--depth` (`quick` / `standard` / `deep`) picks ranking effort only; `deep` adds a cross-encoder rerank.
- **Cost** for every LLM/embedding call is priced from the config and logged; `tyamka stats` totals it.

---

## Architecture

Two thin surfaces (a `rich` Typer CLI and a `fastmcp` MCP server) and the Claude Code hooks all call **one shared service layer** — nothing is implemented twice. Everything persists to a single local SQLite file; the LLM and embedding providers sit behind `Protocol`s, so the whole test suite runs with **no network and no keys**.

```mermaid
flowchart TB
    CLI["wiki CLI · Typer"] --> SVC
    MCP["MCP server · fastmcp"] --> SVC
    HOOKS["Claude Code hooks<br/>capture · recall · why-guardrail"] --> SVC
    SVC["Service core — one shared layer<br/>(services.py)"] --> STORE["SQLite · WAL<br/>FTS5 + sqlite-vec"]
    SVC --> LLM["LLM providers · Protocol<br/>Anthropic API / claude -p<br/>+ Governed budget"]
    SVC --> EMB["Local embeddings<br/>paraphrase-multilingual-MiniLM-L12-v2"]
```

- **Two paths, one core.** The *paid* path (`research → compile → query`) is always explicit; the *free* path (capture, recall, the why-guardrail, MCP excerpts) runs on local embeddings and makes **zero LLM calls**.
- **Immutable provenance.** Raw sources are content-addressed and never mutated; every citation is audited against them.
- **Injection defense is uniform.** A single `seal_source_data` chokepoint neutralizes `<source_data>` delimiters in every untrusted or model-generated string — on the way *in* and *out*.

**Full design doc → [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).**

---

## Agent memory that costs no tokens

tyamka remembers *why* your code got to be the way it is — and hands that memory back to
your coding agent automatically. The whole loop runs on local embeddings and makes **zero LLM
calls**.

> **Off by default.** None of the rows below fire until you opt in: copy
> `hooks/hooks.json.example` to `hooks/hooks.json` (or paste the entries into a project's
> `.claude/settings.json`) **and** flip `[capture] auto`, `[recall] enabled` and `[why] guardrail`
> to `true` in that wiki's `config.toml`. Each surface spawns one CLI process per prompt, stop or
> edit, and each loads the embedding runtime — on a busy session that is real RAM, so the wiring is
> a decision you make, not a default you inherit. Everything works manually without it:
> `tyamka capture --note`, `tyamka why <path>`, `tyamka query`.

| When | What happens | LLM cost |
|---|---|---|
| A task edits files | `Stop` hook records a **dev event**: your request (the why), changed files + `git diff --stat` (the what), an inferred type, the time | **none** |
| Session starts | `tyamka maintain --hook` runs its free jobs (backfill dev-log vectors, keep the file index current, purge expired hook dedup rows, check registered peers) then its paid jobs while the budget allows (digests, consolidation — see [Maintenance budget](#maintenance-budget)) | **bounded by `[maintain]`** (default ≤8 calls / $0.50 per rolling 24h) |
| You type a prompt | `UserPromptSubmit` hook injects the most relevant wiki + dev-log excerpts (multilingual, recency-weighted, deduped within the session) into the agent's context, so it skips re-exploring what's already known | **none** |
| The agent asks the wiki | MCP `search_knowledge` returns cited excerpts for the agent to synthesize in its own (already-paid-for) context | **none** |
| You run `research` / `compile` / `--digests` | the only paths that spend tokens — always explicit | you choose |

That last column is the whole design: on a Claude subscription every LLM call carries ~22K
tokens of Claude Code harness overhead, so the cheapest call is the one never made.

It captures **uncommitted** work, so you never have to commit for the wiki to remember.

- **Automatic:** fires when a task changed files. No action needed.
- **Subagent work is captured too.** A `SubagentStop` hook records what each subagent changed, keyed
  by its own session so parent and child never double-capture. Without it, work delegated to
  subagents is invisible to the dev log — which, in a subagent-driven workflow, is most of the work.
  Turn it off with `[capture] subagents = false`.
- **Decisions that changed no file are captured before they're lost.** A `PreCompact` hook fires while
  the pre-compaction transcript is still intact and sweeps up the turns nothing else records — the
  design discussion, the investigation, the rejected alternative. (`Stop` only records turns that
  edited files, so those conversations previously vanished at compaction.) `[capture] precompact`,
  capped by `precompact_max_chars`.
- **Research notes:** for investigations that changed no files, run `/tyamka:wiki-note "what you
  found and why it matters"`.
- **Where it lands:** the **main repository's** `.tyamka/` when you're in a git repo — resolved via
  `git rev-parse --git-common-dir`, so a subagent running in its own worktree still writes to the one
  project wiki instead of forking memory per worktree — else the project-local `.tyamka/`, else your
  default wiki. Run `tyamka init` there first. Every other command resolves the same way, so `wiki
  compile` in that project compiles these events rather than a different wiki's.
- **Re-reading a transcript is safe.** Each event is stamped with its *turn's* timestamp, not the time
  the hook ran, so an event's content hash is stable — if a watermark is ever lost (a rotated or
  replaced transcript) the replay dedups against what is already stored instead of duplicating it.
- **Each event records where it happened:** the branch, the short HEAD SHA, and whether it was a
  worktree. Capture still records *uncommitted* work, so these say where a decision was made — they
  don't tie the event to a commit.
- **Summarization is zero-LLM by default.** `[capture] summarize` is `"off"` | `"sync"` | `"deferred"`
  (default **`"deferred"`**): short requests (`<= summarize_min_chars`, default 200) become their own
  summary verbatim, no LLM call, ever; longer ones are stored with no summary and marked
  digest-pending. `"sync"` is the old behavior — one cheap-tier call per event, at capture time.
  `"off"` never summarizes.
- **Clearing the backlog:** `tyamka capture --flush` backfills any dev-log chunks missing vectors (free,
  no LLM), and with `[capture] auto_digest_batches` (default **1**, `0` disables) also drains up to
  that many digest batches — one cheap-tier call per batch of up to 25 events. Add `--digests` to force
  a full manual drain (unbounded batches). At `SessionStart` this backfill now runs as the free
  `vectors` job inside `tyamka maintain --hook` (see [Maintenance budget](#maintenance-budget)) rather
  than as its own hook line — same work, now probed first and accounted.
- **Consolidation:** `tyamka consolidate` rolls dev events older than `[consolidate] min_age_days`
  (default 14) into a versioned **development-log** article, grouped by `period` (`week` | `month`) —
  one cheap-tier call per period. Consolidated events drop out of recall (the rollup represents them)
  but stay searchable via `--scope devlog`. Each consolidated event is also **routed** into its most
  relevant compiled topic — a local embedding match above `[consolidate] route_min_similarity`, **zero
  LLM** — and attached there, so that subject article cites the internal dev event on the next
  `tyamka compile`. The dev log stops being write-only history and compounds into the knowledge base.
  Still gated by `[consolidate] auto` (default off); at
  `SessionStart` that check now runs as the `consolidate` job inside `tyamka maintain --hook` instead of
  its own `--if-auto` hook line — a run with nothing eligible is a free no-op either way.
- **Read it back:** `tyamka query "why did we change the retriever?"` — dev events are searched by
  default (`--scope all`, the default) at any `--depth`; scope, not depth, controls whether the dev
  log is included. Use `--scope devlog` to search only dev events, `--scope articles` for only
  compiled articles. `--depth deep` now only affects *ranking* (adds a cross-encoder rerank) — it no
  longer changes what's visible.
- **Proactive recall:** a `UserPromptSubmit` hook (`tyamka recall --hook`, zero LLM, 15s timeout) injects
  the most relevant wiki/dev-log excerpts into the session before the agent even starts, so it can
  skip re-exploring what the wiki already knows. It embeds your prompt once and gates candidates
  against their stored vectors (no re-embedding), so the retrieval work is ~20 ms; a fresh project
  with no chunks exits before loading the model at all. Configure it under `[recall]` in `config.toml`:
  `enabled` (default `true`), `max_excerpts` (default 3), `max_chars` (default 600),
  `min_similarity` (default **0.42** — measured on the default multilingual embedder against a live
  wiki: 18 relevant / 12 adjacent-technical / 12 off-topic uk+en prompts, where 0.42 keeps 13/18
  relevant and admits 2/24 noise; adjacent-technical prompts, not off-topic ones, are what set it),
  `dedup` (default `true` — never re-inject a
  chunk already shown this session), `devlog_half_life_days` (default 14 — fresher dev events outrank
  staler ones at equal relevance; `0` disables), and `routing_hint` (default `false` — append a
  zero-LLM task-type hint for an orchestrator's model-routing policy; a hook cannot switch the active
  session's model, so it is a hint only), and `annotate` (default `true` — prefix each excerpt with its
  epistemic status, e.g. `(article · confidence 0.61 · researched 42d ago · HIGH volatility)` or
  `(dev event · 3d ago · bugfix)`, so the agent knows how far to trust it; missing fields are omitted,
  never guessed).
- **Privacy / control:** the raw request is stored (best-effort secret redaction). Turn capture
  off with `[capture] auto = false`, or raw-only with `summarize = "off"`, in `config.toml`.

### Why is this code the way it is?

`git blame` tells you *who* changed a line and *when*. tyamka answers *why* — because the dev log
already records the reasoning behind every change, indexed by the files it touched.

```bash
tyamka why tyamka/ops/recall.py      # decision history for a file, newest first
tyamka why src/api/routes.py --limit 10
```

Matching accepts an absolute path or any `/`-anchored suffix (`recall.py`, `ops/recall.py`, the full
path). A `path:line` argument is accepted and the line part is honestly ignored — attribution is
file-level until hunk capture lands. Agents get the same data through the MCP tool `why_file`, sealed
as untrusted data to synthesize from. Both paths are **zero-LLM** — pure SQL, no model is ever loaded.

**The guardrail.** A `PreToolUse` hook (`tyamka why --hook`, default **off** — `[why] guardrail`) runs before the agent edits
a file and, when that file carries decision history, hands the agent the past reasoning — so it doesn't
silently undo a decision it can't remember. It **only informs, never blocks**: it always returns an
`allow` decision and delivers the note as `additionalContext` (plain hook stdout would reach only Claude
Code's debug log, never the model — verified against Claude Code 2.1.207). Tuning lives under `[why]`:
`guardrail` (default `true`), `guardrail_exclude_types` (default `["chore", "docs"]` — everything else
warns, so routine edits stay quiet while real decisions surface), and `guardrail_max_events` (default 2).
The older `guardrail_types` whitelist is still read for one release; when both are set the exclude-list
wins. Each file warns at most once per session, and the whole lookup is pure SQL.

### Changelog and impact: the dev log as two more views

`tyamka changelog` and `tyamka impact` read the same file-indexed dev log `tyamka why` does — a changelog
for a range of commits, and the blast radius of a source, file, or topic.

```bash
tyamka changelog                        # dev-log changelog for upstream/main..HEAD, newest first
tyamka changelog v1.2.0..v1.3.0         # any git range: A..B, A...B, or a single ref
tyamka changelog --prose                # one cheap LLM call rewrites it as release notes / a PR body

tyamka impact tyamka/ops/why.py      # what rests on a file
tyamka impact development-log           # ...or a topic slug
tyamka impact https://example.com/post  # ...or a source (URL, content hash, or id)
```

`tyamka changelog <range>` selects the dev events behind the range's commits — matched by changed file,
and by a file-less time window for the design discussions the `PreCompact` hook otherwise leaves
undiscoverable — then renders them grouped by type, newest first. **The coverage footer is not
decoration.** It states exactly how many of the range's changed files have a recorded decision, and how
many entries were matched by file versus by time window — read it as an honest measure of how much of
the range the dev log actually explains, not as a claim that the changelog is complete. Zero LLM by
default; `--prose` spends one cheap-tier call turning the same rendered data into prose.

`tyamka impact <target>` reports what rests on a source (live citing claims and research findings), a
file (the decisions that touched it, plus files it has historically changed alongside), or a topic (the
sources its current article cites, and which other topics share them). **"Changed together with" is
historical correlation, not a causal rule** — two files edited in the same turns are a coupling *hint*
worth checking, not a rule that one must change whenever the other does. Both commands are zero-LLM
(pure SQL over the same dev log and citation graph `tyamka why` and `tyamka audit` already use) and
MCP-exposed as `build_changelog` / `impact_report`, sealed as untrusted data for the calling agent to
synthesize.

---

## Federated memory

`tyamka peers` extends the read surfaces above — `why`, `changelog`, `impact`, `recall`, and `query
--extract` / MCP `search_knowledge` — across every other wiki you register, read-only. A decision
captured in one project's dev log becomes visible from another's.

```bash
tyamka peers add ~/other-project/.tyamka   # register another wiki as a read-only peer
tyamka peers list                             # reachability, model, compatibility, and the fix for anything not ok
tyamka peers rm <alias>                       # the per-peer off switch
```

- **The registry is machine-global, not per-project.** It lives at
  `$XDG_CONFIG_HOME/tyamka/peers.toml` (falling back to `~/.config/tyamka/peers.toml`) — outside
  every wiki and every repo, because a peer entry is an absolute machine path and a project's
  `config.toml` can travel with its repository. Each wiki still opts in to *reading* the registry
  independently (`[federation] enabled`, default `true`); disabling it, or removing every peer, restores
  exactly today's single-wiki behavior.
- **Read-only is SQLite's guarantee, not this codebase's.** A peer's `wiki.db` is opened with
  `mode=ro` — every write, including a raw `conn.execute`, is refused at the driver level. Federation
  can read another wiki; it cannot write to it, migrate it, or upgrade its schema.
- **Three compatibility states**, read from the peer's *stamped* `wiki_meta.embedding_model` — never
  from its `config.toml`, which only says what the model would be on the *next* run, not what the
  stored vectors were actually built with:

  | verdict | meaning | `recall` / `query --extract` / `search_knowledge` | `why` / `changelog` / `impact` |
  |---|---|---|---|
  | `ok` | peer's stamped model matches yours | contributes | contributes |
  | `mismatch` | a different model is stamped | skipped entirely | contributes |
  | `unknown` | no stamp (no `wiki_meta` table, or a row missing the `embedding_model` key) | skipped entirely | contributes |

  **Vector federation needs a matching stamped model.** `unknown` is deliberately not treated as
  "probably fine" — feeding unverified vectors into a measured similarity gate isn't a risk
  worth taking on a guess. `tyamka reindex --embeddings --home <peer>` is both the diagnosis and the fix,
  run by that peer's own owner (repairing another wiki's index from here would itself be a cross-wiki
  write).

Two limitations, stated plainly rather than buried:

- **An unstamped or mismatched peer contributes nothing to vector paths** (`recall`,
  `query --extract`, `search_knowledge`) until its owner reindexes it.
- **A peer without a `dev_event_files` table contributes nothing to `why`, `changelog`, or `impact`.**
  Those commands index by file path; a peer that has never captured a dev event, or predates that
  table, is skipped — not errored.

Every federated result carries its origin: `· from <alias>` in recall excerpts, `[alias]` in `why`
output, a per-origin line in `changelog`'s coverage footer — a cross-project answer is never presented
as if it were local history. Recall's excerpt cap is applied *after* the merge across wikis, so
federation changes *which* excerpts arrive, never how many. An unreachable, locked, or slow peer is
dropped within `[federation] peer_timeout_ms` (default 500 ms) and never blocks the caller.

---

## Maintenance budget

`tyamka maintain` is the one accounted entry point for automatic upkeep — it replaced two separate
`SessionStart` hook lines (`capture --flush`, `consolidate --if-auto`) with a single job queue, each
job gated by its own cheap probe so nothing pays for work that doesn't exist:

| # | job | cost | does |
|---|---|---|---|
| 1 | `vectors` | free | backfill dev-log chunk vectors missing an embedding |
| 2 | `paths` | free | build/backfill the file→event index (`dev_event_files`) |
| 3 | `peers` | free | check each registered peer's reachability and compatibility — reports the fix, repairs nothing (a cross-wiki write) |
| 4 | `digests` | paid | batch-summarize pending dev events, one cheap call per 25 |
| 5 | `consolidate` | paid | roll old events into the versioned development-log article |

```bash
tyamka maintain --dry-run   # the plan: is there work, is it free or paid, would the quota allow it
tyamka maintain             # run it
tyamka maintain --force     # ignore the quota for this run (still recorded; still counts against later runs)
tyamka maintain --hook      # SessionStart mode: silent, always exits 0 — what the hook actually runs
```

- **The ledger is derived, not a new table.** `llm_calls` already records `purpose`, both token
  counts, `cost_usd` and a timestamp; maintenance spend in the current rolling window (`[maintain]
  window_hours`, default 24) is `SUM(cost_usd) WHERE purpose LIKE 'maintain:%' AND ts >= window_start`.
  One source of truth — nothing to keep in sync, nothing to drift.
- **The `maintain:` purpose prefix is what makes that query complete.** `GovernedProvider` wraps the
  real LLM provider for the duration of a run and rewrites every call's `purpose` to
  `maintain:{purpose}` before forwarding it, so a job added later is counted automatically with no
  per-job plumbing to remember. (Without it, `digests` would record plain `purpose="capture"` —
  indistinguishable from interactive, non-budgeted capture.)
- **Overshoot is bounded by exactly one call.** A call's cost is only known after it returns, so
  enforcement is pre-call: the ledger is re-read before every call, and a call is refused once
  `max_calls_24h` (default 8) or `max_usd_24h` (default $0.50) is already reached. Worst case, the one
  call already in flight completes past the cap — fractions of a cent for a cheap-tier call — stated
  as a bound, not engineered away.
- **The governor spends nothing the user hadn't already opted into.** `digests` still honors
  `[capture] auto_digest_batches`; `consolidate` still honors `[consolidate] auto` (default off). What
  changes is that the spend is now accounted, capped across sessions, inspectable with `--dry-run`, and
  reached through one hook line instead of two.

Configure under `[maintain]`: `enabled`, `window_hours`, `max_calls_24h`, `max_usd_24h`, and `jobs`
(the ordered list to run — an unrecognized name is ignored, so a config written for a later version
still runs here).

The free `hygiene` job purges the two per-session dedup tables once their rows are past retention
(`why_log` after 7 days, `capture_watermark` after 30). It is a maintenance job specifically so the
`PreToolUse` guardrail doesn't do it: that hook runs before *every* `Edit`/`Write`, and week-old
retention has no business costing write transactions on that path.

---

## Command reference

| Command | What it does |
|---|---|
| `tyamka init <name>` | Create a wiki (config, DB, `topics/`). |
| `tyamka ingest <url\|path>` | Ingest a URL, PDF, or text file into the indexed knowledge base. `--topic <slug>` also attaches it to a topic (add `--new-topic` to create the topic) so it compiles into that article — no web search, no LLM spend. |
| `tyamka attach <source> <topic>` | Bind an already-ingested source (numeric id, `#id`, content hash, or URL) to a topic so it compiles into the article. `--new-topic` creates the topic. Zero LLM. |
| `tyamka research "<topic>"` | Research a topic with persona agents. `--mode standard\|deep\|max`, `--new-topic`, `--budget <usd>`, `--resume <session-id>`. |
| `tyamka thesis "<claim>"` | Evaluate a claim with FOR/AGAINST agents → a cited verdict. `--mode`, `--budget`. |
| `tyamka compile` | Compile active topics into cited articles. `--full` recompiles everything. |
| `tyamka query "<question>"` | Answer a question over compiled knowledge, citing sources. `--depth quick\|standard\|deep` (ranking effort only), `--scope all\|articles\|devlog` (default `all` — what's searched), `--extract` (zero-LLM: print excerpts instead of a synthesized answer). |
| `tyamka related <topic>` | List knowledge-graph neighbours of a topic. |
| `tyamka generate <kind> <topic>` | Generate a derived document (`report`, `slides-outline`, `summary`, `study-guide`, `timeline`, `glossary`, `comparison`). `--out <file>`. |
| `tyamka export <target>` | Export to `obsidian` (vault + frontmatter), `site` (static HTML + graph page), or `json` (structured dump). `--out <dir>`. |
| `tyamka lint` | Audit for broken wikilinks, orphans, missing citations, staleness. `--fix` applies safe repairs. |
| `tyamka audit <topic>` | Re-verify a topic's citation quotes still match their immutable raw sources. |
| `tyamka refresh` | List topics whose freshness window has lapsed; `--run` re-researches them. |
| `tyamka collect <collection> <url\|path>` | Catalogue a source into a named collection (recorded, not search-indexed). |
| `tyamka dataset add <name> <path>` | Track an on-disk dataset (name, path, size). |
| `tyamka archive <topic>` | Archive a topic (excluded from default query/retrieval). |
| `tyamka feedback <target> <approve\|reject\|correct> [note]` | Record a verdict against an article (`article:<id>`) or finding (`finding:<id>`). |
| `tyamka capture` | Record a dev event. `--hook` (reads Claude Code `Stop` JSON on stdin), `--note "<text>"` + `--type`, or `--flush` to backfill dev-log vectors (free) with optional `--digests` to batch-summarize. |
| `tyamka recall --hook` | Read a Claude Code `UserPromptSubmit` payload on stdin and print relevant wiki excerpts for the agent's context. Zero LLM. Always exits 0. |
| `tyamka why <path>` | Show WHY a file is the way it is — the dev events that touched it, newest first. `--limit N`; `--hook` reads a `PreToolUse` payload and emits the guardrail warning. Zero LLM. |
| `tyamka changelog [range]` | Why-annotated changelog for a git range (default: upstream/main..HEAD; also accepts `A..B`, `A...B`, or a single ref). `--limit N`, `--exclude-types a,b`. Zero LLM; `--prose` rewrites it as release notes / a PR body (one cheap LLM call). |
| `tyamka impact <target>` | Blast radius of a source (URL/hash/id), a file path, or a topic slug — what claims, decisions, or co-changed files rest on it. `--limit N`, `--as source\|file\|topic` to force the reading. Zero LLM. |
| `tyamka peers add\|rm\|list` | Manage federated read-only peer wikis: register (`add <path> [--alias NAME]`), remove (`rm <alias>`), or list all with reachability, model, and compatibility (`list`). |
| `tyamka consolidate` | Roll dev events older than `[consolidate] min_age_days` into a versioned `development-log` article, **and route** each event into its most similar compiled topic (zero-LLM embedding match) so it becomes a citation there. `--if-auto` runs only when `[consolidate] auto = true`. |
| `tyamka maintain` | Run automatic maintenance (vectors, file index, peer check, digests, consolidate) within its budget. `--dry-run` shows the plan and spends nothing, `--force` ignores the quota once, `--hook` is the silent `SessionStart` mode. |
| `tyamka reindex --embeddings` | Rebuild every chunk vector with the active embedding model (local, zero LLM) — required after changing `local_model`. |
| `tyamka stats` | Project identity (`project_id`, `remote_key`), wiki size + LLM spend. `--since <YYYY-MM-DD>` adds a spend window. |
| `tyamka context` | Print a recent-activity digest for pasting into an agent's context. |
| `tyamka sync push --dry-run --out <f.ndjson>` | Serialize unsent rows as NDJSON (natural keys only, zero LLM, no network). `--since-zero` exports everything — a portable backup. Never advances `sync_state`. Without `--dry-run` it exits 2: there is no server yet. |
| `tyamka sync schema` | Print the JSON Schema one payload line must satisfy. `--out <file>` writes it. |
| `tyamka serve-mcp` | Serve the wiki over MCP (stdio transport). |
| `tyamka migrate` | Rename a pre-0.6 `.wikiforge/` wiki (and the peer registry) to `.tyamka/`. Free, offline, idempotent; refuses if both exist. |
| `tyamka version` | Print the installed tyamka version. |

---

## Configuration (`<home>/config.toml`)

`tyamka init` writes a documented default. Highlights:

```toml
wiki_name = "My Wiki"

[models]                      # model routing by tier
cheap = "claude-haiku-4-5"
flagship = "claude-sonnet-5"
reasoning = "claude-opus-4-8" # optional 3rd tier; opt in per task below

[models.tasks]                # which tier each task uses (cheap | flagship | reasoning)
research = "flagship"
normalize = "cheap"
query = "flagship"
# thesis = "reasoning"        # e.g. route the hardest judgement calls to opus
# …

[models.effort]               # subscription backend only: claude -p --effort per task
thesis = "medium"             # every unlisted task defaults to "low"
synthesize = "medium"         # compile stays low — high effort exceeds the timeout

[pricing."claude-sonnet-5"]   # USD per 1M tokens — drives cost tracking
input = 3.0
output = 15.0

[embedding]
provider = "auto"             # auto | local | voyage
local_model = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"  # 384-dim, uk+en, ONNX
voyage_model = "voyage-3.5"               # 1024-dim, needs VOYAGE_API_KEY
dim = 1024                    # vector dim when using voyage
local_dim = 384               # vector dim when using local

[recall]                      # UserPromptSubmit memory injection (see "Agent memory")
min_similarity = 0.42         # measured gate; dedup, devlog_half_life_days, routing_hint also here
annotate = true               # prefix excerpts with confidence / staleness / event type

[why]                         # decision memory (see "Why is this code the way it is?")
guardrail = true              # PreToolUse: warn before editing a file with decision history
guardrail_exclude_types = ["chore", "docs"]   # types to STAY QUIET about; everything else warns
guardrail_max_events = 2      # max past decisions quoted per warning
# guardrail_types = [...]     # deprecated whitelist, still read for one release

[capture]
auto_digest_batches = 1       # SessionStart flush: max cheap digest batches (0 = off)
subagents = true              # SubagentStop: record what each subagent changed
precompact = true             # PreCompact: sweep decisions that touched no file
precompact_max_chars = 20000

[consolidate]
period = "week"               # week | month
min_age_days = 14             # only consolidate events older than this
auto = false                  # also run at SessionStart when true
route = true                  # attach each consolidated event to its matching topic (zero LLM)
route_min_similarity = 0.46   # cosine gate for routing — re-measure per embedding model
route_max_topics = 1          # attach each event to at most N topics

[federation]                   # see "Federated memory"
enabled = true                 # read peers registered with `tyamka peers add` (none by default)
peer_timeout_ms = 500          # per-peer wall clock; a slow peer is dropped, never awaited

[maintain]                     # see "Maintenance budget"
enabled = true
window_hours = 24              # rolling budget window
max_calls_24h = 8              # max LLM calls automatic maintenance may make per window
max_usd_24h = 0.50             # and the USD ceiling; whichever binds first stops the run
jobs = ["vectors", "paths", "hygiene", "peers", "digests", "consolidate"]

[llm]
backend = "api"               # api | subscription
subprocess_timeout_s = 300    # per `claude -p` call; raise it if you route tasks to high effort

[retrieval]
top_k = 12
rrf_k = 60                    # Reciprocal Rank Fusion constant
rerank_model = "cross-encoder/ms-marco-MiniLM-L-6-v2"   # deep-depth rerank

[research]                    # personas per research mode
standard_personas = ["academic", "technical", "applied", "news", "contrarian"]
deep_extra = ["historical", "adjacent_fields", "data_stats"]
max_extra = ["methodological", "speculative"]

[volatility]                  # freshness windows (days) by topic volatility
LOW = 365
MEDIUM = 90
HIGH = 14

[confidence]                  # weights for the in-code confidence formula
w_count = 0.35
w_diversity = 0.25
w_recency = 0.25
w_evidence = 0.15
conflict_penalty_per = 0.1
conflict_penalty_cap = 0.4
```

**Embeddings.** With `provider = "auto"`, tyamka uses Voyage when `VOYAGE_API_KEY` is set and the local model otherwise. The default local model is multilingual (`sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2`), so recall works for non-English prompts. It is served through fastembed (ONNX); a model outside fastembed's registry still works but falls back to torch, which is roughly 8x slower per call and says so on stderr. The chosen provider's dimension **sizes the vector table at `tyamka init`** — switching providers (and therefore dimension) on an existing wiki requires re-initializing it. Pick your embedding provider before you build up a wiki.

**Changing the local model / reindexing.** A wiki records which embedding model built its chunk vectors. Change `local_model` and the next indexed call refuses to run rather than fuse incompatible vectors — rebuild them with `tyamka reindex --embeddings` (local, zero LLM cost, one-time). Same-dimension swaps (e.g. another 384-dim model) keep the vector table; a different dimension needs a re-init.

**Cost & budgets.** Every call is priced from `[pricing]` and recorded. `tyamka research`/`tyamka thesis` take `--budget <usd>`: once cumulative session spend reaches the cap, no new persona wave starts and the session is marked `PARTIAL` (resume it later with `--resume`). Unknown models price at `0.0` — add them to `[pricing]` to track their cost.

---

## Choosing an LLM backend

tyamka can run its LLM calls two ways, selected in `config.toml`:

    [llm]
    backend = "api"          # or "subscription"

- **`api`** (default) — the Anthropic developer API. Needs an API key / credit balance
  from [console.anthropic.com](https://console.anthropic.com) (billed separately from a
  Claude subscription). Efficient, with a hard structured-output guarantee and native web
  search. Recommended for heavy research or when extraction robustness matters.
- **`subscription`** — routes calls through the Claude Code CLI (`claude -p`), using your
  Claude subscription (no API credits). Requires the `claude` binary installed and logged
  in (`ant`/Claude Code). **Caveats:** every call loads the Claude Code harness
  (~22K tokens of overhead), so a `tyamka research` fan-out consumes subscription usage
  limits quickly — best for light/occasional use. Structured extraction is
  prompt-and-validate (slightly less robust than the API path), each call is slower, and
  the cost shown by `tyamka stats` is a notional API-equivalent estimate, not a real charge.

**Per-task effort (subscription only).** `[models.effort]` maps a task to `claude -p --effort`
(`low` | `medium` | `high`); every unlisted task defaults to `low`. `compile` must stay `low` —
high effort makes its structured-output call exceed `[llm] subprocess_timeout_s`. The `api` backend
ignores effort. Combine with a `reasoning` tier (`[models.tasks] thesis = "reasoning"`) to send the
hardest judgement calls to a stronger model; raise `subprocess_timeout_s` if you do.

---

## Keeping a wiki fresh

Topics carry a volatility and a staleness window. `tyamka refresh` lists lapsed topics; `--run` re-researches them. A daily cron entry:

```cron
# Re-research any stale topics every day at 03:00
0 3 * * *  ANTHROPIC_API_KEY=sk-ant-... TYAMKA_HOME=$HOME/wiki /path/to/uv run tyamka refresh --run
```

---

## MCP server

`tyamka serve-mcp` exposes the wiki over the Model Context Protocol (stdio transport) for use by MCP-capable agents. Registered tools: `search_knowledge`, `get_article`, `list_topics`, `ingest_source`, `start_research`, `evaluate_thesis`, `find_related`, `get_activity_context`, `get_stats`, `generate_output` — each calling the same service functions the CLI uses.

`search_knowledge(question, depth, mode, scope)` defaults to **`mode="extract"`** — zero LLM calls, returns cited excerpts for the calling agent to synthesize in its own context. Pass `mode="synthesize"` to have the wiki's own LLM write the prose answer instead (the `tyamka query` behavior). `scope` (`all` | `articles` | `devlog`) controls what's searched; `depth` (`quick` | `standard` | `deep`) controls ranking effort only.

`ingest_source(target, topic, new_topic)` ingests a source and, when `topic` is given, attaches it to that topic (the same internal-source bridge as the CLI's `tyamka ingest --topic`) so it compiles into that article — no web search.

Example client entry (Claude Desktop / any MCP client):

```json
{
  "mcpServers": {
    "tyamka": {
      "command": "uv",
      "args": ["run", "tyamka", "serve-mcp", "--home", "/absolute/path/to/wiki"],
      "env": { "ANTHROPIC_API_KEY": "sk-ant-..." }
    }
  }
}
```

---

## Development

```bash
uv run pytest        # full suite — runs with NO live API keys (providers are faked)
uv run ruff check .  # lint
uv run ruff format . # format
uv run mypy tyamka  # strict type-check
```

The test suite makes **no network calls**: LLM and embedding providers sit behind `Protocol`s and are injected as fakes, HTTP is stubbed, and each test gets a fresh temporary SQLite database.

---

## Documented assumptions & limitations

These are deliberate scoping decisions, not oversights:

- **Secrets are environment-only.** API keys are never written to or read from `config.toml`.
- **Capture's secret redaction is prefix-anchored and best-effort.** It masks shapes carrying an
  unambiguous vendor marker (`sk-`, `AKIA`, `ghp_`/`github_pat_`, `xox*-`, `AIza`, JWTs, `Bearer`) and
  deliberately leaves git object ids and sha256 content hashes alone — capture writes those on
  purpose. It is a last line of defence, not a licence to paste credentials into a session.
- **An unknown key in `config.toml` is reported, not fatal.** The installed `tyamka` CLI drifts from
  whatever sources are on disk until you reinstall it, so opening a wiki with an older build than
  the one that wrote its config is ordinary; a key this build doesn't know is named on stderr and
  ignored rather than taking the whole wiki down. A typo still announces itself.
- **Federation is bounded twice.** Each peer gets `peer_timeout_ms` (default 500 ms, covering the
  database open as well as the read), and the whole fan-out gets 3× that — hooks have a hard
  wall-clock budget, and a per-peer bound alone lets N registered peers multiply into N × timeout.
  Peers still waiting when the total budget is gone contribute nothing, exactly like a slow one.
- **Raw sources are immutable.** Re-ingesting updates provenance, never the stored text; the auditor checks citation quotes against that immutable text.
- **Injection defense is uniform.** Every place untrusted or model-generated text is wrapped in `<source_data>` seals the delimiter first (shared `seal_source_data`), so a crafted source can't break out of the data envelope.
- **`tyamka research` shows a live agent table; `tyamka thesis` does not** (thesis runs to completion and prints its verdict).
- **The static-site export renders article Markdown as escaped, pre-wrapped text** — there is no Markdown→HTML dependency, so bodies are shown verbatim (and safely escaped) rather than rendered.
- **Dev events compound two ways.** They roll up into the time-bucketed `development-log` article (`tyamka consolidate`), and each consolidated event is also **routed** — a conservative local embedding match (`[consolidate] route`, zero LLM) — into its most relevant compiled topic, whose article then cites it. Below the similarity gate an event stays dev-log-only; you can always bind one by hand with `tyamka attach`. The gate (`route_min_similarity`, default 0.46) is a *conservative* starting value, not a measured one — raise it if routing pulls weakly-related events into articles, lower it if genuine matches are missed, and re-measure after changing the embedding model (like `[recall] min_similarity`).
- **The recall similarity gate is tuned for the default `paraphrase-multilingual-MiniLM-L12-v2` embedder, on one corpus.** The 0.42 threshold was measured on a live wiki (47 chunks) against 18 relevant, 12 adjacent-technical and 12 off-topic uk+en prompts: it keeps 13/18 relevant and admits 2/24 noise. The bands overlap — no threshold separates them cleanly — so 0.42 is a chosen trade-off on that curve, not a clean boundary, and it was measured against one wiki's content. If you switch embedding models, re-measure it and run `tyamka reindex --embeddings`: the gate is a per-model number and a threshold from the wrong model either mutes recall entirely or injects noise into every prompt.
- **Dev-event attribution is file-level, and events carry no commit anchor.** Capture records the files a change touched (that is what `tyamka why` indexes), not hunk line ranges, and deliberately captures *uncommitted* work — so an event is not tied to a branch or a SHA. `tyamka why <path>:52` therefore accepts the line and ignores it.
- **Subagents do not receive wiki memory.** The `SubagentStart` hook's `additionalContext` output does reach a subagent's own context (verified against Claude Code's hooks reference), but the hook's stdin payload carries no `prompt`/task field to retrieve against — its documented fields are `session_id`, `transcript_path`, `hook_event_name`, `permission_mode`, `agent_id`, and `agent_type`, and the event's schema isn't otherwise documented (tracking issue: [anthropics/claude-code#19170](https://github.com/anthropics/claude-code/issues/19170)) — so there is nothing to key retrieval on. `SubagentStop` capture (recording what a subagent changed) is unaffected and does work.
- **An unstamped or mismatched peer contributes nothing to vector paths.** `recall`, `query --extract`, and `search_knowledge` only admit a peer whose stamped `wiki_meta.embedding_model` matches the local one — `tyamka reindex --embeddings` on that peer, run by its owner, is what changes that. `why`, `changelog`, and `impact` are unaffected: they never touch a vector.
- **`tyamka sync` exports; it does not synchronize.** There is no server and no network code:
  `push --dry-run` writes a file and `push` without it exits 2. `sync_state` exists and is read, but
  nothing advances it, because only a server's acknowledgement legitimately could.
- **`remote_key` does not resolve `~/.ssh/config` aliases.** `git@github-personal:you/x.git`
  normalizes to `github-personal/you/x`, not `github.com/you/x` — resolving it would mean reading
  the user's ssh config, and the alias is genuinely how *that machine* reaches the server. The cost
  is that one repository can appear as two projects, which is recoverable; the opposite mistake
  (folding two different repositories into one key) is not.
- **The Viewer never migrates a schema.** It opens every database read-only, so a wiki whose owner
  has not run `tyamka maintain` since upgrading still lacks `UNIQUE(topic_id, version)` — the
  Viewer keeps its `MAX(version)` → `MAX(id)` workaround for exactly that case.
- **A peer without `dev_event_files` contributes nothing to `why`, `changelog`, or `impact`.** Those commands index by file path; a peer that predates that table, or has simply never captured a dev event, is skipped rather than erred on. `tyamka peers list` names the fix — run in that project, by its owner, since repairing a peer from here would itself be a cross-wiki write.

### Deferred toggles (not built)

- MCP `streamable-http` transport (stdio only for now).
- A second `LLMProvider` implementation beyond Anthropic.
- Multiple wikis per process.

---

## Viewer UI (`viewer/`)

A local, strictly read-only Spring Boot 4 + React web UI over every tyamka database on the
machine — the global wiki (`$TYAMKA_HOME`, default `~/wiki`) plus any project-local
`.tyamka/wiki.db` found under the configured scan roots (default `~/dev`, depth 3).

**Auto-start.** Inside a checkout of this repo, a `SessionStart` hook brings the viewer up for you —
it runs `${CLAUDE_PROJECT_DIR}/hooks/viewer-autostart.sh`, so in any other project it no-ops.
Idempotent and non-blocking, it never delays session start:

- If something is already on the port, it does nothing (one shared instance serves every wiki).
- If the jar is built and `java` is on your PATH, it launches it in the background.
- If the jar isn't built yet, and both `java` and `npm` are on your PATH, it runs a **one-time**
  `./gradlew bootJar` in the background; when the build finishes a few minutes later, that same
  background process launches the viewer — no new session needed. Missing toolchain → it silently
  no-ops (build the jar manually, below).

Controls: `TYAMKA_VIEWER_AUTOSTART=0` disables it; `TYAMKA_VIEWER_PORT` overrides `8080`;
launch/build output goes to `$TMPDIR/tyamka-viewer.log` (`/tmp/tyamka-viewer.log` when
`TMPDIR` is unset). Auto-start is macOS/Linux only — the hook is a bash script, so on Windows it
simply no-ops and you use the manual launch below. Note that `SessionStart` hooks run in a
**non-interactive** shell — if your `java`/`npm` come from a version manager (sdkman, nvm) that only
patches your interactive shell, the hook won't see them and will no-op; add them to your
non-interactive PATH, or just launch manually:

```bash
cd viewer && ./gradlew bootJar && java -jar build/libs/tyamka-viewer.jar
# open http://127.0.0.1:8080
```

Dev mode: `./gradlew bootRun` + `cd frontend && npm run dev` (Vite proxies `/api` to :8080).

Views per wiki: dashboard (counts, confidence distribution, staleness), topics & articles with
citations/conflicts/related, raw sources with provenance and cited-by, research sessions with
persona findings and thesis verdicts, LLM spend charts, the dev-cycle log, the topic graph, and
FTS5 search. The viewer opens SQLite strictly read-only (WAL readers don't block writers) and
never migrates the schema — `tyamka/storage/schema.sql` stays Python-owned. The copy at
`viewer/src/test/resources/schema-test.sql` must be re-trimmed when the Python schema changes.
