diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d4fff95..dccec54f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,28 @@ All notable changes to vouch are documented here. Format follows ## [Unreleased] +### 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. +### 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 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). + ## [0.1.0] — 2026-05-26 ### Packaging @@ -15,13 +37,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 +76,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 +106,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 +129,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..74879fbc 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,14 @@ 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 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 +181,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 +261,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 +275,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/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