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
57 changes: 57 additions & 0 deletions .github/scripts/check_prompt_sync.py
Original file line number Diff line number Diff line change
@@ -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())
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
27 changes: 20 additions & 7 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<meta property="og:type" content="website">
<meta name="twitter:card" content="summary">
<link rel="canonical" href="https://iamitp.github.io/vera/">
<link rel="icon" href="data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2064%2064'%3E%3Crect%20width%3D'64'%20height%3D'64'%20rx%3D'14'%20fill%3D'%230e0e10'%2F%3E%3Ctext%20x%3D'50%25'%20y%3D'54%25'%20text-anchor%3D'middle'%20dominant-baseline%3D'middle'%20font-family%3D'-apple-system%2CSF%20Pro%20Text%2Csystem-ui%2Csans-serif'%20font-weight%3D'700'%20font-size%3D'38'%20fill%3D'%234a8cff'%3EV%3C%2Ftext%3E%3C%2Fsvg%3E">
<style>
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
Expand Down Expand Up @@ -130,6 +131,13 @@
text-align: center;
}
footer a { color: #9a9aa2; }
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden; clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
</style>
</head>
<body>
Expand All @@ -144,7 +152,8 @@ <h1>AI with clerk discipline.</h1>
<span class="step-title">Copy the Vera prompt</span>
</div>
<div class="step-body">
<button class="btn" id="copy-btn" type="button">Tap to copy</button>
<button class="btn" id="copy-btn" type="button" aria-describedby="copy-status">Tap to copy</button>
<span id="copy-status" role="status" aria-live="polite" class="sr-only"></span>
<pre class="prompt-preview" id="prompt-preview" aria-label="Vera prompt preview"></pre>
</div>
</section>
Expand All @@ -155,9 +164,9 @@ <h1>AI with clerk discipline.</h1>
<span class="step-title">Open your AI app</span>
</div>
<div class="step-body">
<a class="btn btn-link" href="https://claude.ai/new" target="_blank" rel="noopener">Open Claude</a>
<a class="btn btn-link" href="https://chatgpt.com/" target="_blank" rel="noopener">Open ChatGPT</a>
<a class="btn btn-link" href="https://gemini.google.com/app" target="_blank" rel="noopener">Open Gemini</a>
<a class="btn btn-link" href="https://claude.ai/new" target="_blank" rel="noopener noreferrer">Open Claude</a>
<a class="btn btn-link" href="https://chatgpt.com/" target="_blank" rel="noopener noreferrer">Open ChatGPT</a>
<a class="btn btn-link" href="https://gemini.google.com/app" target="_blank" rel="noopener noreferrer">Open Gemini</a>
</div>
</section>

Expand All @@ -170,14 +179,14 @@ <h1>AI with clerk discipline.</h1>
Paste the prompt as your first message. Ask a factual question. Every claim comes tagged <code>[CITED]</code>, <code>[INFERRED]</code>, or <code>[ASSUMED]</code>. Type <code>/audit</code> after a few turns for self-critique.
</div>
<div class="tip">
<strong>Keep it on permanently.</strong> Paste the prompt into Custom Instructions of a <a href="https://www.anthropic.com/news/projects" target="_blank" rel="noopener">Claude Project</a>, a <a href="https://chatgpt.com/gpts" target="_blank" rel="noopener">ChatGPT GPT</a>, or a <a href="https://gemini.google.com" target="_blank" rel="noopener">Gemini Gem</a>. Every new chat in that container inherits it.
<strong>Keep it on permanently.</strong> Paste the prompt into Custom Instructions of a <a href="https://www.anthropic.com/news/projects" target="_blank" rel="noopener noreferrer">Claude Project</a>, a <a href="https://chatgpt.com/gpts" target="_blank" rel="noopener noreferrer">ChatGPT GPT</a>, or a <a href="https://gemini.google.com" target="_blank" rel="noopener noreferrer">Gemini Gem</a>. Every new chat in that container inherits it.
</div>
</section>

<footer>
<a href="https://github.com/iamitp/vera" target="_blank" rel="noopener">github.com/iamitp/vera</a>
<a href="https://github.com/iamitp/vera" target="_blank" rel="noopener noreferrer">github.com/iamitp/vera</a>
&nbsp;·&nbsp; MIT
&nbsp;·&nbsp; <a href="https://github.com/iamitp/vera/blob/main/PROMPT.md" target="_blank" rel="noopener">PROMPT.md</a>
&nbsp;·&nbsp; <a href="https://github.com/iamitp/vera/blob/main/PROMPT.md" target="_blank" rel="noopener noreferrer">PROMPT.md</a>
</footer>
</main>

Expand All @@ -198,6 +207,7 @@ <h1>AI with clerk discipline.</h1>
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;

Expand Down Expand Up @@ -228,13 +238,16 @@ <h1>AI with clerk discipline.</h1>
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);
});
</script>
Expand Down
3 changes: 2 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
7 changes: 6 additions & 1 deletion launch/ship.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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/*
Expand Down
46 changes: 46 additions & 0 deletions tests/test_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
11 changes: 9 additions & 2 deletions vera/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion vera/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
Loading