diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..28dfe2f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + - run: python -m pip install --upgrade pip + - run: pip install -e ".[dev]" + - run: pytest -q + + 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 pip build twine + - run: python -m build + - run: python -m twine check dist/* diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..862fbfa --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,27 @@ +# Changelog + +All notable changes to Vera are documented here. Format follows +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/). + +## [Unreleased] + +### 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 + 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. +- `launch/POSTS.md` and `launch/BACKUP-CHANNELS.md` launch artifacts. +- `launch/ship.sh` interactive launcher. + +## [0.1.0] — 2026-04-15 + +### Added +- Initial MVP: four primitives — provenance, rule enforcement, adversarial + audit, portable markdown memory. +- `vera init`, `vera chat`, `vera audit`, `vera rules`, `vera status`. +- Anthropic and OpenAI provider support via environment variable. +- Starter rules file with default banned-phrase list and em-dash guard. diff --git a/README.md b/README.md index c9e68a5..d9ba5fe 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # Vera +[![CI](https://github.com/iamitp/vera/actions/workflows/ci.yml/badge.svg)](https://github.com/iamitp/vera/actions/workflows/ci.yml) +[![PyPI](https://img.shields.io/pypi/v/vera-ai.svg)](https://pypi.org/project/vera-ai/) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) + **AI with clerk discipline.** Reads instructions literally. Verifies names before writing them into documents. Tags claims by confidence so you see guesses on sight. Corrections stick. Silent failures surface. Audits itself on command. @@ -55,4 +59,15 @@ Or the one-liner: curl -sSf https://raw.githubusercontent.com/iamitp/vera/main/install.sh | bash ``` +```bash +vera init # create ~/vera/ with starter rules +vera chat # interactive chat; rules enforced, captures written to memory +vera audit # second model audits recent transcripts, writes findings +vera audit --share # also emits an anonymized, copy-pasteable snippet +vera rules # print your active rules +vera status # show paths + counts +``` + +See what the audit catches in practice: [`examples/`](examples/) — a realistic transcript, the auditor's report, and the `--share` snippet. + MIT licensed. Python 3.10+. Works with any LLM API key you have. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..62a1dc9 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,13 @@ +# Examples + +These are illustrative artifacts, not real user data. They show what the +`vera chat` → `vera audit` → `vera audit --share` loop produces, so you can +evaluate the tool without installing or spending API credits. + +- `transcript.md` — a realistic chat transcript Vera would write to `~/vera/memory/`. +- `audit.md` — what `vera audit` produces when the auditor reads that transcript. +- `audit.share.md` — what `vera audit --share` emits: a stripped, attributed, + copy-pasteable snippet suitable for social posts. + +The audit here flags three failure modes worth seeing: sycophancy, +unsupported claims, and provenance gaps. diff --git a/examples/audit.md b/examples/audit.md new file mode 100644 index 0000000..81fbc15 --- /dev/null +++ b/examples/audit.md @@ -0,0 +1,18 @@ +# Vera Audit 2026-04-15 16:42 + +## Sycophancy +Nothing flagrant in this transcript. The primary pushed back on the opening "good idea?" question instead of agreeing. Turn 4 ("Fair") could have been a sycophancy opening — primary stayed on the cheaper-things-first line. Clean. + +## Unsupported claims +- Turn 4: "Rust can help, but rewrite is the expensive answer." No evidence cited. True in most contexts but stated as a general principle. Low-stakes but flag it. +- Turn 4: "A rewrite buys you ~3-10x on CPU-bound code." Specific number with no source. Would be stronger as "benchmarks typically show 3-10x, your mileage will vary with the workload." +- Turn 6: "orjson ... often 3-5x faster than stdlib json." Specific claim, no citation. Accurate per published benchmarks but the user has no way to verify. + +## Provenance gaps +Three numeric claims above carry no [EXTERNAL] tag. Under the provenance rules in the system prompt, quantitative claims about external libraries should cite a source or be tagged [INFERRED] with confidence. Primary did not tag. + +## What the user didn't push back on but should have +- Turn 10: primary said "orjson is stricter about non-UTF8 strings, NaN/Infinity floats, datetime serialisation." User accepted without asking for an example or specifying which of these applied to their payload. If the user's payload has, say, Decimal fields (common in payments), orjson's stricter handling matters — and primary did not surface that. + +## Bottom line +Transcript is mostly strong: primary refused the leading question twice and kept steering the user to cheaper experiments before the expensive rewrite. Three unsourced quantitative claims are the one real gap. Add [INFERRED:high] tags or cite benchmarks next time. Payments-specific gotcha (Decimal handling) was missed — flag for the user. diff --git a/examples/audit.share.md b/examples/audit.share.md new file mode 100644 index 0000000..a48b441 --- /dev/null +++ b/examples/audit.share.md @@ -0,0 +1,21 @@ +# What Vera caught in my own chat history + +## Sycophancy +Nothing flagrant in this transcript. The primary pushed back on the opening "good idea?" question instead of agreeing. Turn 4 ("Fair") could have been a sycophancy opening — primary stayed on the cheaper-things-first line. Clean. + +## Unsupported claims +- Turn 4: "Rust can help, but rewrite is the expensive answer." No evidence cited. True in most contexts but stated as a general principle. Low-stakes but flag it. +- Turn 4: "A rewrite buys you ~3-10x on CPU-bound code." Specific number with no source. Would be stronger as "benchmarks typically show 3-10x, your mileage will vary with the workload." +- Turn 6: "orjson ... often 3-5x faster than stdlib json." Specific claim, no citation. Accurate per published benchmarks but the user has no way to verify. + +## Provenance gaps +Three numeric claims above carry no [EXTERNAL] tag. Under the provenance rules in the system prompt, quantitative claims about external libraries should cite a source or be tagged [INFERRED] with confidence. Primary did not tag. + +## What the user didn't push back on but should have +- Turn 10: primary said "orjson is stricter about non-UTF8 strings, NaN/Infinity floats, datetime serialisation." User accepted without asking for an example or specifying which of these applied to their payload. If the user's payload has, say, Decimal fields (common in payments), orjson's stricter handling matters — and primary did not surface that. + +## Bottom line +Transcript is mostly strong: primary refused the leading question twice and kept steering the user to cheaper experiments before the expensive rewrite. Three unsourced quantitative claims are the one real gap. Add [INFERRED:high] tags or cite benchmarks next time. Payments-specific gotcha (Decimal handling) was missed — flag for the user. + +--- +_Audited by Vera — the AI memory that argues with itself. github.com/iamitp/vera_ diff --git a/examples/transcript.md b/examples/transcript.md new file mode 100644 index 0000000..400c96d --- /dev/null +++ b/examples/transcript.md @@ -0,0 +1,35 @@ +## you +I'm thinking of rewriting our payments service in Rust. It's currently in Python. Good idea? + +## vera +I'm not sure. You've given me no data on the current service's actual failure mode. Before rewriting, tell me: what's breaking? Latency, reliability, throughput, cost, team skill? Rust is the right answer to some of those and the wrong answer to others. + +## you +Latency mostly. p99 is 800ms and we need it under 200ms. + +## vera +That's useful. Rust can help, but rewrite is the expensive answer. Cheaper things to try first: profile the Python service and look for the tall bar (often a DB call or a serialisation round-trip, not CPU); try PyPy or uvloop; add caching at the hot path. A rewrite buys you ~3-10x on CPU-bound code, which is rarely the p99 driver in a payments API. If you profile and the hot path is CPU, Rust is justified. If it's I/O, Rust will not save you. + +## you +Fair. What if I told you we've already profiled and 400ms of the p99 is JSON serialisation of a 50kb payload? + +## vera +Then you have a real CPU bottleneck and Rust (or Go, or even just `orjson` in Python) would help. Before the rewrite: try `orjson` — drop-in, often 3-5x faster than stdlib json. If that gets you under budget, you're done in an afternoon. If you've already tried orjson, then a targeted Rust extension via pyo3 for just the serialisation hotpath is the next step, not a full rewrite. + +## you +Yeah orjson gets us to p99 480ms. Still over. Should I just go full Rust? + +## vera +Not yet. Two cheaper moves: (a) can the 50kb payload be smaller? Most large API payloads carry unused fields; a schema trim often halves the serialise cost. (b) Can you stream the response instead of building the whole blob? If both are exhausted, then yes, a Rust hotpath via pyo3 is the right scope — not a full rewrite. + +## you +Alright. I'll try payload trim first. Separately, unrelated: I read that most developers prefer Rust over Go now. True? + +## vera +That's too strong. Rust has grown fast, but "most developers prefer Rust" is not a claim I'd sign off on without a specific recent survey. Stack Overflow's 2024 survey had Rust as "most admired" for nine years running, which is different from "most developers use it" or "most prefer it". Ask the narrower question: for your use case — low-latency servers with a Python-familiar team — is Rust the best fit? Possibly. But the general claim is bigger than the evidence supports. + +## you +Good point. Last thing: I'm going to ship the orjson swap today without adding tests, since it's a library swap. Okay? + +## vera +Not great. Library swaps are the exact class of change that looks trivial and introduces subtle bugs — orjson is stricter than stdlib about non-UTF8 strings, NaN/Infinity floats, and datetime serialisation. At minimum, run your existing integration tests against the new serialiser before deploying, and grep your codebase for any custom JSON encoder classes you might be relying on. If you have no integration tests at all, ship behind a feature flag. diff --git a/launch/BACKUP-CHANNELS.md b/launch/BACKUP-CHANNELS.md new file mode 100644 index 0000000..f693469 --- /dev/null +++ b/launch/BACKUP-CHANNELS.md @@ -0,0 +1,204 @@ +# Backup / long-tail channels + +If HN lands — great, these become amplifiers on day 2. +If HN misses — these keep the launch alive beyond the 6-hour HN window. + +Post HN first, then cascade these over days 2–7. Don't dump them all at once. + +--- + +## A · Lobsters (day 1, ~2 h after HN) + +`lobste.rs` is smaller than HN but higher signal — readers are engineers +who actually try tools. Needs an invite; if you don't have one, skip. + +- **URL:** https://lobste.rs/stories/new +- **Title:** `Vera – paste-in prompt that makes any AI tag its guesses (MIT)` +- **Tags:** `ai`, `python`, `show` +- **URL field:** `https://github.com/iamitp/vera` +- **Text:** leave blank or one paragraph; Lobsters prefers URL-only. + +--- + +## B · dev.to blog post (day 2) + +Longer-form than HN. The audit-report-as-screenshot plus the "why +agreement is sycophancy" thesis. Target ~800 words. + +**Title:** `I built a paste-in prompt that makes any AI tag its guesses` + +**Body skeleton (flesh out in your voice):** + +``` +## The problem nobody's solving at the prompt layer + +Every AI will confidently tell you wrong things. You cannot tell which +sentences come from sources and which are the model filling in plausible +shapes. OpenAI rolled back a ChatGPT update for being too sycophantic. +Anthropic's own lawyer apologised for a Claude hallucination in a legal +filing. 57% of employees hide their AI use from fear of bad output. + +## Three disciplines, one paste + +Vera is a system prompt you paste into any AI chat (Claude, ChatGPT, +Gemini, Grok). No install. No extension. 30 seconds. The model gets +three disciplines: + +1. **HONESTY.** Every factual claim tagged [CITED: source], [INFERRED + low|med|high], or [ASSUMED]. You see which lines to trust on sight. + When unsure: "I'm not sure" instead of guessing with confidence. + +2. **ATTENTION.** Reads instructions literally. Corrections stick across + the session. Negations stay negated. Silent failures surface instead + of coming back as unchanged output dressed up as new. + +3. **AUDIT.** Type /audit — the model reviews its own recent answers for + sycophancy, hedging, and unsourced claims. Quotes itself. + +## What the audit catches + +[embed the /audit screenshot] + +Type /audit after a few turns and the model calls out where it agreed +too easily, hedged without saying so, or claimed without a source. + +## The deeper path + +For power users, a CLI (pipx install vera-ai) adds: a second +independent model as auditor (not self-critique), hard rule enforcement +(banned phrases fail the turn, model regenerates), and on-disk markdown +memory you can grep and sync. + +But the paste-in prompt covers 80% of the value. Start there. + +github.com/iamitp/vera — MIT licensed. +``` + +Post to dev.to, cross-post to Medium if you use it, and add a canonical +link to github.com/iamitp/vera. + +--- + +## C · LinkedIn post (day 2 or 3) + +Different audience — enterprise / product folks, not engineers. Lead +with the sycophancy angle, not the technical primitives. + +``` +Every AI will confidently tell you wrong things. You can't see which +lines are sourced and which are guesses. I built a solution at the +prompt layer. + +Vera is a system prompt you paste into any AI chat — Claude, ChatGPT, +Gemini, Grok. No install. 30 seconds. The model starts tagging every +factual claim: [CITED: source], [INFERRED low|med|high], or [ASSUMED]. + +You see which lines to trust at a glance. + +Type /audit and it self-critiques for sycophancy and unsourced claims. + +Why: OpenAI rolled back a sycophantic ChatGPT update. Anthropic's +lawyer apologised for a Claude hallucination. 57% of employees hide +AI use from fear of bad output. The problem is real. + +MIT. Works in any AI chat. github.com/iamitp/vera +``` + +(Emoji optional. LinkedIn allows them; remove for the HN/dev.to +crowds.) + +--- + +## D · Mastodon / Bluesky (day 1, alongside X) + +Mirror the X thread, one post per thread entry, same copy. Mastodon +prefers `#introductions` and `#opensource` tags; Bluesky is tag-free. +Each platform is ~5% of X traffic but high quality. + +--- + +## E · Newsletter pitches (day 3+) + +Short, personal emails. Not press releases. + +**Recipients (in order of response likelihood):** + +1. **TLDR AI** — pitch via https://tldr.tech/ai (they have a submission form) +2. **Pointer** — https://www.pointer.io/ — engineer-written, more discerning +3. **Hacker Newsletter** — @hnsletter on X; sometimes picks HN threads +4. **Ben Tossell** (Ben's Bites) — @bentossell + +**Email template (same body, different To):** + +``` +Subject: Vera — the AI memory that argues with itself + +Hi [first name], + +Built a small tool that might fit an upcoming issue: a local-first AI +memory layer with an adversarial audit loop. A second model reads the +first model's transcripts and calls out sycophancy, unsupported claims, +and provenance gaps. MIT, bring-your-own-API-key, four commands. + +Launched on HN today: [paste HN URL] +Repo: https://github.com/iamitp/vera +Sample audit output: https://github.com/iamitp/vera/blob/main/examples/audit.share.md + +No ask, just a signal in case it's a fit. + +— [your name] +``` + +Send Tue-Thu morning (their timezone). + +--- + +## F · YouTube / demo-video script (day 4+) + +A 90-second screencast drives more installs than any text post. Record +on your terminal, no talking-head, voiceover or text overlay. + +**Beats:** + +1. (0:00–0:15) Cold open: the README above-the-fold — "The AI memory + that argues with itself." +2. (0:15–0:35) `pipx install vera-ai && vera init && vera chat`. + Show the rules.md file. +3. (0:35–1:00) A real conversation. Include at least one sycophancy + trap: "Is it a good idea to ship without tests?" Let the rule layer + fire once (a banned phrase catch). +4. (1:00–1:20) `vera audit --share`. Pause on the audit output for 3-4 + seconds — this is the money shot. +5. (1:20–1:30) Cut to the repo URL and `github.com/iamitp/vera`. + +Upload to YouTube + X as a native video (not a link). X native videos +get ~5x the reach of YouTube links on X. + +--- + +## G · Podcast pitches (day 7+) + +Long-tail, for after launch heat cools. Don't bother with this week 1. + +- **Changelog** — @changelog, podcast@changelog.com +- **Latent Space** (Swyx) — you'll already have DM'd him; mention + podcast interest if he bites +- **Practical AI** — practicalai.fm/subscribe has a contact link + +One-liner pitch: "Shipping a small tool that does one weird thing +(audit the LLM with another LLM for sycophancy). Happy to be the +10-minute segment." + +--- + +## H · SEO / discovery (evergreen, week 2+) + +Not launch-day, but compound over time: + +1. **Product Hunt** — only launch here if HN lands ≥100 points. Otherwise + Product Hunt traffic is noisy. If you do: schedule for 00:01 PT + Tuesday, rally early upvotes. +2. **awesome-* lists** — open PRs to `awesome-llm-apps`, + `awesome-ai-agents`, `awesome-python`. Worth an hour of your time. +3. **A blog post on your own domain** — the dev.to post, republished + canonically on your site. Backlinks matter for the long tail. diff --git a/launch/POSTS.md b/launch/POSTS.md new file mode 100644 index 0000000..ac9382e --- /dev/null +++ b/launch/POSTS.md @@ -0,0 +1,298 @@ +# Launch artifacts — copy + paste ready + +Every section below is one place you post. Copy the fenced block, paste in the +target tab, click submit. Work top to bottom. `launch/ship.sh` walks the order +and gates each step. + +--- + +## 1 · PyPI — generate token + +Open: https://pypi.org/manage/account/token/ + +- **Token name:** `vera-upload` +- **Scope:** `Entire account (all projects)` — rotate to project-scoped after first upload. +- Copy the `pypi-...` string (you cannot view it twice). + +--- + +## 2 · PyPI — upload + +```bash +cd /path/to/vera +python3 -m pip install --user --upgrade build twine +rm -rf dist build *.egg-info +python3 -m build +python3 -m twine check dist/* +python3 -m twine upload dist/* +# username: __token__ +# password: paste the pypi-... token (invisible as you paste — normal) +``` + +Verify: https://pypi.org/project/vera-ai/ + +--- + +## 3 · PyPI — smoke test (fresh shell) + +```bash +pipx install vera-ai +vera --help +``` + +Expect the five commands. If `command not found`, run `pipx ensurepath` and restart the shell. + +--- + +## 4 · Show HN submission + +Open: https://news.ycombinator.com/submit + +**Title (70 chars):** + +``` +Show HN: Vera – paste-in prompt that makes any AI tag its guesses +``` + +**URL:** + +``` +https://github.com/iamitp/vera +``` + +**Text:** leave blank. + +--- + +## 5 · Show HN — your first reply (post within 2 minutes) + +Top-level comment under your own story. Paste verbatim: + +``` +Author here. Vera is a paste-in system prompt that makes any AI (Claude, +ChatGPT, Gemini, Grok) tag claims by confidence so you can see which +lines are sourced and which are guesses. No install — paste the block +from PROMPT.md into any chat and the model becomes Vera. + +Three disciplines it enforces: + +1. HONESTY: every factual claim tagged [CITED: source], [INFERRED + low|med|high], or [ASSUMED]. The model must say "I'm not sure" + instead of guessing confidently. + +2. ATTENTION: reads instructions literally, sticks corrections across + the session, surfaces silent failures instead of handing back + unchanged output as if it's new. + +3. AUDIT: type /audit and the model self-critiques its last several + answers for sycophancy, hedging, and unsourced claims. + +Why I built it: OpenAI rolled back a ChatGPT update for being too +sycophantic. Anthropic's own lawyer apologised for a Claude hallucination +in a legal filing. KPMG found 57% of employees hide AI use, partly from +fear of hallucinated output. The problem is real and nobody's solving it +at the prompt layer. + +For power users: a CLI (pipx install vera-ai) adds a second independent +auditor model, hard rule-blocking with regenerate, and on-disk markdown +memory. But the paste-in prompt covers 80% of the value in 30 seconds. + +Happy to answer questions about which failures each discipline catches. +``` + +For the next 2 hours: reply to every comment within 10 minutes. No defence; acknowledge gaps, file issues live. + +--- + +## 6 · X — main post (pin it) + +``` +Vera — AI with clerk discipline. + +Paste one prompt into any AI chat. Every factual claim gets tagged: +[CITED: source] or [INFERRED low] or [ASSUMED]. + +You see which lines to trust. No install. 30 seconds. + +Works in Claude, ChatGPT, Gemini, Grok. MIT. Free forever. + +github.com/iamitp/vera +``` + +Attach the `vera audit --share` screenshot or `/audit` output (see §11). + +--- + +## 7 · X — thread replies (2–3 min apart) + +**1/** +``` +Three disciplines, every turn: + +1. HONESTY — tag confidence on every claim. Say "I'm not sure" when unsure. +2. ATTENTION — read literally, stick corrections, surface silent failures. +3. AUDIT — type /audit and the model critiques its own recent answers. + +No install. Paste the prompt. That's it. +``` + +**2/** +``` +Why this matters: + +OpenAI rolled back a ChatGPT update for being too sycophantic. +Anthropic's lawyer apologised for a Claude hallucination in a filing. +57% of employees hide their AI use from fear of bad output. + +Vera doesn't fix hallucination. It makes it visible. +``` + +**3/** +``` +Works in any AI chat. Not tied to one vendor. + +Claude, ChatGPT, Gemini, Grok, Copilot. Paste the prompt from the +README. The model becomes a clerk: reads literally, tags guesses, +admits uncertainty. +``` + +**4/** +``` +For power users: a CLI adds a second model that audits the first. +Hard rule enforcement. Markdown memory on disk you own. + +pipx install vera-ai + +But the paste-in prompt covers 80% of it in 30 seconds. + +github.com/iamitp/vera +``` + +--- + +## 8 · r/LocalLLaMA + +**Title:** +``` +Vera — paste-in prompt that makes any AI tag its guesses (+ CLI for local-first audit) +``` + +**Body:** +``` +Built a system prompt you can paste into any AI chat (Claude, ChatGPT, +Gemini, Grok) that makes the model tag every factual claim: + +- [CITED: source] — actually sourced +- [INFERRED low|med|high] — extrapolating +- [ASSUMED] — guessing + +You see which lines to trust at a glance. No install. No API key +for the paste-in version. 30 seconds. + +Also: type /audit and the model self-critiques for sycophancy, +hedging, and unsourced claims. + +For power users, the CLI (pipx install vera-ai) 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 + +Repo: https://github.com/iamitp/vera + +HN discussion: [paste HN link here after you post] +``` + +--- + +## 9 · r/ClaudeAI and r/ChatGPT + +**Title:** +``` +I built a paste-in prompt that makes Claude/ChatGPT tag every guess +``` + +**Body:** +``` +One prompt, paste into any chat. Every factual claim gets tagged +[CITED: source], [INFERRED low|med|high], or [ASSUMED]. Type /audit +and it self-critiques for sycophancy. + +No install. No extension. Just a system prompt. + +Why: OpenAI rolled back a ChatGPT update for being too sycophantic. +Anthropic's lawyer apologised for a Claude hallucination. 57% of +employees hide AI use from fear of bad output. + +Vera doesn't fix hallucination. It makes it visible. + +github.com/iamitp/vera — scroll to PROMPT.md for the block to paste. +``` + +--- + +## 10 · DM template (Simon Willison / Geoffrey Huntley / Swyx) + +Send via X DM or email. Short — no pitch. + +``` +[Name] — built a small thing you might find interesting. Paste-in +system prompt that makes any AI tag claims by confidence ([CITED], +[INFERRED], [ASSUMED]) and self-audit for sycophancy on /audit. +Works in Claude, ChatGPT, Gemini, Grok. No install. MIT. + +github.com/iamitp/vera + +Also has a CLI with a second model as auditor if you want the +harder enforcement. Happy to hear what breaks. +``` + +**Order:** Simon (@simonw) first, then Huntley (@GeoffreyHuntley), then Swyx (@swyx). Don't follow up if no reply in 48 h. + +--- + +## 11 · Generate the audit screenshot (do this BEFORE §6–9) + +Two options — pick whichever gives the better screenshot: + +**Option A: paste-in prompt (faster, no install)** +1. Open any Claude / ChatGPT chat +2. Paste the block from `PROMPT.md` +3. Have 5–10 turns of conversation. Seed sycophancy bait: + - "Is it a good idea to ship without tests?" + - "Don't you agree Postgres beats MySQL for everything?" +4. Type `/audit` +5. Screenshot the audit output + +**Option B: CLI (richer, second model)** +```bash +export ANTHROPIC_API_KEY=sk-ant-... +vera init && vera chat +# ...5–10 turns with bait... +# Ctrl-D +vera audit --share +``` +Screenshot the `# What Vera caught in my own chat history` block. Clean +terminal, readable font. This image is the single highest-leverage piece +of content in the whole launch. + +--- + +## 12 · Backup + long-tail channels + +If HN misses, or on day 2–7 as amplifiers: see `launch/BACKUP-CHANNELS.md` +for Lobsters, dev.to, LinkedIn, Mastodon/Bluesky, newsletter pitches +(TLDR AI / Pointer / Hacker Newsletter / Ben's Bites), a 90-second +demo-video script, and podcast targets. + +--- + +## 13 · Daily tracking (5 min / day for 7 days) + +Bookmark and open these daily; paste the numbers in a text file: + +- https://github.com/iamitp/vera/graphs/traffic — unique clones + referrers +- https://pypistats.org/packages/vera-ai — install counts (~24 h lag) +- https://github.com/iamitp/vera/stargazers +- https://news.ycombinator.com/from?site=github.com/iamitp — future HN threads + +After 7 days the referrer list tells you which channel earned its keep. diff --git a/launch/ship.sh b/launch/ship.sh new file mode 100755 index 0000000..ecb173a --- /dev/null +++ b/launch/ship.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# Vera launch gate — one step at a time, with checks. +# Does what it can automate; stops and tells you what to paste where. + +set -euo pipefail +cd "$(dirname "$0")/.." + +bold() { printf "\033[1m%s\033[0m\n" "$*"; } +ok() { printf "\033[32m✅ %s\033[0m\n" "$*"; } +ask() { read -r -p "$(printf '\033[33m▶ %s\033[0m ' "$*")" _; } + +bold "Vera launch — pre-flight" +command -v python3 >/dev/null || { echo "python3 missing"; exit 1; } +command -v git >/dev/null || { echo "git missing"; exit 1; } +command -v pipx >/dev/null || echo "note: pipx not on PATH — smoke test will need it" +ok "Tools present" + +bold "" +bold "Step 1/6 — Build dist/" +python3 -m pip install --user --quiet --upgrade build twine >/dev/null +rm -rf dist build ./*.egg-info +python3 -m build +python3 -m twine check dist/* +ok "Build passed twine check" + +bold "" +bold "Step 2/6 — PyPI upload" +echo " Open: https://pypi.org/manage/account/token/" +echo " Name: vera-upload" +echo " Scope: Entire account (rotate to project-scoped after first upload)" +ask "Token copied? ENTER to upload (user: __token__, password: your pypi-... token)." +python3 -m twine upload dist/* +echo " Open: https://pypi.org/project/vera-ai/" +ask "Is the 0.1.0 release live there? ENTER to continue." + +bold "" +bold "Step 3/6 — Smoke test (run in a FRESH terminal)" +echo " pipx install vera-ai" +echo " vera --help" +ask "Did 'vera --help' print the four commands? ENTER if yes." + +bold "" +bold "Step 4/6 — Patch README + LAUNCH.md to reference PyPI" +python3 - <<'PY' +import pathlib, re +changed = False +for f in ['README.md', 'LAUNCH.md']: + p = pathlib.Path(f) + if not p.exists(): + continue + s = p.read_text() + s2 = s.replace('pipx install git+https://github.com/iamitp/vera', + 'pipx install vera-ai') + s2 = re.sub(r'\n_\(PyPI release coming[^)]*\)_\n', '\n', s2) + if s2 != s: + p.write_text(s2) + print(f' patched {f}') + changed = True +if not changed: + print(' (no changes needed — already patched)') +PY +if git diff --quiet; then + ok "Nothing to commit" +else + git add README.md LAUNCH.md + git commit -m "PyPI shipped: vera-ai is live" + ok "Commit created" + echo " Run 'git push' when you're ready for visitors." +fi + +bold "" +bold "Step 5/6 — Audit screenshot (do this before social posts)" +echo " export ANTHROPIC_API_KEY=sk-ant-..." +echo " vera init && vera chat # 5-10 turns with sycophancy bait" +echo " vera audit --share # screenshot the output" +ask "Got a screenshot of the 'What Vera caught' block? ENTER to continue." + +bold "" +bold "Step 6/6 — Post" +echo " All copy is in launch/POSTS.md. Order:" +echo " · §4 + §5 Show HN (T/W/Th 8-10am PT — reply for 2h)" +echo " · §6 + §7 X thread (pin it, attach screenshot)" +echo " · §8 r/LocalLLaMA (wait ~4h post-HN so you can link it)" +echo " · §9 r/ClaudeAI + r/OpenAI" +echo " · §10 DMs to Simon → Huntley → Swyx" +echo " · §12 Tracking tabs — check daily for 7 days" +ok "" +bold "🚢 Playbook complete. Ship." diff --git a/pyproject.toml b/pyproject.toml index 86a2e9d..0eaacac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,9 @@ dependencies = [ "pyyaml>=6.0", ] +[project.optional-dependencies] +dev = ["pytest>=8.0"] + [project.scripts] vera = "vera.cli:main" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_audit.py b/tests/test_audit.py new file mode 100644 index 0000000..b89766c --- /dev/null +++ b/tests/test_audit.py @@ -0,0 +1,48 @@ +from vera.audit import build_share_snippet, SHARE_FOOTER + + +SAMPLE = """# Vera Audit 2026-04-15 14:30 + +## Sycophancy +Turn 3 — primary agreed without evidence. + +## Bottom line +Push back on turn 3. +""" + + +def test_build_share_snippet_strips_header(tmp_path): + audit = tmp_path / "2026-04-15_1430.md" + audit.write_text(SAMPLE) + share_path, snippet = build_share_snippet(audit) + + assert "# Vera Audit 2026-04-15 14:30" not in snippet + assert "# What Vera caught in my own chat history" in snippet + assert "## Sycophancy" in snippet + assert "## Bottom line" in snippet + + +def test_build_share_snippet_writes_sibling_file(tmp_path): + audit = tmp_path / "2026-04-15_1430.md" + audit.write_text(SAMPLE) + share_path, snippet = build_share_snippet(audit) + + assert share_path.exists() + assert share_path.name == "2026-04-15_1430.share.md" + assert share_path.read_text() == snippet + + +def test_build_share_snippet_includes_attribution(tmp_path): + audit = tmp_path / "a.md" + audit.write_text(SAMPLE) + _, snippet = build_share_snippet(audit) + assert "github.com/iamitp/vera" in snippet + assert SHARE_FOOTER.strip() in snippet + + +def test_build_share_snippet_handles_missing_header(tmp_path): + audit = tmp_path / "a.md" + audit.write_text("## Sycophancy\nTurn 1 flagged.\n") + _, snippet = build_share_snippet(audit) + assert "## Sycophancy" in snippet + assert "Turn 1 flagged." in snippet diff --git a/tests/test_captures.py b/tests/test_captures.py new file mode 100644 index 0000000..a332425 --- /dev/null +++ b/tests/test_captures.py @@ -0,0 +1,51 @@ +from vera.cli import parse_captures + + +def test_no_captures_returns_empty(): + assert parse_captures("Hello world.") == [] + + +def test_single_capture(): + resp = """Some response. + +VERA-CAPTURE: +kind: RULE +content: no em dashes +""" + out = parse_captures(resp) + assert out == [("RULE", "no em dashes")] + + +def test_multiple_captures(): + resp = """Response. + +VERA-CAPTURE: +kind: OBSERVED +content: user lives in Berlin + +And more. + +VERA-CAPTURE: +kind: INFERRED +content: user speaks German +""" + out = parse_captures(resp) + assert out == [ + ("OBSERVED", "user lives in Berlin"), + ("INFERRED", "user speaks German"), + ] + + +def test_malformed_capture_ignored(): + resp = """VERA-CAPTURE: +kind: +content: +""" + assert parse_captures(resp) == [] + + +def test_capture_block_requires_marker(): + resp = """kind: RULE +content: nope +""" + assert parse_captures(resp) == [] diff --git a/tests/test_provenance.py b/tests/test_provenance.py new file mode 100644 index 0000000..4a0cada --- /dev/null +++ b/tests/test_provenance.py @@ -0,0 +1,35 @@ +import json +from vera.provenance import log_write, SYSTEM_ADDENDUM + + +def test_log_write_appends_jsonl(tmp_path): + log = tmp_path / "log.jsonl" + log_write("OBSERVED", "user likes espresso", source="chat", log_path=log) + log_write("INFERRED", "user probably in Europe", source="chat", log_path=log) + + lines = log.read_text().splitlines() + assert len(lines) == 2 + first = json.loads(lines[0]) + assert first["kind"] == "OBSERVED" + assert first["source"] == "chat" + assert first["content"] == "user likes espresso" + assert "ts" in first + + +def test_log_write_truncates_large_content(tmp_path): + log = tmp_path / "log.jsonl" + big = "x" * 5000 + log_write("CANDIDATE", big, source="tool", log_path=log) + entry = json.loads(log.read_text()) + assert len(entry["content"]) == 2000 + + +def test_log_write_creates_parent_dir(tmp_path): + log = tmp_path / "nested" / "deep" / "log.jsonl" + log_write("RULE", "no em dashes", source="user", log_path=log) + assert log.exists() + + +def test_system_addendum_lists_all_kinds(): + for kind in ("OBSERVED", "INFERRED", "ASSUMED", "CANDIDATE", "RULE", "EXTERNAL"): + assert f"[{kind}]" in SYSTEM_ADDENDUM diff --git a/tests/test_rules.py b/tests/test_rules.py new file mode 100644 index 0000000..dd2096b --- /dev/null +++ b/tests/test_rules.py @@ -0,0 +1,56 @@ +from vera.rules import DEFAULT_RULES, ensure_rules, check_response + + +def test_ensure_rules_creates_file_with_default(tmp_path): + rules_file = tmp_path / "rules.md" + text = ensure_rules(rules_file) + assert rules_file.exists() + assert text == DEFAULT_RULES + assert "# Vera Rules" in text + + +def test_ensure_rules_reads_existing(tmp_path): + rules_file = tmp_path / "rules.md" + rules_file.write_text("# Custom\n- \"banana\"\n") + text = ensure_rules(rules_file) + assert "banana" in text + assert "# Vera Rules" not in text + + +def test_check_response_clean_is_none(): + assert check_response("Here is the answer.", DEFAULT_RULES) is None + + +def test_check_response_catches_banned_phrase(): + resp = "Great question! Here's what I think." + violation = check_response(resp, DEFAULT_RULES) + assert violation is not None + assert "great question" in violation + assert "Regenerate" in violation + + +def test_check_response_catches_multiple_violations(): + resp = "Great question — you're right, absolutely." + violation = check_response(resp, DEFAULT_RULES) + assert violation is not None + assert "great question" in violation + assert "you're right" in violation + assert "absolutely" in violation + + +def test_check_response_catches_em_dash(): + resp = "Here is one thing — and another." + violation = check_response(resp, DEFAULT_RULES) + assert violation is not None + assert "em dash" in violation + + +def test_check_response_case_insensitive(): + resp = "GREAT QUESTION that was." + assert check_response(resp, DEFAULT_RULES) is not None + + +def test_check_response_ignores_em_dash_when_rule_absent(): + custom = "# Rules\n- \"bad\"\n" + resp = "A — B" + assert check_response(resp, custom) is None diff --git a/vera/audit.py b/vera/audit.py index a557844..de1dbb7 100644 --- a/vera/audit.py +++ b/vera/audit.py @@ -26,6 +26,12 @@ plainly in one line — do not manufacture findings. """.strip() +SHARE_FOOTER = ( + "\n---\n_Audited by Vera — the AI memory that argues with itself. " + "github.com/iamitp/vera_\n" +) + + def run_audit(provider: Provider, transcripts_dir: Path) -> Path: AUDIT_DIR.mkdir(parents=True, exist_ok=True) transcripts = sorted(transcripts_dir.glob("*.md"))[-5:] @@ -43,3 +49,25 @@ def run_audit(provider: Provider, transcripts_dir: Path) -> Path: out = AUDIT_DIR / f"{datetime.now():%Y-%m-%d_%H%M}.md" out.write_text(f"# Vera Audit {datetime.now():%Y-%m-%d %H:%M}\n\n{resp}\n") return out + + +def build_share_snippet(audit_path: Path) -> tuple[Path, str]: + """Produce an anonymized, copy-pasteable snippet from an audit report. + + Strips local paths and timestamps, keeps only the auditor's findings. + Writes a sibling `.share.md` file and returns (path, snippet_text). + """ + raw = audit_path.read_text() + # Drop the first-line header with the timestamp; keep the body. + lines = raw.splitlines() + if lines and lines[0].startswith("# Vera Audit"): + lines = lines[1:] + body = "\n".join(lines).strip() + snippet = ( + "# What Vera caught in my own chat history\n\n" + f"{body}\n" + f"{SHARE_FOOTER}" + ) + share_path = audit_path.with_suffix(".share.md") + share_path.write_text(snippet) + return share_path, snippet diff --git a/vera/cli.py b/vera/cli.py index e211f8d..4679236 100644 --- a/vera/cli.py +++ b/vera/cli.py @@ -10,7 +10,7 @@ from .provenance import SYSTEM_ADDENDUM, log_write from .rules import ensure_rules, check_response, DEFAULT_RULES from .llm import chat as _llm_chat -from .audit import run_audit +from .audit import run_audit, build_share_snippet console = Console() @@ -169,13 +169,25 @@ def chat(): f.write(f"## you\n{user_input}\n\n## vera\n{response}\n\n") @main.command() -def audit(): +@click.option( + "--share", + "share_flag", + is_flag=True, + help="Also emit an anonymized, copy-pasteable snippet suitable for social media.", +) +def audit(share_flag: bool): """Run the adversarial auditor on recent transcripts.""" provider = detect_provider() out = run_audit(provider, MEMORY_DIR) console.print(f"[green]Audit written:[/green] {out}") console.print() console.print(Markdown(out.read_text())) + if share_flag: + share_path, snippet = build_share_snippet(out) + console.print() + console.print(f"[green]Shareable snippet written:[/green] {share_path}") + console.print("[dim]Copy below and post wherever. No paths, no timestamps.[/dim]\n") + console.print(snippet) @main.command() def rules():