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
18 changes: 18 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Keep the build context to what the Dockerfile actually copies
# (pyproject.toml, README.md, src/) plus room for local experiments.
.git
.venv
.vouch
.claude
desktop
docs
eval
examples
web
tests
benchmarks
adapters
skills
proposals
**/__pycache__
**/*.py[cod]
129 changes: 129 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
name: release

# Publish a version tag (e.g. v1.1.0): build the sdist + wheel, publish to
# PyPI via Trusted Publishing (OIDC — no API token stored), push the
# container image to ghcr.io/vouchdev/vouch, and create the GitHub release
# with the dists attached and the matching CHANGELOG section as the body.
# The release title comes from the annotated tag's subject (falls back to
# the tag name for lightweight tags).
# One-time setup on PyPI: add a trusted publisher for project `vouch-kb`
# pointing at vouchdev/vouch, workflow `release.yml`, environment `pypi`.
# Every job is idempotent, so a re-cut tag republishes cleanly.

on:
push:
tags:
- "v*"

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: python -m pip install --upgrade build
- run: python -m build
- uses: actions/upload-artifact@v7
with:
name: dist
path: dist/

publish:
needs: build
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write # required for trusted publishing
steps:
- uses: actions/download-artifact@v7
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1
with:
# tolerate re-runs and re-cut tags — pypi versions are immutable,
# so an already-published dist is skipped instead of failing 400.
skip-existing: true

container:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # push ghcr.io/vouchdev/vouch
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- uses: docker/metadata-action@v5
id: meta
with:
images: ghcr.io/${{ github.repository }}
# v1.1.0 -> 1.1.0, 1.1, latest (latest is automatic for semver)
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

github-release:
needs: build
runs-on: ubuntu-latest
permissions:
contents: write # create the release for the tag
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v7
with:
name: dist
path: dist/
- name: collect release title and notes
id: notes
run: |
tag="${GITHUB_REF_NAME}"
version="${tag#v}"
# checkout leaves the tag peeled to its commit; fetch the real tag
# object so an annotated tag's subject can become the title.
git fetch --force --quiet origin "refs/tags/$tag:refs/tags/$tag" || true
awk -v ver="$version" '
$0 ~ "^## \\[" ver "\\]" { on = 1; next }
on && /^## \[/ { exit }
on { print }
' CHANGELOG.md > release-notes.md
if ! [ -s release-notes.md ]; then
echo "no CHANGELOG section for ${version}; falling back" >&2
printf 'See CHANGELOG.md.\n' > release-notes.md
fi
if [ "$(git cat-file -t "$tag" 2>/dev/null)" = "tag" ]; then
title="$(git tag -l --format='%(contents:subject)' "$tag")"
else
title="$tag"
fi
printf 'title=%s\n' "${title:-$tag}" >> "$GITHUB_OUTPUT"
- name: create release
env:
GH_TOKEN: ${{ github.token }}
TITLE: ${{ steps.notes.outputs.title }}
run: |
# idempotent: a re-run or a moved tag replaces the stale release
# instead of failing on "already exists".
gh release delete "${GITHUB_REF_NAME}" --yes --repo "$GITHUB_REPOSITORY" || true
gh release create "${GITHUB_REF_NAME}" dist/* \
--title "$TITLE" \
--notes-file release-notes.md
91 changes: 49 additions & 42 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,7 @@ All notable changes to vouch are documented here. Format follows

## [Unreleased]

### Docs
- example KBs now carry their own screenshots: `examples/README.md` and the
`tiny/` + `decision-log/` READMEs embed terminal renders of `vouch status`,
`search`, `show`, `audit`, and a supersession `diff` against the shipped
fixtures, so a reader can see what vouch looks like before installing it.
Images live under `docs/img/examples/` and are generated deterministically
from the fixtures by `docs/img/examples/render.py` (`make
examples-screenshots`); `tests/test_example_screenshots.py` asserts the
committed SVGs stay reproducible (#286).
## [1.1.0] — 2026-07-03

### Added
- auto-capture: claude code sessions are harvested via hooks and filed as a
Expand Down Expand Up @@ -54,6 +46,53 @@ All notable changes to vouch are documented here. Format follows
one-shot propose-all and `--json` for machine-readable output. Configurable
via `themes.min_sessions`, `themes.min_claims`, `themes.top_k`, and
`themes.enabled` in `config.yaml` (#311).
- dual-solve JSON, review-ui job, and choose responses now include
`changed_files` for each candidate and the kept branch, so desktop and browser
clients can show the resulting files without parsing unified diffs.

### Changed
- `vouch dual-solve --sandbox` default docker image is now
`vouch/coder:latest` (was `amika/coder:latest`).

### Fixed
- `vouch pending` (and every bulk `list_*` path) no longer crashes when a
single artifact file is unreadable — a corrupt or mojibake yaml is skipped
with a warning instead of aborting the whole listing.
- all text-mode file i/o under `src/vouch/` now pins `encoding="utf-8"`, so a
non-utf-8 locale (e.g. latin-1) can no longer mangle non-ascii claim text
into raw control bytes that the yaml loader rejects, nor crash on write.

### Packaging
- restored the tag-triggered `release.yml` workflow that was accidentally
deleted alongside unrelated files in the #95 squash. It publishes to PyPI
via Trusted Publishing (OIDC) exactly as before, and now also creates the
GitHub release for the tag with the built sdist and wheel attached and the
matching CHANGELOG section as the release body.
- restored the `vouch-kb` distribution name in `pyproject.toml` — the same
#95 squash had reverted it to `vouch`, which PyPI rejects (the name belongs
to an unrelated project, and the trusted publisher is registered for
`vouch-kb`). The installed command is still `vouch`.
- container image: every release now also pushes `ghcr.io/vouchdev/vouch`
(linux/amd64 + linux/arm64, tagged `X.Y.Z`, `X.Y`, and `latest`). The
entrypoint is the `vouch` CLI with the stdio MCP server as the default
command; bind-mount the project root at `/data`. Built from the new
repo-root `Dockerfile`; installs the `web` extra, leaves embeddings out.
- the `[1.0.0]` section below was restored: a merge after the release folded
its entries back under `[Unreleased]`, dropping the version header.

### Docs
- example KBs now carry their own screenshots: `examples/README.md` and the
`tiny/` + `decision-log/` READMEs embed terminal renders of `vouch status`,
`search`, `show`, `audit`, and a supersession `diff` against the shipped
fixtures, so a reader can see what vouch looks like before installing it.
Images live under `docs/img/examples/` and are generated deterministically
from the fixtures by `docs/img/examples/render.py` (`make
examples-screenshots`); `tests/test_example_screenshots.py` asserts the
committed SVGs stay reproducible (#286).

## [1.0.0] — 2026-06-26

### Added
- `vouch dual-solve <issue-url>` — run claude + codex on one github issue in
isolated git worktrees, compare the two diffs, keep the branch you pick, and
propose the chosen solution's rationale into the KB. A sibling tool to
Expand All @@ -66,14 +105,11 @@ All notable changes to vouch are documented here. Format follows
with elapsed time and diff size) reports progress to stderr while it works.
- `vouch dual-solve --sandbox` and
`vouch review-ui --dual-solve-sandbox` — run Claude Code and Codex inside a
Docker image (default `vouch/coder:latest`) while leaving git/GitHub commands
Docker image (default `amika/coder:latest`) while leaving git/GitHub commands
on the host. The sandbox runner mounts only each candidate worktree plus a
temporary copied home containing known Claude/Codex credential files, so agent
writes stay in the throwaway dual-solve branches and host credential files are
not modified.
- dual-solve JSON, review-ui job, and choose responses now include
`changed_files` for each candidate and the kept branch, so desktop and browser
clients can show the resulting files without parsing unified diffs.
- `vouch review-ui --allow-dual-solve` — a browser SPA that runs `dual-solve`
on a github issue link, streams progress over the review-ui's websocket, shows
both engines' diffs side by side, and lets you pick the winner. Off by default;
Expand Down Expand Up @@ -128,35 +164,6 @@ All notable changes to vouch are documented here. Format follows
KB under `eval/fixture-kb/`, and an `eval` workflow gating retrieval changes
(#226).
### Fixed
- the OpenClaw plugin packaging now targets the current (2026.6) plugin
loader, verified against a real `openclaw plugins install --link` of the
repo: `openclaw.plugin.json` moved to the `id` + JSON-Schema `configSchema`
dialect (`kind: context-engine`, `skills` as SKILL.md directories under
`adapters/openclaw/skills/`), a root `package.json` now carries the
loader-facing `openclaw.extensions` entry-module pointer and the
`openclaw.compat.pluginApi` floor, and the engine id was renamed
`vouch-context` → `vouch` so it matches the plugin id — OpenClaw's
installer auto-binds the contextEngine slot to the *plugin* id and
resolves it by *engine* id, so distinct ids silently quarantined the
engine in favour of the legacy engine. The old dialect's `mcpServers`,
`contracts`, `family`, `shared_deps`, and `openclaw.*` fields were
silently ignored by current loaders and are gone; the kb.* MCP server is
deployment config (`openclaw mcp add vouch -- vouch serve`). A Tier-2
e2e suite (`tests/test_openclaw_plugin_load_real.py`, skipped when the
`openclaw` CLI is absent) now links the repo into an isolated profile and
asserts import, engine registration, slot auto-bind, skill publication,
and a clean plugins doctor.
- `vouch openclaw-rpc` no longer crashes serializing `assemble` responses:
`contextPack.generated_at` is a `datetime`, which `json.dumps` rejected on
any turn that found a KB — OpenClaw quarantined the engine for the process
and silently fell back to its legacy engine. Found by running a real
OpenClaw agent turn against the linked plugin.
- `vouch pending` (and every bulk `list_*` path) no longer crashes when a
single artifact file is unreadable — a corrupt or mojibake yaml is skipped
with a warning instead of aborting the whole listing.
- all text-mode file i/o under `src/vouch/` now pins `encoding="utf-8"`, so a
non-utf-8 locale (e.g. latin-1) can no longer mangle non-ascii claim text
into raw control bytes that the yaml loader rejects, nor crash on write.
- `parse_since` (the `--since` parser behind `vouch metrics`/`vouch audit`) now raises a clean `MetricsError` for a duration too large to represent (e.g. `--since 1000000000000d`), instead of letting an uncaught `OverflowError` traceback escape — restoring the documented "clean error, not a traceback" contract.
- `sync_apply` now loads the sync source exactly once and passes the same `_SyncSource` instance into `sync_check`, closing a TOCTOU window where a bundle replaced on disk between the two `_load_source` calls could cause the validation and write phases to operate on different snapshots. Also eliminates redundant directory walks (KB sources) and triple tarball opens (bundle sources). Fixes #217.
- `vault_to_kb` now passes `slug_hint=page_id` to `propose_page` so vault edit proposals target the existing page id from frontmatter instead of a slugified copy of the title (fixes #219).
Expand Down
53 changes: 53 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# The canonical vouch container, published to ghcr.io/vouchdev/vouch on every
# release tag by .github/workflows/release.yml. General-purpose: the
# entrypoint is the `vouch` CLI itself and the default command is the stdio
# MCP server, so the same image serves MCP hosts, the HTTP transport, and
# one-off CLI calls.
#
# MCP (stdio, the canonical surface — note -i):
# docker run -i --rm -v "$PWD:/data" ghcr.io/vouchdev/vouch
# HTTP transport (refuses a public bind without a bearer token):
# docker run --rm -p 8731:8731 -v "$PWD:/data" -e VOUCH_HTTP_TOKEN=... \
# ghcr.io/vouchdev/vouch serve --transport http --host 0.0.0.0 --allow-public
# CLI:
# docker run --rm -v "$PWD:/data" ghcr.io/vouchdev/vouch status
#
# Bind-mount the project root (the directory containing .vouch/) at /data;
# the KB is discovered from the working directory exactly as on a host
# checkout. adapters/http-tunnel/Dockerfile remains the opinionated
# HTTP-only deployment reference.

FROM python:3.13-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
LANG=C.UTF-8

LABEL org.opencontainers.image.source="https://github.com/vouchdev/vouch" \
org.opencontainers.image.description="Git-native, review-gated knowledge base for LLM agents. MCP server + CLI." \
org.opencontainers.image.licenses="MIT"

WORKDIR /app

# Install from this checkout so local dev images never need a PyPI release
# in the loop. The web extra pulls in the review-ui / fastapi surface;
# embeddings stay out (torch does not belong in the default image).
COPY pyproject.toml README.md ./
COPY src ./src
RUN pip install --no-cache-dir '.[web]'

# /data is the KB volume mount point: the host's project root, containing
# .vouch/, is served exactly as-is.
VOLUME ["/data"]
WORKDIR /data

# uid 1000 matches the default first user on Linux hosts, so files created
# through the /data bind mount stay owned by the host user. Override with
# `docker run --user "$(id -u):$(id -g)"` where that assumption is wrong.
RUN useradd --create-home --uid 1000 vouch && chown vouch:vouch /data
USER vouch

EXPOSE 8731

ENTRYPOINT ["vouch"]
CMD ["serve"]
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<a href="https://pypi.org/project/vouch-kb/"><img src="https://img.shields.io/pypi/v/vouch-kb.svg" alt="PyPI"></a>
<a href="https://pypi.org/project/vouch-kb/"><img src="https://img.shields.io/pypi/pyversions/vouch-kb.svg" alt="Python versions"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/vouchdev/vouch.svg" alt="MIT licensed"></a>
<a href="https://x.com/vouch_dev"><img src="https://img.shields.io/badge/follow-%40vouch__dev-000000?logo=x&logoColor=white" alt="Follow @vouch_dev on X"></a>
</p>

> Agents should not start every session with amnesia — but they shouldn't get to write whatever they want either.
Expand Down Expand Up @@ -67,6 +68,19 @@ Claude Code install to point you at the next step (`vouch install-mcp
claude-code`). Inspect it first if you'd like — it's [`install.sh`](install.sh)
at the repo root.

Or skip the install entirely and run the released container image
([`ghcr.io/vouchdev/vouch`](https://github.com/vouchdev/vouch/pkgs/container/vouch)),
bind-mounting the project root (the directory containing `.vouch/`) at
`/data`:

```bash
# stdio MCP server (the canonical surface — note -i)
docker run -i --rm -v "$PWD:/data" ghcr.io/vouchdev/vouch:latest

# any CLI command
docker run --rm -v "$PWD:/data" ghcr.io/vouchdev/vouch:latest status
```
Comment on lines +71 to +82

## Running the tests

From a clone with the dev extras installed (`pip install -e '.[dev]'`):
Expand Down
40 changes: 35 additions & 5 deletions openclaw.plugin.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,39 @@
{
"id": "vouch",
"name": "Vouch",
"version": "1.0.0",
"description": "Git-native, review-gated knowledge base. Registers vouch's context engine (cited retrieval + salience reflex + hot memory) and the four vouch skills. The kb.* MCP server is deployment config: `openclaw mcp add vouch -- vouch serve`.",
"kind": "context-engine",
"name": "vouch",
"version": "1.1.0",
"description": "Git-native, review-gated knowledge base for LLM agents. MCP server + JSONL tool server + CLI.",
"family": "bundle-plugin",
"homepage": "https://github.com/vouchdev/vouch",
Comment on lines +2 to +6
"configSchema": {
"kb_path": {
"type": "string",
"required": false,
"description": "Absolute path to the project root containing .vouch/. When unset, vouch walks up from cwd to find the nearest KB.",
"default": null
},
"agent": {
"type": "string",
"required": false,
"description": "Identity string recorded on every proposal + audit event when this plugin is the writer. Exported as VOUCH_AGENT to the child process.",
"default": "openclaw"
},
"transport": {
"type": "string",
"required": false,
"description": "MCP wire format. 'stdio' is the canonical surface; 'jsonl' is the AKBP-style newline-delimited fallback for harnesses that can't speak MCP.",
"default": "stdio",
"enum": ["stdio", "jsonl"]
}
},
"mcpServers": {
"vouch": {
"command": "vouch",
"args": ["serve"],
"env": {
"VOUCH_AGENT": "openclaw"
}
}
},
Comment on lines +28 to +36
"skills": [
"adapters/openclaw/skills"
],
Expand Down
Loading
Loading