Skip to main content

TermProof

CI Release Verified by TermProof License: MIT Python 3.11+ Stars Forks

Evidence-first verification for terminal and TUI applications. No more "trust me, it works in my terminal." Record the real session, replay it, and ship the proof.

TermProof is a harness that drives your TUI from JSON recipes, records the actual terminal as an asciinema v2 cast, replays the cast into screenshots and text snapshots, optionally renders a 60-fps MP4 via agg + ffmpeg, and writes Markdown and JSON reports. Your reviewers inspect evidence instead of trusting a log line.


What is this?

  • You ship a TUI — built with Textual, Bubble Tea, Ratatui, Ink, or plain curses.
  • You write a recipe — JSON that says: launch the binary, wait for dashboard>, type open, wait for DASHBOARD READY, assert it appeared.
  • TermProof runs it — real PTY, real asciinema cast, deterministic, CI-friendly. No external recorder to install.
  • You get proof — session.cast, final.svg, final.txt, session.mp4, per-step screenshots, result.json, report.md. Upload the folder as a CI artifact and link it from the PR.

Product-agnostic by design. Pi coding-agent workflows are included as the flagship showcase because they exercise realistic multi-turn agent UI flows.

Why not X?

Tool Approach Where it falls short for TUI evidence
Screenshots in docs Manual screencap Stale within one PR; no replay; no assertion.
expect / pexpect alone Scripted PTY driving No cast, no video, no per-step screenshots, no report.
Playwright / Cypress Browser DOM automation Designed for web; cannot drive terminal PTY, ANSI, or Ink renderers.
VHS (Charm) Tape files → GIF Great for demos, not for assertions, CI gates, or evidence bundles.
Asciinema alone Manual asciinema rec No driving, no assertions, no report pipeline.
TermProof Recipe → PTY → cast → screenshots → video → report → artifact Assertions, deterministic runs, PR comments, evidence archives.

If you want demo GIFs, use VHS. If you want verifiable, reviewable, replayable proof that your TUI behaves, use TermProof.

Demo

Portable non-Pi TUI (included in this repo) — no Pi binary required:

uv run termproof run examples/generic --video
open .termproof/runs/<run-id>/session.mp4
open .termproof/runs/<run-id>/final.svg
cat .termproof/runs/<run-id>/report.md

Final screenshot from examples/generic (checked-in evidence):

Generic TUI final screenshot

Pi coding-agent showcase (deterministic fixtures, reproducible on any runner):

uv run termproof run examples/pi_workflow_guarded_edit.recipe.json --video --video-fps 60 --out .termproof/ci
cat .termproof/ci/latest-report.md

Sample artifacts are checked into examples/artifacts/ so you can inspect without running anything:

Full evidence packs (screenshots, casts, videos, reports) are published as termproof-ci-evidence on every PR and as termproof-release-evidence.tgz on each release tag.

GitHub Pages demo: Once Pages is enabled on this repository (ENABLE_PAGES=true + Settings → Pages → Source: GitHub Actions), the rendered site will be at https://md-mt.github.io/termproof/. For now, preview locally with python3 -m http.server 8000 --directory site.

3-command quickstart

Install (Python 3.11+):

brew tap md-mt/termproof https://github.com/md-mt/termproof
brew install termproof
# or from GitHub with pip
pip install git+https://github.com/md-mt/termproof.git
# or from source
git clone https://github.com/md-mt/termproof.git && cd termproof
uv run termproof --help

Create a recipe pack for your TUI:

termproof init .termproof/recipes --name my-tui --command "my-tui"

Run it with video evidence:

termproof run .termproof/recipes --video --out .termproof/runs

Each run writes under .termproof/runs/<run-id>/ (or the --out you provide):

  • session.cast — asciinema v2 recording (source of truth)
  • final.svg / final.txt — final screenshot and screen text
  • steps/ — per-step screenshots and text snapshots
  • session.mp4 — H.264 video rendered via agg + ffmpeg
  • result.json — machine-readable verdict and artifact paths
  • report.md — per-run review summary
  • latest-report.md — aggregate report for multi-recipe runs

Recipe example

{
  "name": "my-tui-main-flow",
  "description": "Open dashboard, filter, export.",
  "priority": "P0",
  "execution": "scripted",
  "determinism": "deterministic",
  "checks": ["dashboard opens", "filter applies", "export completes"],
  "command": { "argv": ["my-tui"], "pty": true },
  "timeout_seconds": 30,
  "cols": 100,
  "rows": 30,
  "steps": [
    { "name": "wait for prompt", "action": "wait_for_text", "text": "my-tui>", "timeout_seconds": 5 },
    { "name": "open dashboard", "action": "send_line", "text": "open dashboard" },
    { "name": "wait for dashboard", "action": "wait_for_text", "text": "DASHBOARD READY" }
  ],
  "assertions": [
    { "type": "output_contains", "value": "DASHBOARD READY" }
  ],
  "expect_exit_code": 0
}

Step actions: wait_for_text, wait_for_idle, send_text, send_line, press, sleep, wait_for_count Assertions: output_contains, output_not_contains, screen_contains, screen_not_contains, step_screen_contains, exit_code, file_exists, file_contains

See docs/recipe-packs.md for layout and examples/generic/generic_tui.recipe.json for a minimal working recipe.

CI snippet

Copy-paste for GitHub Actions. Identical to what this repository uses:

- name: Install agg + ffmpeg
  run: |
    sudo apt-get update && sudo apt-get install -y ffmpeg
    if ! command -v agg >/dev/null 2>&1; then
      cargo install --locked --git https://github.com/asciinema/agg --tag v1.9.0
    fi

- name: Run TermProof
  run: |
    uv run termproof run .termproof/recipes --video --video-fps 60 --out .termproof/ci

- name: Upload TermProof evidence
  uses: actions/upload-artifact@v4
  if: always()
  with:
    name: termproof-ci-evidence
    path: .termproof/ci
    if-no-files-found: ignore

- name: Publish report to summary
  if: always()
  run: cat .termproof/ci/latest-report.md >> "$GITHUB_STEP_SUMMARY"

This repo also posts a sticky TermProof CI Report comment on every PR with the run link, base-commit report, head report, and behavioral delta. Release tags package the same receipt-backed report as termproof-release-evidence.tgz. For same-repository PRs, screenshot links are copied to the termproof-evidence branch and rewritten to raw GitHub URLs so they are directly viewable from the comment. Videos remain in the workflow artifact until hosted video evidence is implemented in #69. See .github/workflows/ci.yml for the full implementation.

Reuse as a GitLab template or CircleCI orb by porting the same three steps — no Docker image required (see #27 for generic image).

Verified by TermProof badge

If you verify your TUI with TermProof, add the badge to your README:

Verified by TermProof

Markdown:

[![Verified by TermProof](https://img.shields.io/badge/verified%20by-TermProof-0a7a2e?style=flat-square)](https://github.com/md-mt/termproof)

HTML:

<a href="https://github.com/md-mt/termproof"><img src="https://img.shields.io/badge/verified%20by-TermProof-0a7a2e?style=flat-square" alt="Verified by TermProof"></a>

See docs/verified-badge.md for variants (flat, plastic, for-the-badge) and usage guidelines.

Community & plugins

Upgrading from tui-verifier

TermProof is the renamed distribution, import package, and CLI: install termproof, import termproof, invoke termproof.

Existing project and user configuration remains readable without being modified. During migration, configuration is loaded in order: built-ins, legacy ~/.config/tui-verifier/config.yaml, ~/.config/termproof/config.yaml, legacy .tui-verifier/config.yaml, then .termproof/config.yaml. A value in the new location takes precedence over the legacy value.

Plugin references using tui_verifier.*:ClassName are translated to termproof.*:ClassName at load time. This narrow compat path is intentionally limited to configured plugin references; the legacy CLI and import package are not shipped.

Configuration

Optional configuration lives in ~/.config/termproof/config.yaml (user) or .termproof/config.yaml (project). The defaults block exposes the post-script idle wait cap:

defaults:
  # Cap (seconds) for the post-script idle wait in PTY mode. After the last
  # step, TermProof waits for the screen to quiesce before capturing the
  # final state. Slow-quiescing TUIs may need a larger cap; set to null to
  # wait up to the recipe's timeout_seconds instead of a fixed cap.
  idle_cap_seconds: 3.0

idle_cap_seconds is the documented replacement for the former hard-coded 3-second idle cap in runner.py. Defaults to 3.0 to preserve existing behavior; raise it (or set null) for TUIs that take longer to settle. The value must be a finite, nonnegative number: negative, NaN, or infinite values are rejected at config load.

The idle wait — both the wait_for_idle step and this post-script wait — starts measuring at the session's first byte of output, so a session that has produced no output is never treated as idle. The trade: a target that stays alive and never emits anything is never idle. A wait_for_idle step over such a target fails with no output observed from the session after its timeout_seconds, and the post-script wait burns its full budget — with idle_cap_seconds: null that is the whole recipe timeout_seconds, so prefer a finite cap for targets that may be silent. Once the first byte has arrived, quiescence is measured on rendered screen text only: terminal-title updates, colour changes, and repaints that redraw the same characters all count as quiet.

Evidence rendering

The evidence block sets the screenshot and video parameters that used to be hard-coded in the renderers and the video pipeline, split into svg, png, and video, plus the run-wide dedup_step_screenshots switch:

evidence:
  svg:
    font_size: 14
    fg: "#e6edf3"
    bg: "#101418"
  png:
    scale: 1
    font_path: null
  video:
    fps: 60
    pix_fmt: yuv420p
    crf: null
  dedup_step_screenshots: false

BUILTIN_DEFAULTS in termproof/config.py lists every knob with its default. Each default reproduces the behavior from before the knobs existed, so a run with no evidence block renders byte-identical artifacts.

  • evidence.video.fps is the default for --video-fps; the flag wins when passed.
  • A null video knob means "omit that flag"; fps_cap: null keeps agg's cap tied to the output fps.
  • png.font_size applies only when png.font_path is set — the bundled bitmap face has one fixed size.
  • png.scale multiplies the canvas, the padding and the line pitch, not the glyphs of that bitmap face, so it spreads the same text over a larger image unless png.font_path is set too.
  • Unknown keys under evidence are rejected at config load, so a misspelled knob fails loudly instead of silently doing nothing. So are a value of the wrong type, a section that is not a mapping, a non-positive size or frame rate, and a negative padding.
  • Evidence values are part of the --skip-unchanged cache key, so changing one re-renders cached runs. The video knobs only count towards it for a run that renders video, as --video-fps and the video backend already do.
  • dedup_step_screenshots skips the screenshot for a step whose screen is unchanged from the immediately preceding step, so an unbroken run of identical screens costs one image instead of one per step. A screen that reappears after a different one is rendered again. Half of the consecutive step screenshots in the shipped corpus are byte-identical. Every step still gets its .txt, and steps/steps-manifest.json names the image that represents each one, so no step is lost — but a consumer that globs steps/*.svg has to read the manifest instead. Off by default for that reason.

See docs/evidence-quality.md for what the research measured about these defaults and which alternatives it recommends.

Packaging

uv build
uv pip install dist/termproof-*.whl
termproof --help

See docs/releases.md for versioning and release flow.

Why the cast comes first

The cast is the source of truth. Screenshots, videos, assertions and reports all derive from the same recording, so reviewers inspect what happened instead of trusting a private terminal session.

The default pexpect session backend writes the asciinema v2 cast itself, from the PTY output it is already reading — nothing extra to install. Rendering it:

cat session.exitcode
agg --quiet --fps-cap 60 session.cast session.agg.gif
ffmpeg -y -loglevel error -i session.agg.gif \
  -vf 'fps=60,scale=trunc(iw/2)*2:trunc(ih/2)*2' \
  -pix_fmt yuv420p -movflags +faststart session.mp4

The attributed_rsvg video backend skips agg entirely and renders each frame from the same attributed grid final.svg is rendered from, so a video frame and the final screenshot of the same moment are the same image. This does not extend to the per-step screenshots under steps/, which are still rendered from plain text and are monochrome. It needs rsvg-convert and ffmpeg.

If you specifically want a cast that the asciinema CLI wrote, install the extra and select that backend:

pip install 'termproof[record]'
# .termproof.yaml
session_backend: pexpect_asciinema

Metadata

Release files for termproof 0.3.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for termproof 0.3.3
File Size Uploaded
termproof-0.3.3.tar.gz 711.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for termproof 0.3.3
File Interpreter ABI Platform
termproof-0.3.3-py3-none-any.whl Python 3 none any Details

Total release size: 810.7 kB

Release files / termproof-0.3.3.tar.gz

Download URL termproof-0.3.3.tar.gz
Size 711.9 kB
Tags Source
SHA-256 checksum
How to use checksums
f3556477dbfb35dfd541e4decf42fc0556757f667d81ca4a3280548978657e7a
BLAKE2b-256 checksum
How to use checksums
ee49ada28256509e89831ed10d2570342c9e46c68054dad216446a267ff7f768
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 16, 2026.

Transparency log

Release files / termproof-0.3.3-py3-none-any.whl

Download URL termproof-0.3.3-py3-none-any.whl
Size 98.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
516994ed8963427aaa3f73e811a9c2b7ba724ebc4d541d0c6284ca288f0eed62
BLAKE2b-256 checksum
How to use checksums
1e24adcd18196dff362d7d535fd1af88c547557459778ade24065ad4839078c6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 16, 2026.

Transparency log

Release history Release notifications | RSS feed

0.4.3

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.4

2 release files

This release

0.3.3 This release

2 release files

0.3.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page