diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d4fff95..8489172e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,32 @@ All notable changes to vouch are documented here. Format follows ## [Unreleased] +### Added +- `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). + +### Fixed +- `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 +41,38 @@ All notable changes to vouch are documented here. Format follows Trusted Publishing (OIDC). ### Added +- `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 +- `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 +80,23 @@ 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). +- 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. +- `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 +110,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 +133,26 @@ 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. ## [0.0.1] — 2026-05-17 diff --git a/README.md b/README.md index a3f298c9..3148b2bc 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,9 @@ vouch status # one-line summary vouch pending # list pending proposals vouch show # full details vouch approve # → durable artifact +vouch approve ... # approve several reviewed proposals at once vouch reject --reason "..." +vouch expire --apply # optional: clear stale pending proposals # 4. commit git add .vouch/ && git commit -m "kb: approve auth-uses-jwt" @@ -143,11 +145,15 @@ vouch capabilities # emit the JSON capabilities descrip vouch status [--json] # KB counts + pending proposals 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 approve ... [--reason ...] [--keep-going] 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 +182,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] ``` @@ -254,11 +262,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 +276,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/docs/PROJECT-INDEX.md b/docs/PROJECT-INDEX.md new file mode 100644 index 00000000..c0511fc2 --- /dev/null +++ b/docs/PROJECT-INDEX.md @@ -0,0 +1,550 @@ +# vouch — Project Index + +> Git-native, review-gated knowledge base for LLM agents. +> MCP server + JSONL tool server + CLI. + +**Package:** `vouch-kb` (PyPI) | **CLI command:** `vouch` | **Version:** 0.1.0 (alpha) +**Language:** Python 3.11+ | **Build:** Hatchling | **License:** MIT + +--- + +## Table of Contents + +1. [Architecture Overview](#architecture-overview) +2. [Module Map](#module-map) +3. [Object Model](#object-model) +4. [Data Flow](#data-flow) +5. [Storage Layout](#storage-layout) +6. [Transport Surfaces](#transport-surfaces) +7. [CLI Reference](#cli-reference) +8. [MCP / JSONL Tool Surface](#mcp--jsonl-tool-surface) +9. [Embeddings Stack](#embeddings-stack) +10. [Test Structure](#test-structure) +11. [CI/CD](#cicd) +12. [Supporting Directories](#supporting-directories) +13. [Dependencies](#dependencies) +14. [Codebase Statistics](#codebase-statistics) + +--- + +## Architecture Overview + +``` + +------------------+ + | LLM Agents | + | (Claude, Cursor, | + | Codex, etc.) | + +--------+---------+ + | + +--------------+--------------+ + | | + +------v------+ +--------v--------+ + | MCP Server | | JSONL Server | + | (server.py) | | (jsonl_server.py)| + | FastMCP/stdio| | stdin/stdout | + +------+------+ +--------+--------+ + | | + +--------------+--------------+ + | + +--------v--------+ + | Business Logic | + | proposals.py | + | lifecycle.py | + | sessions.py | + | context.py | + +--------+--------+ + | + +--------------+--------------+ + | | + +------v------+ +--------v--------+ + | Storage | | Index (SQLite) | + | (storage.py)| | (index_db.py) | + | YAML/MD on | | FTS5 + embedding| + | disk | | state.db | + +------+------+ +--------+--------+ + | | + +------v------+ +--------v--------+ + | Audit Log | | Embeddings | + | (audit.py) | | (embeddings/) | + | JSONL append| | pluggable models| + +-------------+ +-----------------+ + + +------------------+ + | CLI (cli.py) | + | Click commands | + | Human review | + +------------------+ +``` + +**Core principle:** Agents *propose*, humans *approve*. Files on disk are the source of truth. SQLite is a derived cache. Everything is audited. + +--- + +## Module Map + +### `src/vouch/` — Core Package (27 files, ~4,500 LOC) + +| Module | Purpose | Key Classes/Functions | +|---|---|---| +| `__init__.py` | Package root, version | `__version__` | +| `models.py` | Pydantic domain models (AKBP v0.1 compatible) | `Source`, `Evidence`, `Claim`, `Entity`, `Relation`, `Page`, `Session`, `AuditEvent`, `Proposal`, `ContextPack`, `Capabilities` | +| `storage.py` | File-backed CRUD (YAML/MD on disk) | `KBStore`, `discover_root()`, `ArtifactNotFoundError`, `KBNotFoundError` | +| `server.py` | MCP server (FastMCP over stdio) | `mcp`, `kb_*` tool functions, `run_stdio()` | +| `jsonl_server.py` | JSONL tool server (stdin/stdout) | `HANDLERS` dict, `handle_request()`, `run_jsonl()` | +| `cli.py` | Click CLI for humans | `cli` group, 30+ commands | +| `proposals.py` | Review gate: propose -> approve/reject | `propose_claim()`, `propose_page()`, `propose_entity()`, `propose_relation()`, `approve()`, `reject()` | +| `lifecycle.py` | Claim lifecycle mutations (direct, audited) | `supersede()`, `contradict()`, `archive()`, `confirm()`, `cite()` | +| `sessions.py` | Agent session lifecycle | `session_start()`, `session_end()`, `crystallize()` | +| `context.py` | ContextPack assembly for agent prompts | `build_context_pack()` | +| `index_db.py` | SQLite FTS5 index + embedding storage | `open_db()`, `search()`, `search_semantic()`, `search_embedding()` | +| `audit.py` | Append-only JSONL audit log | `log_event()`, `read_events()` | +| `health.py` | Status, lint, doctor, index rebuild | `status()`, `lint()`, `doctor()`, `rebuild_index()` | +| `verify.py` | Source integrity verification (hash drift) | `verify_source()`, `verify_all()` | +| `bundle.py` | Portable tar.gz export/import with manifest | `export()`, `import_check()`, `import_apply()`, `export_check()` | +| `capabilities.py` | AKBP capabilities descriptor | `capabilities()`, `METHODS` list | +| `onboarding.py` | Starter KB content for `vouch init` | `seed_starter_kb()` | + +### `src/vouch/embeddings/` — Semantic Retrieval (12 files) + +| Module | Purpose | +|---|---| +| `__init__.py` | Registry, auto-discovers adapters | +| `base.py` | `Embedder` protocol, `register()` / `get_embedder()` registry | +| `st_mpnet.py` | sentence-transformers `all-mpnet-base-v2` adapter (default) | +| `st_minilm.py` | sentence-transformers `all-MiniLM-L6-v2` adapter | +| `fastembed_bge.py` | FastEmbed BGE adapter (ONNX-backed) | +| `cache.py` | Query embedding cache (SQLite-backed) | +| `fusion.py` | Reciprocal Rank Fusion (RRF) for hybrid search | +| `hyde.py` | Hypothetical Document Embedding query expansion | +| `rerank.py` | Cross-encoder reranking | +| `dedup.py` | Near-duplicate detection via cosine similarity | +| `scorer.py` | Retrieval evaluation (recall@k, MRR, NDCG) | +| `migration.py` | Embedding model migration / backfill | + +--- + +## Object Model + +``` +Source (immutable, content-addressed by sha256) + | + +-- Evidence (span pointer: line range, timestamp, quote) + | + +-- Claim (atomic assertion, typed, status-tracked, confidence-scored) + | |-- cites: [Source/Evidence ids] (mandatory, >=1) + | |-- status: working -> actionable -> stable -> contested/superseded/archived + | |-- approved_by: review gate audit trail + | +-- type: fact | decision | preference | workflow | observation | question | warning + | + +-- Entity (typed node: person, project, repo, concept, ...) + | + +-- Relation (typed edge: uses, depends_on, supersedes, contradicts, ...) + | + +-- Page (markdown document with YAML frontmatter, links claims/entities) + | + +-- Session (agent work block, bundles proposals) + | + +-- Proposal (pending write, gated by review) + | |-- kind: claim | page | entity | relation + | +-- status: pending -> approved | rejected + | + +-- AuditEvent (append-only log entry for every mutation) + | + +-- ContextPack (retrieval result bundle with quality gate) +``` + +### Enums + +| Enum | Values | +|---|---| +| `SourceType` | file, url, transcript, message, commit, issue, screenshot, pdf, audio, video, folder | +| `ClaimType` | fact, decision, preference, workflow, observation, question, warning | +| `ClaimStatus` | working, actionable, stable, contested, superseded, archived, redacted | +| `EntityType` | person, project, repo, company, concept, decision, workflow, file, api, incident, source, agent, tool, team, system | +| `RelationType` | uses, depends_on, contradicts, supersedes, supports, caused_by, owned_by, derived_from, similar_to, blocks, implements, references | +| `PageType` | entity, concept, decision, workflow, session, index, log, report, source-summary | +| `Scope` | private, project, team, public | + +--- + +## Data Flow + +### Write Path (Review-Gated) + +``` +Agent calls kb_propose_claim(text, evidence, ...) + | + v +proposals.py: propose_claim() + |-- validates evidence ids exist (Source or Evidence) + |-- generates slug from text + |-- creates Proposal(kind=CLAIM, status=PENDING) + |-- storage.put_proposal() -> .vouch/proposed/.yaml (gitignored) + |-- audit.log_event("proposal.claim.create") + v +Human runs `vouch approve ` + | + v +proposals.py: approve() + |-- checks proposal is PENDING + |-- checks approved_by != proposed_by (unless trusted-agent config) + |-- checks no existing artifact with that id + |-- creates Claim from payload + |-- storage.put_claim() -> .vouch/claims/.yaml (committed) + |-- index_db.index_claim() -> state.db FTS5 + |-- storage._embed_and_store() -> state.db embedding_index + |-- moves proposal: proposed/ -> decided/ + |-- audit.log_event("proposal.claim.approve") +``` + +### Read Path (Unrestricted) + +``` +Agent calls kb_search(query, backend="auto") + | + v +1. Try semantic search (index_db.search_semantic) + |-- embeddings.get_embedder().encode(query) + |-- index_db.search_embedding() via sqlite-vec or NumPy cosine + | +2. Fallback: FTS5 search (index_db.search) + |-- BM25 ranking across claims_fts, pages_fts, entities_fts + | +3. Fallback: substring scan (storage.search_substring) + |-- brute-force text match across all artifacts +``` + +### Session + Crystallize Flow + +``` +kb_session_start(task="...") + |-> Session record in .vouch/sessions/ + +Agent proposes claims/pages/entities during session + |-> each Proposal gets session_id + +kb_session_end(session_id) + |-> backfills proposal_ids + +kb_crystallize(session_id) + |-> approve() every PENDING proposal in session + |-> write session-summary Page + |-> audit everything +``` + +--- + +## Storage Layout + +``` +/ + .vouch/ + |-- config.yaml # KB settings (version, review policy, retrieval config) + |-- .gitignore # ignores proposed/, state.db + |-- audit.log.jsonl # append-only audit (committed) + |-- state.db # SQLite: FTS5 + embeddings (derived, gitignored) + |-- claims/.yaml # durable approved claims + |-- pages/.md # markdown pages with YAML frontmatter + |-- sources// + | |-- meta.yaml # source metadata + | +-- content # raw source bytes + |-- entities/.yaml # graph nodes + |-- relations/.yaml # graph edges + |-- evidence/.yaml # citation pointers into sources + |-- sessions/.yaml # session records + |-- proposed/.yaml # pending proposals (gitignored, local-only) + +-- decided/.yaml # approved/rejected proposals (committed) +``` + +**Key invariant:** Files are the source of truth. `state.db` is a derived cache rebuilt by `vouch index`. Losing it is never fatal. + +--- + +## Transport Surfaces + +### 1. MCP Server (`server.py`) + +- Transport: stdio (FastMCP) +- Entry: `vouch serve` or configured in `.mcp.json` +- Agent identification: `VOUCH_AGENT` env var +- 43 tools registered via `@mcp.tool()` decorators + +### 2. JSONL Server (`jsonl_server.py`) + +- Transport: newline-delimited JSON over stdin/stdout +- Entry: `vouch serve --transport jsonl` +- Same tool surface as MCP, different wire format +- Request: `{"id": "r1", "method": "kb.search", "params": {...}}` +- Response: `{"id": "r1", "ok": true, "result": {...}}` + +### 3. CLI (`cli.py`) + +- Framework: Click +- Entry: `vouch` command (registered via `[project.scripts]`) +- Actor: `VOUCH_AGENT` > `VOUCH_USER` > OS username +- Human-facing review interface + +--- + +## CLI Reference + +### Bootstrap +| Command | Description | +|---|---| +| `vouch init [--path P]` | Initialize `.vouch/` KB with starter content | +| `vouch discover [--path P]` | Find nearest `.vouch/` root | +| `vouch capabilities` | Emit JSON capabilities descriptor | + +### Health & Status +| Command | Description | +|---|---| +| `vouch status [--json]` | Artifact counts + pending proposals | +| `vouch lint [--stale-days N]` | Broken citations, stale claims, dangling refs | +| `vouch doctor` | Full sweep: lint + source verify + index check | + +### Review Gate +| Command | Description | +|---|---| +| `vouch pending` | List proposals awaiting review | +| `vouch show ` | Full proposal details | +| `vouch approve [--reason]` | Approve -> durable artifact | +| `vouch reject --reason "..."` | Reject with reason | + +### Propose (CLI shortcuts) +| Command | Description | +|---|---| +| `vouch propose-claim --text ... --source ...` | Propose a claim | +| `vouch propose-page --title ... [--body -]` | Propose a page | +| `vouch propose-entity --name ... --type ...` | Propose an entity | +| `vouch propose-relation --from ... --rel ... --to ...` | Propose a relation | + +### Sources +| Command | Description | +|---|---| +| `vouch source add PATH` | Register file as Source | +| `vouch source verify [--fail-on-issue]` | Re-hash and detect drift | + +### Lifecycle +| Command | Description | +|---|---| +| `vouch supersede OLD NEW` | Mark old claim superseded by new | +| `vouch contradict A B` | Record contradiction (symmetric) | +| `vouch archive CLAIM_ID` | Archive a claim | +| `vouch confirm CLAIM_ID` | Re-confirm (bumps last_confirmed_at) | +| `vouch cite CLAIM_ID` | Resolve citations | + +### Sessions +| Command | Description | +|---|---| +| `vouch session start [--task ...] [--note ...]` | Start agent session | +| `vouch session end SESSION_ID` | End session | +| `vouch crystallize SESSION_ID [--no-page]` | Approve all pending + summary page | + +### Retrieval +| Command | Description | +|---|---| +| `vouch search QUERY [--backend ...] [--rerank] [--hyde]` | Search KB | +| `vouch context TASK [--max-chars N]` | Build ContextPack for agent prompt | +| `vouch index` | Rebuild state.db | +| `vouch reindex [--embeddings] [--backfill]` | Rebuild FTS5 + optional embedding backfill | +| `vouch dedup [--threshold 0.95]` | Near-duplicate scan | + +### Embeddings +| Command | Description | +|---|---| +| `vouch embeddings stats` | Model identity, counts, cache stats | +| `vouch eval embedding --queries FILE` | Retrieval quality metrics | + +### Audit +| Command | Description | +|---|---| +| `vouch audit [--tail N] [--json]` | Read audit log | + +### Export / Import +| Command | Description | +|---|---| +| `vouch export --out path.tar.gz` | Bundle KB into portable archive | +| `vouch export-check path.tar.gz` | Verify bundle integrity | +| `vouch import-check path.tar.gz` | Diff bundle against destination | +| `vouch import-apply path.tar.gz [--on-conflict ...]` | Apply bundle | + +### Server +| Command | Description | +|---|---| +| `vouch serve [--transport stdio\|jsonl]` | Start MCP or JSONL server | + +--- + +## MCP / JSONL Tool Surface + +43 methods organized by intent: + +### Read (unrestricted) +`kb_capabilities`, `kb_status`, `kb_search`, `kb_context`, `kb_read_page`, `kb_read_claim`, `kb_read_entity`, `kb_read_relation`, `kb_list_pages`, `kb_list_claims`, `kb_list_entities`, `kb_list_relations`, `kb_list_sources`, `kb_list_pending` + +### Source Intake (not gated) +`kb_register_source`, `kb_register_source_from_path`, `kb_source_verify` + +### Write (gated -> proposals) +`kb_propose_claim`, `kb_propose_page`, `kb_propose_entity`, `kb_propose_relation` + +### Decisions +`kb_approve`, `kb_reject` + +### Lifecycle (direct mutation, audited) +`kb_supersede`, `kb_contradict`, `kb_archive`, `kb_confirm`, `kb_cite` + +### Sessions +`kb_session_start`, `kb_session_end`, `kb_crystallize` + +### Maintenance +`kb_index_rebuild`, `kb_lint`, `kb_doctor`, `kb_audit`, `kb_export`, `kb_export_check`, `kb_import_check`, `kb_import_apply` + +### Embeddings +`kb_reindex_embeddings`, `kb_dedup_scan`, `kb_eval_embeddings`, `kb_embeddings_stats` + +--- + +## Embeddings Stack + +Pluggable adapter architecture with auto-registration: + +``` +embeddings/ + base.py -- Embedder protocol + registry (register / get_embedder) + st_mpnet.py -- all-mpnet-base-v2 (default, pip install vouch-kb[embeddings]) + st_minilm.py -- all-MiniLM-L6-v2 (lighter alternative) + fastembed_bge.py -- BGE via fastembed/ONNX (pip install vouch-kb[embeddings-fast]) +``` + +### Search dispatch order +1. **Embedding** (semantic) -- sqlite-vec ANN or NumPy cosine fallback +2. **FTS5** -- BM25 over tokenized text +3. **Substring** -- brute-force text match (always available) +4. **Hybrid** -- RRF fusion of embedding + FTS5 + +### Advanced retrieval features +- **Query embedding cache** -- avoids re-encoding repeated queries +- **HyDE** -- hypothetical document expansion before encoding +- **Reranking** -- cross-encoder reranking of candidate hits +- **Dedup** -- cosine-based near-duplicate detection with audit logging +- **Evaluation** -- recall@k, MRR, NDCG via JSONL query sets + +--- + +## Test Structure + +### `tests/` (27 files) + +| Test File | Covers | +|---|---| +| `test_storage.py` | KBStore CRUD, init, search, path safety | +| `test_cli.py` | CLI commands end-to-end (Click test runner) | +| `test_audit.py` | Audit log write/read, malformed line handling | +| `test_bundle.py` | Export/import, manifest integrity, path traversal | +| `test_capabilities.py` | Capabilities descriptor, method list sync | +| `test_context.py` | ContextPack assembly, budget enforcement | +| `test_health.py` | Status, lint findings, doctor, index rebuild | +| `test_index.py` | FTS5 indexing and search | +| `test_jsonl_server.py` | JSONL envelope handling, all methods | +| `test_onboarding.py` | Starter KB seeding | +| `test_sessions.py` | Session lifecycle, crystallize | +| `test_verify.py` | Source verification, drift detection | + +### `tests/embeddings/` (12 files) + +| Test File | Covers | +|---|---| +| `test_core.py` | Embedder registry, encode/decode | +| `test_cli.py` | Embedding CLI commands | +| `test_dedup.py` | Near-duplicate scanning | +| `test_fusion.py` | RRF fusion | +| `test_hyde.py` | HyDE query expansion | +| `test_integration.py` | End-to-end semantic search (marked `integration`) | +| `test_migration.py` | Model migration, backfill | +| `test_rerank.py` | Cross-encoder reranking | +| `test_scorer.py` | Retrieval evaluation metrics | +| `test_search.py` | Semantic search paths | +| `test_storage.py` | Embedding storage in SQLite | +| `conftest.py` / `_fakes.py` | Shared fixtures, fake embedder | + +### Running tests +```bash +pytest # unit tests (fast, no model loading) +pytest -m integration # integration tests (loads real embedding model) +``` + +--- + +## CI/CD + +### `.github/workflows/` + +| Workflow | Purpose | +|---|---| +| `ci.yml` | Lint (ruff), type check (mypy), test (pytest), coverage | +| `release.yml` | Build + publish to PyPI on release tags | +| `schema-check.yml` | Validate JSON schemas match pydantic models | + +### Quality gates +- **Ruff** -- linter (E, F, I, B, UP, SIM, RUF rules, line-length 100) +- **Mypy** -- strict type checking (numpy/sentence-transformers/fastembed stubs ignored) +- **Pytest** -- unit tests by default; integration tests in separate job +- **Pre-commit** -- configured via `.pre-commit-config.yaml` + +--- + +## Supporting Directories + +| Directory | Purpose | Audience | +|---|---|---| +| `docs/` | User-facing documentation (9 guides + banner/demo) | End users, operators | +| `spec/` | Normative specification snapshots | Implementers, auditors | +| `schemas/` | 15 JSON Schemas (Draft 2020-12, generated from pydantic) | Tooling, validation | +| `proposals/` | VEP (Vouch Enhancement Proposals) | Protocol evolution | +| `templates/` | Copy-paste YAML/MD templates for every artifact type | Learning | +| `examples/` | Example KB layouts (tiny, decision-log) | Learning | +| `adapters/` | Per-runtime wiring guides (Claude Code, Cursor, Codex, Continue, generic MCP, JSONL shell) | Integration | +| `benchmarks/` | Benchmark fixtures and harness | Performance testing | +| `scripts/` | `gen_schemas.py` -- generate JSON schemas from models | Development | + +--- + +## Dependencies + +### Core (required) +| Package | Version | Purpose | +|---|---|---| +| pydantic | >=2.13.4, <3 | Domain models, validation | +| click | >=8.4.0, <9 | CLI framework | +| pyyaml | >=6, <7 | YAML serialization | +| mcp | >=1.0, <2 | MCP server SDK | + +### Optional extras +| Extra | Packages | Purpose | +|---|---|---| +| `[embeddings]` | sentence-transformers, numpy | Semantic search (mpnet/minilm) | +| `[embeddings-fast]` | fastembed, onnxruntime, numpy, sqlite-vec | Semantic search (BGE, ONNX) | +| `[rerank]` | sentence-transformers | Cross-encoder reranking | +| `[dev]` | pytest, pytest-cov, mypy, ruff, types-pyyaml | Development | + +--- + +## Codebase Statistics + +| Metric | Count | +|---|---| +| Total Python LOC | ~8,400 | +| Source modules | 27 | +| Test modules | 27 | +| CLI commands | 30+ | +| MCP/JSONL tools | 43 | +| Pydantic models | 15 | +| JSON schemas | 15 | +| Embedding adapters | 3 | +| CI workflows | 3 | +| Documentation guides | 9 | + +--- + +*Generated 2026-05-28 from source analysis of vouch v0.1.0 on branch `release/0.1.0`.* 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/multi-agent.md b/docs/multi-agent.md index b4918dea..65c8cdb7 100644 --- a/docs/multi-agent.md +++ b/docs/multi-agent.md @@ -94,13 +94,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/review-gate.md b/docs/review-gate.md index 9aff9bb0..29e75a2a 100644 --- a/docs/review-gate.md +++ b/docs/review-gate.md @@ -40,6 +40,7 @@ vouch status # snapshot vouch pending # what's waiting vouch show # full proposal details vouch approve # → durable artifact + decided/ +vouch approve # approve several reviewed proposals at once vouch reject --reason "duplicates auth-uses-jwt" ``` @@ -151,10 +152,14 @@ next review; you can remove the entries with `git rm` afterwards. Update your local `.gitignore` so it stops happening. **Q: Can I approve in bulk?** -Today, one at a time. `--batch` is planned for 0.1 -([ROADMAP](../ROADMAP.md)). Until then, -`vouch pending --json | jq -r '.[].id' | xargs -n1 vouch approve` is -the workaround — read what you're approving first. +Yes. Review proposals, then approve them together: + +```bash +vouch approve --reason "reviewed together" +``` + +Default is all-or-nothing: if any id is invalid or already decided, +nothing is approved. Use `--keep-going` for best-effort approval. **Q: What if I want a *softer* gate — "warn, don't block"?** You don't. If you want soft, use a tool without a gate. vouch's diff --git a/docs/superpowers/plans/2026-05-27-claude-code-adapter.md b/docs/superpowers/plans/2026-05-27-claude-code-adapter.md new file mode 100644 index 00000000..51d0d57f --- /dev/null +++ b/docs/superpowers/plans/2026-05-27-claude-code-adapter.md @@ -0,0 +1,832 @@ +# Claude Code adapter — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Ship a complete Claude Code adoption story under `adapters/claude-code/` (four composable tiers: MCP config, CLAUDE.md, slash commands, settings/hooks) plus a `vouch install-mcp claude-code` CLI helper that writes the missing pieces into a target project, idempotently. + +**Architecture:** Files-only adapter (no new runtime code in vouch core) + one CLI writer command. The four tiers stack: T1 alone equals today's behavior; T4 is the fully-integrated review-gated workflow with hooks and read-only auto-allow. The writer command resolves a project root and writes only files that don't already exist. + +**Tech Stack:** Python 3.11+, Click, pytest, ruff, mypy. Markdown for templates/slash commands. JSON for `.mcp.json` and `settings.json`. + +--- + +## Scope Check +Single subsystem — the Claude Code adapter and its installer. One plan, one PR. + +## File Structure + +Files created or modified: + +``` +adapters/claude-code/ + README.md ← MODIFY: tiered adoption guide, vouch-kb install + CLAUDE.md.snippet ← MODIFY (light edits): keep, normalize header + .mcp.json ← CREATE: T1 template + .claude/ + commands/ + vouch-recall.md ← CREATE: T3 + vouch-status.md ← CREATE: T3 + vouch-resolve-issue.md ← CREATE: T3 + vouch-propose-from-pr.md ← CREATE: T3 + settings.json ← CREATE: T4 hooks + read-only auto-allow + +src/vouch/install_adapter.py ← CREATE: pure file-writer (no Click) +src/vouch/cli.py ← MODIFY: add `install-mcp` group + claude-code cmd +tests/test_install_adapter.py ← CREATE: TDD for the writer +CHANGELOG.md ← MODIFY: [Unreleased] Added entry +``` + +Adapter files live under `adapters/claude-code/` and are the *source of truth* templates committed to the vouch repo. The writer command copies them into a user's project under matching paths. + +--- + +## Task 0: Branch setup + +**Files:** +- Modify: none + +- [ ] **Step 1: Stash the pre-existing storage.py edit** (it predates this work) + +```bash +git stash push -m "pre-existing WIP storage.py" src/vouch/storage.py 2>/dev/null || true +``` + +- [ ] **Step 2: Branch off latest main** + +```bash +git fetch origin main +git switch -c feat/claude-code-adapter origin/main +git status --porcelain | grep -v '^??' || echo "(clean)" +``` + +Expected: working tree clean except `?? .claude/` and other untracked workdir files. + +--- + +## Task 1: T1 — `.mcp.json` template + +**Files:** +- Create: `adapters/claude-code/.mcp.json` + +- [ ] **Step 1: Write the file** verbatim + +```json +{ + "mcpServers": { + "vouch": { + "command": "vouch", + "args": ["serve"], + "env": { + "PYTHONUTF8": "1", + "VOUCH_AGENT": "claude-code" + } + } + } +} +``` + +- [ ] **Step 2: Validate it parses as JSON** + +```bash +python3 -c "import json; json.load(open('adapters/claude-code/.mcp.json'))" +``` + +Expected: no output (success). + +- [ ] **Step 3: Commit** + +```bash +git add adapters/claude-code/.mcp.json +git commit -m "feat(adapter/claude-code): T1 — .mcp.json template" +``` + +--- + +## Task 2: T2 — normalize `CLAUDE.md.snippet` + +**Files:** +- Modify: `adapters/claude-code/CLAUDE.md.snippet` + +The existing snippet is good content. Only change: rename intent — it's the *template* you append to an existing CLAUDE.md, not a standalone file. Add a one-line `` fence so the installer can detect prior installs and avoid duplicating. + +- [ ] **Step 1: Read the current snippet** to confirm content + +```bash +wc -l adapters/claude-code/CLAUDE.md.snippet +``` + +- [ ] **Step 2: Prepend the fence + appendix-style header** + +Open `adapters/claude-code/CLAUDE.md.snippet` and replace the file with: + +```markdown + +## 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. + +### Lifecycle hygiene + +When you find a claim that's wrong or out-of-date: + +- If you can replace it with a corrected version, use `kb_supersede` rather + than proposing a contradicting claim. +- If two existing claims conflict, mark them with `kb_contradict` so the + human can choose. +- Re-cite a claim you used recently with `kb_confirm` — it bumps + `last_confirmed_at` so lint doesn't flag it as stale. + +### Identity + +You are recorded as `proposed_by: claude-code` in the audit log. Everything +you propose is visible to whoever runs `vouch pending`. + +``` + +- [ ] **Step 3: Verify fence + 9-section structure** + +```bash +grep -c "" adapters/claude-code/CLAUDE.md.snippet +grep -c "" adapters/claude-code/CLAUDE.md.snippet +``` + +Expected: `1` and `1`. + +- [ ] **Step 4: Commit** + +```bash +git add adapters/claude-code/CLAUDE.md.snippet +git commit -m "feat(adapter/claude-code): T2 — fence CLAUDE.md snippet for idempotent install" +``` + +--- + +## Task 3: T3 — `vouch-recall` slash command + +**Files:** +- Create: `adapters/claude-code/.claude/commands/vouch-recall.md` + +- [ ] **Step 1: Create the directory and file** + +```bash +mkdir -p adapters/claude-code/.claude/commands +``` + +- [ ] **Step 2: Write `vouch-recall.md`** verbatim + +```markdown +--- +description: Recall cited knowledge from the vouch KB about the given topic before answering. +--- + +Before answering, fetch a context pack from vouch: + +1. Call `mcp__vouch__kb_context` with `query: "$ARGUMENTS"`, `limit: 8`, + `require_citations: true`. +2. Read each returned item; quote the cited source id on any claim you reuse. +3. If the pack is empty or quality.ok is false, say so explicitly before + answering — don't fabricate citations. + +Then continue with the user's question, grounded in what the KB returned. +``` + +- [ ] **Step 3: Commit** + +```bash +git add adapters/claude-code/.claude/commands/vouch-recall.md +git commit -m "feat(adapter/claude-code): T3 — /vouch-recall command" +``` + +--- + +## Task 4: T3 — `vouch-status` slash command + +**Files:** +- Create: `adapters/claude-code/.claude/commands/vouch-status.md` + +- [ ] **Step 1: Write the file** + +```markdown +--- +description: Show vouch KB health — counts, pending proposals, audit/index state. +--- + +Run this single shell command and show me its raw output: + +```bash +vouch status +``` + +Then call `mcp__vouch__kb_list_pending` and summarise the queue in one line +(how many pending, who proposed each). +``` + +- [ ] **Step 2: Commit** + +```bash +git add adapters/claude-code/.claude/commands/vouch-status.md +git commit -m "feat(adapter/claude-code): T3 — /vouch-status command" +``` + +--- + +## Task 5: T3 — `vouch-resolve-issue` slash command + +**Files:** +- Create: `adapters/claude-code/.claude/commands/vouch-resolve-issue.md` + +- [ ] **Step 1: Write the file** + +```markdown +--- +description: Resolve a GitHub issue end-to-end with a vouch session bracketing the work. +--- + +You are resolving the issue at $ARGUMENTS. + +Run this loop: + +1. `gh issue view $ARGUMENTS` — read the issue. +2. `mcp__vouch__kb_context` with the issue title — surface prior decisions. +3. `mcp__vouch__kb_session_start` with `task: " (#)"` and + note the returned `session_id`. +4. Do the work (read code, propose fix). Each meaningful finding goes in as + `mcp__vouch__kb_propose_claim` citing the source that justifies it. +5. `mcp__vouch__kb_session_end` with the session id. +6. Tell the user the session id and that `vouch crystallize ` + will approve the proposed claims after they review with `vouch pending`. + +Do not call `kb_approve`; the human reviews and approves. +``` + +- [ ] **Step 2: Commit** + +```bash +git add adapters/claude-code/.claude/commands/vouch-resolve-issue.md +git commit -m "feat(adapter/claude-code): T3 — /vouch-resolve-issue command" +``` + +--- + +## Task 6: T3 — `vouch-propose-from-pr` slash command + +**Files:** +- Create: `adapters/claude-code/.claude/commands/vouch-propose-from-pr.md` + +- [ ] **Step 1: Write the file** + +```markdown +--- +description: Capture a merged PR's decision as a proposed, cited vouch claim. +--- + +You are capturing the durable decision from the PR at $ARGUMENTS. + +1. `gh pr view $ARGUMENTS --json title,body,mergedAt,mergeCommit` — read it. + If `mergedAt` is null, stop and tell the user the PR isn't merged yet. +2. `mcp__vouch__kb_register_source` with the PR URL as `locator` and the + PR title as `title`. Note the returned source id. +3. Draft one sentence that captures the decision the PR establishes — the + *why* future agents need to remember, not a summary of the diff. +4. `mcp__vouch__kb_propose_claim` with that sentence, citing the source id + from step 2. Type `decision`. +5. Tell the user the proposal id; they review with `vouch show ` and + approve with `vouch approve `. + +Never approve your own proposal. The review gate is the point. +``` + +- [ ] **Step 2: Commit** + +```bash +git add adapters/claude-code/.claude/commands/vouch-propose-from-pr.md +git commit -m "feat(adapter/claude-code): T3 — /vouch-propose-from-pr command" +``` + +--- + +## Task 7: T4 — `settings.json` (hooks + auto-allow) + +**Files:** +- Create: `adapters/claude-code/.claude/settings.json` + +- [ ] **Step 1: Write the file** + +```json +{ + "permissions": { + "alwaysAllow": [ + "mcp__vouch__kb_status", + "mcp__vouch__kb_search", + "mcp__vouch__kb_context", + "mcp__vouch__kb_read_claim", + "mcp__vouch__kb_read_page", + "mcp__vouch__kb_read_entity", + "mcp__vouch__kb_read_relation", + "mcp__vouch__kb_list_claims", + "mcp__vouch__kb_list_pages", + "mcp__vouch__kb_list_entities", + "mcp__vouch__kb_list_relations", + "mcp__vouch__kb_list_sources", + "mcp__vouch__kb_list_pending", + "mcp__vouch__kb_capabilities" + ] + }, + "hooks": { + "SessionStart": [ + { + "command": "vouch status 2>/dev/null || true", + "comment": "Show KB counts + pending proposals at the start of every session." + } + ] + } +} +``` + +- [ ] **Step 2: Validate JSON parses** + +```bash +python3 -c "import json; json.load(open('adapters/claude-code/.claude/settings.json'))" +``` + +Expected: no output. + +- [ ] **Step 3: Commit** + +```bash +git add adapters/claude-code/.claude/settings.json +git commit -m "feat(adapter/claude-code): T4 — settings.json (SessionStart hook + read-only auto-allow)" +``` + +--- + +## Task 8: Adapter README — tiered adoption guide + +**Files:** +- Modify: `adapters/claude-code/README.md` + +- [ ] **Step 1: Replace the file** with the tiered guide + +```markdown +# Claude Code adapter + +Wires [vouch][v] (an MCP server) into [Claude Code][cc] in four composable +tiers. Stop at any tier — each one works on its own. + +[v]: https://github.com/vouchdev/vouch +[cc]: https://claude.com/claude-code + +## Prerequisite + +```bash +pipx install vouch-kb # the command is `vouch` +vouch init # create .vouch/ in your project +``` + +## The four tiers + +| Tier | File | What it does | +|---|---|---| +| T1 | `.mcp.json` | Registers the `vouch` MCP server so the agent has the `kb_*` tools. | +| T2 | `CLAUDE.md` (append the snippet) | Teaches the agent the review-gate workflow. | +| T3 | `.claude/commands/vouch-*.md` | Four slash commands: `/vouch-recall`, `/vouch-status`, `/vouch-resolve-issue`, `/vouch-propose-from-pr`. | +| T4 | `.claude/settings.json` | `SessionStart` hook prints `vouch status`; reads/`list_*` are auto-allowed (writes still prompt). | + +## One-shot install + +```bash +vouch install-mcp claude-code # writes T1..T4 into the current project +vouch install-mcp claude-code --tier T2 # stop at T2 (skip slash commands + settings) +``` + +The command is idempotent — files that already exist are left alone (verbose +mode lists them). + +## Manual install (if you prefer) + +1. Copy `adapters/claude-code/.mcp.json` to your project root. +2. Append `adapters/claude-code/CLAUDE.md.snippet` to your project's + `CLAUDE.md` (or `AGENTS.md`). The snippet is fenced with + `` / `` so it's safe to re-append. +3. Copy `adapters/claude-code/.claude/commands/*.md` to + `.claude/commands/` in your project. +4. Merge `adapters/claude-code/.claude/settings.json` into your project's + `.claude/settings.json` (or commit it as-is if you have none). + +## Verify + +```bash +vouch status # KB present? +claude --debug-mcp 2>&1 | grep vouch # MCP server visible to Claude Code? +``` + +In a fresh Claude session, ask "what knowledge-base tools do you have?" — +it should enumerate `kb_search`, `kb_propose_claim`, etc. + +## Notes + +- `VOUCH_AGENT=claude-code` is the default in the bundled `.mcp.json`; + change it if you run multiple Claude Code seats against the same KB. +- The auto-allow rules in T4 only cover read-only `kb_*` methods — writes + (`kb_approve`, `kb_reject`, `kb_crystallize`, `kb_propose_*`) still + prompt, protecting the review gate. +- For hosts that launch from a default cwd (e.g. Claude Desktop), set + `VOUCH_KB_PATH=/abs/path/.vouch` in the `env` block of `.mcp.json`. +``` + +- [ ] **Step 2: Commit** + +```bash +git add adapters/claude-code/README.md +git commit -m "docs(adapter/claude-code): tiered adoption guide + install-mcp reference" +``` + +--- + +## Task 9: Writer module — TDD (test-first) + +**Files:** +- Create: `tests/test_install_adapter.py` + +- [ ] **Step 1: Write the failing tests** + +```python +"""Tests for the Claude Code adapter installer.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from vouch.install_adapter import ( + AdapterError, + available_adapters, + install, +) + +ADAPTER_ROOT = Path(__file__).resolve().parent.parent / "adapters" / "claude-code" + + +def test_available_adapters_lists_claude_code() -> None: + assert "claude-code" in available_adapters() + + +def test_install_t1_writes_only_mcp_json(tmp_path: Path) -> None: + result = install("claude-code", target=tmp_path, tier="T1") + assert (tmp_path / ".mcp.json").is_file() + assert not (tmp_path / "CLAUDE.md").exists() + assert not (tmp_path / ".claude" / "commands").exists() + assert not (tmp_path / ".claude" / "settings.json").exists() + body = json.loads((tmp_path / ".mcp.json").read_text()) + assert body["mcpServers"]["vouch"]["command"] == "vouch" + assert sorted(result.written) == [".mcp.json"] + + +def test_install_t4_writes_all_tiers(tmp_path: Path) -> None: + result = install("claude-code", target=tmp_path, tier="T4") + assert (tmp_path / ".mcp.json").is_file() + assert (tmp_path / "CLAUDE.md").is_file() + cmds = tmp_path / ".claude" / "commands" + assert (cmds / "vouch-recall.md").is_file() + assert (cmds / "vouch-status.md").is_file() + assert (cmds / "vouch-resolve-issue.md").is_file() + assert (cmds / "vouch-propose-from-pr.md").is_file() + assert (tmp_path / ".claude" / "settings.json").is_file() + assert len(result.written) == 7 # mcp + claude.md + 4 commands + settings + + +def test_install_is_idempotent(tmp_path: Path) -> None: + install("claude-code", target=tmp_path, tier="T4") + second = install("claude-code", target=tmp_path, tier="T4") + assert second.written == [] + assert sorted(second.skipped) == sorted([ + ".mcp.json", + "CLAUDE.md", + ".claude/commands/vouch-recall.md", + ".claude/commands/vouch-status.md", + ".claude/commands/vouch-resolve-issue.md", + ".claude/commands/vouch-propose-from-pr.md", + ".claude/settings.json", + ]) + + +def test_install_claude_md_appends_when_existing(tmp_path: Path) -> None: + (tmp_path / "CLAUDE.md").write_text("# My project\n\nExisting content.\n") + result = install("claude-code", target=tmp_path, tier="T2") + final = (tmp_path / "CLAUDE.md").read_text() + assert "Existing content." in final + assert "" in final + assert "" in final + assert "CLAUDE.md" in result.appended + + +def test_install_claude_md_skips_when_already_fenced(tmp_path: Path) -> None: + snippet = (ADAPTER_ROOT / "CLAUDE.md.snippet").read_text() + (tmp_path / "CLAUDE.md").write_text("# Existing\n\n" + snippet) + result = install("claude-code", target=tmp_path, tier="T2") + assert "CLAUDE.md" in result.skipped + assert "CLAUDE.md" not in result.appended + + +def test_install_unknown_adapter_raises() -> None: + with pytest.raises(AdapterError, match="unknown adapter"): + install("bogus", target=Path("/tmp"), tier="T1") + + +def test_install_unknown_tier_raises(tmp_path: Path) -> None: + with pytest.raises(AdapterError, match="unknown tier"): + install("claude-code", target=tmp_path, tier="T9") +``` + +- [ ] **Step 2: Run to confirm RED** + +```bash +.venv/bin/python -m pytest tests/test_install_adapter.py -q +``` + +Expected: collection error — `ModuleNotFoundError: No module named 'vouch.install_adapter'`. + +--- + +## Task 10: Writer module — GREEN + +**Files:** +- Create: `src/vouch/install_adapter.py` + +- [ ] **Step 1: Write the minimal implementation** + +```python +"""Idempotently install a host adapter (e.g. Claude Code) into a target project. + +The adapter templates live under `adapters//` in the vouch repo. The +installer copies them into `target` paths, skipping files that already exist +verbatim and appending fenced blocks into existing files (CLAUDE.md). +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path + +ADAPTERS_DIR = Path(__file__).resolve().parent.parent.parent / "adapters" + +_TIER_FILES: dict[str, list[tuple[str, str]]] = { + # (adapter-relative source, target-relative dest) + "T1": [(".mcp.json", ".mcp.json")], + "T2": [("CLAUDE.md.snippet", "CLAUDE.md")], + "T3": [ + (".claude/commands/vouch-recall.md", ".claude/commands/vouch-recall.md"), + (".claude/commands/vouch-status.md", ".claude/commands/vouch-status.md"), + (".claude/commands/vouch-resolve-issue.md", ".claude/commands/vouch-resolve-issue.md"), + (".claude/commands/vouch-propose-from-pr.md", ".claude/commands/vouch-propose-from-pr.md"), + ], + "T4": [(".claude/settings.json", ".claude/settings.json")], +} + +_TIER_ORDER = ("T1", "T2", "T3", "T4") +_FENCE_BEGIN = "" + + +class AdapterError(RuntimeError): + pass + + +@dataclass +class InstallResult: + written: list[str] = field(default_factory=list) + skipped: list[str] = field(default_factory=list) + appended: list[str] = field(default_factory=list) + + +def available_adapters() -> list[str]: + if not ADAPTERS_DIR.is_dir(): + return [] + return sorted( + p.name for p in ADAPTERS_DIR.iterdir() + if p.is_dir() and (p / ".mcp.json").is_file() + ) + + +def install(adapter: str, *, target: Path, tier: str = "T4") -> InstallResult: + if adapter not in available_adapters(): + raise AdapterError( + f"unknown adapter {adapter!r} (available: {', '.join(available_adapters())})" + ) + if tier not in _TIER_ORDER: + raise AdapterError( + f"unknown tier {tier!r} (available: {', '.join(_TIER_ORDER)})" + ) + + src_root = ADAPTERS_DIR / adapter + target = target.resolve() + target.mkdir(parents=True, exist_ok=True) + + result = InstallResult() + selected = _TIER_ORDER[: _TIER_ORDER.index(tier) + 1] + for t in selected: + for src_rel, dst_rel in _TIER_FILES[t]: + src = src_root / src_rel + dst = target / dst_rel + if dst_rel == "CLAUDE.md": + _install_claude_md(src, dst, result) + else: + _install_plain(src, dst, dst_rel, result) + return result + + +def _install_plain(src: Path, dst: Path, dst_rel: str, result: InstallResult) -> None: + if dst.exists(): + result.skipped.append(dst_rel) + return + dst.parent.mkdir(parents=True, exist_ok=True) + dst.write_text(src.read_text(encoding="utf-8"), encoding="utf-8") + result.written.append(dst_rel) + + +def _install_claude_md(src: Path, dst: Path, result: InstallResult) -> None: + snippet = src.read_text(encoding="utf-8") + if not dst.exists(): + dst.write_text(snippet, encoding="utf-8") + result.written.append("CLAUDE.md") + return + existing = dst.read_text(encoding="utf-8") + if _FENCE_BEGIN in existing: + result.skipped.append("CLAUDE.md") + return + sep = "" if existing.endswith("\n") else "\n" + dst.write_text(existing + sep + "\n" + snippet, encoding="utf-8") + result.appended.append("CLAUDE.md") +``` + +- [ ] **Step 2: Run tests to confirm GREEN** + +```bash +.venv/bin/python -m pytest tests/test_install_adapter.py -q +``` + +Expected: all 7 tests pass. + +- [ ] **Step 3: Commit** + +```bash +git add src/vouch/install_adapter.py tests/test_install_adapter.py +git commit -m "feat(install): adapter installer with tier selection + idempotent CLAUDE.md fence" +``` + +--- + +## Task 11: CLI wiring — `vouch install-mcp claude-code` + +**Files:** +- Modify: `src/vouch/cli.py` + +- [ ] **Step 1: Add the import** near the other onboarding-style imports (top of cli.py) + +Edit: in the import section, add a line: + +```python +from .install_adapter import AdapterError, available_adapters, install as install_adapter +``` + +- [ ] **Step 2: Add the `install-mcp` command group** at the end of cli.py, before `if __name__ == "__main__":` + +```python +# --- install-mcp ---------------------------------------------------------- + + +@cli.group(name="install-mcp") +def install_mcp_group() -> None: + """Install vouch into a Claude Code-style project (idempotently).""" + + +@install_mcp_group.command(name="claude-code") +@click.option("--path", default=".", show_default=True, + type=click.Path(file_okay=False), + help="Target project root.") +@click.option("--tier", default="T4", show_default=True, + type=click.Choice(["T1", "T2", "T3", "T4"]), + help="Stop at the given tier (T1=mcp only; T4=full integration).") +def install_mcp_claude_code(path: str, tier: str) -> None: + """Drop .mcp.json + CLAUDE.md + slash commands + settings into PATH.""" + target = Path(path).resolve() + try: + result = install_adapter("claude-code", target=target, tier=tier) + except AdapterError as e: + raise click.ClickException(str(e)) from e + for f in result.written: + click.echo(f" + {f}") + for f in result.appended: + click.echo(f" ~ {f} (appended fenced block)") + for f in result.skipped: + click.echo(f" · {f} (already present)") + click.echo( + f"Done — {len(result.written)} written, " + f"{len(result.appended)} appended, {len(result.skipped)} skipped." + ) +``` + +- [ ] **Step 3: Run a smoke test against a tmp dir** + +```bash +tmp=$(mktemp -d) && .venv/bin/vouch install-mcp claude-code --path "$tmp" --tier T1 +ls -1 "$tmp" +rm -rf "$tmp" +``` + +Expected: prints `+ .mcp.json` and a `Done — 1 written, ...` summary; tmp dir contains `.mcp.json`. + +- [ ] **Step 4: Run the full suite + mypy + ruff** + +```bash +.venv/bin/python -m pytest tests/ -q --ignore=tests/embeddings +.venv/bin/python -m mypy src +.venv/bin/python -m ruff check src tests +``` + +Expected: all green; mypy "Success: no issues found"; ruff "All checks passed!". + +- [ ] **Step 5: Commit** + +```bash +git add src/vouch/cli.py +git commit -m "feat(cli): vouch install-mcp claude-code [--tier T1..T4]" +``` + +--- + +## Task 12: CHANGELOG + push + +**Files:** +- Modify: `CHANGELOG.md` + +- [ ] **Step 1: Add the `[Unreleased] → Added` entry** + +Edit `CHANGELOG.md`. Under `## [Unreleased]`, add (or extend) an `### Added` section: + +```markdown +### Added +- Claude Code adapter at `adapters/claude-code/` ships four composable tiers: `.mcp.json` (T1), a fenced `CLAUDE.md` snippet (T2), four slash commands `/vouch-recall`, `/vouch-status`, `/vouch-resolve-issue`, `/vouch-propose-from-pr` (T3), and `.claude/settings.json` with a `SessionStart` hook + read-only `kb_*` auto-allow (T4). New `vouch install-mcp claude-code [--tier T1..T4]` writes them into a project idempotently — existing files are left alone, and `CLAUDE.md` gets a fenced appended block so it's safe to re-run. +``` + +- [ ] **Step 2: Final verify** + +```bash +.venv/bin/python -m pytest tests/ -q --ignore=tests/embeddings +.venv/bin/python -m mypy src +.venv/bin/python -m ruff check src tests +``` + +Expected: all three green. + +- [ ] **Step 3: Commit + push** + +```bash +git add CHANGELOG.md +git commit -m "docs: CHANGELOG entry for Claude Code adapter + install-mcp" +GIT_SSH_COMMAND="ssh -i ~/.ssh/plind-junior -o IdentitiesOnly=yes" \ + git push -u origin feat/claude-code-adapter +``` + +- [ ] **Step 4: Restore the pre-existing storage.py edit on `release/0.1.0`** + +```bash +git switch release/0.1.0 +git stash pop +git status --porcelain src/vouch/storage.py +``` + +Expected: ` M src/vouch/storage.py` (the pre-existing edit is back). + +--- + +## Self-Review Checklist (run before handoff) + +1. **Spec coverage** — every tier (T1–T4) has its own task; the CLI front door has its own; idempotency is tested. +2. **Placeholder scan** — every code/template step contains the literal file contents; no "TODO / fill in details". +3. **Type consistency** — `install()`, `InstallResult`, `available_adapters()`, `AdapterError` names match across the tests, the implementation, and the CLI. +4. **Idempotency** — `_install_plain` checks `dst.exists()`; `_install_claude_md` checks for the fence; tests cover both. diff --git a/docs/superpowers/specs/2026-05-25-vouch-diff-design.md b/docs/superpowers/specs/2026-05-25-vouch-diff-design.md new file mode 100644 index 00000000..f5bb4ea0 --- /dev/null +++ b/docs/superpowers/specs/2026-05-25-vouch-diff-design.md @@ -0,0 +1,95 @@ +# vouch diff — claim/page revision diff + +## Problem + +When a claim is superseded or a page is revised, there is no way to see *what +changed* between the old and new artifact. You can `show` each one and compare +by eye, but nothing renders the delta. ROADMAP 0.1 lists `vouch diff + ` for exactly this. + +## Goal + +`vouch diff ` shows what changed between two claim revisions +or two page revisions — field-level changes plus a line-diff of the long text +field. Read-only: no writes, no proposals, no audit events. + +## Decisions + +- **Auto-detect kind.** Resolve both ids as claims; if that fails, resolve both + as pages. Mismatched kinds or unknown ids are a clear error — no `--kind` + flag. +- **Semantic fields only.** Diff the fields that carry meaning; hide + always-churning metadata (`id`, `created_at`, `updated_at`, + `last_confirmed_at`, `approved_by`). +- **Line-diff the long text.** `claim.text` / `page.body` render as a + `difflib` unified diff; everything else as `field: old → new`. +- **CLI-only.** Read-only inspection; does not touch the `kb.*` capability set. + +## Components — `src/vouch/diff.py` + +### `DiffError(Exception)` +Raised for unknown ids and mismatched kinds. + +### `FieldChange` (dataclass) +`field: str, old, new` — one changed scalar/list field. + +### `ArtifactDiff` (dataclass) +`kind: str, old_id: str, new_id: str, changes: list[FieldChange], +text_diff: list[str]`. + +### `diff_artifacts(store, old_id, new_id) -> ArtifactDiff` +- **Kind resolution:** try `store.get_claim` on both ids → both succeed ⇒ + `kind="claim"`. Otherwise try `store.get_page` on both → `kind="page"`. If an + id resolves to neither, raise `DiffError("unknown artifact: ")`. If one is + a claim and the other a page, raise + `DiffError("cannot diff claim against page")`. +- **changes:** for each semantic field whose value differs, append a + `FieldChange`. The long text field is handled separately (not in `changes`). +- **text_diff:** `list(difflib.unified_diff(old_text.splitlines(), + new_text.splitlines(), lineterm=""))` for `claim.text` / `page.body`; empty + when unchanged. + +Field sets (long text field rendered as `text_diff`, the rest as changes): +- **Claim** — text *(diff)*; type, status, confidence, evidence, entities, + tags, supersedes, superseded_by, contradicts, scope. +- **Page** — body *(diff)*; title, type, status, claims, entities, sources, + tags. + +## CLI — `vouch diff OLD NEW [--json]` + +Follows existing patterns (`_load_store`, `_cli_errors`, `_emit_json`). + +Human output: +``` +diff claim + status: working → stable + confidence: 0.7 → 0.9 + evidence: ['s1'] → ['s1', 's2'] + text: + --- a + +++ b + -old wording + +new wording +``` +- `--json` → `_emit_json` of the `ArtifactDiff` as a dict. +- No differences → prints `no differences`. + +## Error handling + +- Unknown id (neither claim nor page) → `DiffError` → clean CLI `Error:` line. +- Mismatched kinds → `DiffError` → clean CLI `Error:` line. + +## Testing (TDD) + +- `diff_artifacts`: two claims differing in status/confidence → matching + `FieldChange`s; a text change → `text_diff` contains `-`/`+` lines; identical + claims → empty `changes` and `text_diff`; two pages differing in title/body; + unknown id → `DiffError`; claim-vs-page → `DiffError`. +- CLI: `vouch diff a b` prints the changed fields; `--json` emits a dict with + `kind`/`changes`; unknown id → clean `Error:`; identical → `no differences`. + +## Non-goals + +- Following supersede chains automatically (caller passes both ids). +- Diffing entities/relations/sources (claims and pages only, per ROADMAP). +- MCP/JSONL parity (`kb.*` surface unchanged). diff --git a/docs/superpowers/specs/2026-05-27-init-templates-design.md b/docs/superpowers/specs/2026-05-27-init-templates-design.md new file mode 100644 index 00000000..ac67a49d --- /dev/null +++ b/docs/superpowers/specs/2026-05-27-init-templates-design.md @@ -0,0 +1,100 @@ +# vouch init --template — domain starter packs + +## Problem + +A fresh `.vouch/` KB starts nearly empty (one generic starter claim). When you +spin up vouch inside a domain-specific repo — e.g. Gittensor (SN74) — it knows +nothing about that domain, so the first agent session has no cited context to +retrieve. We want `vouch init` to optionally seed a domain-aware starter pack. + +## Goal + +Generalize `vouch init`'s seeding into a named-template registry and ship a +`gittensor` pack as the first non-default template: + +``` +vouch init [--path P] [--template starter|gittensor] +``` + +Default stays the current `starter` seed (behavior unchanged). `gittensor` +seeds a small, cited, **already-approved** knowledge base about how SN74 +contribution scoring works. + +## Decisions + +- **Registry, not a one-off flag.** A `TEMPLATES` dict maps name → seed + function so future packs (research-notes, codebase-audit) plug in the same + way. +- **Approved-direct seeding.** Like the existing `seed_starter_kb`, template + artifacts are written approved at init time — this is bootstrap content, not + the review queue. +- **Idempotent.** Stable ids / content hashes mean re-running `init` creates + nothing new. +- **In-code templates, not files.** No `templates//` on disk and no + reliance on `vouch ingest`; the pack is a seed function. + +## Components — `src/vouch/onboarding.py` + +### `SeedResult` (dataclass) +`template: str`, `created: list[str]` (artifact ids actually created), +`created_anything: bool`. Replaces/absorbs the current `StarterSeedResult`. + +### `TEMPLATES` registry +```python +TEMPLATES: dict[str, Callable[[KBStore, str], SeedResult]] = { + "starter": seed_starter_kb, + "gittensor": seed_gittensor_kb, +} +``` +`available_templates() -> list[str]` returns sorted keys (for help + errors). + +### `seed_starter_kb(store, approved_by) -> SeedResult` +The current default seed, refactored to return `SeedResult`. Behavior unchanged +(same source + claim ids). + +### `seed_gittensor_kb(store, approved_by) -> SeedResult` +Seeds, idempotently and approved-direct: +- **1 Source** — SN74 description text (`title="Gittensor SN74"`, + `locator="vouch:template/gittensor"`, `source_type="message"`, + `media_type="text/markdown"`), content from Gittensor's public README facts. +- **1 Entity** — `gittensor-sn74` (`type=project`). +- **4 Claims** (`type=fact`, `status=stable`, each citing the source, linked to + the `gittensor-sn74` entity): + 1. Miners earn TAO for pull requests merged into whitelisted repositories. + 2. Validators verify GitHub account ownership via a fine-grained PAT before scoring. + 3. Contributions are scored by code quality, repository allocation, and language factors. + 4. Sybil-resistant: GitHub account verification + merged-PR requirement prevent gaming. +- Each artifact keyed by a stable id; existence checks make re-runs no-ops. + +## CLI — `src/vouch/cli.py` + +`init` gains `--template` (default `"starter"`): +- Resolve the name against `TEMPLATES`; unknown → `_cli_errors`-style + `Error: unknown template '' (available: gittensor, starter)`. +- Call the selected seed; print a generalized summary + (`Seeded