Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ All notable changes to vouch are documented here. Format follows
## [Unreleased]

### Added
- `vouch install-mcp <host>` now bootstraps the KB when no `.vouch/` is
discoverable at or above the target, making install a one-command
setup. previously it exited 0 with "0 failed" in a fresh project while
every installed hook silently no-oped forever (`|| true`) and the mcp
server exited 2 — with nothing ever telling the user to run
`vouch init`. opt out with `--no-init` (a loud stderr warning then
names the remedy); an ancestor KB is reported ("Using existing KB
at …"), never shadowed by a second one; unknown hosts still fail
cleanly without planting a KB. staging-dir hosts opt out via a new
`kb_bootstrap: false` manifest key (claude-desktop does — its target
is a paste-ready staging dir, and a KB planted at an arbitrary cwd
would ambiently capture every child project). the preflight ignores
a shell-exported `VOUCH_KB_PATH` (it answers "what does this tree
resolve to", and notes the override on stderr instead). `vouch init`
and the auto-init share one code path so they cannot drift.
- cli mirrors for the last five `kb.*` methods that had none —
`vouch propose-delete`, `vouch source list`, `vouch session list`,
`vouch session transcript`, `vouch session summarize` — and the
Expand Down
49 changes: 10 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,14 @@ make check # the CI gate: lint + type + test

After exploring the demo above, set up vouch in your own project:

**1. Set up the KB and wire Claude Code** (one-time, per repo):
**1. Set up the KB and wire Claude Code** (one command, one-time, per repo):

```bash
cd /path/to/your/project
vouch init # creates .vouch/ with starter config
vouch install-mcp claude-code # wires capture hooks into Claude Code
vouch install-mcp claude-code # creates .vouch/ (if missing) + wires Claude Code
```

`install-mcp` writes `.mcp.json` (the `kb.*` MCP tools), the `/vouch-*` slash commands, and three hooks — `PostToolUse` capture, `SessionEnd` rollup, `SessionStart` recall. Restart Claude Code so they load.
`install-mcp` initialises the KB when no `.vouch/` is discoverable (pass `--no-init` to skip; `vouch init` still exists for KB-only setup), then writes `.mcp.json` (the `kb.*` MCP tools), the `/vouch-*` slash commands, and five hooks — `SessionStart` recall, `UserPromptSubmit` per-prompt recall, `PostToolUse` capture, `Stop` answer capture, `SessionEnd` rollup. Restart Claude Code so they load.

**2. Point `compile` at an LLM** — the only step that needs a model. In `.vouch/config.yaml`:

Expand All @@ -145,43 +144,15 @@ compile:
vouch review # walk pending proposals one at a time
```

**Want a browser UI for reviewing and proposing?** The video shows the **vouch webapp** — chat, review queue, claims, and stats. You have four options:
**Want a browser UI for reviewing and proposing?** The video shows the **vouch webapp** — chat, review queue, claims, and stats. Your options:

- **No setup**: Use the Docker demo (recommended)
- **pip, no clone**: `pipx install 'vouch-kb[web]'` then `vouch console` — serves the same React console from the installed package (Python only, no Docker, no node), open http://localhost:5173
- **Local development**: Clone the repo, run `make console`, open http://localhost:5173
- **CLI-only**: Use `vouch review`, `vouch show <id>`, `vouch approve <id>` commands instead
- **No setup**: the Docker demo above
- **pip, no clone**: `pipx install 'vouch-kb[web]'` then `vouch console` (Python only, no Docker, no node) — open http://localhost:5173
- **Local development**: clone the repo, run `make console`
- **Lighter built-in queue**: `vouch review-ui` (also in the `[web]` extra)
- **CLI-only**: `vouch pending`, `vouch show <id>`, `vouch approve <id>`, `vouch reject <id> --reason "…"`

**Point the webapp at your existing KB:**

```bash
# Terminal 1: start the vouch server pointing at your .vouch/
cd /path/to/your/project
vouch serve --transport http --port 8731

# Terminal 2: run the Docker UI pointing at that server
docker run --rm -p 127.0.0.1:5173:5173 \
-e VOUCH_TARGET=http://host.docker.internal:8731 \
ghcr.io/plind-junior/vouch-demo
# then open http://localhost:5173
```

Or serve that same console with no Docker — `vouch console` in place of Terminal 2 (needs the `[web]` extra), then add the `:8731` backend in the connect dialog:

```bash
vouch console # http://localhost:5173, proxying to the server above
```

Or to skip the browser entirely and use the CLI tools:

```bash
vouch review # walk pending proposals
vouch show <id> # inspect a claim or page
vouch approve <id> # approve a proposal
vouch reject <id> --reason "…" # reject with feedback
```

Both browser UIs ship with vouch under the `[web]` extra (`pipx install 'vouch-kb[web]'`): `vouch console` is the full React console shown in the video; `vouch review-ui` is a lighter built-in review queue. Or go piecemeal: `vouch pending`, `vouch show <id>`, `vouch approve <id>`, `vouch reject <id> --reason "…"`.
To point any of them at an existing KB, start a backend in your project — `vouch serve --transport http --port 8731` — then connect the console to `:8731` (for the Docker demo, pass `-e VOUCH_TARGET=http://host.docker.internal:8731`).

**5. Compile the wiki.**

Expand Down
12 changes: 9 additions & 3 deletions adapters/claude-code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,22 @@ Wires `vouch serve` (MCP, stdio) into [Claude Code][cc].
## 1. Install vouch

```bash
pip install -e '/path/to/vouch[dev]'
# or, once on PyPI: pipx install vouch
pipx install vouch-kb
# or, from a checkout: pip install -e '/path/to/vouch[dev]'
```

Make sure `vouch` is on the `PATH` Claude Code will see.

The one-command path — `vouch install-mcp claude-code` from your project
root — does everything below in one go, and initialises the `.vouch/` KB
first when the project doesn't have one yet (`vouch init` also does that
on its own; `--no-init` skips it). The rest of this file is the manual
equivalent.

## 2. Drop the MCP server into your project

Add `.mcp.json` at the root of your project (the same directory that
contains `.vouch/`):
contains `.vouch/` — created by `vouch init` if you're wiring by hand):

```json
{
Expand Down
5 changes: 5 additions & 0 deletions adapters/claude-desktop/install.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
# would conflict with both). The README walks through the copy step.
host: claude-desktop
pretty: Claude Desktop
# The target is a review-and-copy staging dir, not project wiring: a KB
# auto-bootstrapped there would land at whatever cwd the user ran the
# command from (often $HOME for a user-global host) and, via upward
# discovery, shadow every project beneath it.
kb_bootstrap: false
tiers:
T1:
- { src: claude_desktop_config.json, dst: claude-desktop/claude_desktop_config.json }
Expand Down
12 changes: 9 additions & 3 deletions docs/INSTALL_FOR_AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,14 @@ optional flags (all real — verified against `cli.py`):

- `--tier T1|T2|T3|T4` — how much to install; tiers stack (default `T4`).
- `--path <dir>` or `--target <dir>` — project root to write into (default `.`).
- `--no-init` — do not bootstrap a KB when `.vouch/` is missing.

re-runs are flat-noop: expect lines containing `written`, `appended`, `merged`,
or `skipped`. you may run this command unconditionally on every session start.
when no `.vouch/` is discoverable at or above the target, this command
bootstraps one first (same path as `vouch init`; staging-dir hosts like
`claude-desktop` are exempt) — expect a `No .vouch/ found — initialised KB
at …` line in that case. re-runs are flat-noop: expect lines containing
`written`, `appended`, `merged`, or `skipped`. you may run this command
unconditionally on every session start.

**assert:** exits 0; no `error:` lines.

Expand All @@ -72,7 +77,8 @@ see step 6.)

## 4. create or locate a kb

if `.vouch/` is missing:
step 2 already bootstraps the KB when it is missing, so normally there is
nothing to create here. if you wired with `--no-init` (or skipped step 2):

```bash
vouch init
Expand Down
8 changes: 4 additions & 4 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -279,11 +279,10 @@ claude_code_nudge() {
if [ -d "$HOME/.claude" ] || has_cmd claude; then
printf '\n'
info "${C_BOLD}Claude Code detected.${C_RESET}"
info "Wire vouch into a project (one-time, per repo):"
info "Wire vouch into a project (one command, one-time, per repo):"
printf '\n'
printf ' %scd /path/to/your/project%s\n' "$C_BOLD" "$C_RESET"
printf ' %svouch init%s\n' "$C_BOLD" "$C_RESET"
printf ' %svouch install-mcp claude-code%s\n' "$C_BOLD" "$C_RESET"
printf ' %svouch install-mcp claude-code%s # creates .vouch/ if missing\n' "$C_BOLD" "$C_RESET"
printf '\n'
info "Then restart Claude Code — vouch's kb.* tools and slash commands"
info "(/vouch-recall, /vouch-status, …) will be available."
Expand Down Expand Up @@ -311,7 +310,8 @@ main() {

printf '\n'
info "${C_BOLD}Next:${C_RESET}"
info " ${C_BOLD}vouch init${C_RESET} # create a .vouch/ KB in your project"
info " ${C_BOLD}vouch install-mcp <host>${C_RESET} # wire a host (project hosts create .vouch/ if missing)"
info " ${C_BOLD}vouch init${C_RESET} # KB-only setup, no host wiring"
info " ${C_BOLD}vouch serve${C_RESET} # start the MCP server"
info " ${C_BOLD}vouch --help${C_RESET} # the rest"
claude_code_nudge
Expand Down
92 changes: 81 additions & 11 deletions src/vouch/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
from .onboarding import (
DEFAULT_TEMPLATE,
TEMPLATES,
SeedResult,
StarterSeedResult,
available_templates,
seed_starter_kb,
)
Expand Down Expand Up @@ -185,6 +187,25 @@ def cli() -> None:
# --- bootstrap ------------------------------------------------------------


def _bootstrap_kb(
root: Path, *, template: str = DEFAULT_TEMPLATE
) -> tuple[KBStore, StarterSeedResult, SeedResult | None]:
"""Create + seed a KB at ``root`` — the single init path.

Shared by `vouch init` and `vouch install-mcp`'s auto-init so the two
cannot drift: same starter seed, same index rebuild, same audit event.
"""
root.mkdir(parents=True, exist_ok=True)
store = KBStore.init(root)
seed = seed_starter_kb(store, approved_by=_whoami())
template_result: SeedResult | None = None
if template != DEFAULT_TEMPLATE:
template_result = TEMPLATES[template](store, approved_by=_whoami())
health.rebuild_index(store)
audit_mod.log_event(store.kb_dir, event="kb.init", actor=_whoami())
Comment on lines +198 to +205

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
ast-grep outline src/vouch/onboarding.py \
  --items all \
  --match '_starter_claim|_starter_source|_starter_page|seed_starter_kb'

rg -n -C4 \
  '\b(proposals\.approve|approve|put_claim|put_page|put_source)\s*\(' \
  src/vouch/onboarding.py src/vouch/proposals.py

Repository: vouchdev/vouch

Length of output: 10032


🏁 Script executed:

#!/bin/bash
sed -n '1,220p' src/vouch/onboarding.py
printf '\n--- CLI ---\n'
sed -n '150,240p' src/vouch/cli.py

Repository: vouchdev/vouch

Length of output: 10914


bootstrap seeding bypasses the review gate seed_starter_kb and the template seeders call store.put_source, store.put_claim, and store.put_page directly, so vouch init and install-mcp can create durable kb artifacts outside proposals.approve(). route these writes through the approval path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vouch/cli.py` around lines 198 - 205, Update the initialization flow
around seed_starter_kb and the selected TEMPLATES seeder so all bootstrap
source, claim, and page writes go through proposals.approve() rather than direct
store.put_source, store.put_claim, or store.put_page calls. Preserve the
existing seeding behavior while ensuring vouch init and install-mcp cannot
create durable KB artifacts outside the review gate.

Sources: Coding guidelines, Path instructions

return store, seed, template_result


@cli.command()
@click.option("--path", default=".", type=click.Path(file_okay=False), show_default=True)
@click.option(
Expand All @@ -196,15 +217,9 @@ def cli() -> None:
)
def init(path: str, template: str) -> None:
"""Initialise a .vouch/ knowledge base at PATH."""
root = Path(path).resolve()
root.mkdir(parents=True, exist_ok=True)
store = KBStore.init(root)
seed = seed_starter_kb(store, approved_by=_whoami())
template_result = None
if template != DEFAULT_TEMPLATE:
template_result = TEMPLATES[template](store, approved_by=_whoami())
health.rebuild_index(store)
audit_mod.log_event(store.kb_dir, event="kb.init", actor=_whoami())
store, seed, template_result = _bootstrap_kb(
Path(path).resolve(), template=template
)
click.echo(f"Initialised KB at {store.kb_dir}")
if seed.created_anything:
click.echo(f"Seeded starter claim: {seed.claim_id}")
Expand Down Expand Up @@ -3899,15 +3914,29 @@ def pr_cache_show(repo: str, state: str, limit: int, as_json: bool, cache_dir: s
"T2 = +CLAUDE.md/AGENTS.md, T3 = +slash commands, "
"T4 = +host hooks/settings. Tiers stack.",
)
@click.option(
"--init/--no-init",
"auto_init",
default=True,
show_default=True,
help="Initialise a .vouch/ KB at the target when none is discoverable, "
"so install-mcp is a one-command setup.",
)
def install_mcp(
host: str | None, list_hosts: bool, path: str, target_alias: str | None, tier: str
host: str | None,
list_hosts: bool,
path: str,
target_alias: str | None,
tier: str,
auto_init: bool,
) -> None:
"""Install vouch into HOST (claude-code, cursor, …) idempotently.

\b
Examples:
vouch install-mcp --list # show known hosts
vouch install-mcp claude-code # write T1..T4 into cwd
vouch install-mcp claude-code # one command: init .vouch/ if
# missing + write T1..T4 into cwd
vouch install-mcp cursor --tier T2 # stop at AGENTS.md
vouch install-mcp claude-desktop # drop a paste-ready config
vouch install-mcp windsurf --path /abs/path/to/project
Expand All @@ -3930,6 +3959,47 @@ def install_mcp(
)

target = Path(target_alias or path).resolve()
if host in hosts and install_mod.wants_kb_bootstrap(host):
# a wired host without a KB is worse than a failed install: every
# installed hook is `|| true` (silent no-op forever) and `vouch serve`
# exits 2, with nothing left to tell the user why. unknown hosts skip
# this so the AdapterError below stays the first and only error;
# staging-dir hosts (manifest `kb_bootstrap: false`) skip it because
# their target is not project wiring. the probe ignores VOUCH_KB_PATH:
# the question is what this tree resolves to, not this shell.
if os.environ.get("VOUCH_KB_PATH"):
click.echo(
"note: VOUCH_KB_PATH is set — it takes precedence over the "
"project KB wherever that variable is exported.",
err=True,
)
try:
kb_root = discover_root(target, respect_env=False)
except KBNotFoundError:
if auto_init:
try:
store, seed, _tmpl = _bootstrap_kb(target)
except Exception as e:
# cli boundary: a half-done setup must read as an error,
# not a traceback.
raise click.ClickException(
f"could not initialise a KB at {target}: {e} — fix the "
"cause and rerun, run `vouch init` yourself, or pass "
"--no-init"
) from e
click.echo(f"No .vouch/ found — initialised KB at {store.kb_dir}")
if seed.created_anything:
click.echo(f"Seeded starter claim: {seed.claim_id}")
else:
click.echo(
f"warning: no .vouch/ at or above {target} — the installed "
"hooks and MCP server will do nothing until you run "
"`vouch init` there.",
err=True,
)
else:
if kb_root != target:
click.echo(f"Using existing KB at {kb_root / '.vouch'}")
try:
result = install_mod.install(host, target=target, tier=tier)
except install_mod.AdapterError as e:
Expand Down
22 changes: 22 additions & 0 deletions src/vouch/install_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ class _Manifest:
tiers: dict[str, list[_FileEntry]]
fence_begin: str = _DEFAULT_FENCE_BEGIN
fence_end: str = _DEFAULT_FENCE_END
# false for hosts whose install target is a staging dir for user-global
# config (claude-desktop) rather than project wiring — a KB bootstrapped
# at the target would land wherever the user happened to be standing.
kb_bootstrap: bool = True


def _load_manifest(host: str) -> _Manifest:
Expand Down Expand Up @@ -188,15 +192,33 @@ def _flag(name: str, raw: Any = raw, tier_name: str = tier_name) -> bool:
fence_begin = _DEFAULT_FENCE_BEGIN
fence_end = _DEFAULT_FENCE_END

kb_bootstrap = data.get("kb_bootstrap", True)
if not isinstance(kb_bootstrap, bool):
raise AdapterError(
f"{host}: install.yaml `kb_bootstrap` must be a boolean, "
f"got {type(kb_bootstrap).__name__} ({kb_bootstrap!r})"
)

return _Manifest(
host=host,
pretty=str(data.get("pretty") or host),
tiers=parsed,
fence_begin=fence_begin,
fence_end=fence_end,
kb_bootstrap=kb_bootstrap,
)


def wants_kb_bootstrap(host: str) -> bool:
"""Whether ``install-mcp`` may bootstrap a KB at this host's target.

Manifest-declared (`kb_bootstrap`, default true) so the knowledge lives
with the adapter: false for staging-dir hosts like claude-desktop whose
real config is user-global and whose target is not project wiring.
"""
return _load_manifest(host).kb_bootstrap


def available_adapters() -> list[str]:
"""Every directory under ``adapters/`` that has an ``install.yaml``."""
if not ADAPTERS_DIR.is_dir():
Expand Down
7 changes: 5 additions & 2 deletions src/vouch/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,15 +143,18 @@ def sha256_hex(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()


def discover_root(start: Path | None = None) -> Path:
def discover_root(start: Path | None = None, *, respect_env: bool = True) -> Path:
"""Walk up from `start` looking for a `.vouch` directory.

Mirrors how git locates its repo root. The walk can be skipped entirely
by setting `VOUCH_KB_PATH=/abs/path/.vouch` (documented in
`adapters/generic-mcp/README.md`) — useful when the host launches the
server from a default cwd (e.g. Claude Desktop on macOS / Windows).
`respect_env=False` ignores that override and answers purely from the
filesystem walk — for callers asking "what would this tree resolve to",
not "what would this process resolve to".
"""
forced = os.environ.get("VOUCH_KB_PATH")
forced = os.environ.get("VOUCH_KB_PATH") if respect_env else None
if forced:
kb = Path(forced).resolve()
if not kb.is_dir():
Expand Down
Loading
Loading