Skip to content
Closed
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
49 changes: 48 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,59 @@ All notable changes to Vera are documented here. Format follows

## [Unreleased]

## [0.2.0] — 2026-04-21

Theme: visibility and control. 0.1.0 worked; 0.2.0 makes it durable
for week-over-week use.

### Added
- **Cost tracking.** `vera.llm.chat()` returns `(text, usage)` with token
counts plus an estimated USD cost from a public rate table. Every LLM
call (chat / chat-retry / audit) appends a `USAGE` entry to the
provenance log. `vera status` prints `Spend (30d): $X.XX over N calls`
when any USAGE entries exist.
- **Visible regenerate.** Rule violations no longer trigger a silent
retry. A dim `⟲ regenerating: <violation>` line prints before attempt
2; if the rule layer disagrees twice, a `⚠ rule layer disagreed
twice` footer is appended to the response.
- **`vera prune`.** New command archives transcripts older than N days
(default 90) into `MEMORY_DIR/_archive/`, always retaining the most
recent `--keep` (default 50). Supports `--dry-run`. `vera status`
surfaces archive counts.
- **Smarter memory summary.** `load_memory_summary()` now layers durable
facts (deduped `RULE` / `OBSERVED` captures from the last 30 days)
on top of headlines from the last 5 transcripts. No mid-word
truncation.
- **Audit controls.** `vera audit --last N`, `--since YYYY-MM-DD`,
`--model MODEL`, plus `VERA_AUDIT_MODEL` env var read in
`detect_provider()`.
- **Lenient VERA-CAPTURE parsing.** Accepts markdown blockquotes, list
bullets, indentation, case-insensitive keys, and blank-line
separators. Malformed blocks produce a console warning plus a
`MALFORMED` provenance entry instead of silently dropping.

### Changed
- `chat()` signature: now returns `(text, usage)`. All internal callers
updated.
- `ProvenanceKind` widened to include `MALFORMED`.
- `load_memory_summary()` rewritten (backwards-compatible at call-site;
output format changed).
- `run_audit()` signature: adds `n`, `since`, `model_override` kwargs.

### Fixed
- Install instructions across `README.md`, `LAUNCH.md`, `install.sh`,
and `launch/READY-TO-PASTE.md` now consistently point at
`pipx install vera-clerk` (PyPI) rather than the stale
`git+https://github.com/iamitp/vera` form.

## [0.1.1] — Unreleased pre-0.2.0 work

### Added
- `vera audit --share` flag: writes an anonymized, copy-pasteable `.share.md`
snippet alongside the audit report, with a Vera attribution footer.
- `examples/` directory with a realistic transcript, audit report, and share
snippet so the loop can be evaluated without running the CLI.
- Test suite under `tests/` (21 tests covering provenance, rules, audit
- Test suite under `tests/` (24 tests covering provenance, rules, audit
share-snippet, and VERA-CAPTURE parsing).
- GitHub Actions CI running pytest on Python 3.10 / 3.11 / 3.12 plus a
build+twine-check job.
Expand Down
4 changes: 2 additions & 2 deletions LAUNCH.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
>
> Local markdown. Any model. One command:
>
> `pipx install git+https://github.com/iamitp/vera && vera init && vera chat`
> `pipx install vera-clerk && vera init && vera chat`
>
> MIT. Free forever. github.com/iamitp/vera

Expand Down Expand Up @@ -43,7 +43,7 @@ What makes it different from Khoj / Mem0 / Letta / MemPalace:
- **Provenance tags** on every memory write (OBSERVED / INFERRED / ASSUMED / CANDIDATE — the four categories are enforced in the system prompt)
- **No vendor lock-in** (markdown on your disk, BYO API key, works with Claude or GPT)

MIT. Python 3.10+. `pipx install git+https://github.com/iamitp/vera`.
MIT. Python 3.10+. `pipx install vera-clerk`.

Repo: https://github.com/iamitp/vera

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ To avoid re-pasting every chat, put the prompt into Custom Instructions for a [C
For users who want hard rule-blocking, a second independent auditor model, and markdown memory on disk.

```bash
pipx install git+https://github.com/iamitp/vera
pipx install vera-clerk
export ANTHROPIC_API_KEY=sk-ant-... # or OPENAI_API_KEY
vera init && vera chat
```
Expand Down
2 changes: 1 addition & 1 deletion install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ if ! command -v pipx >/dev/null 2>&1; then
fi

# 2. install vera
pipx install --force git+https://github.com/iamitp/vera >/dev/null
pipx install --force vera-clerk >/dev/null
echo "✓ Vera installed."
echo ""

Expand Down
6 changes: 3 additions & 3 deletions launch/READY-TO-PASTE.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ The conversation was about rewriting a payments service in Rust. The user mentio

A sycophantic AI would have accepted the number and built recommendations on top. That's the point.

For power users, a CLI (pipx install git+https://github.com/iamitp/vera@v0.1.0) adds a second independent auditor model, hard rule-blocking with regenerate, and on-disk markdown memory you can grep and sync. But the paste-in prompt covers 80% of the value in 30 seconds.
For power users, a CLI (pipx install vera-clerk) adds a second independent auditor model, hard rule-blocking with regenerate, and on-disk markdown memory you can grep and sync. But the paste-in prompt covers 80% of the value in 30 seconds.

Happy to answer questions about which failures each discipline catches, why I didn't just ship an extension, or how it compares to guard-rails / constitutional-AI patterns.
```
Expand Down Expand Up @@ -106,7 +106,7 @@ Vera doesn't fix hallucination. It makes it visible.
```
For power users: a CLI adds a second independent model as the auditor, hard rule enforcement, and markdown memory on disk you own.

pipx install git+https://github.com/iamitp/vera@v0.1.0
pipx install vera-clerk

But the paste-in prompt covers 80% of it in 30 seconds.

Expand Down Expand Up @@ -136,7 +136,7 @@ Also: type /audit and the model self-critiques for sycophancy, hedging, and unso

Real example from a run against the Anthropic API this morning (examples/live-demo/ in the repo): the conversation was about a Rust rewrite, and the user said 400 ms of p99 was JSON serialisation of 50 kB. The /audit pass worked out the implied throughput (~0.1 MB/s, ~1000x too slow) and flagged it as likely measurement error.

For power users, the CLI (pipx install git+https://github.com/iamitp/vera@v0.1.0) adds:
For power users, the CLI (pipx install vera-clerk) adds:
- A second independent model as auditor (not self-critique)
- Hard rule enforcement (banned phrases fail the turn, model regenerates)
- Markdown memory on disk you own
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "vera-clerk"
version = "0.1.0"
version = "0.2.0"
description = "The AI memory that argues with itself. Local-first. Model-agnostic. Every answer with a receipt."
readme = "README.md"
requires-python = ">=3.10"
Expand Down
69 changes: 68 additions & 1 deletion tests/test_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def test_audit_cli_share_flag_writes_sibling(tmp_path, monkeypatch):
"# Vera Audit 2026-04-15 14:30\n\n## Sycophancy\nClean.\n"
)

monkeypatch.setattr(cli_mod, "run_audit", lambda provider, mem: report)
monkeypatch.setattr(cli_mod, "run_audit", lambda provider, mem, **kw: report)
monkeypatch.setattr(cli_mod, "detect_provider", lambda: object())
monkeypatch.setattr(cli_mod, "MEMORY_DIR", tmp_path / "memory")

Expand All @@ -92,3 +92,70 @@ def test_audit_cli_share_flag_writes_sibling(tmp_path, monkeypatch):
share_path = report.with_suffix(".share.md")
assert share_path.exists()
assert "# What Vera caught in my own chat history" in share_path.read_text()


def test_audit_cli_passes_flags_to_run_audit(tmp_path, monkeypatch):
from click.testing import CliRunner
from vera import cli as cli_mod

captured = {}

def fake_run_audit(provider, mem, **kw):
captured.update(kw)
out = tmp_path / "r.md"
out.write_text("# Vera Audit now\n\nclean\n")
return out

monkeypatch.setattr(cli_mod, "run_audit", fake_run_audit)
monkeypatch.setattr(cli_mod, "detect_provider", lambda: object())
monkeypatch.setattr(cli_mod, "MEMORY_DIR", tmp_path / "memory")

runner = CliRunner()
result = runner.invoke(
cli_mod.main,
["audit", "--last", "2", "--since", "2026-04-01", "--model", "claude-haiku-4-5-20251001"],
)
assert result.exit_code == 0, result.output
assert captured["n"] == 2
assert str(captured["since"]) == "2026-04-01"
assert captured["model_override"] == "claude-haiku-4-5-20251001"


def test_audit_cli_rejects_bad_since(tmp_path, monkeypatch):
from click.testing import CliRunner
from vera import cli as cli_mod

monkeypatch.setattr(cli_mod, "detect_provider", lambda: object())
runner = CliRunner()
result = runner.invoke(cli_mod.main, ["audit", "--since", "yesterday"])
assert result.exit_code != 0
assert "YYYY-MM-DD" in result.output


def test_select_transcripts_respects_last_and_since(tmp_path):
import os
from datetime import datetime, timedelta
from vera.audit import _select_transcripts

for name, days in [("a.md", 100), ("b.md", 60), ("c.md", 30), ("d.md", 5), ("e.md", 1)]:
p = tmp_path / name
p.write_text("x")
ts = (datetime.now() - timedelta(days=days)).timestamp()
os.utime(p, (ts, ts))

picked = _select_transcripts(tmp_path, n=2, since=None)
assert [p.name for p in picked] == ["d.md", "e.md"]

cutoff = (datetime.now() - timedelta(days=45)).date()
picked = _select_transcripts(tmp_path, n=10, since=cutoff)
assert [p.name for p in picked] == ["c.md", "d.md", "e.md"]


def test_detect_provider_reads_vera_audit_model(monkeypatch):
import importlib
monkeypatch.setenv("ANTHROPIC_API_KEY", "x")
monkeypatch.setenv("VERA_AUDIT_MODEL", "claude-haiku-4-5-20251001")
import vera.config
importlib.reload(vera.config)
provider = vera.config.detect_provider()
assert provider.audit_model == "claude-haiku-4-5-20251001"
92 changes: 84 additions & 8 deletions tests/test_captures.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@


def test_no_captures_returns_empty():
assert parse_captures("Hello world.") == []
captures, warnings = parse_captures("Hello world.")
assert captures == []
assert warnings == []


def test_single_capture():
Expand All @@ -12,8 +14,9 @@ def test_single_capture():
kind: RULE
content: no em dashes
"""
out = parse_captures(resp)
assert out == [("RULE", "no em dashes")]
captures, warnings = parse_captures(resp)
assert captures == [("RULE", "no em dashes")]
assert warnings == []


def test_multiple_captures():
Expand All @@ -29,23 +32,96 @@ def test_multiple_captures():
kind: INFERRED
content: user speaks German
"""
out = parse_captures(resp)
assert out == [
captures, _ = parse_captures(resp)
assert captures == [
("OBSERVED", "user lives in Berlin"),
("INFERRED", "user speaks German"),
]


def test_malformed_capture_ignored():
def test_malformed_capture_emits_warning():
resp = """VERA-CAPTURE:
kind:
content:
"""
assert parse_captures(resp) == []
captures, warnings = parse_captures(resp)
assert captures == []
assert len(warnings) == 1
assert "missing kind" in warnings[0]


def test_capture_block_requires_marker():
resp = """kind: RULE
content: nope
"""
assert parse_captures(resp) == []
captures, warnings = parse_captures(resp)
assert captures == []
assert warnings == []


def test_capture_inside_blockquote():
resp = """Response text.

> VERA-CAPTURE:
> kind: RULE
> content: no emojis in filenames
"""
captures, warnings = parse_captures(resp)
assert captures == [("RULE", "no emojis in filenames")]
assert warnings == []


def test_capture_with_list_bullets():
resp = """Response.

- VERA-CAPTURE:
- kind: OBSERVED
- content: user uses postgres at work
"""
captures, warnings = parse_captures(resp)
assert captures == [("OBSERVED", "user uses postgres at work")]
assert warnings == []


def test_capture_with_indentation():
resp = """Response.

VERA-CAPTURE:
kind: RULE
content: be terse
"""
captures, warnings = parse_captures(resp)
assert captures == [("RULE", "be terse")]


def test_capture_case_insensitive_keys():
resp = """VERA-CAPTURE:
Kind: INFERRED
Content: user cares about latency
"""
captures, _ = parse_captures(resp)
assert captures == [("INFERRED", "user cares about latency")]


def test_capture_tolerates_blank_line_after_marker():
resp = """VERA-CAPTURE:

kind: RULE
content: confirm before deleting
"""
captures, warnings = parse_captures(resp)
assert captures == [("RULE", "confirm before deleting")]
assert warnings == []


def test_capture_fields_five_lines_apart_still_parsed():
resp = """VERA-CAPTURE:
kind: RULE


content: don't paraphrase code
"""
captures, warnings = parse_captures(resp)
# Blank line after fields started ends the block; content should not be found
assert captures == []
assert len(warnings) == 1
Loading
Loading