Run ./demo.sh for an auto-narrated walkthrough that runs the full
PASS → tamper → FAIL → guard-block story end-to-end and restores the
fixture when it's done. Everything below is the manual version if
you want to pace the demo yourself.
Pre-registration + CI for AI-agent claims: lock a hypothesis, run the
experiment, get a deterministic PASS/FAIL verdict, and let guard
block any later text that contradicts the result.
- Python 3.11+
pip install pyyaml(inside a venv if your system blocks global pip)- Clone this repo,
cdinto it.
mkdir -p .falsify/calibration
cp examples/calibration_sample/spec.yaml .falsify/calibration/spec.yaml
python3 falsify.py lock calibrationExpected:
✓ Locked calibration @ 97c1b9bc4a8c
claim: brier_score below 0.25
The spec's canonical YAML is SHA-256 hashed and stored in
.falsify/calibration/spec.lock.json. Any semantic edit to spec.yaml
after this invalidates the lock and blocks run with exit 3.
python3 falsify.py run calibrationExpected:
✓ Run 20260421T184500_000000Z (0.05s)
experiment.command executed with a 5-minute timeout; stdout, stderr,
a copy of the lock, and a metadata file (host, python version,
duration, return code) are written under
.falsify/calibration/runs/<timestamp>/. The run is reproducible from those
artifacts alone.
python3 falsify.py verdict calibrationExpected:
Verdict: PASS
observed brier_score = 0.214265
threshold: below 0.25
The metric_fn (examples.calibration_sample.metric:brier_score) was
imported and checked against the locked criterion. PASS → exit 0,
FAIL → exit 10. verdict.json is written so downstream tools
(list, guard, the claim-auditor subagent) can read it without
re-running the experiment.
python3 falsify.py listExpected:
NAME LOCKED LAST RUN VERDICT OBSERVED
calibration 97c1b9bc4a8c 20260421T184500_000000Z PASS 0.214265
For CI or scripts, python3 falsify.py list --json emits the same
data as a JSON array.
# Scan mode: non-zero if any claim is FAIL or STALE (> spec drift).
python3 falsify.py guard
# Text mode: block affirmative language that contradicts a logged verdict.
python3 falsify.py guard "we've proven the model predictions is well-calibrated"Because the calibration verdict is PASS, the text call exits 0 — the
affirmative language matches a passing claim. If the latest verdict
had been FAIL or INCONCLUSIVE, the same sentence would have exited
11 with a BLOCKED: breakdown on stderr.
Install the hook so every commit gets the same check automatically:
python3 falsify.py hook installInstalls the commit-msg guard; backs up any pre-existing hook. Now a commit message that asserts a falsified claim is rejected before it enters history — the same mechanism CI uses on every push.
python3 falsify.py doctorEnd-to-end diagnostic — what's OK, what needs attention.
sed -i 's/threshold: 0.25/threshold: 0.20/' .falsify/calibration/spec.yaml
python3 falsify.py diff calibrationdiff proves the spec was tampered with — you see exactly what changed
before re-locking. Exit 3 on any drift; --force required to relock.
python3 falsify.py stats
python3 falsify.py stats --json
python3 falsify.py stats --html > dashboard.html && open dashboard.htmlOne-shot dashboard across every locked spec in .falsify/ — name,
state, metric, observed value, threshold, sample size, age in days.
Aggregate counts on the last line. The JSON form is CI-friendly;
the --html form is a self-contained dark-mode-aware page for
demos and quick browser review (no external assets).
python3 falsify.py export > audit.jsonl
wc -l audit.jsonlOne JSON line per event (lock / verdict; add --include-runs for
run records too). Append-only. Deterministic — same .falsify/
contents produce byte-identical output. Share with a peer to
reproduce your verdicts from nothing but this file plus the
original spec.yaml.
python3 falsify.py score
python3 falsify.py score --format shieldsA single number across every claim in .falsify/. The shields
form is a JSON endpoint you can drop straight into your README
for a live badge that turns red when claims start failing.
python3 falsify.py trend calibrationASCII sparkline of the metric across every recorded run, with the
threshold line marked and the trajectory classified (improving /
degrading / flat / mixed). Useful for catching a slow
regression before it tips a claim into FAIL.
python3 falsify.py why calibrationOutput (STALE case, right after someone edits the spec without re-locking):
claim: calibration
state: STALE
reasoning: the spec has been edited (sha256:1038219d75a8) but no run
exists against this hash. Last run was against sha256:164f619d4860.
locked: yes (sha256:164f619d4860, 2h ago)
last run: 2026-04-22T02:10:17+00:00 (2h ago)
next action: `falsify run <name>` to produce a fresh verdict against
the current spec.
Verdict tells you PASS/FAIL; why tells you why — and, more
importantly, what the next honest action is. Always exits 0.
python3 falsify.py replay <run-id>Runs the metric again against the same dataset. Exits 0 only if
the number matches bit-for-bit (or within --tolerance) — the
claim isn't just locked, it's reproducible.
python3 falsify.py verify audit.jsonlWalks the JSONL and checks that each verdict's locked_hash chains
back to a preceding lock's canonical_hash, that timestamps are
monotonic per spec, and that no records were reordered. If anyone
tampered with the file after export, the hash chain breaks and
verify refuses it with exit 10.
python3 falsify.py bench --runs 3Spawns each of init, --help, list, stats, score under a
fresh temp directory and prints per-command latency (min / median
/ p95 / max / mean / stddev). Handy as a "did we regress startup
time?" sanity check before a release.
tests/smoke_test.sh— full pipeline in one bash driver..github/workflows/falsify.yml— unittest + smoke + calibration e2e in CI..claude/skills/—falsify(orchestrator),hypothesis-author(NL → locked spec),claim-audit(fast text audit + subagent)..claude/agents/claim-auditor.md— semantic PR/release auditor.