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 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.
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.
┌──────────────┐
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:
src/vouch/storage.pyis pure I/O. No business logic. If you find yourself doing scope filtering or status transitions insideput_claim, you're in the wrong file.- All three surfaces (MCP, JSONL, CLI) call the same
proposals.*andlifecycle.*functions. Drift between surfaces is the most common contributor mistake.test_capabilitiesenforces method-list parity; you still need to keep behaviour aligned by reading the existing handlers before you add a new one. - The audit log is the only authoritative history.
decided/is the queryable summary;audit.log.jsonlis the legally-authoritative event stream. Both are committed. Never edit either by hand.
# from a clone
python3 -m venv .venv && . .venv/bin/activate
pip install -e '.[dev,web]' # dev,web is what ci.yml installs; mypy needs the web extra
# 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 checkmypy 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.
test is the integration branch — base feature branches on it and
target it in the PR. Docs-only changes may branch off main instead
(see the worktree note below).
git fetch origin test
git switch -c <type>/<topic> origin/test
# … work …
make check
git add <files-by-name> # never `git add -A` — leaks .claude/, web/, etc.
git commit -m "<type>(<scope>): <≤72-char summary>
multi-line lowercase body explaining the why.
no Co-Authored-By trailer."
git push -u origin <branch>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:
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.
Conventional commits, enforced by a pre-commit hook:
<type>(<scope>): <summary, lowercase, ≤72 chars>
<optional body — lowercase prose, multiple paragraphs ok>
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: <AI tool> trailer. The user has been explicit
about this; it's checked in PR review.
- 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.mdif it's installed. - No "we" / "let's" marketing tone in code comments. Comments explain why, not what.
- Specific files only when staging.
git add -Awill pull in.claude/,web/,proposed-features.md, etc. that are local scratch. - Stash + worktree for doc-only changes: branch off
origin/mainin/tmp/vouch-<topic>-wt, do the work, push, remove the worktree.
| 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 + package.json (repo root) |
| Claude Code / Cursor / etc. install templates | adapters/<host>/ |
Tests mirror module names (tests/test_<module>.py); the convention is
strict.
Two files make the vouch repo loadable directly as an OpenClaw plugin
(openclaw plugins install --link <repo>):
openclaw.plugin.json— the manifest the loader parses:id+configSchema(required),kind: context-engine,version, andskills(directories underadapters/openclaw/skills/, one SKILL.md each; OpenClaw publishes them as skills and slash commands).package.json— loader-facing only: theopenclaw.extensionspointer at the JS entry module and theopenclaw.compat.pluginApifloor. The Python package stays inpyproject.toml.
Invariants, all enforced by tests/test_openclaw_plugin_manifest.py:
- One id everywhere. The manifest
id, the PythonENGINE_ID, and the entry module's export id are allvouch. OpenClaw's installer auto-bindsplugins.slots.contextEngineto the plugin id and resolves that slot by engine id — if they ever diverge, the engine is quarantined at resolve time and OpenClaw silently falls back to its legacy engine. - Version in four places.
pyproject.toml,openclaw.plugin.json,package.json, andsrc/vouch/__init__.py(__version__, whatvouch --versionprints) must agree. 1.2.0 shipped self-reporting 1.1.0 because the fourth site wasn't checked; the manifest test now ties all four. - Skills mirror the claude-code commands. Each
adapters/openclaw/skills/<name>/SKILL.mdbody must match the correspondingadapters/claude-code/.claude/commands/<name>.mdbody. - No dead dialect fields.
mcpServers,contracts,family,shared_deps,openclaw.*in the manifest are silently ignored by current loaders — the sync test rejects them. The kb.* MCP server is deployment config (openclaw mcp add vouch -- vouch serve); the trust boundary lives inadapters/openclaw/policy.json.
tests/test_openclaw_plugin_load_real.py is the live gate: with the
openclaw CLI on PATH it links the repo into an isolated profile and
asserts import, engine registration, slot auto-bind, skill publication,
and a clean plugins doctor. It skips (does not fail) where the CLI is
absent, e.g. GitHub CI.
Keep the manifest 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 packaging.
Four registration sites — test_capabilities will fail if you miss one:
- MCP tool in
src/vouch/server.py(decorated with@mcp.tool()) - JSONL handler in
src/vouch/jsonl_server.py(_h_<name>+HANDLERS["kb.<name>"]) METHODSlist insrc/vouch/capabilities.py- CLI command in
src/vouch/cli.py(the human mirror)
Plus a test under tests/test_<feature>.py.
If the method reads the KB, consider whether it should attach the
_meta.vouch_salience sidebar (built in src/vouch/salience.py,
attached inline by kb_context — see kb_context in
src/vouch/server.py). It's added per-tool — there's no global
decorator. Don't confuse it with _meta.vouch_hot_memory, which is
written only by the OpenClaw context engine's assemble()
(src/vouch/openclaw/context_engine.py); src/vouch/hot_memory.py is
the in-process session registry that feeds salience, not a response
field. (_meta.vouch_trust is separate again — stamped on every
dict-shaped result by a global wrapper, not per-tool.)
release.yml cuts a tagged PyPI release via Trusted Publishing on every
v* tag push.
Pre-release checklist (also in CONTRIBUTING.md):
- Bump the version in all four sites:
pyproject.toml,openclaw.plugin.json,package.json,src/vouch/__init__.py. - Move everything under
[Unreleased]inCHANGELOG.mdinto a dated[X.Y.Z]section. make checkgreen.- PR titled
chore(release): prepare X.Y.Z, merge tomain. git tag vX.Y.Z && git push --tags— the workflow does the rest.- After CI finishes, draft the GitHub release with the CHANGELOG section as the body.
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-agentconfig 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 and
proposed-features.md (local scratch — not on
main).
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).
Pre-commit checks the conventional-commit format. If you trigger it via
shell substitution in a git commit -m "$(cat <<EOF…, write the message
to /tmp/commit-msg.txt and use git commit -F.
make helpfor the make targets.vouch capabilitiesfor the JSON method surface.git log --oneline -20for recent commit voice.SPEC.mdfor the protocol contract.docs/getting-started.mdfor the agent-side flow.- The PR template at
.github/pull_request_template.mdif it's present.
Don't escalate to the user before you've checked the spec.