diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f50ca4cc..22dbaa64 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -24,7 +24,7 @@ jobs:
- name: install
run: |
python -m pip install --upgrade pip
- pip install -e '.[dev]'
+ pip install -e '.[dev,web]'
- name: lint
run: python -m ruff check src tests
diff --git a/.github/workflows/eval.yml b/.github/workflows/eval.yml
new file mode 100644
index 00000000..150b6f22
--- /dev/null
+++ b/.github/workflows/eval.yml
@@ -0,0 +1,39 @@
+name: eval
+
+# Gate retrieval quality: score kb.context against the committed labeled set
+# and fail on a P@5 regression beyond tolerance vs eval/baseline.json. Runs
+# only when retrieval code changes.
+on:
+ pull_request:
+ paths:
+ - "src/vouch/embeddings/**"
+ - "src/vouch/context.py"
+ - "src/vouch/eval/**"
+ - "eval/**"
+
+jobs:
+ recall:
+ name: recall eval
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+ cache: pip
+
+ - name: install
+ run: |
+ python -m pip install --upgrade pip
+ pip install -e '.[dev]'
+
+ - name: build fixture index
+ working-directory: eval/fixture-kb
+ run: python -m vouch.cli reindex
+
+ - name: recall eval (fail on P@5 regression > 5%)
+ working-directory: eval/fixture-kb
+ run: >-
+ python -m vouch.cli eval recall ../queries.jsonl
+ --k 5 --baseline ../baseline.json --max-regression 0.05
diff --git a/.github/workflows/install-sh.yml b/.github/workflows/install-sh.yml
new file mode 100644
index 00000000..066ea25e
--- /dev/null
+++ b/.github/workflows/install-sh.yml
@@ -0,0 +1,86 @@
+name: install-sh
+
+# Validates install.sh on every push that touches it:
+# * shellcheck (lint)
+# * POSIX-syntax check via dash
+# * end-to-end smoke run on a fresh ubuntu-latest — installs the published
+# vouch-kb wheel via pipx and verifies `vouch --version`
+#
+# The smoke run intentionally exercises the published PyPI artifact, not the
+# in-repo source, so we catch installation breakage that doesn't show up in
+# the regular pytest suite (e.g. a stale [web] extra reference).
+#
+# Supply-chain notes: this workflow pins third-party actions to their full
+# commit SHA (the comment marks the human-readable tag). The rest of the
+# repo's workflows still use tag pins — a sweep of ci.yml / release.yml /
+# schema-check.yml to match this pattern is a worthwhile follow-up but
+# kept out of this PR to avoid churning unrelated CI.
+
+on:
+ push:
+ branches: [main, release/*]
+ paths:
+ - "install.sh"
+ - ".github/workflows/install-sh.yml"
+ pull_request:
+ paths:
+ - "install.sh"
+ - ".github/workflows/install-sh.yml"
+ workflow_dispatch:
+
+# Least-privilege at the workflow level; the smoke job needs nothing
+# beyond reading the checked-out repo. Jobs that need more bump it up
+# explicitly.
+permissions:
+ contents: read
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ # Don't leave the token sitting in .git/config after checkout —
+ # a leaked credentials file would otherwise allow pushing back.
+ persist-credentials: false
+
+ - name: shellcheck
+ run: |
+ sudo apt-get update -qq
+ sudo apt-get install -y shellcheck dash
+ shellcheck --version
+ shellcheck install.sh
+
+ - name: posix syntax (dash -n)
+ run: dash -n install.sh
+
+ smoke:
+ runs-on: ubuntu-latest
+ needs: lint
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
+
+ - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
+ with:
+ python-version: "3.12"
+
+ - name: --help works
+ run: sh ./install.sh --help
+
+ - name: end-to-end install + smoke
+ run: |
+ set -e
+ sh ./install.sh --no-claude
+ # pipx's bin dir isn't necessarily on PATH for the verification
+ # step — re-export from pipx itself. Query via the pipx CLI: the
+ # runner ships pipx as a standalone binary that's on PATH but NOT
+ # importable as `python -m pipx`, so that form fails with
+ # "No module named pipx". Fall back to the conventional
+ # ~/.local/bin (pipx's default PIPX_BIN_DIR) if the query fails.
+ PIPX_BIN=$(pipx environment --value PIPX_BIN_DIR 2>/dev/null || true)
+ [ -n "$PIPX_BIN" ] || PIPX_BIN="$HOME/.local/bin"
+ export PATH="$PIPX_BIN:$PATH"
+ vouch --version
+ vouch capabilities | head -20
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000..f5735c66
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,181 @@
+# Agents working with vouch
+
+Entry document for any AI coding agent reading this repository — Cursor, Codex,
+OpenClaw, Aider, Continue, JetBrains AI, an LLM fetching the raw URL, anything
+that isn't Claude Code. (Claude Code reads [`CLAUDE.md`](./CLAUDE.md) instead;
+the two files complement each other.)
+
+If your task is to **work on the vouch codebase itself**, read this file and
+then `CLAUDE.md`. If your task is to **use vouch from inside another
+project**, read [`README.md`](./README.md) for install + concepts and
+[`docs/getting-started.md`](./docs/getting-started.md) for the agent-side
+loop.
+
+If you're an **OpenClaw** plugin loader, the plugin manifest is at the repo
+root: [`openclaw.plugin.json`](./openclaw.plugin.json). It declares vouch's
+MCP wiring, the four slash commands, the trust boundary (write tools
+review-gated, lifecycle ops audit-logged, remote-caller filesystem
+confined), and the config schema (`kb_path`, `agent`, `transport` — no
+secrets). No additional wiring is required to surface vouch's `kb.*`
+surface inside an OpenClaw deployment.
+
+## What vouch is, in one paragraph
+
+Vouch is a git-native, review-gated knowledge base for LLM agents. Agents
+propose writes via an MCP server (or a JSONL pipe); a human approves each
+proposal with `vouch approve`. Approved artifacts land as YAML claims and
+markdown pages under `.vouch/` — plain files that diff cleanly in PRs and
+travel as a tarball bundle. The CLI is `vouch`; the PyPI distribution is
+`vouch-kb`; supported Python versions are 3.11, 3.12, 3.13.
+
+## Install (1 minute)
+
+```bash
+curl -fsSL https://raw.githubusercontent.com/vouchdev/vouch/main/install.sh | sh
+```
+
+Or, deterministically from a clone (the path you want when contributing):
+
+```bash
+git clone https://github.com/vouchdev/vouch.git
+cd vouch
+python3 -m venv .venv && . .venv/bin/activate
+pip install -e '.[dev]'
+```
+
+## Read in this order
+
+1. **This file** — entry, install, trust boundary, common tasks.
+2. [`CLAUDE.md`](./CLAUDE.md) — orientation for working on the repo:
+ architecture, conventions, ship rules, voice. Read even if you aren't
+ Claude Code; the conventions are universal.
+3. [`README.md`](./README.md) — the user-facing pitch, install, quick start,
+ full CLI surface, MCP / JSONL method list.
+4. [`SPEC.md`](./SPEC.md) — the canonical protocol description: `.vouch/`
+ layout, object model, `kb.*` method shapes, review-gate state machine.
+ Authoritative when in doubt.
+5. [`ROADMAP.md`](./ROADMAP.md) — what's planned for 0.2 and 0.3. Don't
+ propose features that are already scoped here.
+
+For everything else, see [`llms.txt`](./llms.txt) — the LLM-readable map of
+every document in the repo.
+
+## Trust boundary
+
+Every `kb.*` write tool goes through the review gate. There is no "trusted
+agent shortcut" except an opt-in `review.approver_role: trusted-agent` in
+the KB's `config.yaml` (off by default). Concretely:
+
+* `kb.propose_*` writes a YAML file to `.vouch/proposed/` — never to
+ `.vouch/claims/` directly.
+* `kb.approve` requires `approved_by != proposed_by` unless the trusted
+ shortcut is on.
+* Lifecycle ops (`kb.supersede`, `kb.contradict`, `kb.archive`,
+ `kb.confirm`) mutate durable artifacts because they're metadata about
+ reviewed knowledge, not new assertions. They still land an audit event.
+* The MCP server, the JSONL server, and the CLI all share the same
+ storage + proposals + audit code path. The web review-ui (PR #195,
+ pending) is a *viewport*, not a parallel data path.
+
+If you're proposing a fix that bypasses the gate, you're proposing the
+wrong fix.
+
+## Common agent tasks
+
+### Use vouch from another project (the normal case)
+
+```bash
+cd /path/to/your/project
+vouch init # create .vouch/
+vouch install-mcp claude-code # …or cursor, codex, continue, cline, windsurf, zed
+# Restart the agent host. kb.* tools + slash commands now available.
+```
+
+`vouch install-mcp ` is manifest-driven: every supported host has a
+single-file declaration under `adapters//install.yaml`. To add a
+host, copy an existing manifest, point it at the host's config paths, and
+open a PR.
+
+### Contribute a bug fix or feature
+
+```bash
+git fetch origin main
+git switch -c / origin/main
+# … edit …
+.venv/bin/python -m pytest tests/ -q --ignore=tests/embeddings
+.venv/bin/python -m mypy src
+.venv/bin/python -m ruff check src tests
+git add
+git commit -m "(): <≤72-char summary>"
+git push -u origin
+```
+
+Commit type vocabulary: `feat | fix | refactor | test | docs | chore |
+perf | ci | style | build | revert`. Body in lowercase prose, multi-line
+ok, **no `Co-Authored-By: Claude` trailer** — see
+[`CONTRIBUTING.md`](./CONTRIBUTING.md). CI runs the same three commands.
+
+### Add a new `kb.*` method
+
+Every kb method must be registered in **four** places — the
+`test_capabilities` test catches drift:
+
+1. `src/vouch/server.py` — the MCP tool function
+2. `src/vouch/jsonl_server.py` — the matching handler + `HANDLERS` map entry
+3. `src/vouch/capabilities.py` — append to `METHODS`
+4. `src/vouch/cli.py` — the human-facing CLI mirror
+
+Add a test in `tests/test_.py` that asserts the JSONL envelope
+shape (`{id, ok, result}` for success, `{id, ok: false, error}` for
+failure).
+
+### Add a new install-mcp host
+
+```
+adapters//
+ install.yaml # tier T1..T4 declarations
+ .mcp.json # T1 — MCP wire (project-local)
+ CLAUDE.md.snippet # T2 — fenced append into the host's instruction file
+ .claude/commands/* # T3 — slash commands (when the host supports them)
+ .claude/settings.json # T4 — hooks + auto-allow lists
+```
+
+The writer is `src/vouch/install_adapter.py`. Strict YAML manifest
+validation; semantic validation (does the `dst` path make sense for that
+host?) is deliberately deferred.
+
+## Before shipping
+
+```bash
+make check # the convenience wrapper
+# expanded:
+.venv/bin/python -m pytest tests/ -q --ignore=tests/embeddings
+.venv/bin/python -m mypy src
+.venv/bin/python -m ruff check src tests
+```
+
+`mypy src` is the gate that misses locally and fails CI; never push
+without it.
+
+## Privacy / disclosure
+
+* Never paste a real Bearer token, GitHub PAT, or PyPI token into a
+ commit, an issue, or a PR body. Use environment variables or
+ `.env.local` (gitignored).
+* Never propose claims with real customer names, internal URLs, or secret
+ identifiers in a public KB. Use generic placeholders.
+* Security issues go to the contact listed in [`SECURITY.md`](./SECURITY.md);
+ do not file public issues for vulnerabilities.
+
+## Hard rules
+
+* **The review gate is non-negotiable.** Bypassing it is a rejected PR,
+ not a feature.
+* **Tests must be added with the change**, not after. CI red on a PR is a
+ reviewer-blocking condition.
+* **No `Co-Authored-By: ` trailers** in commits — the user has
+ been explicit about this.
+* **No silent CHANGELOG omissions** when you add a user-visible feature —
+ update `CHANGELOG.md` under `[Unreleased]` in the same PR.
+
+If your task contradicts any rule above, stop and ask before continuing.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6d4fff95..2c780d73 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,193 @@ All notable changes to vouch are documented here. Format follows
## [Unreleased]
+## [1.0.0] — 2026-06-26
+
+### Added
+- `vouch dual-solve ` — run claude + codex on one github issue in
+ isolated git worktrees, compare the two diffs, keep the branch you pick, and
+ propose the chosen solution's rationale into the KB. A sibling tool to
+ `auto-pr`, and the first that writes to the KB — but only ever as review-gated
+ proposals: the winning commit is registered as a `Source` and the decision
+ plus up to three approach claims land in `proposed/`, so approval still
+ requires a human `vouch approve`. Nothing is auto-approved. `--json` is
+ non-interactive (emits both diffs, keeps both branches); `--no-record` and
+ `--dry-run` propose nothing. Each phase (fetch, ground, and per-engine run
+ with elapsed time and diff size) reports progress to stderr while it works.
+- `vouch dual-solve --sandbox` and
+ `vouch review-ui --dual-solve-sandbox` — run Claude Code and Codex inside a
+ Docker image (default `amika/coder:latest`) while leaving git/GitHub commands
+ on the host. The sandbox runner mounts only each candidate worktree plus a
+ temporary copied home containing known Claude/Codex credential files, so agent
+ writes stay in the throwaway dual-solve branches and host credential files are
+ not modified.
+- `vouch review-ui --allow-dual-solve` — a browser SPA that runs `dual-solve`
+ on a github issue link, streams progress over the review-ui's websocket, shows
+ both engines' diffs side by side, and lets you pick the winner. Off by default;
+ localhost-first; edit-only over http; the pick keeps the branch and proposes
+ the rationale into the KB through the existing review gate (nothing
+ auto-approves). See `proposals/VEP-0006-dual-solve-web.md`.
+- `vouch auto-pr ` — open N mergeable PRs against any github repo.
+ Sources open issues first then agent-discovered improvements, bootstraps a
+ contribution skill from the repo's merged PRs when it ships no guidance, and
+ cross-verifies each diff by alternating claude/codex as fixer and the other
+ as reviewer; a PR opens only when the repo's own test gate is green and the
+ reviewer signs off. A sibling tool — it never writes to the KB or the review
+ gate. Paired with the `auto-pr` skill.
+- typed page kinds (#234): a KB can declare extra page kinds in
+ `.vouch/config.yaml` under `page_kinds`, each with `required_fields`, a
+ JSON-Schema-subset `frontmatter_schema`, `required_citations`, and one level
+ of `extends`. `kb.propose_page` now takes a `metadata` frontmatter dict and
+ validates the kind at both the propose and approve gates, surfacing one error
+ per offending field. The built-in `PageType` kinds keep working unchanged.
+ New `vouch schema list` and `vouch schema sync` commands inspect declared
+ kinds and audit existing pages against them; `propose-page` gains `--kind`
+ and repeatable `--meta key=value`.
+- `kb.synthesize` — answer-mode retrieval over the review-gated KB. Answers a
+ query in prose from approved claims only, with an inline `[claim_id]`
+ citation behind every sentence, an explicit `gaps` block listing query
+ topics no approved claim covered, and a `synthesis_confidence` grade derived
+ from the cited claims' lifecycle status. Deterministic in v1 (no LLM in the
+ loop). Exposed across the CLI (`vouch synthesize`), MCP (`kb_synthesize`),
+ and JSONL (`kb.synthesize`) surfaces (#222).
+- `_meta.vouch_trust` on every dict-shaped kb.* response: `{remote, caller_kind,
+ auth_subject}` so clients can detect remote confinement and surface it in
+ their UI. HTTP MCP calls report `remote: true, caller_kind: mcp_http`; CLI
+ `--json` reports `remote: false, caller_kind: cli`. Bearer-authenticated
+ HTTP calls include a stable token fingerprint as `auth_subject` (#233).
+- `vouch-context` OpenClaw context engine (#228): `src/vouch/openclaw/context_engine.py`
+ wraps `kb.context` retrieval, the entity-salience reflex, and session hot
+ memory into a cited `systemPromptAddition` on every `assemble()`. The plugin
+ manifest declares `contracts.contextEngines: ["vouch-context"]` and registers
+ `adapters/openclaw/vouch-context-engine.mjs`; engine identity is advertised
+ on `kb.capabilities.context_engines`. Compaction stays delegated to the
+ legacy OpenClaw runtime (`ownsCompaction: false`).
+- Entity-salience retrieval reflex: a per-session, in-memory ring buffer of
+ recent caller queries drives a zero-LLM substring/FTS entity pass that
+ attaches top-K matched claim candidates as `_meta.vouch_salience` on
+ `kb_context` read responses. Config-gated via `retrieval.reflex`
+ (`enabled`/`window`/`top_k`); the buffer is never persisted and resets on
+ `session_end` (#223).
+- `vouch eval recall ` — score `kb.context` retrieval against a
+ labeled query set with pure-Python P@k / R@k / MRR / nDCG, compare against a
+ committed `eval/baseline.json`, and fail CI on a P@5 regression beyond
+ tolerance (default 5%). Ships a starter labeled set, a reproducible fixture
+ KB under `eval/fixture-kb/`, and an `eval` workflow gating retrieval changes
+ (#226).
+### Fixed
+- `parse_since` (the `--since` parser behind `vouch metrics`/`vouch audit`) now raises a clean `MetricsError` for a duration too large to represent (e.g. `--since 1000000000000d`), instead of letting an uncaught `OverflowError` traceback escape — restoring the documented "clean error, not a traceback" contract.
+- `sync_apply` now loads the sync source exactly once and passes the same `_SyncSource` instance into `sync_check`, closing a TOCTOU window where a bundle replaced on disk between the two `_load_source` calls could cause the validation and write phases to operate on different snapshots. Also eliminates redundant directory walks (KB sources) and triple tarball opens (bundle sources). Fixes #217.
+- `vault_to_kb` now passes `slug_hint=page_id` to `propose_page` so vault edit proposals target the existing page id from frontmatter instead of a slugified copy of the title (fixes #219).
+- `vault_to_kb` skips mirror files whose page no longer exists in the KB, preventing ghost-page proposals that would fail on approve (fixes #219).
+- `vault_to_kb` skips filing a second proposal when a pending proposal already targets the same page id (with differing body), preventing duplicate proposals on repeated sync runs before approval (fixes #219).
+- `vault_to_kb` now warns when a user edits a claim stub instead of silently dropping the edit, directing the user to edit the citing page instead, and reports it via the dedicated `claim_stubs_edited` field on `VaultSyncResult` (fixes #219).
+- `approve()` now supports updating an existing page via `KBStore.update_page` when a PAGE proposal's id matches an existing artifact (the vault-edit flow), instead of raising `cannot approve: page already exists` for every vault edit (fixes #219).
+
+### Fixed
+- `vouch serve` now fails fast with a clear `vouch init` hint when no `.vouch/` KB is present, instead of starting a server that immediately misbehaves (#95).
+
+### Added
+- `kb.volunteer_context` — confidence-gated push context for active sessions.
+ `kb.session_start(task=…)` opens a background watch on retrieval salience;
+ when an approved claim's normalized relevance exceeds the configured
+ threshold (default `0.85`), vouch queues `{claim_id, relevance, why}` and
+ emits an MCP notification (`kb.volunteer_context`). JSONL and CLI clients
+ poll via `kb.volunteer_context` / `vouch session volunteer`. Pushes are
+ throttled (default 30s) and respect scope visibility (#236).
+- Auto-extracted typed edges: approving a page now files `mentions` (wiki-links),
+ `relates_to` (entity frontmatter), and `derived_from` (source frontmatter)
+ relation proposals automatically, tagged `proposed_by: vouch-extractor`.
+ They land in `proposed/` like any hand-filed relation and need the usual
+ review; `vouch reject-extracted [--page ]` mass-rejects them (#224).
+- Visibility-aware `kb.audit` / `vouch audit`: audit reads accept optional
+ `project` / `agent` viewer context (or nested `viewer_scope` on JSONL).
+ Events whose `object_ids` reference scoped claims, sources, or claim
+ proposals outside the viewer context are filtered out; events with no
+ `object_ids` remain visible to everyone (#232).
+- `vouch install-mcp openclaw` — ninth host in the adapter catalogue.
+ Declares plugin enablement (`.openclaw/plugins.json`), an `AGENTS.md`
+ fenced snippet, the four slash commands reused in place from the
+ `claude-code` adapter, and a project-local trust-boundary policy
+ (`.openclaw/policy.json`). Complements the repo-root
+ `openclaw.plugin.json` bundle manifest, which covers loading vouch into
+ an OpenClaw deployment rather than into one managed project (#230).
+- `vouch sync --vault ` — bidirectional sync between the KB and an
+ Obsidian/Logseq-style markdown vault. Forward (vault → KB): edits to
+ `/vouch/pages/.md` become page-edit proposals citing a
+ `vault:` source. Backward (KB → vault): approved pages mirror
+ to `/vouch/pages/` and approved claims surface as markdown stubs
+ in `/vouch/claims/` with Obsidian wikilink backlinks to citing
+ pages. `--watch` keeps a polling loop alive; `--direction` lets you
+ run forward-only or backward-only. The starter KB now seeds an
+ approved "Edit in Obsidian" walkthrough page so new users discover the
+ workflow the moment they `vouch init` (#181).
+- `vouch install-mcp ` — one-command adapter writer that drops the
+ right MCP config templates into a project tree, idempotently. Eight hosts
+ ship in the catalogue: `claude-code`, `claude-desktop`, `cursor`,
+ `continue`, `codex`, `windsurf`, `cline`, `zed`. Each adapter is described
+ by a declarative `adapters//install.yaml` manifest so adding a new
+ host is a single-file PR. `--tier T1..T4` stacks adoption layers (T1 = MCP
+ wire only, T2 = CLAUDE.md/AGENTS.md fenced snippet, T3 = optional slash
+ commands, T4 = optional host hooks/settings). `--list` enumerates the
+ catalogue; `--path` (or `--target`) installs into a project other than
+ cwd. Existing files are left alone; CLAUDE.md gets a fenced append so
+ re-runs stay flat-noop (#179).
+- Propose-time similarity warnings: `propose_claim` / `kb.propose_claim` return
+ non-blocking `warnings` (`similar_approved`, `similar_pending`) when the
+ embeddings extra is installed. Configurable via `review.similarity_threshold`
+ (default `0.95`). CLI prints warnings to stderr; dry-run included.
+- `vouch stats` and `kb.stats` expose read-only KB observability: pending
+ proposals by agent (with median/max age), review decision counts and
+ approval rate over a configurable window (`--days`, default 30; `0` for
+ all-time), citation coverage (valid / invalid / broken), plus audit-log
+ cross-check totals. Available on MCP, JSONL, and HTTP transports.
+- `vouch fsck` performs deep consistency checks beyond `vouch doctor`:
+ orphaned embeddings, dangling supersede/contradict chains, decided
+ proposals whose artifact is missing, and FTS5 index-vs-file drift
+ (orphan rows, missing rows, status drift). Read-only; reports findings
+ with object ids. `--fix` is intentionally out of scope (#96).
+- `vouch migrate` checks, dry-runs, and applies on-disk KB format migrations,
+ preserving audit history and rebuilding derived indexes after successful
+ upgrades.
+- `vouch expire` garbage-collects stale pending proposals: dry-run by default,
+ `--apply` moves them to `decided/` with `decision_reason: expired`, emits
+ `proposal.expire` audit events, and honors `review.expire_pending_after_days`
+ in `config.yaml` (default 90; `0` disables). `kb.expire` on MCP/JSONL.
+- `vouch init --template ` seeds a domain starter pack. The default `starter` template is unchanged; the new `gittensor` template seeds a small, cited, approved KB about Gittensor (SN74) contribution scoring (1 source, 1 entity, 7 claims — merged-PR rewards, PAT verification, scoring factors, sybil-resistance, repo allow-list policy, issue-solving multiplier, and emission split) so a fresh KB in a Gittensor repo has retrievable context on day one. Templates are an in-code registry — future packs plug in the same way.
+- Structured JSON logging via `VOUCH_LOG_FORMAT=json`. When set, the
+ `vouch` logger emits one JSON object per line with `level`, `logger`,
+ `event`, and any structured extras (e.g. `actor`, `object_ids`) passed
+ through stdlib `extra=`. Unset (or any other value) keeps the existing
+ human-readable format — no behaviour change beyond formatting. Wired
+ into the CLI, MCP server, and JSONL server entry points. `VOUCH_LOG_FORMAT`
+ was already documented in `ROADMAP.md` and `adapters/generic-mcp/README.md`
+ but had no implementation (#97).
+- Performance benchmark suite in `benchmarks/` covering search latency, proposal write throughput, bundle export/import/verify round-trips, and index rebuild time at 1k/10k claim sizes. Run with `pytest benchmarks/ --benchmark-only`.
+
+### Fixed
+- `put_claim` / `update_claim` now reject a Claim whose `entities`,
+ `supersedes`, `superseded_by`, or `contradicts` reference an artifact that
+ is not in the KB, via a new `KBStore._validate_claim_refs`. `bundle.import_check`
+ gains the matching check so a bundle can no longer land a claim with dangling
+ graph refs through `import_apply`'s direct write. Previously only `claim.evidence`
+ was checked: the graph-integrity fix for Relations/Pages (#124) skipped the
+ Claim model's own four reference fields, even though `fsck` already declared
+ `dangling_supersedes` / `dangling_superseded_by` / `dangling_contradicts` as
+ error-severity findings — the invariant was articulated but enforced by no
+ writer. Same model-layer/storage pattern as #81 / #123. Closes #196.
+- `lifecycle.supersede` / `lifecycle.contradict` pre-validate both touched
+ claims before the first disk write so a legacy dangling ref on either
+ side can't half-apply the operation (one update written without the
+ reciprocal, no relation, no audit event).
+- `proposals.check_approvable` dry-runs the put_*-side ref guards so the
+ default `vouch approve a b` batch flow (#93) catches a dangling
+ `claim.entities` (or relation endpoint / page reference) before any
+ disk write, preserving the all-or-nothing contract.
+- `vouch fsck` reports `claim.entities` pointing at a missing entity as a
+ `dangling_claim_entity` error finding, alongside the existing
+ `dangling_supersedes` / `_superseded_by` / `_contradicts` checks.
+- `discover_root()` now honours `VOUCH_KB_PATH=/abs/path/.vouch` and returns the parent root, instead of always walking up from cwd. The env var was already documented in `adapters/generic-mcp/README.md` but wasn't wired into the code — closing the doc-vs-code drift removes the `"cwd": "..."` ceremony hosts like Claude Desktop need today to point at a specific KB.
+
## [0.1.0] — 2026-05-26
### Packaging
@@ -15,13 +202,49 @@ All notable changes to vouch are documented here. Format follows
Trusted Publishing (OIDC).
### Added
+- HTTP transport: `vouch serve --transport http` exposes the full `kb.*`
+ surface over HTTP, reusing the same dispatch table as the MCP/JSONL
+ transports (#94, implements VEP-0004). `POST /rpc` carries the JSONL
+ envelope; `GET /capabilities` and `GET /healthz` are unauthenticated.
+ Binds `127.0.0.1` by default and refuses any non-loopback bind without
+ both `--allow-public` and a bearer token (`--token` / `VOUCH_HTTP_TOKEN`,
+ constant-time compared). The `X-Vouch-Agent` header sets the audit actor
+ per request. Zero new runtime dependencies (stdlib `http.server`); no TLS
+ in-process — terminate at a reverse proxy. `kb.capabilities.transports`
+ now includes `http`.
+- `vouch approve …` approves multiple proposals in one
+ non-interactive call for CI and backlog clearing (#93). Default is
+ all-or-nothing: every id is validated as an approvable pending proposal
+ before any is written, so a typo or already-decided id aborts the batch
+ without approving anything. `--keep-going` switches to best-effort
+ (approve what you can, report the rest, exit non-zero on partial failure).
+ One audit event is still recorded per approved artifact. Complements the
+ interactive `vouch review` queue.
+- Friendlier CLI output (#54, track 2): colourised `vouch status` / `lint` /
+ `doctor` / `search` (honours `NO_COLOR`, `FORCE_COLOR`, and TTY detection);
+ `--json` on `vouch lint` and `vouch search` for machine-readable output
+ while the default stays human-readable; progress callbacks on the long ops
+ (`rebuild_index`, `doctor`, bundle `export`/`import_apply`) surfaced as
+ status lines on interactive terminals; and `vouch index` / `vouch export`
+ now report a clean `Error:` instead of a traceback on a malformed artifact.
+- `vouch sync-check` and `vouch sync-apply` reconcile another `.vouch`
+ directory or bundle by importing only non-conflicting durable artifacts and
+ reporting conflicts without overwriting reviewed knowledge.
+- `vouch pending --json` emits pending proposals as structured JSON for shell
+ scripts, CI checks, and multi-agent review dashboards.
+- `vouch diff ` shows what changed between two claim revisions or two page revisions — field-level changes plus a line-diff of the long text/body. Auto-detects the artifact kind and hides always-churning metadata. Read-only; supports `--json`.
- Seed a cited starter source and claim during `vouch init`, print first-run
next steps, and document a 30-second onboarding tour (#54).
+- Add `vouch review`, a guided CLI queue for approving, rejecting, skipping,
+ or dry-running pending proposals without bypassing the review gate.
### Fixed
+- `store.put_relation`, `store.put_relation_idempotent`, and `store.put_page` now reject artifacts whose foreign-id references don't resolve in the KB (relation `source` / `target` / `evidence`; page `entities` / `sources`). `proposals.propose_relation` and `proposals.propose_page` surface the same checks at proposal time as `ProposalError`. `bundle.import_check` and `sync.sync_check` run an equivalent cross-artifact pass against the post-merge id set so manifest-consistent bundles can't smuggle relations / pages whose references resolve to nothing — closes the write-time counterpart of the after-the-fact `dangling_relation` finding in `health.lint` (`src/vouch/health.py:135-145`).
+- `bundle.import_check` / `import_apply` and `sync.sync_check` / `sync_apply` now enforce the Source content-addressing invariant: a `sources//content` member must hash to ``, and a `sources//meta.yaml` must carry a matching `id`/`hash`. Previously the import side trusted the bundle's directory layout, so a manifest-consistent bundle could land a Source whose content did not match its claimed id — `verify_source` would report `stored_ok=False` only after the import had already succeeded with a clean `bundle.import` audit event. The per-file sha256 gate (#74) only proves bytes match the manifest; this closes the write-time counterpart of the `verify.verify_source` detection.
- Add `put_relation_idempotent()` to `KBStore` and use it in `supersede()` and `contradict()` so retrying after a partial failure converges to a consistent state instead of raising `ValueError`.
- Raise `ProposalError("forbidden_self_approval")` in `proposals.approve()` when `approved_by == proposal.proposed_by`, enforcing the review-gate guarantee documented in the README and CONTRIBUTING.
- `crystallize()` now sets `review.approver_role: trusted-agent` context so single-agent sessions can be crystallized without hitting the `forbidden_self_approval` guard (#47).
+- Narrow `except Exception` to `except ArtifactNotFoundError` in `propose_claim()` evidence validation so I/O and parse errors propagate with their original type instead of being masked as `unknown source/evidence id` (#48).
- Bundle import rejects tar members whose path escapes `kb_dir`
(CVE-2007-4559, #9). Previously a crafted `.tar.gz` with a member
named `../../evil.txt` could write outside `.vouch/`; the manifest
@@ -29,6 +252,12 @@ All notable changes to vouch are documented here. Format follows
the same tarball. `import_apply`, `import_check`, and `export_check`
now validate every member path and raise on unsafe names.
- Fix `vouch search` CLI: assign backend label per code path so substring fallback results are no longer mislabelled as `fts5`; update stale docstring to reflect multi-backend search surface (#52).
+- `context._retrieve` now honors `retrieval.backend` in `config.yaml`
+ instead of always running embeddings first (#92). Accepts `auto`
+ (default — embedding → FTS5 → substring), `embedding`, `fts5`, or
+ `substring`; a legacy `retrieval.backends` list is still read for
+ back-compat. `vouch init` now writes `retrieval.backend: auto`, and the
+ README/ROADMAP describe the actual behavior.
- `vouch crystallize` now indexes its session-summary page into FTS5 so it
surfaces from `vouch search` / `kb.search` / `kb.context` without a
`vouch index` rebuild. Previously the summary was written via
@@ -42,6 +271,18 @@ All notable changes to vouch are documented here. Format follows
a silent no-op. Existing Linux/macOS bundles are unchanged (their paths
were already POSIX); Windows bundles produced before this fix should be
re-exported.
+- `kb.context` no longer returns claims whose status is `archived`,
+ `superseded`, or `redacted` (#78). Two compounding bugs were combining
+ to leak retracted knowledge back to agents: `build_context_pack` had
+ no status filter, and `store.update_claim` only refreshed the
+ embedding cache without keeping `claims_fts.status` in sync — so even
+ after `lifecycle.archive` / `supersede` / `contradict`, the FTS5 row
+ kept its first-index status and `kb.search` / `kb.context` matched the
+ retracted claim. `update_claim` now re-indexes the FTS5 row (mirroring
+ what `proposals.approve` does on first index), and
+ `build_context_pack` drops retracted claims from the assembled pack.
+ `CONTESTED` claims continue to surface so contradictions remain
+ visible.
- `bundle.import_check` and `bundle.import_apply` now verify each tar
member's `sha256` against `manifest.json` (#74). Previously the
per-file hash was only enforced by `export_check`; the import side
@@ -53,6 +294,37 @@ All notable changes to vouch are documented here. Format follows
with between `import_check` and the apply re-open is rejected
before anything reaches disk and the audit log does not record
a `bundle.import` event.
+- `Claim.evidence` now enforces "at least one citation" at the model
+ layer via a `@field_validator` (#81). Previously the
+ README-documented guarantee ("Claims must cite sources … a claim
+ without at least one Source/Evidence id is a validation error")
+ was enforced only in `proposals.propose_claim`, so every other
+ write path — direct `store.put_claim`, `store.update_claim`, and
+ `bundle.import_apply` via `_validate_content` — silently accepted
+ `evidence: []` and landed an uncited claim. The validator closes
+ all three paths at once; `store.update_claim` additionally
+ re-validates via `Claim.model_validate(...)` before persisting so
+ in-place mutation (`c.evidence = []; store.update_claim(c)`)
+ also raises before the YAML hits disk. **Migration note:** because
+ the validator also fires when claims are read back, a KB that
+ already has an uncited `claims/.yaml` on disk from before this
+ fix would otherwise crash `vouch lint` / `vouch doctor` with a
+ `pydantic.ValidationError`. `vouch lint` now iterates `claims/`
+ per-file and surfaces unparseable / uncited YAMLs as
+ `invalid_claim` findings ("edit the YAML to add a citation, or
+ delete the file") instead of bailing out — so existing KBs get a
+ clean repair list rather than a traceback.
+- Close the review-gate bypass in `sessions.crystallize` (#76). The
+ durable session-summary page wrote `sess.task`, `sess.note`, and
+ `sess.agent` verbatim into rendered markdown, letting an agent
+ land arbitrary content into `pages/` by calling
+ `kb.session_start(task=...)` and getting any one claim approved
+ via crystallize. The summary body now contains only fields the
+ proposing agent cannot influence (session id, server-clock
+ timestamps, list of approved artifact ids). The
+ `session.crystallize` audit event now also includes the summary
+ page id in `object_ids` when a page is written, so `vouch audit`
+ truthfully attributes the write.
## [0.0.1] — 2026-05-17
@@ -80,6 +352,5 @@ Initial alpha. Surface intentionally small; expect breaking changes pre-1.0.
- Claim validation: at least one source/evidence citation required.
- Per-agent attribution via `VOUCH_AGENT` env var.
-[Unreleased]: https://github.com/vouchdev/vouch/compare/v0.1.0...HEAD
-[0.1.0]: https://github.com/vouchdev/vouch/compare/v0.0.1...v0.1.0
-[0.0.1]: https://github.com/vouchdev/vouch/releases/tag/v0.0.1
+[Unreleased]: https://github.com/plind-junior/vouch/compare/v0.0.1...HEAD
+[0.0.1]: https://github.com/plind-junior/vouch/releases/tag/v0.0.1
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000..d5dc673a
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,259 @@
+# CLAUDE.md — orientation for Claude Code working on vouch
+
+This file is read automatically by Claude Code when you open the vouch
+repository. It exists to make a fresh session productive without you
+having to re-onboard it every time.
+
+If you're working *with* vouch from inside a different project (proposing
+claims, approving them), read [`README.md`](./README.md) instead. This
+file is for working *on* vouch — fixing a bug, adding a feature,
+shipping a release.
+
+For any non-Claude-Code agent — start with [`AGENTS.md`](./AGENTS.md).
+
+## North star
+
+Vouch is a knowledge base where every write goes through a review gate.
+That's the load-bearing invariant. Every other design choice — files on
+disk, append-only audit log, manifest-driven adapters, thin viewports
+over the storage layer — is downstream of "writes must be reviewed."
+
+If a PR adds a parallel data path that bypasses `proposals.approve()`,
+the PR is wrong. Push back. Find the right factoring.
+
+## Architecture (90 seconds)
+
+```
+ ┌──────────────┐
+ Claude Code ─MCP──▶ │ │
+ Cursor ─MCP──▶ │ server.py │
+ Codex ─MCP──▶ │ jsonl_ │ ─┐
+ CLI human ──────▶ │ server.py │ │
+ │ cli.py │ │
+ └──────┬───────┘ │
+ │ │
+ ▼ │
+ ┌──────────────┐ │
+ │ proposals.py │ │ review gate
+ │ lifecycle.py │ │ (kb.approve etc.)
+ └──────┬───────┘ │
+ │ │
+ ▼ │
+ ┌──────────────┐ │
+ │ storage.py │ ◀──┘
+ │ audit.py │
+ │ index_db.py │
+ └──────┬───────┘
+ │
+ ▼
+ .vouch/ ─── filesystem (yaml + md + jsonl)
+ ─── state.db (FTS5 + optional embeddings, derived)
+```
+
+Three rules that fall out of the layout:
+
+1. **`src/vouch/storage.py` is pure I/O.** No business logic. If you find
+ yourself doing scope filtering or status transitions inside `put_claim`,
+ you're in the wrong file.
+2. **All three surfaces (MCP, JSONL, CLI) call the same `proposals.*` and
+ `lifecycle.*` functions.** Drift between surfaces is the most common
+ contributor mistake. `test_capabilities` enforces method-list parity;
+ you still need to keep behaviour aligned by reading the existing
+ handlers before you add a new one.
+3. **The audit log is the only authoritative history.** `decided/` is the
+ queryable summary; `audit.log.jsonl` is the legally-authoritative event
+ stream. Both are committed. Never edit either by hand.
+
+## Build + test + ship
+
+```bash
+# from a clone
+python3 -m venv .venv && . .venv/bin/activate
+pip install -e '.[dev]'
+
+# the CI gate — exactly what .github/workflows/ci.yml runs
+.venv/bin/python -m pytest tests/ -q --ignore=tests/embeddings
+.venv/bin/python -m mypy src
+.venv/bin/python -m ruff check src tests
+
+# the convenience wrapper
+make check
+```
+
+`mypy src` is the gate that gets missed locally and turns CI red.
+
+For the embedding-heavy tests (separate job in CI): `pip install
+-e '.[embeddings]'` then drop the `--ignore=tests/embeddings` flag.
+
+## Ship a feature branch
+
+```bash
+git fetch origin main
+git switch -c / origin/main
+# … work …
+make check
+git add # never `git add -A` — leaks .claude/, web/, etc.
+git commit -m "(): <≤72-char summary>
+
+multi-line lowercase body explaining the why.
+no Co-Authored-By trailer."
+git push -u origin
+```
+
+If there's a pre-existing `M src/vouch/storage.py` edit in the working
+tree on `release/0.1.0` (the user's WIP), stash it first:
+
+```bash
+git stash push -m "preserve user wip" src/vouch/storage.py
+```
+
+…work on the feature branch, then `git stash pop` after switching back.
+The `.claude/skills/vouch-ship/SKILL.md` skill encodes this dance.
+
+## Commit messages
+
+Conventional commits, enforced by a pre-commit hook:
+
+```
+():
+
+
+```
+
+Types: `feat | fix | refactor | test | docs | chore | perf | ci | style |
+build | revert`. Scope optional. Anchor voice against `git log --oneline
+-10` before drafting a new one.
+
+**No `Co-Authored-By: ` trailer.** The user has been explicit
+about this; it's checked in PR review.
+
+## Conventions you'll trip on otherwise
+
+* **Lowercase prose** in PR bodies, commit bodies, and review comments.
+ Match the existing voice.
+* **No inline `##` headers inside a postable PR-review comment block** —
+ reviews are 4-6 short paragraphs in vouch's house style. See
+ `.claude/skills/vouch-pr-comment/SKILL.md` if it's installed.
+* **No "we" / "let's" marketing tone in code comments.** Comments
+ explain why, not what.
+* **Specific files only when staging.** `git add -A` will pull in
+ `.claude/`, `web/`, `proposed-features.md`, etc. that are local scratch.
+* **Stash + worktree** for doc-only changes: branch off
+ `origin/main` in `/tmp/vouch--wt`, do the work, push, remove the
+ worktree.
+
+## Where things live
+
+| Concern | File |
+|---|---|
+| MCP tool surface | `src/vouch/server.py` |
+| JSONL handler map | `src/vouch/jsonl_server.py` |
+| CLI commands | `src/vouch/cli.py` |
+| Pure file I/O | `src/vouch/storage.py` |
+| Proposal lifecycle | `src/vouch/proposals.py` |
+| Claim lifecycle (supersede, etc.) | `src/vouch/lifecycle.py` |
+| Audit log writer | `src/vouch/audit.py` |
+| Pydantic models | `src/vouch/models.py` |
+| Capabilities + method list | `src/vouch/capabilities.py` |
+| Context-pack builder | `src/vouch/context.py` |
+| SQLite FTS5 + embeddings | `src/vouch/index_db.py` |
+| Sessions | `src/vouch/sessions.py` |
+| Manifest-driven adapter writer | `src/vouch/install_adapter.py` |
+| Web review-ui (when PR #195 lands) | `src/vouch/web/` |
+| OpenClaw plugin manifest | `openclaw.plugin.json` (repo root) |
+| Claude Code / Cursor / etc. install templates | `adapters//` |
+
+Tests mirror module names (`tests/test_.py`); the convention is
+strict.
+
+## The OpenClaw plugin manifest
+
+[`openclaw.plugin.json`](./openclaw.plugin.json) at the repo root makes the
+vouch repo loadable directly as an OpenClaw plugin: drop the repo into a
+deployment, and the loader picks up the MCP server, the four slash commands
+under `adapters/claude-code/.claude/commands/`, and the trust-boundary
+declaration. Touch it whenever you:
+
+* bump the package version (`version` field must stay in step with
+ `pyproject.toml`),
+* add or rename a slash command (sync the `skills` array),
+* add a new MCP method that's safe to expose to remote callers (consider
+ whether to list it under `contracts.mcpMethods`),
+* change the trust boundary (e.g. a new "must-be-confined" surface that
+ arrives with the HTTP transport).
+
+Keep it small. Anything that would require a runtime decision (which kb to
+use, whose audit log to write to) belongs in the deployment's own config,
+not in the plugin manifest.
+
+## When you add a new `kb.*` method
+
+Four registration sites — `test_capabilities` will fail if you miss one:
+
+1. **MCP tool** in `src/vouch/server.py` (decorated with `@mcp.tool()`)
+2. **JSONL handler** in `src/vouch/jsonl_server.py` (`_h_` +
+ `HANDLERS["kb."]`)
+3. **`METHODS` list** in `src/vouch/capabilities.py`
+4. **CLI command** in `src/vouch/cli.py` (the human mirror)
+
+Plus a test under `tests/test_.py`.
+
+If the method *reads* the KB, consider whether it should attach the
+`_meta.vouch_hot_memory` sidebar from `src/vouch/hot_memory.py`. The
+sidebar is added per-tool — there's no global decorator.
+
+## Release flow
+
+`release.yml` cuts a tagged PyPI release via Trusted Publishing on every
+`v*` tag push.
+
+Pre-release checklist (also in `CONTRIBUTING.md`):
+
+1. Bump `version = "X.Y.Z"` in `pyproject.toml`.
+2. Move everything under `[Unreleased]` in `CHANGELOG.md` into a dated
+ `[X.Y.Z]` section.
+3. `make check` green.
+4. PR titled `chore(release): prepare X.Y.Z`, merge to `main`.
+5. `git tag vX.Y.Z && git push --tags` — the workflow does the rest.
+6. After CI finishes, draft the GitHub release with the CHANGELOG section
+ as the body.
+
+## What's not in scope right now
+
+Don't propose:
+
+* A SaaS mode / hosted vouch (explicitly out of scope; vouch is
+ local-first by design)
+* Removing the review gate "for trusted agents" (the `trusted-agent`
+ config flag exists; the gate stays)
+* Replacing yaml with json/sqlite as the storage format (the diff-in-PRs
+ property requires plaintext)
+* A custom config DSL (yaml + pydantic is sufficient)
+
+Roadmap items that ARE in scope live in [`ROADMAP.md`](./ROADMAP.md) and
+[`proposed-features.md`](./proposed-features.md) (local scratch — not on
+`main`).
+
+## Privacy
+
+Same rule as gbrain's: never bake real customer names, internal URLs, or
+PII into public artifacts. Test fixtures should use generic placeholders
+(`alice-example`, `acme-example`).
+
+## Hooks installed in this repo
+
+Pre-commit checks the conventional-commit format. If you trigger it via
+shell substitution in a `git commit -m "$(cat <" [--depth N] [--max-chars N]`.
+- `CHANGELOG.md` — `### Added` bullet under `## [Unreleased]`.
+
+## Why / root cause
+
+`kb.context` is a retrieval primitive: it ranks and budgets items but leaves
+answer composition (and the discipline of *only* using approved knowledge) to
+the caller. There was no first-class way to ask the KB a question and get a
+prose answer whose every clause is provably backed by a reviewed claim, with
+the uncovered parts of the question surfaced rather than silently dropped.
+`kb.synthesize` fills that gap deterministically — citation-gated by
+construction, so it cannot fabricate an unbacked sentence — and grades its own
+confidence from the lifecycle status of the claims it actually cited.
+
+## Test plan
+
+`tests/test_synthesize.py` covers:
+
+- 3 approved `auth` claims → non-empty answer citing all 3 ids by `[id]`,
+ confidence `high`.
+- A query the KB doesn't cover → `answer == ""`, `claims == []`, `gaps`
+ populated with the query's salient terms.
+- Fuzz/traceability: every sentence in a non-empty answer carries at least one
+ `[id]` citation whose id is in `claims` and resolves via `store.get_claim`.
+- `max_chars` drops trailing claims without cutting a citation
+ (citation count == cited-claim count).
+- Confidence reflects claim status (`working` → medium, `contested` → low).
+- `llm=True` raises the reserved-backend `ValueError`.
+- `kb.synthesize` is in `capabilities().methods` and in the JSONL `HANDLERS`,
+ and is callable via `handle_request` end-to-end.
+
+Verification gate (fresh venv, editable install of this worktree):
+
+```
+$ ./.venv/bin/ruff check src tests
+All checks passed!
+
+$ ./.venv/bin/mypy src
+Success: no issues found in 30 source files
+
+$ ./.venv/bin/python -m pytest -q
+94 passed, 6 skipped in 0.81s
+```
+
+(The 6 skips are pre-existing numpy/embedding-optional tests, unrelated to this
+change.)
+
+Closes #222
diff --git a/README.md b/README.md
index a3f298c9..05d2078a 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,12 @@
+
+
+
+
+
+
> Agents should not start every session with amnesia — but they shouldn't get to write whatever they want either.
@@ -13,6 +19,8 @@
Still alpha — surface is small on purpose; expect breaking changes pre-1.0.
+> **Featured for Gittensor (SN74).** vouch ships a one-command starter pack for **Gittensor** — Bittensor subnet 74. `vouch init --template gittensor` seeds a cited, already-approved decision-memory of SN74's scoring model: merged-PR rewards, PAT verification, sybil-resistance, the repo allow-list, the issue multiplier, and the emission split. It's the durable *why* behind each rule — reviewed, cited, and committed alongside your code. → **[docs/gittensor.md](docs/gittensor.md)**
+
## Why this exists
Three opinionated choices distinguish vouch from the neighbours:
@@ -25,6 +33,7 @@ Three opinionated choices distinguish vouch from the neighbours:
Worth it when:
+- **You run or contribute to a Gittensor (SN74) repo.** Scoring weights, the repo allow-list, anti-sybil thresholds, and emission splits get debated across PRs, Discord, and validator changes — then settle into nobody's notes. `vouch init --template gittensor` gives you a cited, reviewed record of *why* each rule exists and what it superseded. See [docs/gittensor.md](docs/gittensor.md).
- **Multiple agents share a repo** (Claude Code + Cursor + a CI bot). Per-agent attribution in the audit log makes "which agent claimed what" answerable.
- **Sessions keep re-explaining the same context.** Curated, cited claims let new sessions start from your team's agreed answer instead of re-grepping.
- **You want decision records without the ADR ceremony.** `vouch crystallize` promotes a session's durable parts into proposals; one approve and they're permanent.
@@ -40,13 +49,21 @@ Skip it if:
## Install
```bash
-# from PyPI (published as vouch-kb; the command is still `vouch`)
+# one-liner (Linux + macOS) — picks a Python, ensures pipx, installs vouch-kb
+curl -fsSL https://raw.githubusercontent.com/vouchdev/vouch/main/install.sh | sh
+
+# …or directly via pipx (vouch-kb on PyPI; the command stays `vouch`)
pipx install vouch-kb
# …or from the cloned repo, in a venv
pip install -e '.[dev]'
```
+The one-liner is POSIX `sh`, never needs `sudo`, and detects an existing
+Claude Code install to point you at the next step (`vouch install-mcp
+claude-code`). Inspect it first if you'd like — it's [`install.sh`](install.sh)
+at the repo root.
+
## Quick start
```bash
@@ -63,6 +80,7 @@ vouch pending # list pending proposals
vouch show # full details
vouch approve # → durable artifact
vouch reject --reason "..."
+vouch expire --apply # optional: clear stale pending proposals
# 4. commit
git add .vouch/ && git commit -m "kb: approve auth-uses-jwt"
@@ -88,6 +106,24 @@ with your project's first real source and claim when you are ready.
The full captured walkthrough lives at [docs/example-session.md](docs/example-session.md); re-render the GIF from [docs/demo.tape](docs/demo.tape) with `vhs docs/demo.tape`.
+## Gittensor (SN74)
+
+vouch's first domain template targets **Gittensor** — Bittensor subnet 74, which rewards open-source contribution by rule. Its scoring model evolves across PRs, Discord, and validator changes, and the rationale usually lives in people's heads. vouch is the durable, cited memory for it:
+
+```bash
+cd your-gittensor-repo
+vouch init --template gittensor # seeds 1 source, 1 entity, 7 cited claims about SN74 scoring
+vouch status # durable: 7 claims · 1 source · 1 entity
+vouch search "emission split"
+git add .vouch && git commit -m "chore: add vouch decision-memory KB"
+```
+
+The seeded pack covers merged-PR rewards, PAT verification, scoring factors, sybil-resistance, the repo allow-list, the issue-solving multiplier, and the emission split — each a cited, approved, supersede-able claim. When a rule changes, `vouch supersede` the old claim with the new one so the history of *what changed* stays queryable.
+
+vouch stores **no** live signals — it is not a validator or miner client and never reads on-chain scores. It is the institutional memory that sits beside the live layer (Gittensory). The seeded claims are starter-grade; `vouch supersede` them with the real spec or PR once you confirm the live rule.
+
+Full adoption guide — install, seed, wire the MCP server, capture decisions as cited claims: **[docs/gittensor.md](docs/gittensor.md)**.
+
## Object model
```
@@ -141,13 +177,18 @@ vouch init # set up .vouch/ at PATH
vouch discover [--path P] # find the nearest .vouch/ root
vouch capabilities # emit the JSON capabilities descriptor
vouch status [--json] # KB counts + pending proposals
+vouch stats [--days N] [--json] # observability: queue, review rates, citations
vouch lint [--stale-days N] # user-actionable problems
vouch doctor # full sweep incl. source verification
+vouch fsck # deep consistency: indexes, lifecycle, decided
+vouch migrate [--check] [--dry-run] # upgrade .vouch/ format safely
vouch pending # list pending proposals
+vouch review [--limit N] [--type KIND] # guided proposal review queue
vouch show
vouch approve [--reason ...]
vouch reject --reason "..."
+vouch expire [--apply] [--days N] [--json] # GC stale pending proposals
vouch propose-claim --text ... --source ... [--type ...] [--confidence X]
vouch propose-page --title ... [--body -] [--claim ID ...]
@@ -176,6 +217,8 @@ vouch export --out path.tar.gz
vouch export-check path.tar.gz
vouch import-check path.tar.gz
vouch import-apply path.tar.gz [--on-conflict skip|overwrite|fail]
+vouch sync-check PATH_OR_BUNDLE
+vouch sync-apply PATH_OR_BUNDLE [--on-conflict fail|skip|propose]
vouch serve [--transport stdio|jsonl]
```
@@ -214,6 +257,31 @@ In your project's `.mcp.json`:
`VOUCH_AGENT` is recorded as `proposed_by` and as the actor on every audit event, so multi-agent setups can attribute writes correctly.
+## Running vouch as an OpenClaw plugin
+
+Vouch ships an [OpenClaw](https://github.com/dripsmvcp/openclaw) plugin manifest at the
+repo root — [`openclaw.plugin.json`](openclaw.plugin.json). Drop the vouch repo
+into an OpenClaw deployment and the plugin loader picks it up automatically:
+the MCP server, the four slash commands (`/vouch-recall`, `/vouch-status`,
+`/vouch-resolve-issue`, `/vouch-propose-from-pr`), and the CLAUDE.md fenced
+snippet become available as one bundle.
+
+The manifest declares vouch's trust boundary explicitly — remote callers'
+filesystem access is confined, every write tool routes through the review
+gate, every lifecycle op is audit-logged. The `configSchema` exposes only
+`kb_path`, `agent`, and `transport` — no API keys, no secrets; vouch is
+local-first.
+
+```bash
+# Inside an OpenClaw deployment that vendors plugin repos:
+openclaw plugin add vouchdev/vouch
+openclaw plugin enable vouch
+```
+
+The plugin's `mcpServers.vouch` block matches the same `.mcp.json` shape
+Claude Code uses — both platforms drive the same `vouch serve` process,
+so the kb.* surface is identical regardless of host.
+
## JSONL request/response shape
The JSONL transport reads one envelope per line on stdin, writes one per line on stdout:
@@ -225,6 +293,36 @@ The JSONL transport reads one envelope per line on stdin, writes one per line on
Errors come back with `ok:false` and a structured `error.code` (`method_not_found`, `missing_param`, `invalid_request`, `internal_error`).
+Every successful `kb.*` result that is object-shaped carries read-only trust metadata so clients can detect remote confinement:
+
+```json
+{
+ "id": "r1",
+ "ok": true,
+ "result": {
+ "backend": "fts5",
+ "hits": [],
+ "_meta": {
+ "vouch_trust": {
+ "remote": false,
+ "caller_kind": "jsonl",
+ "auth_subject": null
+ }
+ }
+ }
+}
+```
+
+| Transport | `remote` | `caller_kind` | `auth_subject` |
+|-----------|----------|---------------|----------------|
+| JSONL stdio | `false` | `jsonl` | `null` |
+| HTTP `/rpc` | `true` | `jsonl_http` | bearer fingerprint when authenticated |
+| MCP stdio | `false` | `mcp_stdio` | `null` |
+| HTTP `/mcp` | `true` | `mcp_http` | bearer fingerprint when authenticated |
+| CLI `--json` | `false` | `cli` | `null` |
+
+The block is server-attached metadata — client mutations are ignored. Array-shaped read results (e.g. `kb.list_claims`) pass through unchanged; trust rides on dict-shaped responses only (#233).
+
## Portable bundles
```bash
@@ -254,11 +352,11 @@ vouch import-apply kb.tar.gz --on-conflict skip # apply (default skip; never de
| Area | Current support |
|------|-----------------|
| Knowledge base | `.vouch/` folder, YAML claims/entities/relations/evidence/sessions, markdown pages with frontmatter, JSONL audit log, content-addressed sources |
-| CLI | `init`, `discover`, `capabilities`, `status`, `lint`, `doctor`, `pending`, `show`, `approve`, `reject`, `propose-{claim,page,entity,relation}`, `source add`, `source verify`, `supersede`, `contradict`, `archive`, `confirm`, `cite`, `session {start,end}`, `crystallize`, `search`, `context`, `index`, `audit`, `export`, `export-check`, `import-check`, `import-apply`, `serve` |
+| CLI | `init`, `discover`, `capabilities`, `status`, `lint`, `doctor`, `fsck`, `pending`, `show`, `approve`, `reject`, `propose-{claim,page,entity,relation}`, `source add`, `source verify`, `supersede`, `contradict`, `archive`, `confirm`, `cite`, `session {start,end}`, `crystallize`, `search`, `context`, `index`, `audit`, `export`, `export-check`, `import-check`, `import-apply`, `serve` |
| Tool servers | MCP over stdio + JSONL over stdin/stdout, same `kb.*` surface across both transports, capabilities + knowledge-capability descriptor |
| Schemas | 13 JSON Schemas (Draft 2020-12) generated from pydantic in [schemas/](schemas/), plus hand-maintained `bundle.manifest` and `jsonl-envelope` schemas |
| Write safety | review-gated writes via [proposed/](spec/review-gate.md), `dry_run:true` previews, host trust required for `approve`/`reject`, atomic exclusive-create storage, path-traversal blocked on source intake and bundle import |
-| Retrieval | SQLite FTS5 + substring fallback; optional semantic backends (`all-mpnet-base-v2`, `MiniLM-L6`, fastembed-BGE) behind install extras; context packs with citations + quality gate |
+| Retrieval | `retrieval.backend` in `config.yaml` selects the path: `auto` (default — embedding → FTS5 → substring), `embedding`, `fts5`, or `substring`. Semantic backends (`all-mpnet-base-v2`, `MiniLM-L6`, fastembed-BGE) ship behind install extras; `auto` degrades to FTS5 when they aren't installed. Context packs with citations + quality gate |
| Lifecycle | `supersede`, `contradict`, `archive`, `confirm`, `cite` — direct mutations, all audited |
| Portability | tar.gz bundles with per-file sha256 `manifest.json`, `export-check`, `import-check`, `import-apply` with skip/overwrite/fail conflict modes |
| Audit | append-only `audit.log.jsonl`, per-event actor (`VOUCH_AGENT`), object ids, dry-run flag, reversible flag |
@@ -268,7 +366,7 @@ vouch import-apply kb.tar.gz --on-conflict skip # apply (default skip; never de
## Status
-Pre-1.0. What's *not* in this implementation: vector embeddings (BM25/FTS5 only), per-runtime adapter templates, benchmark fixtures, multi-agent sync, scopes beyond a single field on Claim/Source. If a hole matters to you, file an issue.
+Pre-1.0. What's *not* in this implementation: per-runtime adapter templates, benchmark fixtures, multi-agent sync, scopes beyond a single field on Claim/Source. If a hole matters to you, file an issue.
## License
diff --git a/ROADMAP.md b/ROADMAP.md
index 4e5e661f..46101d1f 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -6,8 +6,11 @@ promise. Items marked **[VEP]** require a written proposal in
## 0.1 — surface stabilises (next)
-- Vector embeddings as an *optional* retrieval backend alongside FTS5.
- Default stays FTS5; embeddings opt-in via `config.yaml`. **[VEP]**
+- Vector embeddings as a retrieval backend alongside FTS5 (shipped).
+ Retrieval is controlled by `retrieval.backend` in `config.yaml`:
+ `auto` (default) tries embedding → FTS5 → substring, gracefully
+ degrading to FTS5 when the embeddings extras aren't installed; set
+ `embedding`, `fts5`, or `substring` to pin a single path.
- `vouch diff ` for claim/page revisions.
- `vouch approve --batch` for reviewing N proposals in one transaction.
- HTTP transport (`vouch serve --transport http`) behind a localhost
diff --git a/adapters/README.md b/adapters/README.md
index c9e15b42..f4edce8c 100644
--- a/adapters/README.md
+++ b/adapters/README.md
@@ -12,6 +12,7 @@ file you need into your project and edit it.
| [cursor/](cursor/) | Cursor IDE | `mcp.json` snippet |
| [codex/](codex/) | OpenAI's Codex CLI | `config.toml` snippet |
| [continue/](continue/) | Continue.dev | `config.json` snippet |
+| [openclaw/](openclaw/) | OpenClaw plugin host | `.openclaw/plugins.json`, `AGENTS.md` excerpt |
| [generic-mcp/](generic-mcp/) | Any MCP-speaking host | annotated reference |
| [jsonl-shell/](jsonl-shell/) | bash scripts via the JSONL transport | example pipeline |
diff --git a/adapters/claude-code/.claude/commands/vouch-propose-from-pr.md b/adapters/claude-code/.claude/commands/vouch-propose-from-pr.md
new file mode 100644
index 00000000..aca31fc7
--- /dev/null
+++ b/adapters/claude-code/.claude/commands/vouch-propose-from-pr.md
@@ -0,0 +1,25 @@
+---
+description: Distill a merged PR into vouch claim proposals
+---
+
+# /vouch-propose-from-pr
+
+A PR is a decision: someone proposed a change, the team accepted it. The
+"why" gets compressed into the merge message and forgotten. This command
+preserves the why as cited claims in the KB.
+
+Steps:
+
+1. Parse `$ARGUMENTS` as a PR URL or `/#`; default to the
+ most-recently-merged PR you authored.
+2. Fetch the PR title, body, and the merged-commit SHA via `gh`.
+3. Register the merge commit as a `kb_register_source` so subsequent claims
+ can cite it.
+4. Read the diff. For each *behavioural* change (not formatting / renaming),
+ draft one `kb_propose_claim` whose text summarises the new invariant the
+ code now upholds, citing the source from step 3.
+5. Propose at most five claims per PR. If a PR is that big, suggest the
+ contributor split it next time.
+
+Do not auto-approve. The KB's review gate is intentional; this command
+only fills the queue.
diff --git a/adapters/claude-code/.claude/commands/vouch-recall.md b/adapters/claude-code/.claude/commands/vouch-recall.md
new file mode 100644
index 00000000..d72d7168
--- /dev/null
+++ b/adapters/claude-code/.claude/commands/vouch-recall.md
@@ -0,0 +1,20 @@
+---
+description: Recall what the project's vouch KB knows about a topic
+---
+
+# /vouch-recall
+
+Use vouch's `kb_context` MCP tool to assemble a working set of claims, sources,
+and entities the KB already has on the current topic. Print them with their
+ids and citations; do not write anything.
+
+Steps:
+
+1. Call `kb_context` with `query: "$ARGUMENTS"`.
+2. List every returned claim by id + one-line text; for each, show the
+ source ids it cites.
+3. End with a one-sentence summary of what's *missing* from the KB on this
+ topic — the gap the user can fill with `/vouch-propose-from-pr` or
+ `kb_propose_claim`.
+
+Be terse. The KB is meant to remove ambiguity, not pad it.
diff --git a/adapters/claude-code/.claude/commands/vouch-resolve-issue.md b/adapters/claude-code/.claude/commands/vouch-resolve-issue.md
new file mode 100644
index 00000000..1b5aaf63
--- /dev/null
+++ b/adapters/claude-code/.claude/commands/vouch-resolve-issue.md
@@ -0,0 +1,27 @@
+---
+description: Use vouch's KB to ground a fix for a GitHub issue
+---
+
+# /vouch-resolve-issue
+
+Wire vouch's `kb_context` into an issue-resolution flow: the KB should
+inform the fix, and the act of solving should propose new claims that
+make the next contributor faster.
+
+Steps:
+
+1. Parse `$ARGUMENTS` as a GitHub issue URL or `/#` shorthand.
+ If neither, ask for clarification.
+2. `kb_context` with the issue title + body — what does the KB already know
+ about this area? Show the top 5 claims.
+3. Read the relevant code paths.
+4. Propose the smallest fix (run the project's tests first to confirm the
+ bug reproduces).
+5. After the fix is committed, propose **at most three** new claims via
+ `kb_propose_claim` that capture:
+ * the root cause in one sentence (cited by the offending file:line),
+ * the chosen fix pattern (cited by the patch commit), and
+ * any policy/precedent established (only if novel).
+
+Do not auto-approve. Leave the proposals in `.vouch/proposed/` for the
+maintainer to review with `vouch approve` after the PR merges.
diff --git a/adapters/claude-code/.claude/commands/vouch-status.md b/adapters/claude-code/.claude/commands/vouch-status.md
new file mode 100644
index 00000000..3320c642
--- /dev/null
+++ b/adapters/claude-code/.claude/commands/vouch-status.md
@@ -0,0 +1,23 @@
+---
+description: Show the project's vouch KB at a glance
+---
+
+# /vouch-status
+
+Run vouch's `kb_status` MCP tool and surface the result. Use this when the
+user asks "what's in our KB?" or before/after a long claim-proposal flow so
+they can see the proposal count tick up.
+
+Format:
+
+```
+KB at
+ claims:
+ sources:
+ entities:
+ pending: ← review queue depth
+ last audit:
+```
+
+If `pending > 0`, suggest the user run `vouch approve ` (or `vouch lint`
+if they want to inspect anything first). Do not propose anything yourself.
diff --git a/adapters/claude-code/.claude/settings.json b/adapters/claude-code/.claude/settings.json
new file mode 100644
index 00000000..11a1949a
--- /dev/null
+++ b/adapters/claude-code/.claude/settings.json
@@ -0,0 +1,34 @@
+{
+ "permissions": {
+ "allow": [
+ "mcp__vouch__kb_status",
+ "mcp__vouch__kb_capabilities",
+ "mcp__vouch__kb_search",
+ "mcp__vouch__kb_context",
+ "mcp__vouch__kb_read_claim",
+ "mcp__vouch__kb_read_source",
+ "mcp__vouch__kb_read_page",
+ "mcp__vouch__kb_read_entity",
+ "mcp__vouch__kb_read_relation",
+ "mcp__vouch__kb_list_claims",
+ "mcp__vouch__kb_list_sources",
+ "mcp__vouch__kb_list_pages",
+ "mcp__vouch__kb_list_entities",
+ "mcp__vouch__kb_list_relations",
+ "mcp__vouch__kb_list_pending"
+ ]
+ },
+ "hooks": {
+ "SessionStart": [
+ {
+ "matcher": "*",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "vouch status --json || true"
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/adapters/claude-code/install.yaml b/adapters/claude-code/install.yaml
new file mode 100644
index 00000000..fb063cee
--- /dev/null
+++ b/adapters/claude-code/install.yaml
@@ -0,0 +1,24 @@
+# Claude Code adapter manifest.
+#
+# T1 = MCP wire (project-local `.mcp.json` picked up by `claude` on launch).
+# T2 = CLAUDE.md fenced snippet (idempotent append, see install_adapter._install_fenced).
+# T3 = four custom slash commands (`/vouch-recall`, `/vouch-status`,
+# `/vouch-resolve-issue`, `/vouch-propose-from-pr`).
+# T4 = `.claude/settings.json` with a SessionStart hook + read-only kb_* auto-allow.
+host: claude-code
+pretty: Claude Code
+fence:
+ begin: ""
+ end: ""
+tiers:
+ T1:
+ - { src: .mcp.json, dst: .mcp.json }
+ T2:
+ - { src: CLAUDE.md.snippet, dst: CLAUDE.md, fenced_append: true }
+ T3:
+ - { src: .claude/commands/vouch-recall.md, dst: .claude/commands/vouch-recall.md }
+ - { src: .claude/commands/vouch-status.md, dst: .claude/commands/vouch-status.md }
+ - { src: .claude/commands/vouch-resolve-issue.md, dst: .claude/commands/vouch-resolve-issue.md }
+ - { src: .claude/commands/vouch-propose-from-pr.md, dst: .claude/commands/vouch-propose-from-pr.md }
+ T4:
+ - { src: .claude/settings.json, dst: .claude/settings.json }
diff --git a/adapters/claude-desktop/README.md b/adapters/claude-desktop/README.md
new file mode 100644
index 00000000..e02fe5f0
--- /dev/null
+++ b/adapters/claude-desktop/README.md
@@ -0,0 +1,12 @@
+# claude-desktop adapter
+
+Wires vouch into Anthropic's Claude Desktop app via Claude Desktop's MCP
+config file. Unlike Claude Code (project-local `.mcp.json`), Claude Desktop
+reads a single user-global JSON, so the writer drops a reviewable copy
+under `/claude-desktop/` and the user copies it into place — see
+the snippet for OS-specific paths.
+
+```sh
+vouch install-mcp claude-desktop --path .
+# then follow the printed README.md to finish the install
+```
diff --git a/adapters/claude-desktop/README.snippet.md b/adapters/claude-desktop/README.snippet.md
new file mode 100644
index 00000000..b4d3673a
--- /dev/null
+++ b/adapters/claude-desktop/README.snippet.md
@@ -0,0 +1,18 @@
+# Claude Desktop install
+
+This template is **not** auto-installed into Claude Desktop because the
+config file is user-global, not project-local. To finish the install:
+
+1. Locate Claude Desktop's config:
+ - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
+ - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
+ - Linux: `~/.config/Claude/claude_desktop_config.json`
+2. If the file doesn't exist, copy `claude_desktop_config.json` from this
+ directory to that path.
+3. If it does exist, merge the `mcpServers.vouch` entry into the existing
+ `mcpServers` map.
+4. Restart Claude Desktop. Then test with `kb_status` in a new chat.
+
+Set `VOUCH_KB_PATH=/abs/path/to/your-project/.vouch` in the `env:` block to
+point Claude Desktop at a specific KB (otherwise vouch walks up from the
+working directory it was launched in, which is rarely useful for a GUI app).
diff --git a/adapters/claude-desktop/claude_desktop_config.json b/adapters/claude-desktop/claude_desktop_config.json
new file mode 100644
index 00000000..be5b737b
--- /dev/null
+++ b/adapters/claude-desktop/claude_desktop_config.json
@@ -0,0 +1,11 @@
+{
+ "mcpServers": {
+ "vouch": {
+ "command": "vouch",
+ "args": ["serve"],
+ "env": {
+ "VOUCH_AGENT": "claude-desktop"
+ }
+ }
+ }
+}
diff --git a/adapters/claude-desktop/install.yaml b/adapters/claude-desktop/install.yaml
new file mode 100644
index 00000000..4ef13769
--- /dev/null
+++ b/adapters/claude-desktop/install.yaml
@@ -0,0 +1,20 @@
+# Claude Desktop adapter manifest.
+#
+# Claude Desktop's MCP config is user-global, NOT project-local:
+# macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
+# Windows: %APPDATA%\Claude\claude_desktop_config.json
+# Linux: ~/.config/Claude/claude_desktop_config.json
+#
+# `vouch install-mcp claude-desktop --path .` therefore drops the template
+# into `/claude-desktop/claude_desktop_config.json` so the user can
+# review the JSON and copy it themselves -- we intentionally don't reach
+# into the user's home directory from a project-scoped command (issue #179
+# explicitly defers uninstall + auto-detection; touching user-global state
+# would conflict with both). The README walks through the copy step.
+host: claude-desktop
+pretty: Claude Desktop
+tiers:
+ T1:
+ - { src: claude_desktop_config.json, dst: claude-desktop/claude_desktop_config.json }
+ T2:
+ - { src: README.snippet.md, dst: claude-desktop/README.md, fenced_append: true }
diff --git a/adapters/cline/README.md b/adapters/cline/README.md
new file mode 100644
index 00000000..7b24db34
--- /dev/null
+++ b/adapters/cline/README.md
@@ -0,0 +1,19 @@
+# cline adapter
+
+Wires vouch into Cline (the VSCode extension formerly known as
+Claude Dev). Cline reads MCP servers from two places:
+
+- **Workspace-scoped:** `.vscode/settings.json` under `cline.mcpServers`.
+- **User-global:** `~/.cline/mcp_servers.json`.
+
+```sh
+vouch install-mcp cline --path .
+```
+
+T1 drops a project-local `.cline/mcp_servers.json` (paste-ready into the
+user-global path), and T2 drops a `.vscode/settings.json` fragment that
+takes effect for the current workspace as soon as Cline reloads.
+
+If you have an existing `.vscode/settings.json`, the writer skips T2 — merge
+the `cline.mcpServers` block from this directory's `vscode_settings.json`
+into your existing file by hand.
diff --git a/adapters/cline/cline_mcp_servers.json b/adapters/cline/cline_mcp_servers.json
new file mode 100644
index 00000000..1961dd1b
--- /dev/null
+++ b/adapters/cline/cline_mcp_servers.json
@@ -0,0 +1,11 @@
+{
+ "mcpServers": {
+ "vouch": {
+ "command": "vouch",
+ "args": ["serve"],
+ "env": {
+ "VOUCH_AGENT": "cline"
+ }
+ }
+ }
+}
diff --git a/adapters/cline/install.yaml b/adapters/cline/install.yaml
new file mode 100644
index 00000000..74c531f8
--- /dev/null
+++ b/adapters/cline/install.yaml
@@ -0,0 +1,15 @@
+# Cline (VSCode extension) adapter manifest.
+#
+# Cline supports per-workspace MCP servers via the VSCode settings JSON
+# (`.vscode/settings.json` under the `cline.mcpServers` key) and a
+# user-global form under `~/.cline/mcp_servers.json`. Workspace settings
+# win for that workspace, so the writer drops both:
+# - the standalone `cline_mcp_servers.json` (paste-ready into the global file)
+# - a `.vscode/settings.json` fragment (workspace-scoped, ready as-is).
+host: cline
+pretty: Cline (VSCode)
+tiers:
+ T1:
+ - { src: cline_mcp_servers.json, dst: .cline/mcp_servers.json }
+ T2:
+ - { src: vscode_settings.json, dst: .vscode/settings.json }
diff --git a/adapters/cline/vscode_settings.json b/adapters/cline/vscode_settings.json
new file mode 100644
index 00000000..3c422fa4
--- /dev/null
+++ b/adapters/cline/vscode_settings.json
@@ -0,0 +1,11 @@
+{
+ "cline.mcpServers": {
+ "vouch": {
+ "command": "vouch",
+ "args": ["serve"],
+ "env": {
+ "VOUCH_AGENT": "cline"
+ }
+ }
+ }
+}
diff --git a/adapters/codex/install.yaml b/adapters/codex/install.yaml
new file mode 100644
index 00000000..7ba68b2f
--- /dev/null
+++ b/adapters/codex/install.yaml
@@ -0,0 +1,10 @@
+# OpenAI Codex CLI adapter manifest.
+#
+# Codex reads `/.codex/config.toml` (also `~/.codex/config.toml` for
+# user-global). We ship the project-local form so `vouch install-mcp codex`
+# doesn't touch home-directory state -- see issue #179 scope decision.
+host: codex
+pretty: OpenAI Codex CLI
+tiers:
+ T1:
+ - { src: config.toml, dst: .codex/config.toml }
diff --git a/adapters/continue/install.yaml b/adapters/continue/install.yaml
new file mode 100644
index 00000000..65573851
--- /dev/null
+++ b/adapters/continue/install.yaml
@@ -0,0 +1,12 @@
+# Continue.dev adapter manifest.
+#
+# Continue reads `/.continue/config.yaml` -- our template carries
+# only the `mcpServers:` block, so installs into a project that already has
+# a config.yaml are intentionally skipped (the dest exists -> ``skipped``).
+# Users with a pre-existing Continue config should merge by hand: the
+# mcpServers entry is one block; eyeballing it is trivial.
+host: continue
+pretty: Continue.dev
+tiers:
+ T1:
+ - { src: config.yaml, dst: .continue/config.yaml }
diff --git a/adapters/cursor/AGENTS.md.snippet b/adapters/cursor/AGENTS.md.snippet
new file mode 100644
index 00000000..7f309e72
--- /dev/null
+++ b/adapters/cursor/AGENTS.md.snippet
@@ -0,0 +1,31 @@
+# Vouch — knowledge base
+
+This repo uses **vouch** for durable agent knowledge. The KB lives in
+`.vouch/` and is reviewed in PRs like any other code.
+
+## How to remember things
+
+To preserve a fact, decision, or workflow across sessions:
+
+1. Register evidence: `kb_register_source` (or
+ `kb_register_source_from_path` for a file).
+2. Propose a claim that cites it: `kb_propose_claim`. Every claim
+ MUST cite at least one source or evidence id.
+3. For richer write-ups, propose pages: `kb_propose_page` with a
+ markdown body that references claims.
+
+You **cannot** write durable knowledge directly. Proposals land in
+`.vouch/proposed/` and require human approval via `vouch approve`.
+This is intentional.
+
+## How to read
+
+- `kb_search` for keyword search.
+- `kb_context` to fill a working set for a task ("what does this KB know
+ about X?").
+- `kb_read_*` for specific ids.
+
+## Identity
+
+Set `VOUCH_AGENT=cursor` in your env (or the MCP entry's `env:` block) so the
+audit log can attribute writes to this host.
diff --git a/adapters/cursor/install.yaml b/adapters/cursor/install.yaml
new file mode 100644
index 00000000..900a752f
--- /dev/null
+++ b/adapters/cursor/install.yaml
@@ -0,0 +1,17 @@
+# Cursor adapter manifest.
+#
+# T1 = Cursor's project-local MCP config at `/.cursor/mcp.json`.
+# Cursor honours this on workspace open; the user-level form at
+# `~/.cursor/mcp.json` is intentionally out of scope (see #179 acceptance —
+# we don't touch user-global config from a project-scoped install).
+# T2 = AGENTS.md fenced snippet (Cursor reads AGENTS.md the way Claude Code reads CLAUDE.md).
+host: cursor
+pretty: Cursor
+fence:
+ begin: ""
+ end: ""
+tiers:
+ T1:
+ - { src: mcp.json, dst: .cursor/mcp.json }
+ T2:
+ - { src: AGENTS.md.snippet, dst: AGENTS.md, fenced_append: true }
diff --git a/adapters/http-tunnel/Dockerfile b/adapters/http-tunnel/Dockerfile
new file mode 100644
index 00000000..893afb5e
--- /dev/null
+++ b/adapters/http-tunnel/Dockerfile
@@ -0,0 +1,46 @@
+# Reference container for `vouch serve --transport http`.
+#
+# Build context expected to be the repo root, OR a directory containing the
+# vouch-kb package on PyPI (uncomment the alternate install line below).
+# Either way, the resulting image listens on :8731 and reads the KB at
+# /data/.vouch — mount the host's .vouch/ into that path.
+
+FROM python:3.13-slim AS runtime
+
+ENV PYTHONDONTWRITEBYTECODE=1 \
+ PYTHONUNBUFFERED=1 \
+ VOUCH_HTTP_PORT=8731 \
+ VOUCH_KB_PATH=/data
+
+WORKDIR /app
+
+# Option A (default): install from this checkout. Allows local dev images
+# without a PyPI release in the loop.
+COPY pyproject.toml README.md ./
+COPY src ./src
+RUN pip install --no-cache-dir .
+
+# Option B (uncomment if you'd rather pin a released version):
+# RUN pip install --no-cache-dir vouch-kb>=0.2
+
+# /data is the KB volume mount point. Hosts that bind-mount a directory
+# containing .vouch/ here will be served as-is.
+VOLUME ["/data"]
+WORKDIR /data
+
+# Drop privileges -- the server reads/writes only inside /data/.vouch and
+# never spawns child processes, so a non-root UID is safe and prevents a
+# bug from rooting the container.
+RUN useradd --system --uid 10001 vouch && chown -R vouch:vouch /data
+USER vouch
+
+EXPOSE 8731
+
+# Bind 0.0.0.0 + --allow-public is what makes the container externally
+# reachable, but vouch refuses to start that combination unless at least one
+# bearer token is configured (VOUCH_HTTP_TOKEN env, --token flag, or
+# config.yaml serve.bearer_tokens). The deployment templates set
+# VOUCH_HTTP_TOKEN via secrets — see README.md.
+ENTRYPOINT ["vouch", "serve", "--transport", "http", \
+ "--host", "0.0.0.0", "--port", "8731", \
+ "--allow-public"]
diff --git a/adapters/http-tunnel/README.md b/adapters/http-tunnel/README.md
new file mode 100644
index 00000000..f6f62fca
--- /dev/null
+++ b/adapters/http-tunnel/README.md
@@ -0,0 +1,106 @@
+# http-tunnel — public-internet reference deployments for `vouch serve --transport http`
+
+`vouch serve --transport http` is loopback-only by default. To expose it to
+Claude.ai Custom Connectors (or any other off-host MCP client), you need:
+
+1. A public hostname with TLS.
+2. The vouch HTTP server bound on a non-loopback host, **with at least one
+ bearer token** (vouch refuses to run that combination without it — see
+ `--allow-public` in `vouch serve --help`).
+3. A way to keep the kb file-tree on disk reachable by the server process.
+
+This directory ships three drop-in templates for the same outcome — pick the
+one that matches your hosting:
+
+| File | Where | What you get |
+|------|-------|--------------|
+| [`Dockerfile`](Dockerfile) | anywhere with a container runtime | A minimal Python image that runs `vouch serve --transport http --host 0.0.0.0 --allow-public` against the `/data/.vouch` volume. |
+| [`fly.toml`](fly.toml) | fly.io | TLS-terminated public URL at `https://.fly.dev`, the Dockerfile is built and deployed by `fly deploy`. |
+| [`cloudflare-tunnel/compose.yml`](cloudflare-tunnel/compose.yml) | self-hosted | The same Dockerfile, fronted by a Cloudflare Tunnel sidecar so the vouch instance never opens an inbound port. |
+
+Every template treats the **bearer token as the trust boundary** — there is
+no public deployment of vouch without one. The CLI's own
+`--allow-public + --token` (or `serve.bearer_tokens` in `config.yaml`) gate is
+the last line of defence; the tunnel just makes the box reachable.
+
+## Common setup (every deployment)
+
+```bash
+# In the directory holding your .vouch/ knowledge base:
+export VOUCH_TOKEN=$(openssl rand -hex 32)
+cat > .vouch/config.yaml </mcp` (or `/messages` — both work)
+3. Auth: **Bearer**, paste your `VOUCH_TOKEN`
+4. Hit "Test" — Claude's validator probes `/health` (unauthenticated) and
+ then runs an MCP `initialize` handshake against `/mcp` (with the bearer).
+ Both should be green.
+
+## Wiring Claude mobile, Managed Agents, Computer Use, Messages API
+
+All four take an `mcp_servers` list with `{"url": "...", "type": "url",
+"authorization_token": "..."}`. Use the same URL + token you just gave
+Claude.ai.
+
+## Why a reference deployment lives in the repo
+
+PRs that add a new public-internet surface to a credential-bearing service
+are easier to land when the deployment scaffolding ships at the same time as
+the code. Reviewers can see *exactly* how the trust boundary is meant to be
+configured — there's no "and now you must do this safely off-screen" gap
+where a contributor follows a Medium tutorial and ends up with an open
+relay. See also #176 ("five Claude surfaces blocked") for the deployment
+contexts these templates were designed for.
diff --git a/adapters/http-tunnel/cloudflare-tunnel/compose.yml b/adapters/http-tunnel/cloudflare-tunnel/compose.yml
new file mode 100644
index 00000000..1e8a958c
--- /dev/null
+++ b/adapters/http-tunnel/cloudflare-tunnel/compose.yml
@@ -0,0 +1,55 @@
+# Cloudflare Tunnel reference deployment for `vouch serve --transport http`.
+#
+# Why this exists: the vouch instance opens NO inbound port -- the
+# `cloudflared` sidecar dials Cloudflare's edge from inside and proxies
+# requests in. That eliminates an entire class of "is my home network
+# safe?" worries and gives you a stable https://.cloudflare URL.
+#
+# Run from this directory:
+# export VOUCH_TOKEN="$(openssl rand -hex 32)"
+# export CLOUDFLARE_TUNNEL_TOKEN=""
+# docker compose up -d
+#
+# Then in the Cloudflare Zero Trust dashboard, set the tunnel's hostname to
+# point at `http://vouch:8731` (the in-compose service name).
+
+services:
+ vouch:
+ build:
+ # Build context is the repo root so the Dockerfile can COPY src/.
+ context: ../../..
+ dockerfile: adapters/http-tunnel/Dockerfile
+ container_name: vouch-http
+ environment:
+ # vouch's CLI flag --token reads VOUCH_HTTP_TOKEN by default; the
+ # config.yaml form (preferred for multi-token rotation) reads whatever
+ # name you reference. We use VOUCH_TOKEN here -- match it in your
+ # .vouch/config.yaml `bearer_token: env:VOUCH_TOKEN`.
+ VOUCH_HTTP_TOKEN: "${VOUCH_TOKEN:?set VOUCH_TOKEN before starting compose}"
+ volumes:
+ # Mount the host's .vouch/ knowledge base read-write into the container.
+ - "${VOUCH_KB_HOST_PATH:-./data}:/data"
+ restart: unless-stopped
+ # Internal network only -- nothing exposed to the host.
+ expose:
+ - "8731"
+ healthcheck:
+ # /healthz is unauthenticated by design (see VEP-0004).
+ test: ["CMD", "python", "-c",
+ "import urllib.request; urllib.request.urlopen('http://localhost:8731/healthz').read()"]
+ interval: 30s
+ timeout: 5s
+ retries: 3
+
+ cloudflared:
+ image: cloudflare/cloudflared:2024.10.0
+ container_name: vouch-cloudflared
+ restart: unless-stopped
+ command: tunnel --no-autoupdate run
+ environment:
+ # Token from the Cloudflare Zero Trust dashboard for the tunnel you
+ # created. cloudflared dials *out* with this token -- nothing inbound.
+ TUNNEL_TOKEN: "${CLOUDFLARE_TUNNEL_TOKEN:?set CLOUDFLARE_TUNNEL_TOKEN before starting compose}"
+ depends_on:
+ vouch:
+ condition: service_healthy
diff --git a/adapters/http-tunnel/fly.toml b/adapters/http-tunnel/fly.toml
new file mode 100644
index 00000000..bcc9aab6
--- /dev/null
+++ b/adapters/http-tunnel/fly.toml
@@ -0,0 +1,50 @@
+# fly.io deployment for `vouch serve --transport http`.
+#
+# Run from this directory:
+# fly launch --copy-config --name
+# fly secrets set VOUCH_TOKEN="$(openssl rand -hex 32)"
+# fly volumes create vouchdata --size 1
+# fly deploy
+#
+# Then point Claude.ai's Custom Connector at https://.fly.dev/mcp
+# with the same VOUCH_TOKEN as the bearer.
+
+# REPLACE with your app name before `fly deploy`. Either run
+# `fly launch --name --copy-config` (which rewrites this line for
+# you), or edit it here. `fly deploy` will refuse the literal "REPLACEME-vouch".
+app = "REPLACEME-vouch"
+primary_region = "iad"
+
+[build]
+dockerfile = "Dockerfile"
+
+[env]
+# vouch reads VOUCH_HTTP_TOKEN as a single-value fallback for serve's --token.
+# We re-bind it to VOUCH_TOKEN (the convention used by config.yaml's
+# `bearer_token: env:VOUCH_TOKEN`) at deploy time via `fly secrets`.
+VOUCH_HTTP_PORT = "8731"
+
+[[mounts]]
+source = "vouchdata"
+destination = "/data"
+
+[[services]]
+internal_port = 8731
+protocol = "tcp"
+auto_stop_machines = "stop"
+auto_start_machines = true
+min_machines_running = 0
+
+# fly's edge terminates TLS and forwards the connection over a private mesh.
+[[services.ports]]
+port = 443
+handlers = ["tls", "http"]
+
+[[services.http_checks]]
+# Liveness probe -- /healthz is unauthenticated by design so fly can probe
+# without needing the bearer token.
+path = "/healthz"
+interval = "15s"
+timeout = "5s"
+grace_period = "5s"
+method = "get"
diff --git a/adapters/openclaw/AGENTS.md.snippet b/adapters/openclaw/AGENTS.md.snippet
new file mode 100644
index 00000000..68852c4c
--- /dev/null
+++ b/adapters/openclaw/AGENTS.md.snippet
@@ -0,0 +1,21 @@
+# Vouch — knowledge base (OpenClaw plugin)
+
+This project loads vouch via the OpenClaw plugin manifest at
+[`openclaw.plugin.json`](https://github.com/vouchdev/vouch/blob/main/openclaw.plugin.json)
+in the vouch repo. The plugin exports the `vouch serve` MCP surface,
+the four bundled slash commands (`/vouch-recall`, `/vouch-status`,
+`/vouch-resolve-issue`, `/vouch-propose-from-pr`), and a trust
+boundary: every write tool is review-gated, every lifecycle op is
+audit-logged, and remote callers' filesystem access is confined.
+
+You **cannot** write durable knowledge directly. Proposals land in
+`.vouch/proposed/` and require human approval via `vouch approve`.
+This is intentional.
+
+- `kb_search` / `kb_context` to read.
+- `kb_propose_claim` / `kb_propose_page` to suggest additions — every
+ claim MUST cite at least one source or evidence id.
+- `kb_supersede` to replace a stale claim, `kb_contradict` to flag a
+ conflict for a human to resolve.
+
+You are recorded as `proposed_by: openclaw` in the audit log.
diff --git a/adapters/openclaw/README.md b/adapters/openclaw/README.md
new file mode 100644
index 00000000..d4f5b4cd
--- /dev/null
+++ b/adapters/openclaw/README.md
@@ -0,0 +1,33 @@
+# openclaw adapter
+
+Two different things both go by "the OpenClaw integration":
+
+1. **Loading vouch into an OpenClaw deployment.** That's the repo-root
+ [`openclaw.plugin.json`](../../openclaw.plugin.json) manifest — drop
+ the vouch repo into a deployment that vendors plugin repos and the
+ loader picks up the MCP server, the four slash commands, and the
+ trust-boundary declaration automatically. See the README section
+ "Running vouch as an OpenClaw plugin".
+2. **Enabling vouch in one OpenClaw-managed project.** That's this
+ adapter, run with `vouch install-mcp openclaw --path `.
+
+The writer drops:
+
+- `.openclaw/plugins.json` — declares the vouch plugin enabled for this
+ project (T1).
+- `AGENTS.md` — a fenced snippet pointing at the plugin manifest and
+ summarizing the review-gate contract (T2).
+- `.claude/commands/vouch-*.md` — the same four slash commands the
+ claude-code adapter ships, referenced in place rather than duplicated
+ (T3).
+- `.openclaw/policy.json` — the trust boundary as project-local policy:
+ review-gated writes, audit-logged lifecycle ops, confined filesystem
+ access for remote callers (T4).
+
+```sh
+vouch install-mcp openclaw --path .
+```
+
+Re-running is idempotent: existing files are left alone, and `AGENTS.md`
+gets the vouch block appended once inside a fence so reruns don't
+duplicate it.
diff --git a/adapters/openclaw/install.yaml b/adapters/openclaw/install.yaml
new file mode 100644
index 00000000..efd2ff8a
--- /dev/null
+++ b/adapters/openclaw/install.yaml
@@ -0,0 +1,37 @@
+# OpenClaw adapter manifest.
+#
+# OpenClaw loads vouch as a bundle plugin via the repo-root
+# `openclaw.plugin.json` manifest (see README.md "Running vouch as an
+# OpenClaw plugin"). That covers the *deployment* side -- dropping the
+# vouch repo into an OpenClaw install. This adapter covers the
+# per-*project* side: a project that wants vouch enabled gets a small
+# set of OpenClaw-native files, mirroring the claude-code adapter's tiers.
+#
+# T1 = `.openclaw/plugins.json` -- declares the vouch plugin enabled for
+# this project (the project-local analogue of running
+# `openclaw plugin enable vouch`).
+# T2 = AGENTS.md fenced snippet pointing at the plugin manifest and
+# summarizing the review-gate contract.
+# T3 = the same four slash commands the claude-code adapter ships,
+# referenced in place rather than duplicated (see
+# install_adapter.py's docstring; OpenClaw's loader bundles these
+# from adapters/claude-code/ directly).
+# T4 = `.openclaw/policy.json` -- the trust boundary as project-local
+# policy (review-gated writes, audit-logged lifecycle, confined fs).
+host: openclaw
+pretty: OpenClaw
+fence:
+ begin: ""
+ end: ""
+tiers:
+ T1:
+ - { src: plugins.json, dst: .openclaw/plugins.json }
+ T2:
+ - { src: AGENTS.md.snippet, dst: AGENTS.md, fenced_append: true }
+ T3:
+ - { src: ../claude-code/.claude/commands/vouch-recall.md, dst: .claude/commands/vouch-recall.md }
+ - { src: ../claude-code/.claude/commands/vouch-status.md, dst: .claude/commands/vouch-status.md }
+ - { src: ../claude-code/.claude/commands/vouch-resolve-issue.md, dst: .claude/commands/vouch-resolve-issue.md }
+ - { src: ../claude-code/.claude/commands/vouch-propose-from-pr.md, dst: .claude/commands/vouch-propose-from-pr.md }
+ T4:
+ - { src: policy.json, dst: .openclaw/policy.json }
diff --git a/adapters/openclaw/plugins.json b/adapters/openclaw/plugins.json
new file mode 100644
index 00000000..6525f04f
--- /dev/null
+++ b/adapters/openclaw/plugins.json
@@ -0,0 +1,11 @@
+{
+ "plugins": {
+ "vouch": {
+ "source": "github:vouchdev/vouch",
+ "enabled": true,
+ "config": {
+ "agent": "openclaw"
+ }
+ }
+ }
+}
diff --git a/adapters/openclaw/policy.json b/adapters/openclaw/policy.json
new file mode 100644
index 00000000..b646e77f
--- /dev/null
+++ b/adapters/openclaw/policy.json
@@ -0,0 +1,8 @@
+{
+ "vouch": {
+ "review_gated_writes": true,
+ "audit_logged_lifecycle": true,
+ "remote_callers_filesystem": "confined",
+ "agent": "openclaw"
+ }
+}
diff --git a/adapters/openclaw/vouch-context-engine.mjs b/adapters/openclaw/vouch-context-engine.mjs
new file mode 100644
index 00000000..81c15eac
--- /dev/null
+++ b/adapters/openclaw/vouch-context-engine.mjs
@@ -0,0 +1,115 @@
+/**
+ * OpenClaw plugin entry for the vouch-context engine (#228).
+ *
+ * The host loads this module via openclaw.plugin.json → openclaw.extensions.
+ * Runtime assembly delegates to the Python engine through `vouch openclaw-rpc`
+ * so the cited synthesis path stays identical to unit tests and kb.context.
+ *
+ * Enable in openclaw.json:
+ * plugins.slots.contextEngine: "vouch-context"
+ */
+
+import { spawnSync } from 'node:child_process';
+
+export const ENGINE_ID = 'vouch-context';
+export const ENGINE_NAME = 'Vouch Context Engine';
+
+/** @typedef {import('node:child_process').SpawnSyncReturns} SpawnResult */
+
+/**
+ * @param {string} method
+ * @param {Record} params
+ * @returns {Record}
+ */
+function callPythonEngine(method, params) {
+ const envelope = JSON.stringify({
+ id: 'openclaw',
+ method,
+ params,
+ });
+ /** @type {SpawnResult} */
+ const proc = spawnSync('vouch', ['openclaw-rpc'], {
+ input: envelope,
+ encoding: 'utf8',
+ env: process.env,
+ maxBuffer: 16 * 1024 * 1024,
+ });
+ if (proc.error) {
+ throw proc.error;
+ }
+ if (proc.status !== 0) {
+ const detail = (proc.stderr || proc.stdout || '').trim();
+ throw new Error(
+ `vouch openclaw-rpc exited ${proc.status}${detail ? `: ${detail}` : ''}`,
+ );
+ }
+ let parsed;
+ try {
+ parsed = JSON.parse(String(proc.stdout || '{}'));
+ } catch (err) {
+ throw new Error(`vouch openclaw-rpc returned invalid json: ${err}`);
+ }
+ if (!parsed.ok) {
+ const msg = parsed.error?.message || 'engine rpc failed';
+ throw new Error(msg);
+ }
+ return parsed.result;
+}
+
+/** @type {{ id: string; name: string; description: string; register: (api: any) => void }} */
+const entry = {
+ id: 'vouch-context-engine',
+ name: 'Vouch Context Engine',
+ description:
+ 'Review-gated KB context: cited retrieval + salience reflex + hot memory on every assemble()',
+
+ register(api) {
+ api.registerContextEngine(ENGINE_ID, (ctx) => {
+ const workspaceDir = ctx.workspaceDir;
+ const kbPath = ctx.kbPath ?? ctx.kb_path;
+ const agent = ctx.agent;
+ const project = ctx.project;
+
+ const baseParams = () => ({
+ workspaceDir,
+ kbPath,
+ agent,
+ project,
+ });
+
+ return {
+ info: {
+ id: ENGINE_ID,
+ name: ENGINE_NAME,
+ version: '0.1.0',
+ ownsCompaction: false,
+ },
+
+ async ingest({ sessionId, message, isHeartbeat }) {
+ return callPythonEngine('ingest', {
+ ...baseParams(),
+ sessionId,
+ message,
+ isHeartbeat,
+ });
+ },
+
+ async assemble(params) {
+ return callPythonEngine('assemble', {
+ ...baseParams(),
+ ...params,
+ });
+ },
+
+ async compact(params) {
+ return callPythonEngine('compact', {
+ ...baseParams(),
+ ...params,
+ });
+ },
+ };
+ });
+ },
+};
+
+export default entry;
diff --git a/adapters/windsurf/README.md b/adapters/windsurf/README.md
new file mode 100644
index 00000000..50de9fda
--- /dev/null
+++ b/adapters/windsurf/README.md
@@ -0,0 +1,14 @@
+# windsurf adapter
+
+Codeium's Windsurf IDE reads MCP servers from
+`/.codeium/windsurf/mcp_config.json` (workspace-scoped) or
+`~/.codeium/windsurf/mcp_config.json` (user-global). The writer installs
+the workspace form; if you want vouch available in every Windsurf workspace,
+copy the same JSON into the user-global path manually.
+
+```sh
+vouch install-mcp windsurf --path .
+```
+
+Then reload the Windsurf workspace. `kb_status` should appear in the MCP
+tools list.
diff --git a/adapters/windsurf/install.yaml b/adapters/windsurf/install.yaml
new file mode 100644
index 00000000..a868869c
--- /dev/null
+++ b/adapters/windsurf/install.yaml
@@ -0,0 +1,10 @@
+# Codeium Windsurf adapter manifest.
+#
+# Windsurf reads `/.codeium/windsurf/mcp_config.json` for
+# workspace-scoped MCP servers (also `~/.codeium/windsurf/mcp_config.json`
+# globally, which we leave alone per #179 scope).
+host: windsurf
+pretty: Codeium Windsurf
+tiers:
+ T1:
+ - { src: mcp_config.json, dst: .codeium/windsurf/mcp_config.json }
diff --git a/adapters/windsurf/mcp_config.json b/adapters/windsurf/mcp_config.json
new file mode 100644
index 00000000..a9adc2a8
--- /dev/null
+++ b/adapters/windsurf/mcp_config.json
@@ -0,0 +1,11 @@
+{
+ "mcpServers": {
+ "vouch": {
+ "command": "vouch",
+ "args": ["serve"],
+ "env": {
+ "VOUCH_AGENT": "windsurf"
+ }
+ }
+ }
+}
diff --git a/adapters/zed/README.md b/adapters/zed/README.md
new file mode 100644
index 00000000..26172f16
--- /dev/null
+++ b/adapters/zed/README.md
@@ -0,0 +1,14 @@
+# zed adapter
+
+Zed treats MCP servers as "context servers" under the `context_servers`
+key in its settings JSON. The writer ships the workspace form
+(`.zed/settings.json`); to enable vouch in every Zed workspace, paste the
+same block into `~/.config/zed/settings.json` instead.
+
+```sh
+vouch install-mcp zed --path .
+```
+
+If `.zed/settings.json` already exists, the writer skips it — merge the
+`context_servers.vouch` block from this directory's `settings.json` into
+your existing file by hand.
diff --git a/adapters/zed/install.yaml b/adapters/zed/install.yaml
new file mode 100644
index 00000000..1e8dab44
--- /dev/null
+++ b/adapters/zed/install.yaml
@@ -0,0 +1,11 @@
+# Zed adapter manifest.
+#
+# Zed embeds MCP server config under the `context_servers` key in
+# `.zed/settings.json` (workspace) or `~/.config/zed/settings.json` (user).
+# We ship the workspace form; the file is small enough that the user can
+# paste into their global settings if they prefer that scope.
+host: zed
+pretty: Zed
+tiers:
+ T1:
+ - { src: settings.json, dst: .zed/settings.json }
diff --git a/adapters/zed/settings.json b/adapters/zed/settings.json
new file mode 100644
index 00000000..48fe6045
--- /dev/null
+++ b/adapters/zed/settings.json
@@ -0,0 +1,14 @@
+{
+ "context_servers": {
+ "vouch": {
+ "command": {
+ "path": "vouch",
+ "args": ["serve"],
+ "env": {
+ "VOUCH_AGENT": "zed"
+ }
+ },
+ "settings": {}
+ }
+ }
+}
diff --git a/benchmarks/bench_bundle.py b/benchmarks/bench_bundle.py
new file mode 100644
index 00000000..6bee6382
--- /dev/null
+++ b/benchmarks/bench_bundle.py
@@ -0,0 +1,60 @@
+"""Bundle export + import + verify round-trip benchmarks."""
+from __future__ import annotations
+
+from pathlib import Path
+
+import pytest
+
+from vouch import bundle
+from vouch.health import rebuild_index
+from vouch.storage import KBStore
+
+
+@pytest.fixture(scope="module")
+def exported_bundle_1k(kb_1k, tmp_path_factory):
+ store = KBStore(kb_1k)
+ rebuild_index(store)
+ dest = tmp_path_factory.mktemp("bundles") / "bench_1k.tar.gz"
+ bundle.export(store.kb_dir, dest=dest, actor="bench")
+ return dest
+
+
+@pytest.fixture(scope="module")
+def exported_bundle_10k(kb_10k, tmp_path_factory):
+ store = KBStore(kb_10k)
+ rebuild_index(store)
+ dest = tmp_path_factory.mktemp("bundles") / "bench_10k.tar.gz"
+ bundle.export(store.kb_dir, dest=dest, actor="bench")
+ return dest
+
+
+def test_bundle_export_1k(benchmark, kb_1k, tmp_path):
+ store = KBStore(kb_1k)
+ dest = tmp_path / "out.tar.gz"
+ benchmark(bundle.export, store.kb_dir, dest=dest, actor="bench")
+
+
+def test_bundle_import_1k(benchmark, exported_bundle_1k, tmp_path):
+ dest_kb = KBStore.init(tmp_path)
+ benchmark(
+ bundle.import_apply,
+ dest_kb.kb_dir,
+ exported_bundle_1k,
+ on_conflict="overwrite",
+ actor="bench",
+ )
+
+
+def test_bundle_export_check_1k(benchmark, exported_bundle_1k):
+ benchmark(bundle.export_check, exported_bundle_1k)
+
+
+def test_bundle_import_10k(benchmark, exported_bundle_10k, tmp_path):
+ dest_kb = KBStore.init(tmp_path)
+ benchmark(
+ bundle.import_apply,
+ dest_kb.kb_dir,
+ exported_bundle_10k,
+ on_conflict="overwrite",
+ actor="bench",
+ )
diff --git a/benchmarks/bench_index_rebuild.py b/benchmarks/bench_index_rebuild.py
new file mode 100644
index 00000000..7dcf8da8
--- /dev/null
+++ b/benchmarks/bench_index_rebuild.py
@@ -0,0 +1,19 @@
+"""Index rebuild latency benchmarks at varying KB sizes."""
+from __future__ import annotations
+
+import pytest
+
+from vouch.health import rebuild_index
+from vouch.storage import KBStore
+
+
+def test_index_rebuild_1k(benchmark, kb_1k):
+ """Index rebuild time at 1k claims."""
+ store = KBStore(kb_1k)
+ benchmark(rebuild_index, store)
+
+
+def test_index_rebuild_10k(benchmark, kb_10k):
+ """Index rebuild time at 10k claims."""
+ store = KBStore(kb_10k)
+ benchmark(rebuild_index, store)
diff --git a/benchmarks/bench_propose.py b/benchmarks/bench_propose.py
new file mode 100644
index 00000000..03a1bba1
--- /dev/null
+++ b/benchmarks/bench_propose.py
@@ -0,0 +1,35 @@
+"""Proposal write latency benchmarks."""
+from __future__ import annotations
+
+import pytest
+
+from vouch.proposals import propose_claim
+from vouch.storage import KBStore
+
+
+@pytest.fixture
+def fresh_store(tmp_path):
+ return KBStore.init(tmp_path)
+
+
+@pytest.fixture
+def source_id(fresh_store):
+ src = fresh_store.put_source(b"benchmark evidence")
+ return src.id
+
+
+def test_propose_claim_latency(benchmark, fresh_store, source_id):
+ """propose_claim() write latency — sits in the agent hot loop."""
+ counter = [0]
+
+ def _propose():
+ counter[0] += 1
+ return propose_claim(
+ fresh_store,
+ text=f"benchmark claim {counter[0]}",
+ evidence=[source_id],
+ proposed_by="bench-agent",
+ )
+
+ result = benchmark(_propose)
+ assert result is not None
diff --git a/benchmarks/bench_search.py b/benchmarks/bench_search.py
new file mode 100644
index 00000000..d0866801
--- /dev/null
+++ b/benchmarks/bench_search.py
@@ -0,0 +1,46 @@
+"""Search latency benchmarks at varying KB sizes."""
+from __future__ import annotations
+
+from pathlib import Path
+
+import pytest
+
+from vouch import index_db
+from vouch.health import rebuild_index
+from vouch.storage import KBStore
+
+
+def _store(kb_path: Path) -> KBStore:
+ store = KBStore(kb_path)
+ rebuild_index(store)
+ return store
+
+
+@pytest.fixture(scope="module")
+def store_1k(kb_1k):
+ return _store(kb_1k)
+
+
+@pytest.fixture(scope="module")
+def store_10k(kb_10k):
+ return _store(kb_10k)
+
+
+def test_search_fts5_1k(benchmark, store_1k):
+ """FTS5 search latency on a 1k-claim KB."""
+ benchmark(index_db.search, store_1k.kb_dir, "auth uses JWT", limit=10)
+
+
+def test_search_fts5_10k(benchmark, store_10k):
+ """FTS5 search latency on a 10k-claim KB."""
+ benchmark(index_db.search, store_10k.kb_dir, "auth uses JWT", limit=10)
+
+
+def test_search_substring_1k(benchmark, store_1k):
+ """Substring fallback search latency on a 1k-claim KB."""
+ benchmark(store_1k.search_substring, "auth", limit=10)
+
+
+def test_search_substring_10k(benchmark, store_10k):
+ """Substring fallback search latency on a 10k-claim KB."""
+ benchmark(store_10k.search_substring, "auth", limit=10)
diff --git a/benchmarks/conftest.py b/benchmarks/conftest.py
new file mode 100644
index 00000000..8951f10f
--- /dev/null
+++ b/benchmarks/conftest.py
@@ -0,0 +1,43 @@
+"""pytest-benchmark configuration for vouch benchmarks."""
+from __future__ import annotations
+
+import subprocess
+import sys
+from pathlib import Path
+
+import pytest
+
+
+def pytest_configure(config):
+ """Require pytest-benchmark."""
+ try:
+ import pytest_benchmark # noqa: F401
+ except ImportError:
+ pytest.exit(
+ "pytest-benchmark is required: pip install pytest-benchmark",
+ returncode=1,
+ )
+
+
+@pytest.fixture(scope="session")
+def kb_1k(tmp_path_factory):
+ return _gen_kb(tmp_path_factory, claims=1_000)
+
+
+@pytest.fixture(scope="session")
+def kb_10k(tmp_path_factory):
+ return _gen_kb(tmp_path_factory, claims=10_000)
+
+
+@pytest.fixture(scope="session")
+def kb_100k(tmp_path_factory):
+ return _gen_kb(tmp_path_factory, claims=100_000)
+
+
+def _gen_kb(tmp_path_factory, *, claims: int) -> Path:
+ out = tmp_path_factory.mktemp(f"kb{claims}")
+ subprocess.check_call(
+ [sys.executable, "benchmarks/fixtures/gen_kb.py",
+ "--out", str(out), "--claims", str(claims)],
+ )
+ return out
diff --git a/docs/README.md b/docs/README.md
index 0025fb74..12f4e8fb 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -21,6 +21,8 @@ If you're new, read in this order:
one KB without stepping on each other.
8. [faq.md](faq.md) — questions we keep getting.
9. [embeddings.md](embeddings.md) — semantic retrieval (primary backend)
+10. [gittensor.md](gittensor.md) — adopting vouch as a Gittensor repo's
+ review-gated decision memory.
### How docs relate to other directories
diff --git a/docs/gittensor.md b/docs/gittensor.md
new file mode 100644
index 00000000..9db2abab
--- /dev/null
+++ b/docs/gittensor.md
@@ -0,0 +1,157 @@
+# Adopting vouch for a Gittensor repo
+
+Gittensor (Bittensor subnet 74) rewards open-source contributions, and its
+scoring rules, repo allow-list, anti-sybil measures, and emission-split
+decisions evolve and get debated across PRs, Discord, and validator changes.
+That rationale usually lives in people's heads and scattered threads — so when
+a weight changes or a repo is de-listed, there's no durable, cited answer to
+*"why did we decide this, and what did it replace?"*
+
+vouch is a good fit for that gap: a small, **review-gated, cited** knowledge
+base committed to the repo as the durable memory layer for maintainer and
+validator decisions.
+
+## vouch vs. Gittensory — different layers
+
+These are complementary, not competing:
+
+| Layer | Owner | Holds |
+|---|---|---|
+| Chain / scoring | Gittensor (SN74) | the actual weights and emissions |
+| **Live signals** | **Gittensory** | scores, queues, collision/reviewability |
+| **Durable decisions** | **vouch** | *why* a rule exists, what it superseded — cited & reviewed |
+
+vouch deliberately stores **no** live signals. It is not a validator or miner
+client; it doesn't read on-chain scores, verify PATs, or submit weights. It is
+the institutional memory that sits alongside the live layer.
+
+## 1. Install
+
+```bash
+pipx install vouch-kb # the installed command is `vouch`
+vouch --version
+```
+
+## 2. Seed a KB with the gittensor pack
+
+From the root of the Gittensor repo:
+
+```bash
+vouch init --template gittensor
+```
+
+This creates `.vouch/` and seeds a cited, approved starter pack about SN74
+scoring — **1 source, 1 entity, 7 claims** (merged-PR rewards, PAT
+verification, scoring factors, sybil-resistance, repo allow-list policy,
+issue-solving multiplier, and emission split):
+
+```bash
+vouch status
+# durable: 7 claims • 1 sources • 1 entities • …
+vouch search "scoring"
+# claim/gittensor-merged-pr-base-reward …primary OSS reward signal…
+# claim/gittensor-sybil-resistance …GitHub verification + merged-PR…
+vouch doctor
+# index present, citations resolve, sources verify → clean
+```
+
+Commit it so the whole team shares one memory:
+
+```bash
+git add .vouch && git commit -m "chore: add vouch decision-memory KB"
+```
+
+`.vouch/.gitignore` keeps `proposed/` (drafts) and `state.db` (the derived
+index) out of history automatically.
+
+> **The seeded claims are starter-grade.** They summarize the scoring model as
+> understood when the template was authored. Before you rely on a specific
+> rule or number, `vouch show ` it and `vouch supersede` it with the
+> real spec/PR citation (see §4) so the KB reflects the live rules.
+
+## 3. Wire the MCP server for agents
+
+Add `.mcp.json` at the repo root so any MCP host (Claude Code, Cursor, Codex)
+can query the KB and get cited answers instead of guessing:
+
+```json
+{
+ "mcpServers": {
+ "vouch": { "command": "vouch", "args": ["serve"] }
+ }
+}
+```
+
+An agent working in the repo can now call `kb.search` / `kb.context` ("how does
+scoring work today?") and `kb.propose_claim` to draft new knowledge (still
+gated — see below).
+
+## 4. Capture decisions as cited claims
+
+The whole value is that every scoring/policy decision is **proposed, reviewed,
+cited, and supersede-able**. When a change lands:
+
+```bash
+# 1) register the thing you're citing — the PR, a spec file, a thread export
+vouch source add docs/validator-change-pr-200.md # → a source id
+
+# 2) propose a claim that cites it
+vouch propose-claim \
+ --text "SN74 raised the maintainer issue-solving multiplier from 1.66 to 1.75." \
+ --source --type fact --confidence 0.9 --tag gittensor --tag scoring
+# → proposal id
+
+# 3) a *different* maintainer approves (the proposer can't self-approve)
+vouch pending
+vouch approve
+git add .vouch && git commit -m "kb: record maintainer-multiplier change (PR #200)"
+```
+
+If you try to approve your own proposal you'll get
+`forbidden_self_approval` — that's the gate working. A maintainer with a
+different identity must approve.
+
+**When a rule changes, supersede — don't overwrite.** Propose and approve the
+replacement claim (steps 1–3 above), then link the old one to it by id:
+
+```bash
+vouch supersede
+```
+
+The old claim is kept (marked superseded) so the history of what changed stays
+intact and queryable.
+
+Every write is in `.vouch/audit.log.jsonl` — `vouch audit` shows exactly who
+proposed and who approved each change, so the history of *why* is queryable,
+not lost.
+
+## 5. A CONTRIBUTING note for the repo
+
+Drop a short note into the Gittensor repo's `CONTRIBUTING.md` so the habit
+sticks:
+
+```markdown
+### Recording scoring / policy decisions
+
+When a change alters scoring, the repo allow-list, anti-sybil thresholds, or
+emission split, record it in vouch as a cited claim:
+
+1. `vouch source add` the PR or spec that drives it.
+2. `vouch propose-claim --source --type fact|decision` (or
+ `vouch supersede` the claim it replaces).
+3. A maintainer reviews with `vouch pending` / `vouch approve`.
+
+Cite the PR. Don't bury the rationale in a thread.
+```
+
+## 6. Day-to-day
+
+```bash
+vouch context "how are merged PRs scored and what stops sybil mining"
+# → a ranked, cited pack ready to paste into an agent prompt
+vouch search "emission" --semantic # if installed with the [embeddings] extra
+vouch lint # broken citations / stale claims
+```
+
+That's the loop: live signals come from Gittensory; the durable *why* lives in
+vouch, one cited and reviewed claim at a time.
diff --git a/docs/metrics.md b/docs/metrics.md
new file mode 100644
index 00000000..c7f6f0c4
--- /dev/null
+++ b/docs/metrics.md
@@ -0,0 +1,155 @@
+# `vouch metrics` — observability
+
+`vouch status` tells you the KB is *alive* (artifact counts). `vouch metrics`
+tells you the **review gate** and **corpus** are *healthy*: how often proposals
+get approved, how stale the corpus is, how long claims sit pending, and who is
+doing the proposing and approving.
+
+Everything is computed **read-only** from two sources that already exist on
+disk — there is **no new state**, no schema migration, nothing to back up:
+
+- `.vouch/audit.log.jsonl` — the append-only event stream (proposal
+ create/approve/reject, claim lifecycle), each event carrying a timestamp and
+ an actor.
+- the artifact files (`claims/*.yaml`, `sources/*`, `evidence/*`), read through
+ the normal `KBStore` API.
+
+## Usage
+
+```bash
+vouch metrics # human-readable table, all of history
+vouch metrics --json # stable JSON schema (this document)
+vouch metrics --prometheus # Prometheus textfile-collector exposition
+vouch metrics --since 30d # only the last 30 days of the audit log
+vouch metrics --since 2026-01-01 --until 2026-02-01
+vouch metrics --stale-days 90 # tighten the "stale claim" threshold
+vouch metrics --top 10 # show 10 actors in the leaderboard (0 = all)
+```
+
+### `--since` / `--until` formats
+
+| Form | Example | Meaning |
+|------|---------|---------|
+| duration | `30d`, `12h`, `2w`, `90m`, `45s` | counted back from now |
+| ISO date | `2026-01-01` | midnight UTC on that date |
+| ISO datetime | `2026-01-01T06:30:00+00:00` | exact instant (naive → UTC) |
+| `all` / omitted | — | no bound |
+
+The window applies to **audit-derived** metrics (the review gate, lag,
+actors). **Corpus** metrics (citation coverage, stale ratio, status histogram)
+always reflect *current* on-disk state — a claim is stale now regardless of the
+window you ask for.
+
+## Metrics
+
+| Metric | Meaning |
+|--------|---------|
+| `approval_rate` | `approvals / (approvals + rejections)` over the window. `null` when there were no decisions. |
+| `approval_rate_by_kind` | the same ratio split per `ProposalKind` (`claim`, `page`, `entity`, `relation`). |
+| `citation_coverage` | fraction of claims whose every `evidence` id resolves to a live Source or Evidence. |
+| `citation_broken` | count of claims with at least one unresolved citation. |
+| `stale_ratio` | `stale_claims / claims_active`, where a claim is stale if its freshness anchor (`last_confirmed_at`, else `updated_at`, else `created_at`) is older than `--stale-days` (default 180). Retired claims (superseded/archived/redacted) are exempt. |
+| `proposal_lag_seconds` | latency from a proposal's `*.create` event to its matching `*.approve`, as `p50` / `p90` / `p99` / `mean` / `max` (nearest-rank percentiles, matching Prometheus `histogram_quantile`). |
+| `actors` | top-N actors by total activity, each with `proposed` / `approved` / `rejected` / `confirmed` counts. |
+
+A create event older than `--since` still pairs with an in-window approve, so
+the left edge of the window does not systematically undercount lag.
+
+## JSON schema (stable)
+
+`--json` emits the following shape. Treat field renames or removals as
+breaking; `schema_version` is bumped when that happens.
+
+```json
+{
+ "schema_version": 1,
+ "window": {
+ "since": "2026-05-11T12:00:00+00:00",
+ "until": null,
+ "generated_at": "2026-06-10T12:00:00+00:00"
+ },
+ "review_gate": {
+ "proposals_created": 4,
+ "approvals": 3,
+ "rejections": 1,
+ "approval_rate": 0.75,
+ "approval_rate_by_kind": { "claim": 0.6666666666666666, "page": 1.0 },
+ "decisions_by_kind": {
+ "claim": { "approve": 2, "reject": 1 },
+ "page": { "approve": 1, "reject": 0 }
+ },
+ "pending_now": 0
+ },
+ "corpus": {
+ "claims_total": 5,
+ "claims_active": 4,
+ "claims_cited": 4,
+ "citation_coverage": 0.8,
+ "citation_broken": 1,
+ "stale_claims": 1,
+ "stale_ratio": 0.25,
+ "stale_after_days": 180,
+ "claims_by_status": { "working": 4, "archived": 1 }
+ },
+ "proposal_lag_seconds": {
+ "count": 3,
+ "p50": 20.0,
+ "p90": 30.0,
+ "p99": 30.0,
+ "mean": 20.0,
+ "max": 30.0
+ },
+ "actors": [
+ { "actor": "bob", "proposed": 2, "approved": 2, "rejected": 0, "confirmed": 0, "total": 4 },
+ { "actor": "alice", "proposed": 2, "approved": 1, "rejected": 1, "confirmed": 1, "total": 5 }
+ ],
+ "audit": { "events_total": 9, "events_in_window": 9 }
+}
+```
+
+### `null` semantics
+
+Ratios are `null`, **not** `0`, when their denominator is empty
+(`approval_rate` with no decisions, `citation_coverage` with no claims,
+`stale_ratio` with no active claims). This lets a consumer distinguish "no
+data" from a genuine zero. The Prometheus exposition **omits** null gauges
+entirely for the same reason — emitting `0` would lie about the denominator.
+
+## Prometheus textfile collector
+
+`--prometheus` emits gauges with `# HELP` / `# TYPE` headers, prefixed
+`vouch_`. Wire it up with a cron sidecar writing to the node_exporter textfile
+directory:
+
+```bash
+# /etc/cron.d/vouch-metrics — every 5 minutes
+*/5 * * * * app cd /srv/project && vouch metrics --prometheus \
+ > /var/lib/node_exporter/textfile/vouch.prom.$$ \
+ && mv /var/lib/node_exporter/textfile/vouch.prom.$$ \
+ /var/lib/node_exporter/textfile/vouch.prom
+```
+
+(The temp-file-then-`mv` makes the write atomic so the collector never reads a
+half-written file.)
+
+Example exposition:
+
+```text
+# HELP vouch_approval_rate approve / (approve + reject) over window.
+# TYPE vouch_approval_rate gauge
+vouch_approval_rate 0.75
+# HELP vouch_citation_coverage Fraction of claims fully cited.
+# TYPE vouch_citation_coverage gauge
+vouch_citation_coverage 0.8
+# HELP vouch_claims_by_status Claim count per status.
+# TYPE vouch_claims_by_status gauge
+vouch_claims_by_status{status="archived"} 1
+vouch_claims_by_status{status="working"} 4
+```
+
+## Out of scope
+
+- **Pushing** to Prometheus/Datadog — use the `--json` or `--prometheus`
+ output with a sidecar; `vouch metrics` never makes a network call.
+- **Long-term retention / TSDB** — the audit log is the source of truth;
+ windowing happens at read time.
diff --git a/docs/migrations.md b/docs/migrations.md
new file mode 100644
index 00000000..79627f15
--- /dev/null
+++ b/docs/migrations.md
@@ -0,0 +1,75 @@
+# Schema migrations — `vouch migrate`
+
+The `.vouch/` layout is the durable contract: yaml claims, markdown pages with
+frontmatter, json sessions, the jsonl audit log. As the pydantic models in
+`src/vouch/models.py` evolve, a KB created against an older model would become a
+load-time error. `vouch migrate` gives that evolution a versioned, reversible,
+audit-logged upgrade path so a schema change never silently breaks a KB in the
+wild.
+
+## Two version axes
+
+vouch tracks two independent versions, and `vouch migrate` covers both:
+
+| | Stamp | What it governs | Reached by |
+|---|---|---|---|
+| **Format** (integer) | `config.yaml` `version` | the `.vouch/` directory layout (subdirs, `.gitignore`) | `vouch migrate` *(no subcommand)* |
+| **Schema** (semver) | `.vouch/schema_version` | the model schema of each artifact | `vouch migrate ` |
+
+A KB with no `.vouch/schema_version` file is treated as the baseline (`0.1.0`),
+so existing KBs keep loading until their first migrate. `vouch init` stamps the
+current version on bootstrap.
+
+## Commands
+
+```bash
+vouch migrate status # current schema version, target, pending migrations
+vouch migrate plan # dry-run: every file each pending migration would change
+vouch migrate plan --to 0.3.0 # plan against a specific target
+vouch migrate apply # apply pending migrations (audit-logged, atomic)
+vouch migrate apply --yes # skip the confirmation prompt (CI)
+vouch migrate rollback # reverse the most recently applied migration
+vouch migrate verify # parse-load every artifact under the current version
+```
+
+## Manifests
+
+Migrations are **data, not code**: yaml manifests in the repo-root `migrations/`
+directory, one consecutive version step each. See
+[`migrations/README.md`](../migrations/README.md) for the full format and the
+transform verbs (`rename`, `default`, `drop`, `split`, `merge`). Only consecutive
+manifests apply — a `0.1 → 0.5` KB walks through `0.2`, `0.3`, `0.4` in order.
+
+## Safety model
+
+- **Atomic per file.** Each artifact is rewritten to a temp file, `fsync`-ed,
+ then `os.replace`-d into place. A file is always its old bytes or its new
+ bytes — never a torn mix.
+- **Reversible.** Before any rewrite, the prior content of every file the step
+ touches is journalled to `.vouch/migrations/rollback-.jsonl`. `vouch
+ migrate rollback` replays it, restoring the exact prior bytes — so
+ apply → rollback is byte-equivalent (modulo the audit-log entries recording
+ the round trip).
+- **Crash-safe.** The journal is written and fsynced *before* the first rewrite,
+ and `.vouch/schema_version` is bumped *last*. An interrupted apply therefore
+ leaves the KB reporting its prior version, with a journal to recover from.
+- **Precondition.** Apply refuses if the KB has pending proposals — it won't
+ rewrite a reviewer's in-flight queue out from under them. Resolve them with
+ `vouch list-pending` first.
+- **`state.db` is disposable.** The runner does not migrate the FTS5 / embedding
+ cache; the CLI rebuilds it after a successful apply.
+
+## Audit trail
+
+Each applied manifest logs one `kb.migrate.apply` event (manifest id, file count,
+rollback-journal id); a rollback logs `kb.migrate.rollback`. The legacy format
+migration continues to log `kb.migrate`.
+
+## Out of scope (v1)
+
+- Cross-major jumps in a single manifest (consecutive only).
+- `custom: path/to/migration.py` script transforms (lands once the verb
+ taxonomy stabilises).
+- Migrating `state.db` (disposable; rebuilt by `vouch index`).
+- Bundle version embedding / refusing mismatched imports — a follow-up.
+- Concurrent multi-process migrate runs (single-writer assumption).
diff --git a/docs/multi-agent.md b/docs/multi-agent.md
index b4918dea..2ea14edd 100644
--- a/docs/multi-agent.md
+++ b/docs/multi-agent.md
@@ -66,7 +66,9 @@ Two agents will eventually disagree. What it looks like:
- **Duplicate proposals.** Two agents independently propose the same
fact. The reviewer sees two pending proposals with similar text.
- Approve one, reject the other with reason "duplicate of prop-XYZ".
+ `kb.propose_claim` / `vouch propose-claim` return non-blocking
+ `warnings` (`similar_approved`, `similar_pending`) when embeddings are
+ available — approve one, reject the other with reason "duplicate of prop-XYZ".
- **Contradicting claims approved.** Two reviewers approved
conflicting claims at different times. Use `vouch contradict A B` to
link them; pick a survivor with `vouch supersede`.
@@ -76,12 +78,18 @@ Two agents will eventually disagree. What it looks like:
## Tracking who's busy
+```bash
+vouch stats # pending by agent, review rates, citation coverage
+vouch stats --json # same, for dashboards / CI
+```
+
+Or, if you only need the queue breakdown:
+
```bash
vouch pending --json | jq -r '.[] | "\(.proposed_by)\t\(.id)"' | sort | uniq -c
```
-Tells you which agent has the most pending work. Useful when one
-agent has been spammy or is asleep at the wheel.
+Useful when one agent has been spammy or is asleep at the wheel.
## Crystallisation per agent
@@ -94,13 +102,26 @@ vouch session start --task "implement password reset" \
--note "tag:agent:claude-code-anna"
```
+## Distributed sync
+
+When two teammates each have their own `.vouch/` directory, use the
+sync workflow to reconcile them deterministically:
+
+```bash
+vouch sync-check ../other-repo
+vouch sync-apply ../other-repo --on-conflict fail
+```
+
+`sync-check` accepts either another repo / `.vouch` directory or a
+bundle. It reports new files, identical files, and conflicts without
+writing anything. `sync-apply` imports non-conflicting files only; it
+never overwrites reviewed knowledge. Use `--on-conflict skip` to leave
+conflicts untouched, or `--on-conflict propose` to write a local conflict
+report under `proposed/sync-reports/` for human review. `config.yaml`
+stays local to each KB and is not synced.
+
## What doesn't work yet
-- **Distributed `.vouch/` directories that sync.** Today it's one
- filesystem. If two teammates each have their own `.vouch/` and want
- them to merge, the path is bundle export + import-check, manually,
- for now. See [bundles.md](bundles.md) and the multi-agent-sync
- roadmap item.
- **Live merge conflicts.** Two agents editing the same proposal at
once isn't a scenario vouch addresses — agents create proposals,
they don't edit existing ones.
diff --git a/docs/provenance.md b/docs/provenance.md
new file mode 100644
index 00000000..915aa68b
--- /dev/null
+++ b/docs/provenance.md
@@ -0,0 +1,97 @@
+# Provenance — `vouch why` / `trace` / `impact`
+
+vouch already records everything needed to explain a claim's existence — the
+session that proposed it, the source it cites, the supersedes chain it sits on,
+the contradiction that demoted it, the page that embeds it as evidence — but it
+is scattered across `audit.log.jsonl`, `relations/`, `evidence/`, `sessions/`
+and the claim file itself. The provenance layer reconstructs a single typed
+directed graph from those artifacts so a reviewer in front of a 500-item queue
+can ask the two obvious questions: *why is this claim here, and what depends on
+it?*
+
+Provenance is **derived state**. Nothing here is a source of truth: every edge
+is rebuilt from durable files, and the `prov_edges` table in `state.db` is a
+disposable cache that `vouch provenance rebuild` reconstructs byte-for-byte. All
+mutations still flow through the existing proposal + lifecycle code paths.
+
+## Edge kinds
+
+The graph is typed. An edge `A --kind--> B` means *A is explained by / depends
+on B*, so `why` walks outward and `impact` walks inward.
+
+| Kind | From → To | Source artifact |
+|------|-----------|-----------------|
+| `cites` | claim → source/evidence | `claim.evidence` |
+| `derivedFrom` | evidence span → source | `evidence.source_id` |
+| `supersedes` | newer claim → older claim | `claim.supersedes` |
+| `supersededBy` | older claim → newer claim | reverse view (query-time) |
+| `contradicts` | claim → claim | `claim.contradicts` |
+| `contradictedBy` | claim → claim | reverse view (query-time) |
+| `embeds` | page → claim | `page.claims` |
+| `proposedIn` | claim → session | approved proposal `session_id` |
+| `approvedBy` | claim → audit event | `proposal.*.approve` log entry |
+
+The two `*By` mirrors are computed at query time by walking inbound, so the
+`prov_edges` cache stays free of duplicate rows — only the seven canonical kinds
+are persisted.
+
+## Commands
+
+```bash
+vouch why # backward: cites, session, supersedes, approval
+vouch why --depth 5 --json # machine-readable provenance tree
+vouch trace --to # shortest typed path between two artifacts
+vouch impact # forward: pages, downstream claims that depend on it
+vouch impact --if archive # dry-run a lifecycle op; exits non-zero if it breaks something
+vouch graph --session # render the DAG for one agent run as dot/mermaid
+vouch provenance rebuild # rebuild the prov_edges cache from durable files
+```
+
+- `vouch why` walks edges outward from a claim and prints a tree grouped by edge
+ kind, each leaf carrying its citation target and originating audit timestamp;
+ `--json` emits a stable shape (`schema_version` pinned) suitable for tooling.
+- `vouch impact` walks inward and lists every artifact that points at the
+ target. `--if ` dry-runs the lifecycle op
+ against the in-memory graph and reports the breakage list — the **active**
+ pages that would carry a stale reference — without writing. It exits non-zero
+ when the list is non-empty, so it composes into a pre-flight check.
+- `vouch trace` finds the shortest typed-edge path between two artifacts
+ (edges are crossable either way) and prints it, or exits non-zero with
+ `no path` when they are disconnected.
+
+Reviewer output is bare prose + indentation — no curses, no colours by default —
+so it diffs cleanly into a `gh pr comment` or a session log.
+
+## `kb.*` methods
+
+The same surface is reachable over every transport (MCP stdio, JSONL, HTTP):
+
+| Method | Params |
+|--------|--------|
+| `kb.why` | `claim_id`, `depth` |
+| `kb.trace` | `from`, `to` |
+| `kb.impact` | `claim_id`, `depth`, `op` |
+| `kb.graph_export` | `session`, `format` |
+| `kb.provenance_rebuild` | — |
+
+They appear in `kb.capabilities` and pass the JSONL capabilities cross-check.
+
+## The cache
+
+The `prov_edges(src_id, dst_id, kind, event_ts, session_id)` table in `state.db`
+is a derived index, gitignored alongside the rest of the cache. A freshness
+stamp (claim count + page count + audit-event count) lets a cold query decide
+whether the cache can be trusted; when stale, `load_graph` rebuilds it
+transparently. Correctness never depends on the cache — a rebuild is always an
+exact reconstruction of the live in-memory build, which a CI test asserts.
+
+## Out of scope
+
+- A graphical web visualization of the DAG — a natural extension of the
+ `review-ui`, not this.
+- Mutating the graph directly; provenance is derived state.
+- Cross-KB / federated provenance.
+- Embedding-based "semantic neighbors" — provenance edges are strictly the
+ typed, audit-grounded ones.
+- Auto-blocking lifecycle ops based on impact size — `vouch impact` advises, the
+ human still decides.
diff --git a/docs/releases/v1.0.0.md b/docs/releases/v1.0.0.md
new file mode 100644
index 00000000..b99c1abc
--- /dev/null
+++ b/docs/releases/v1.0.0.md
@@ -0,0 +1,86 @@
+# vouch v1.0.0
+
+the first stable release. everything since 0.1.0, integrated — agent coding
+tools, answer-mode retrieval, a typed knowledge model, vault sync, and a broad
+adapter catalogue. the load-bearing invariant is unchanged: **every durable
+write still goes through the review gate** (`propose → approve`); nothing in
+this release auto-approves.
+
+> **install:** `pipx install vouch-kb` (the command is `vouch`; the PyPI
+> distribution is `vouch-kb`)
+> **upgrade:** `pipx upgrade vouch-kb`
+
+## highlights
+
+**competitive agent coding**
+- `vouch dual-solve ` — run Claude Code and Codex on one GitHub issue
+ in isolated worktrees, compare both diffs, keep the one you pick, and propose
+ its rationale into the KB (winning commit registered as a `Source`; decision +
+ up to 3 approach claims land in `proposed/`). `--json`, `--no-record`,
+ `--dry-run`, live per-phase progress.
+- `vouch dual-solve --sandbox` / `vouch review-ui --dual-solve-sandbox` — run the
+ engines inside a Docker image while keeping git/GitHub on the host; agent
+ writes stay confined to the throwaway branches.
+- `vouch review-ui --allow-dual-solve` — a browser SPA that runs dual-solve from
+ an issue link, streams progress over the review-ui websocket, shows both diffs
+ side by side, and lets you pick the winner. off by default, localhost-first,
+ edit-only over http.
+- `vouch auto-pr ` — open N mergeable PRs against any GitHub repo,
+ cross-verifying each diff by alternating Claude/Codex as fixer and reviewer;
+ opens only when the repo's own tests pass and the reviewer signs off.
+
+**answer-mode retrieval**
+- `kb.synthesize` — prose answers from approved claims only, with an inline
+ `[claim_id]` citation behind every sentence, an explicit `gaps` block, and a
+ `synthesis_confidence` grade (deterministic; CLI/MCP/JSONL).
+- entity-salience reflex — a per-session, zero-LLM pass that attaches candidate
+ claims as `_meta.vouch_salience` on context reads.
+- `kb.volunteer_context` — confidence-gated push context: surfaces a
+ highly-relevant approved claim to an active session.
+
+**typed knowledge model**
+- typed page kinds (`#234`) — declare extra page kinds in `config.yaml` with
+ required fields, a frontmatter schema, and `extends`; validated at propose
+ **and** approve. new `vouch schema list` / `vouch schema sync`.
+- auto-extracted typed edges — approving a page files `mentions` / `relates_to`
+ / `derived_from` relation proposals (still review-gated; `vouch
+ reject-extracted` mass-rejects).
+- propose-time similarity warnings (`similar_approved` / `similar_pending`) when
+ the embeddings extra is installed.
+
+**vault sync & adapters**
+- `vouch sync --vault ` — bidirectional sync with an Obsidian/Logseq
+ markdown vault (`--watch`, `--direction`).
+- `vouch install-mcp ` — one-command adapter writer for 9 hosts
+ (claude-code, claude-desktop, cursor, continue, codex, windsurf, cline, zed,
+ openclaw) with `--tier T1..T4`.
+- `vouch-context` OpenClaw context engine — cited `systemPromptAddition` on
+ every assemble.
+
+**ops & observability**
+- `vouch stats` / `kb.stats` — pending-by-agent, approval rate, citation
+ coverage over a window.
+- `vouch fsck` (deep consistency checks), `vouch migrate` (on-disk format
+ migrations), `vouch expire` (GC stale proposals).
+- `vouch eval recall` — P@k / R@k / MRR / nDCG against a labeled set, gating
+ retrieval changes in CI.
+- structured JSON logging (`VOUCH_LOG_FORMAT=json`), a `benchmarks/` suite,
+ `_meta.vouch_trust` on responses, visibility-aware `kb.audit`, and a
+ `gittensor` starter template (`vouch init --template gittensor`).
+
+## fixes
+- graph-ref integrity: `put_claim` / `update_claim` / `import_check` now reject
+ claims with dangling `entities` / `supersedes` / `superseded_by` /
+ `contradicts` refs; `supersede` / `contradict` pre-validate before the first
+ write (`#196`).
+- `discover_root()` honours `VOUCH_KB_PATH=/abs/.vouch` instead of always walking
+ up from cwd.
+- `vouch serve` fails fast with a `vouch init` hint when no KB is present
+ (`#95`).
+- vault-edit flow: `approve()` updates an existing page instead of erroring;
+ ghost-page, duplicate-proposal, and claim-stub edge cases fixed (`#219`).
+- `sync_apply` TOCTOU window closed; `parse_since` raises a clean `MetricsError`
+ instead of an `OverflowError` traceback.
+
+**Full Changelog:** https://github.com/vouchdev/vouch/blob/main/CHANGELOG.md ·
+compare https://github.com/vouchdev/vouch/compare/v0.1.0...v1.0.0
diff --git a/docs/review-ui.md b/docs/review-ui.md
new file mode 100644
index 00000000..fd75f211
--- /dev/null
+++ b/docs/review-ui.md
@@ -0,0 +1,105 @@
+# `vouch review-ui` — browser-based review console
+
+The review gate is vouch's load-bearing primitive, but the terminal
+(`vouch pending`, `vouch approve `) only scales to a solo reviewer and a
+small queue. `vouch review-ui` adds a browser viewport over the **same**
+review surface — every approve/reject/contradict goes through the identical
+`vouch.proposals` / `vouch.lifecycle` code path as the CLI, so the audit log is
+the same regardless of which surface you used. The CLI is untouched.
+
+Zero new on-disk schema. Zero new `kb.*` RPC methods. The web layer reads and
+mutates through the existing `KBStore`.
+
+## Run it
+
+```bash
+vouch review-ui # 127.0.0.1:7780, opens browser
+vouch review-ui --bind 127.0.0.1:8000
+vouch review-ui --no-open-browser # ssh / headless friendly
+
+# team mode — a non-loopback bind REQUIRES a Bearer token:
+vouch review-ui --bind 0.0.0.0:7780 --auth generate # mints + prints a token
+VOUCH_REVIEW_TOKEN=… vouch review-ui --bind 0.0.0.0:7780 --auth env
+vouch review-ui --bind 0.0.0.0:7780 --auth my-shared-secret --reviewer alice
+```
+
+A non-loopback bind without `--auth` is refused outright — we won't expose an
+unauthenticated approve surface on the network.
+
+## Authentication
+
+When `--auth` is set, every route except `/healthz` and `/static` requires the
+token. Credentials are accepted in three places, in priority order:
+
+1. **`Authorization: Bearer `** header — for the CLI, scripts, and API
+ callers.
+2. **HttpOnly cookie** (`vouch_review_token`) — the steady-state browser path.
+3. **`?token=` query string** — a *one-time bootstrap* only. On a `GET`, a
+ valid query token is moved into an HttpOnly, `SameSite=Strict` cookie and
+ the request is `303`-redirected to the same path with `?token=` stripped,
+ so the bare token never lingers in a bookmarkable URL or in access logs.
+
+The token is never exposed to JavaScript (the cookie is HttpOnly), so an XSS
+can't read it. Token comparisons are constant-time (`secrets.compare_digest`)
+to avoid a timing oracle. `secure` is not set on the cookie by default so the
+localhost-first (plain-http) flow works; terminate TLS at a proxy and have the
+proxy mark the cookie `Secure` for an internet-facing deployment.
+
+## Install
+
+The web stack lives behind an optional extra so the base install stays light:
+
+```bash
+pip install 'vouch-kb[web]'
+```
+
+All HTML/CSS/JS ships **inside the wheel** — no `npm install`, no build step,
+no CDN.
+
+## Views
+
+| Route | What it shows |
+|-------|---------------|
+| `/` | the pending queue, server-side paginated |
+| `/claim/` | one proposal's full payload + approve/reject |
+| `/session/` | every proposal from one agent run, grouped, with status |
+| `/sources/` | reverse index: which durable claims cite this source |
+| `/audit` | the review-decision timeline |
+| `/api/pending?page=N` | machine-readable paginated queue (`{count,page,pages,items}`) |
+| `/healthz` | liveness + pending count + connected client count (always open) |
+| `/ws` | realtime channel (see below) |
+
+## Realtime sync
+
+A single WebSocket channel per KB (`/ws`) keeps two reviewers in sync: when a
+mutation lands, the handling route broadcasts a small `{"type":"refresh"}`
+frame and every connected browser re-pulls the affected view within a second.
+The frame is a *signal*, not data — the client re-fetches through the same
+paginated routes, so there's exactly one rendering path. With `--auth` on, the
+socket authenticates on the same-origin HttpOnly cookie the browser sends with
+the handshake (a `?token=` query param is also accepted for non-browser
+clients like the CLI).
+
+## Progressive enhancement
+
+Every action is a plain `