veriplan checks whether an OpenSpec plan can be built into a working state machine, then runs model checking to prove that all requirements hold — or tells you exactly what needs fixing.
The approach is based on the paper "Specification-Driven Requirements Engineering and Plan Verification" (arXiv:2502.17898), which describes the pipeline this tool implements.
Before writing any tasks or requirements, run veriplan init once to
embed the structural rules directly into your OpenSpec configuration:
veriplan initThis adds a context field and rules to openspec/config.yaml that
describe the temporal keyword grammar, task ID format, scenario structure,
and RFC 2119 conventions — in plain language, without mentioning Promela,
SPIN, or LTL. The idea is that whoever writes the plan (including an AI
assistant) sees these rules up front and can follow them from the start.
The config is a gentle nudge, not a straitjacket. An AI assistant may
still write requirements that don't follow the temporal grammar — that's
what the convertibility check is for. But with init, the rules are
there in the project configuration from day one, making it more likely
that specs come out structurally sound on the first try.
veriplan reads your OpenSpec change directory: tasks.md, specs/,
design.md, proposal.md. It uses tree-sitter (a syntax-aware parser)
to turn the markdown into structured data — tasks with N.M IDs,
requirements with RFC 2119 keywords (MUST / SHALL / SHOULD / MAY),
scenarios with GIVEN / WHEN / THEN steps, and phase groupings.
If the markdown is malformed or missing required fields, parsing fails with a clear error.
Before running any heavy analysis, veriplan asks seven questions:
| # | Check | What it catches |
|---|---|---|
| 1 | Every task has a unique N.M ID | Duplicate or missing IDs |
| 2 | At least one requirement uses SHALL or MUST | Purely aspirational plans can't be verified |
| 3 | Every SHALL references an existing task ID | Requirements that talk about nothing |
| 4 | Every SHALL uses a temporal keyword | SHALLs like "the system shall be fast" can't be modelled — see the golden rules below |
| 5 | Every scenario has WHEN + THEN + keyword | Missing steps leave behaviour untested |
| 6 | Constraint diversity advisory | 20 requirements all saying "ALWAYS" is a red flag |
| 7 | Every task is referenced by at least one SHALL | Unreferenced tasks have no formal purpose |
Checks 1–5 are blockers: if they fail, veriplan tells you exactly what to rephrase and where. Checks 6–7 are warnings.
The golden rule: every SHALL must include a temporal keyword. Think of these as the "grammar" of a formal requirement:
| Keyword | Category | Example |
|---|---|---|
| BEFORE | sequential | T2.1 SHALL complete BEFORE T3.1 |
| CONCURRENTLY | concurrent | T4.2 SHALL run CONCURRENTLY WITH T4.3 |
| AFTER | sequential | T5.1 SHALL complete AFTER T4.1 |
| IF…THEN | conditional (failure‑recovery) | IF T1.1 fails THEN T2.1 SHALL run |
| ALWAYS | global invariant | T6.1 SHALL ALWAYS be reachable |
| AT MOST ONE | exclusive | AT MOST ONE of T3.1, T3.2 SHALL be active |
Requirements without a temporal keyword are NonFormalizable — they block the pipeline. The fix is always to rewrite using one of the six patterns above.
Before translating to LTL, veriplan runs a grounding pass that
maps each requirement's SHALL statement to actual task IDs using
keyword matching and positional heuristics. This uses the
groundcontrol
library's RuleGrounder.
Each requirement is checked against the plan's task signature:
- Every task reference in a SHALL statement is resolved to a real task ID
- Ambiguous references (e.g. "the system" without a task) are flagged
- The grounding status (FullyGrounded, PartiallyGrounded, or NotGrounded) is reported per requirement
This catches requirements that talk about tasks that don't exist, or use vague language that can't be formally checked.
A spec that is formally valid can still be ambiguous — and an ambiguous spec is a liability. veriplan runs a prose-guidance pass (backed by the steve ASD-STE100 engine) over the human-readable parts of the plan to catch wording that a reader (or an implementer) could interpret in more than one way.
Why ambiguity matters. The whole point of a spec is to be a single, unambiguous contract between intent and implementation. When a requirement or scenario is vague:
- Two implementers build different things. "The system SHALL respond appropriately" means nothing concrete — one engineer ships a 200ms response, another ships a 2s one. The spec fails its job.
- Verification is impossible. If the expected behavior isn't pinned down, there is no objective test that can say "implemented" or "not implemented". The scenario is the executable contract; a vague scenario is a contract that can't be enforced.
- Change is dangerous. A later change to an ambiguous requirement can silently alter behavior nobody intended, because the original intent was never captured precisely.
What is checked. The prose pass applies a curated set of STE rules to four zones:
| Zone | Rules | Why |
|---|---|---|
| Requirement bodies (spec.md) | full curated set | the core contract |
| Task descriptions (tasks.md) | minimal (one-instruction, hedging) | grounding aliases |
| Design / proposal bodies | light (passive, pronoun, hedging) | narrative, human-oriented |
| Scenario steps | PronounAmbiguity + SentenceLength | the executable contract |
Scenario steps are the executable contract. A scenario's
**GIVEN**/**WHEN**/**THEN**/**AND** steps pin down the concrete
behavior an implementation must satisfy. veriplan strips the scaffolding
and code spans, then checks the remaining prose with a safe subset of
STE rules:
- PronounAmbiguity — "the valve and the pump are connected, and it is faulty" is genuinely ambiguous: which one is faulty? A pronoun with two plausible antecedents makes the expected behavior unclear.
- SentenceLength — an over-long step is hard to verify.
PassiveVoice and OneInstructionPerSentence are deliberately excluded from scenario steps: a legitimate state assertion like "THEN the plan SHALL be marked VALID" is passive by nature, and a step with two assertions is often intentional. Flagging those would be noise.
Advisory, never blocking. Prose findings are surfaced as warnings and rephrase directives — they never block the pipeline. The structural and semantic checks (convertibility, grounding, model check) are what gate a plan. Prose guidance is a nudge toward clarity, not a gate.
Ambiguity detection is a two-layer defense. The grounding check catches semantic ambiguity (a requirement that can't be mapped to real tasks). The prose pass catches stylistic ambiguity (wording that a reader could interpret multiple ways). Together they make a spec both machine-checkable and human-unambiguous.
Once the plan passes convertibility and grounding, each SHALL requirement is translated into an LTL formula — a precise mathematical statement about sequences of states. For example:
T2.1 SHALL complete BEFORE T3.1 SHALL run
becomes the LTL property:
[](active(t3_1) -> done(t2_1))
("It is always true that if T3.1 is active, T2.1 must already be done.")
This step maps the six temporal categories (sequential, exclusive, conditional, concurrent, global, fixed-time) into LTL patterns that can be checked by either the built-in BFS checker or SPIN.
veriplan offers three model checking backends:
A fast, built-in breadth-first search that explores all possible task execution sequences up to 2^N states (N = number of tasks). No external dependencies required. The BFS checker:
- Builds a state space where each task is inactive, active, or done
- Enforces phase ordering (sequential phases execute in order, concurrent phases allow overlap)
- Evaluates every LTL property against each reachable state
- Reports violations with the offending state and the property that failed
Suitable for plans with up to ~20 tasks. For larger plans, use SPIN or spin-rs.
For larger plans or when deeper liveness analysis is needed, veriplan generates a Promela model and runs SPIN:
- Safety properties (things that must never happen) are checked with a fast bitstate search.
- Liveness properties (things that must eventually happen) trigger an acceptance-cycle search — slower but necessary.
Each property gets a 5-second timeout. If SPIN can't decide within
that window, the property is marked unchecked (~).
SPIN must be installed separately (brew install spin / apt install spin).
If SPIN is not available, veriplan falls back to the BFS checker automatically.
veriplan also supports spin-rs, a Rust-native Promela model checker that runs entirely in-process with no external dependencies. It parses the same Promela model, compiles it to Lua, and runs DFS/BFS verification with LTL→Büchi support via nested DFS.
To use spin-rs instead of the external SPIN binary:
# Via CLI flag
veriplan check my-change --checker spin-rs
# Via environment variable
VERIPLAN_CHECKER=spin-rs veriplan check my-changespin-rs is significantly faster for small-to-medium models because it
skips the spin -a + gcc compile step. It also works on systems
where SPIN is not installed.
Run both SPIN and spin-rs on the same plan and diff the results:
veriplan check my-change --compareOutput:
═══ Backend Comparison: my-change ═══
Constraint spin spin-rs Match?
------------------------------------------------------------
checker-backend-selection::… pass pass ✓
...
spin: 0.55s | valid=✓ | violations=0
spin-rs: 0.01s | valid=✓ | violations=0
11/11 constraints match, 0 mismatches
Useful for validating spin-rs correctness against SPIN, or for performance benchmarking.
veriplan outputs a summary with three phases:
Plan: my-change — ✓ VALID
Convertibility: 7/7 passed
Grounding: 5/5 fully grounded
Model check: 22/22 satisfied
Satisfied: 22 | Violated: 0 | Unchecked: 0 | Total: 22
If there are violations, each one includes:
- The requirement statement and its LTL formula
- The task IDs involved and their phase
- A suggested fix (e.g. "remove CONCURRENTLY keyword or restructure")
- For conditional constraints: which task is the trigger and which is the consequent
- The grounding status (which task references were resolved)
Violations mean the spec demands something the plan structure cannot guarantee — they are spec-plan mismatches, not implementation bugs.
Generate a state-machine diagram of the plan from tasks.md + specs:
veriplan visualize my-change
Three output formats:
| Format | Output | Best for |
|---|---|---|
mermaid (default) |
flowchart TB with phase subgraphs |
Rendering in Obsidian, GitHub, or docs |
dot |
Graphviz digraph with clusters |
Advanced graph layout with Graphviz tools |
markdown |
Table with task relationships and source links | Plain-text review, copy-paste into plans |
The diagram shows:
- Phase subgraphs — numbered groups with phase mode (
[concurrent]if marked) - Task nodes — ✅ prefix for checked/completed tasks, plain for pending
- Structural edges — unlabeled arrows showing phase execution order
- Constraint edges — dashed arrows labeled with the temporal keyword
- Results overlay — if
.veriplan/results.jsonexists from a previouscheck, constraint edges are colour-coded (green = passed, red = violated, orange = timed out)
Markdown format includes a Task Index appendix with clickable source links
(tasks.md#L<N>) for every task — useful for navigation and code review.
veriplan includes a built-in Language Server Protocol (LSP) server for real-time feedback in editors that support LSP (VS Code, Neovim, Helix, etc.). The server provides:
- Diagnostics — convertibility errors and warnings on save
- Completions — task ID suggestions (type
T) and temporal keywords (typeSHALL,MUST, etc.) - Go-to-definition — jump from
T3.2in a spec to its definition in tasks.md - Hover — see task description and phase on hover over task references
- Document symbols — outline of phases/tasks (tasks.md) and requirements/scenarios (spec.md)
- Code actions — quick fixes for convertibility diagnostics
The LSP server runs convertibility check only (Phase 1). Model checking (SPIN) is too expensive for real-time feedback.
# Start the LSP server (for editor integration)
veriplan lsp --stdioCreate a .pi-lens/lsp.json in your project root:
{
"servers": {
"veriplan": {
"command": "veriplan",
"args": ["lsp", "--stdio"],
"extensions": ["tasks.md", "spec.md"],
"rootMarkers": ["openspec/config.yaml"]
}
}
}The extensions field matches files by basename (not file extension),
so "tasks.md" and "spec.md" are matched regardless of directory.
This is intentional: using ".md" would activate the LSP for every
markdown file in the project, but veriplan only processes files named
tasks.md or spec.md. The rootMarkers field tells pi-lens where
the workspace root is.
veriplan can run as a pre-commit hook to catch spec violations before they reach code review. The hook runs the full verification pipeline (convertibility + SPIN model checking when available).
# Run manually with pre-commit mode
veriplan check --pre-commit
# Or set the environment variable
PRE_COMMIT=1 veriplan checkIn pre-commit mode:
- Blockers and violations → exit 1 (blocks the commit)
- Warnings → exit 0 (doesn't block the commit)
- SPIN not found → exit 0 with warning (doesn't block the commit)
This is different from normal veriplan check where missing SPIN
causes exit code 2 (hard failure). In pre-commit mode, you don't
want to block every commit just because someone doesn't have SPIN
installed locally.
Add to your .pre-commit-config.yaml:
Option A: Auto-install (compiles from source)
repos:
- repo: https://github.com/autonomous-toaster/veriplan
rev: v0.1.0 # Use the latest tag
hooks:
- id: veriplanOption B: System install (veriplan must be in PATH)
repos:
- repo: local
hooks:
- id: veriplan
name: veriplan
entry: veriplan check --pre-commit
language: system
files: 'openspec/'
pass_filenames: false
stages: [pre-commit, pre-push, manual]Option C: Reference the repo's hook IDs
repos:
- repo: https://github.com/autonomous-toaster/veriplan
rev: v0.1.0
hooks:
- id: veriplan-system # Uses veriplan from PATHThe hook only runs when openspec/ files are staged — other commits
skip it entirely. veriplan auto-detects which changes to check.
To skip the hook for a specific commit:
VERIPLAN_SKIP=1 git commit -m "work in progress"
# or skip all hooks:
git commit --no-verify -m "work in progress"For CI: Run veriplan check (without --pre-commit) for full
verification including SPIN model checking. The pre-commit hook is a
fast guard rail; CI is the authoritative check.
- Rust toolchain (for building) —
cargo build --release. - SPIN (model checker, optional) — must be on PATH for SPIN-based
model checking. Install via
brew install spin(macOS) orapt install spin(Debian/Ubuntu). veriplan includes a built-in BFS checker and a spin-rs backend that work without SPIN. - gcc (optional) — SPIN generates C code that must be compiled. Not needed when using the built-in BFS checker or spin-rs backend.
# Build
cargo build --release
# Check a change in the current project (full pipeline)
./target/release/veriplan check my-change-name
# Check all active changes
./target/release/veriplan check
# Run a specific phase only
./target/release/veriplan check my-change --phase convertibility
./target/release/veriplan check my-change --phase grounding
./target/release/veriplan check my-change --phase model-check
# Check a change in an external project
./target/release/veriplan check /path/to/project
# Use spin-rs backend (in-process, no external SPIN needed)
./target/release/veriplan check my-change --checker spin-rs
# Compare SPIN and spin-rs results
./target/release/veriplan check my-change --compare
# Set spin-rs as default via environment variable
export VERIPLAN_CHECKER=spin-rs
./target/release/veriplan check my-change
# JSON output for machine consumption
./target/release/veriplan check my-change --format json
# Verbose mode (see tasks, requirements, temporal classifications, grounding)
./target/release/veriplan check my-change --verbose
# Auto-configure an OpenSpec project
./target/release/veriplan init
# Generate a state-machine diagram
./target/release/veriplan visualize my-change
# Alternative formats
./target/release/veriplan visualize my-change --format dot
./target/release/veriplan visualize my-change --format markdown
# Write to a file
./target/release/veriplan visualize my-change -o plan-diagram.md
# Start LSP server for editor integration
./target/release/veriplan lsp --stdio
## Exit codes
| Code | Meaning |
|------|---------|
| 0 | Plan is valid — all requirements are satisfied |
| 1 | Plan is invalid — one or more requirements are violated |
| 2 | Plan is not convertible — blocking issues found |
---
## How the model works
Imagine your plan as a row of dominoes arranged in phases. Tasks
within a phase can fall one after another. Phases happen in order.
veriplan builds a simplified version of this domino row, then asks:
"If I run this row, will every requirement actually hold?"
- If the spec says "T2.1 BEFORE T3.1", veriplan checks that T3.1
never starts before T2.1 finishes.
- If the spec says "T4.2 CONCURRENTLY WITH T4.3", veriplan checks
that the plan structure allows them to overlap.
- If the spec says "IF T1.1 fails THEN T2.1 runs", veriplan checks
that T2.1 actually activates when T1.1 fails — but since the model
uses non-deterministic failure, this requires a liveness check.
The model is deliberately minimal: it only encodes the task-phase
structure. The spec constraints are checked *against* this model,
not baked into it. This catches genuine spec-plan mismatches.
If your spec is valid but the plan can't satisfy it, veriplan tells
you exactly which requirement is unrealistic and why.
---
## Project structure
src/
parser/ — Parse OpenSpec markdown into structured data
ir/ — Intermediate representation (tasks, requirements, phases)
checker/ — Convertibility checks + BFS/SPIN/spin-rs model checking
promela.rs — Shared Promela generation for all backends
spin.rs — External SPIN binary backend
spin_rs.rs — In-process spin-rs library backend
bfs.rs — Built-in BFS fallback checker
translator/ — Map SHALL statements to LTL formulas
grounding/ — Ground requirements against plan task signature
visualizer/ — Generate diagrams (Mermaid, DOT, markdown)
lsp/ — Language Server Protocol (diagnostics, completions, navigation)
annotator/ — Human-readable and JSON report formatting
input/ — Plan loading, source resolution, strictness profiles
main.rs — CLI entry point
kani_harnesses/ — Kani proof harnesses (behind #[cfg(kani)])
## Formal verification with Kani
veriplan uses [Kani](https://model-checking.github.io/kani/), a bit-precise
model checker for Rust, to verify its own core translation logic.
### What Kani proves
The BFS LTL evaluator (`src/checker/bfs.rs`) is verified by structural
induction on the `LtlFormula`/`LtlCondition` enums:
| Property | Harness | Time |
|---|---|---|
| Variable lookup (present) | `verify_atom_present` | 0.9s |
| Variable lookup (absent) | `verify_atom_absent` | 0.9s |
| Negation | `verify_atom_negation` | 1.1s |
| Implication (true) | `verify_implication_true` | 2.0s |
| Implication (false) | `verify_implication_false` | 2.5s |
| Always(Atom) | `verify_always_atom` | 1.9s |
| Always(Not) | `verify_always_not` | 2.7s |
| Always(Eventually) | `verify_always_eventually` | 2.8s |
| Always(Iff) | `verify_always_iff` | 3.6s |
| Always(And) | `verify_always_and` | 3.9s |
### Running harnesses
```bash
# Requires Kani 0.67+: cargo install kani-verifier
just kani
# Or run individual harnesses:
cargo kani --harness verify_always_and --unwind 10
LTL formulas use structured enums (LtlFormula/LtlCondition in
src/ir/ltl.rs) instead of raw strings. This makes the evaluation
verifiable by structural induction and prevents the class of bugs where
string format mismatches cause silent verification failures.
classify() ──▶ generate_ltl() ──▶ LtlFormula ──▶ evaluate_ltl()
│ │ │
│ ltl_to_string() match on variants
│ │ (Kani-verifiable)
▼ ▼
TranslatedConstraint SPIN input