diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..a3ec3b3c --- /dev/null +++ b/.dockerignore @@ -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] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..482b5eb2 --- /dev/null +++ b/.github/workflows/release.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c533599..6961cbd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 ` — 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 @@ -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; @@ -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). diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..9643f325 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md index ee5c6a96..a3ef866b 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ PyPI Python versions MIT licensed + Follow @vouch_dev on X

> Agents should not start every session with amnesia — but they shouldn't get to write whatever they want either. @@ -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 +``` + ## Running the tests From a clone with the dev extras installed (`pip install -e '.[dev]'`): diff --git a/openclaw.plugin.json b/openclaw.plugin.json index 17ed3300..de73bf81 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -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", + "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" + } + } + }, "skills": [ "adapters/openclaw/skills" ], diff --git a/pyproject.toml b/pyproject.toml index 516cb65d..f50e1506 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] -name = "vouch" -version = "1.0.0" +name = "vouch-kb" +version = "1.1.0" description = "Git-native, review-gated knowledge base for LLM agents. MCP server + CLI." readme = "README.md" requires-python = ">=3.11" diff --git a/src/vouch/__init__.py b/src/vouch/__init__.py index 4e2a86c7..b905a610 100644 --- a/src/vouch/__init__.py +++ b/src/vouch/__init__.py @@ -1,3 +1,3 @@ """vouch — git-native, review-gated knowledge base for LLM agents.""" -__version__ = "1.0.0" +__version__ = "1.1.0"