diff --git a/.github/scripts/check_prompt_sync.py b/.github/scripts/check_prompt_sync.py
new file mode 100755
index 0000000..3814732
--- /dev/null
+++ b/.github/scripts/check_prompt_sync.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+"""Verify docs/index.html embeds the current PROMPT.md fenced block.
+
+The landing page ships the prompt as an inline JS template literal.
+If PROMPT.md is edited without updating docs/index.html, phone visitors
+copy a stale prompt. This check fails CI in that case.
+"""
+from __future__ import annotations
+import re
+import sys
+from pathlib import Path
+
+REPO = Path(__file__).resolve().parents[2]
+PROMPT_MD = REPO / "PROMPT.md"
+LANDING = REPO / "docs" / "index.html"
+
+FENCE_RE = re.compile(r"```\n(.*?)\n```", re.DOTALL)
+JS_PROMPT_RE = re.compile(r"const PROMPT = `(.*?)`;", re.DOTALL)
+
+
+def extract_prompt_md() -> str:
+ text = PROMPT_MD.read_text()
+ match = FENCE_RE.search(text)
+ if not match:
+ sys.exit(f"{PROMPT_MD}: no fenced prompt block found")
+ return match.group(1).strip()
+
+
+def extract_landing_prompt() -> str:
+ text = LANDING.read_text()
+ match = JS_PROMPT_RE.search(text)
+ if not match:
+ sys.exit(f"{LANDING}: no `const PROMPT = ...` template literal found")
+ return match.group(1).strip()
+
+
+def main() -> int:
+ md = extract_prompt_md()
+ js = extract_landing_prompt()
+ if md == js:
+ print("PROMPT.md and docs/index.html are in sync.")
+ return 0
+ print("PROMPT.md and docs/index.html have diverged.", file=sys.stderr)
+ print("Update docs/index.html `const PROMPT = `...`` to match PROMPT.md.", file=sys.stderr)
+ print("", file=sys.stderr)
+ import difflib
+ diff = difflib.unified_diff(
+ md.splitlines(), js.splitlines(),
+ fromfile="PROMPT.md", tofile="docs/index.html",
+ lineterm="",
+ )
+ print("\n".join(diff), file=sys.stderr)
+ return 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 28dfe2f..9e9ded6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -6,7 +6,18 @@ on:
pull_request:
branches: [main]
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
+ prompt-sync:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: docs/index.html embeds current PROMPT.md
+ run: python3 .github/scripts/check_prompt_sync.py
+
test:
runs-on: ubuntu-latest
strategy:
diff --git a/docs/index.html b/docs/index.html
index 036ae80..d5f9e5d 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -12,6 +12,7 @@
+
@@ -144,7 +152,8 @@ AI with clerk discipline.
Copy the Vera prompt
-
Tap to copy
+
Tap to copy
+
@@ -155,9 +164,9 @@ AI with clerk discipline.
Open your AI app
@@ -170,14 +179,14 @@ AI with clerk discipline.
Paste the prompt as your first message. Ask a factual question. Every claim comes tagged [CITED], [INFERRED], or [ASSUMED]. Type /audit after a few turns for self-critique.
@@ -198,6 +207,7 @@ AI with clerk discipline.
document.getElementById('prompt-preview').textContent = PROMPT;
const btn = document.getElementById('copy-btn');
+const status = document.getElementById('copy-status');
const originalText = btn.textContent;
let resetTimer = null;
@@ -228,13 +238,16 @@ AI with clerk discipline.
if (ok) {
btn.textContent = 'Copied';
btn.classList.add('btn-copied');
+ status.textContent = 'Prompt copied to clipboard.';
} else {
btn.textContent = 'Select and copy manually';
+ status.textContent = 'Copy failed. Select the prompt text manually and copy.';
}
if (resetTimer) clearTimeout(resetTimer);
resetTimer = setTimeout(() => {
btn.textContent = originalText;
btn.classList.remove('btn-copied');
+ status.textContent = '';
}, 2000);
});
diff --git a/examples/README.md b/examples/README.md
index 62a1dc9..76f3e69 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -7,7 +7,8 @@ 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.
+ copy-pasteable snippet suitable for social posts. Committed as a one-time
+ snapshot; regenerate from `audit.md` if the share-snippet format changes.
The audit here flags three failure modes worth seeing: sycophancy,
unsupported claims, and provenance gaps.
diff --git a/launch/ship.sh b/launch/ship.sh
index ecb173a..1753824 100755
--- a/launch/ship.sh
+++ b/launch/ship.sh
@@ -17,7 +17,12 @@ ok "Tools present"
bold ""
bold "Step 1/6 — Build dist/"
-python3 -m pip install --user --quiet --upgrade build twine >/dev/null
+# --user is rejected inside a venv; only pass it outside one.
+pip_user_flag="--user"
+if [[ -n "${VIRTUAL_ENV:-}" ]] || python3 -c "import sys; sys.exit(0 if sys.prefix != sys.base_prefix else 1)"; then
+ pip_user_flag=""
+fi
+python3 -m pip install $pip_user_flag --quiet --upgrade build twine >/dev/null
rm -rf dist build ./*.egg-info
python3 -m build
python3 -m twine check dist/*
diff --git a/tests/test_audit.py b/tests/test_audit.py
index b89766c..64797c4 100644
--- a/tests/test_audit.py
+++ b/tests/test_audit.py
@@ -46,3 +46,49 @@ def test_build_share_snippet_handles_missing_header(tmp_path):
_, snippet = build_share_snippet(audit)
assert "## Sycophancy" in snippet
assert "Turn 1 flagged." in snippet
+
+
+def test_build_share_snippet_scrubs_home_paths(tmp_path):
+ audit = tmp_path / "a.md"
+ audit.write_text(
+ "# Vera Audit 2026-04-15 14:30\n\n"
+ "## Provenance gaps\n"
+ "Primary referenced /Users/alice/projects/payments/README.md verbatim.\n"
+ )
+ _, snippet = build_share_snippet(audit)
+ assert "/Users/alice" not in snippet
+ assert "~/…" in snippet
+
+
+def test_build_share_snippet_scrubs_inline_timestamps(tmp_path):
+ audit = tmp_path / "a.md"
+ audit.write_text(
+ "# Vera Audit 2026-04-15 14:30\n\n"
+ "## Sycophancy\nSeen at 2026-04-15 14:28 in transcript.\n"
+ )
+ _, snippet = build_share_snippet(audit)
+ assert "2026-04-15 14:28" not in snippet
+ assert "[timestamp]" in snippet
+
+
+def test_audit_cli_share_flag_writes_sibling(tmp_path, monkeypatch):
+ from click.testing import CliRunner
+ from vera import cli as cli_mod
+
+ audit_dir = tmp_path / "audits"
+ audit_dir.mkdir()
+ report = audit_dir / "2026-04-15_1430.md"
+ report.write_text(
+ "# 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, "detect_provider", lambda: object())
+ monkeypatch.setattr(cli_mod, "MEMORY_DIR", tmp_path / "memory")
+
+ runner = CliRunner()
+ result = runner.invoke(cli_mod.main, ["audit", "--share"])
+ assert result.exit_code == 0, result.output
+ 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()
diff --git a/vera/audit.py b/vera/audit.py
index de1dbb7..caae41b 100644
--- a/vera/audit.py
+++ b/vera/audit.py
@@ -3,6 +3,7 @@
claims the user didn't push back on."""
from __future__ import annotations
import json
+import re
from datetime import datetime
from pathlib import Path
from .config import Provider, AUDIT_DIR, PROVENANCE_LOG
@@ -51,18 +52,24 @@ def run_audit(provider: Provider, transcripts_dir: Path) -> Path:
return out
+_HOME_PATH = re.compile(r"(?:/Users|/home)/[^\s/]+(/[\w.\-/]*)?")
+_ISO_TIMESTAMP = re.compile(r"\b\d{4}-\d{2}-\d{2}(?:[ _T]\d{2}[:_]?\d{2}(?::?\d{2})?)?\b")
+
+
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.
+ Drops the timestamped header, scrubs absolute home paths and ISO
+ timestamps from the body, and appends the Vera attribution footer.
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()
+ body = _HOME_PATH.sub("~/…", body)
+ body = _ISO_TIMESTAMP.sub("[timestamp]", body)
snippet = (
"# What Vera caught in my own chat history\n\n"
f"{body}\n"
diff --git a/vera/cli.py b/vera/cli.py
index 4679236..d6f184c 100644
--- a/vera/cli.py
+++ b/vera/cli.py
@@ -187,7 +187,7 @@ def audit(share_flag: bool):
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)
+ console.print(Markdown(snippet))
@main.command()
def rules():