agent-formal-verify 0.0.0-stage → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGES.md +157 -0
  2. package/LICENSE +232 -0
  3. package/README.md +92 -2
  4. package/opencode.mjs +45 -0
  5. package/package.json +42 -4
  6. package/skills/formal-verify/SKILL.md +125 -0
  7. package/skills/formal-verify/examples/kani-template/Cargo.lock +7 -0
  8. package/skills/formal-verify/examples/kani-template/Cargo.toml +7 -0
  9. package/skills/formal-verify/examples/kani-template/src/lib.mutations +11 -0
  10. package/skills/formal-verify/examples/kani-template/src/lib.rs +63 -0
  11. package/skills/formal-verify/examples/lean-template/Model/BoundedQueue.lean +98 -0
  12. package/skills/formal-verify/examples/lean-template/Model/BoundedQueue.mutations +11 -0
  13. package/skills/formal-verify/examples/lean-template/Model/BoundedQueue.statements +231 -0
  14. package/skills/formal-verify/examples/lean-template/lake-manifest.json +6 -0
  15. package/skills/formal-verify/examples/lean-template/lakefile.toml +6 -0
  16. package/skills/formal-verify/examples/lean-template/lean-toolchain +1 -0
  17. package/skills/formal-verify/examples/tla-template/BoundedQueue.matrix +20 -0
  18. package/skills/formal-verify/examples/tla-template/BoundedQueue.mutations +26 -0
  19. package/skills/formal-verify/examples/tla-template/BoundedQueue.tla +42 -0
  20. package/skills/formal-verify/references/lean-brief.md +57 -0
  21. package/skills/formal-verify/references/rust-brief.md +81 -0
  22. package/skills/formal-verify/references/setup.md +85 -0
  23. package/skills/formal-verify/references/tla-brief.md +62 -0
  24. package/skills/formal-verify/scripts/kani-check.sh +8 -0
  25. package/skills/formal-verify/scripts/kani.py +250 -0
  26. package/skills/formal-verify/scripts/lean-check.sh +171 -0
  27. package/skills/formal-verify/scripts/lean-statements.py +135 -0
  28. package/skills/formal-verify/scripts/lean-tools.sh +15 -0
  29. package/skills/formal-verify/scripts/mutate.py +274 -0
  30. package/skills/formal-verify/scripts/setup.sh +35 -0
  31. package/skills/formal-verify/scripts/tlc-matrix.sh +172 -0
  32. package/skills/formal-verify/scripts/tlc-tools.sh +47 -0
  33. package/skills/formal-verify/scripts/tlc-trace.py +64 -0
  34. package/skills/setup/SKILL.md +24 -0
@@ -0,0 +1,172 @@
1
+ #!/bin/bash
2
+ # Copyright (c) 2026 Michael Denyer
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+ # Model-checks TLA+ specs with TLC over a matrix of constants read from a
5
+ # matrix file, and prints one PASS or FAIL line per run with its distinct
6
+ # state count, then a SUMMARY line with the totals. A failing run prints TLC's
7
+ # error and its error trace reduced to the variables each step changed
8
+ # (tlc-trace.py).
9
+ #
10
+ # Matrix file, one directive per line (# starts a comment):
11
+ # spec <path.tla> the spec for the runs below, relative to the file;
12
+ # TLC checks the formula it names Spec
13
+ # check <cfg line> an INVARIANTS or PROPERTIES line, kept until the next spec
14
+ # reach <Operator> | <label-substring> a witness in the first matching run
15
+ # run <label> | <Name=Value ...> one TLC run with those CONSTANTS
16
+ #
17
+ # Usage: tlc-matrix.sh <matrix-file> [label-substring]
18
+ # TLC_JAR=<path> use this existing tla2tools.jar
19
+ # TLC_CACHE=<dir> where the pinned jar lives (default ~/.cache/tla)
20
+ # JAVA=<path> the java binary (default: java on PATH)
21
+ # TLC_WORKERS=<n> TLC worker threads (default auto)
22
+ set -uo pipefail
23
+ HERE=$(cd "$(dirname "$0")" && pwd)
24
+ # shellcheck source=skills/formal-verify/scripts/tlc-tools.sh
25
+ source "$HERE/tlc-tools.sh"
26
+ TLC_WORKERS=${TLC_WORKERS:-auto}
27
+
28
+ [ "$#" -ge 1 ] && [ "$#" -le 2 ] || { echo "usage: tlc-matrix.sh <matrix-file> [label-substring]"; exit 2; }
29
+ matrix=$1
30
+ [ -f "$matrix" ] || { echo "FAIL $matrix does not exist"; exit 1; }
31
+ only=${2:-}
32
+ MATRIX_DIR=$(cd "$(dirname "$matrix")" && pwd)
33
+
34
+ require_tlc_runtime || exit $?
35
+ tlc_jar_ready || exit $?
36
+
37
+ OUT=$(mktemp -d)
38
+ trap 'rm -rf "$OUT"' EXIT
39
+ fail=0
40
+ run=0
41
+ passed=0
42
+ total=0
43
+ spec=""
44
+ checks=""
45
+ reaches=()
46
+ runs=()
47
+ reach_count=0
48
+ reach_passed=0
49
+
50
+ check_run() {
51
+ local label=$1 consts=$2 operator=${3:-} name dir pairs c states wrapper invariant status trace
52
+ local workers=$TLC_WORKERS extra=()
53
+ [ -n "$spec" ] || { echo "FAIL run '$label' before any spec line"; fail=1; return; }
54
+ case $label in *"$only"*) ;; *) return ;; esac
55
+ if [ -n "$operator" ]; then reach_count=$((reach_count + 1)); else run=$((run + 1)); fi
56
+ name=$(basename "$spec" .tla)
57
+ [ -f "$MATRIX_DIR/$spec" ] || { echo "FAIL $name $label: spec $spec not found"; fail=1; return; }
58
+ # A directory per run, so a run sees only the modules beside its own spec.
59
+ dir=$(mktemp -d "$OUT/run.XXXXXX")
60
+ cp "$MATRIX_DIR/$(dirname "$spec")"/*.tla "$dir/"
61
+ wrapper=$name
62
+ if [ -n "$operator" ]; then
63
+ # Neither generated identifier may shadow an imported declaration or module.
64
+ while :; do
65
+ wrapper=Reach$(python3 -c 'import uuid; print(uuid.uuid4().hex)')
66
+ invariant=${wrapper}Negated
67
+ grep -Ewq "$wrapper|$invariant" "$dir/"*.tla || break
68
+ done
69
+ printf '%s\n' "---- MODULE $wrapper ----" "EXTENDS $name" "$invariant == ~$operator" "====" > "$dir/$wrapper.tla"
70
+ workers=1
71
+ extra=(-deadlock)
72
+ fi
73
+ read -ra pairs <<< "$consts"
74
+ {
75
+ echo "CONSTANTS"
76
+ # bash 3.2 treats an empty array as unset under set -u.
77
+ for c in ${pairs[@]+"${pairs[@]}"}; do echo " ${c%%=*} = ${c#*=}"; done
78
+ echo "SPECIFICATION Spec"
79
+ if [ -n "$operator" ]; then echo "INVARIANT $invariant"; else printf '%s\n' "$checks"; fi
80
+ } > "$dir/MC.cfg"
81
+ status=0
82
+ "$JAVA" -XX:+UseParallelGC -cp "$TLC_JAR" tlc2.TLC -workers "$workers" ${extra[@]+"${extra[@]}"} -cleanup -metadir "$dir/states" \
83
+ -config "$dir/MC.cfg" "$dir/$wrapper.tla" > "$dir/tlc.log" 2>&1 || status=$?
84
+ if [ -n "$operator" ]; then
85
+ if [ "$status" -eq 12 ] && grep -Fxq "Error: Invariant $invariant is violated by the initial state:" "$dir/tlc.log"; then
86
+ trace=1
87
+ elif [ "$status" -eq 12 ] && grep -Fxq "Error: Invariant $invariant is violated." "$dir/tlc.log"; then
88
+ trace=$(grep -c '^State [0-9][0-9]*:' "$dir/tlc.log")
89
+ else
90
+ trace=0
91
+ fi
92
+ if [ "$trace" -gt 0 ]; then
93
+ echo "PASS $name reach $operator $label: shortest trace $trace states ($((trace - 1)) transitions)"
94
+ reach_passed=$((reach_passed + 1))
95
+ elif [ "$status" -eq 0 ] && grep -q "No error has been found" "$dir/tlc.log"; then
96
+ echo "FAIL $name reach $operator $label: the model never reaches this state"
97
+ fail=1
98
+ else
99
+ echo "FAIL $name reach $operator $label: TLC did not complete the reach check"
100
+ cat "$dir/tlc.log"
101
+ fail=1
102
+ fi
103
+ elif [ "$status" -eq 0 ] && grep -q "No error has been found" "$dir/tlc.log"; then
104
+ # Progress lines carry interim counts; the last match is the final one.
105
+ states=$(grep -o '[0-9,]* distinct states found' "$dir/tlc.log" | tail -1)
106
+ echo "PASS $name $label $states"
107
+ passed=$((passed + 1))
108
+ states=${states%% *}
109
+ states=${states//,/}
110
+ total=$((total + ${states:-0}))
111
+ else
112
+ echo "FAIL $name $label: $(grep -m1 "^Error:" "$dir/tlc.log")"
113
+ grep "^Error:" "$dir/tlc.log" | sed -n '2,5p'
114
+ python3 "$HERE/tlc-trace.py" "$dir/tlc.log" || grep -v '^[[:space:]]*$' "$dir/tlc.log" | tail -40
115
+ fail=1
116
+ fi
117
+ }
118
+
119
+ check_reaches() {
120
+ local reach entry operator match label found
121
+ for reach in ${reaches[@]+"${reaches[@]}"}; do
122
+ operator=${reach%%|*}; match=${reach#*|}; found=0
123
+ for entry in ${runs[@]+"${runs[@]}"}; do
124
+ label=${entry%%|*}
125
+ case $label in
126
+ *"$match"*) check_run "$label" "${entry#*|}" "$operator"; found=1; break ;;
127
+ esac
128
+ done
129
+ if [ "$found" -eq 0 ]; then
130
+ echo "FAIL $(basename "$spec" .tla) reach $operator: no run matching '$match'"
131
+ reach_count=$((reach_count + 1))
132
+ fail=1
133
+ fi
134
+ done
135
+ }
136
+
137
+ while IFS= read -r line || [ -n "$line" ]; do
138
+ line=${line%%#*}
139
+ [[ $line =~ [^[:space:]] ]] || continue
140
+ read -r kw rest <<< "$line"
141
+ case $kw in
142
+ spec) check_reaches; spec=$rest; checks=""; reaches=(); runs=() ;;
143
+ check)
144
+ [ -n "$spec" ] || { echo "FAIL check '$rest' before any spec line"; fail=1; continue; }
145
+ read -r kind names <<< "$rest"
146
+ case $kind in
147
+ INVARIANT|INVARIANTS|PROPERTY|PROPERTIES)
148
+ [ -n "$names" ] || { echo "FAIL malformed check '$rest'"; fail=1; continue; } ;;
149
+ *) echo "FAIL malformed check '$rest'"; fail=1; continue ;;
150
+ esac
151
+ checks="$checks${checks:+$'\n'}$rest" ;;
152
+ reach)
153
+ [ -n "$spec" ] || { echo "FAIL reach '$rest' before any spec line"; fail=1; continue; }
154
+ if [[ $rest =~ ^([a-zA-Z_][a-zA-Z_0-9]*)[[:space:]]*\|[[:space:]]*(.*[^[:space:]])[[:space:]]*$ ]]; then
155
+ reaches+=("${BASH_REMATCH[1]}|${BASH_REMATCH[2]}")
156
+ else
157
+ echo "FAIL malformed reach '$rest': expected reach <Operator> | <label-substring>"
158
+ fail=1
159
+ fi ;;
160
+ run)
161
+ [[ $rest == *"|"* ]] || { echo "FAIL run '$rest' has no | before its constants"; fail=1; continue; }
162
+ label=${rest%%|*}; label=${label% }; runs+=("$label|${rest#*|}"); check_run "$label" "${rest#*|}" ;;
163
+ *) echo "FAIL unknown directive: $line"; fail=1 ;;
164
+ esac
165
+ done < "$matrix"
166
+ check_reaches
167
+
168
+ [ "$run" -gt 0 ] || { echo "FAIL no run ${only:+matching '$only' }in $matrix"; exit 1; }
169
+ summary="SUMMARY $passed of $run runs passed"
170
+ [ "$reach_count" -eq 0 ] || summary="$summary, $reach_passed of $reach_count reach checks passed"
171
+ echo "$summary, $total distinct states in total"
172
+ exit $fail
@@ -0,0 +1,47 @@
1
+ #!/bin/bash
2
+ # Copyright (c) 2026 Michael Denyer
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+ # Shared tool pin and prerequisite checks; nothing here downloads.
5
+ TLA_VERSION=v1.7.4
6
+ TLA_SHA256=936a262061c914694dfd669a543be24573c45d5aa0ff20a8b96b23d01e050e88
7
+ TLA_RELEASE=https://github.com/tlaplus/tlaplus/releases/tag/$TLA_VERSION
8
+ JAVA=${JAVA:-java}
9
+ TLC_CACHE=${TLC_CACHE:-$HOME/.cache/tla}
10
+ TLC_JAR_SUPPLIED=${TLC_JAR:+yes}
11
+ TLC_JAR=${TLC_JAR:-$TLC_CACHE/tla2tools-$TLA_VERSION.jar}
12
+
13
+ require_tlc_runtime() {
14
+ if ! "$JAVA" -version > /dev/null 2>&1; then
15
+ echo "UNAVAILABLE $JAVA has no runtime; install a JDK and set JAVA to its java binary"
16
+ return 3
17
+ fi
18
+ command -v python3 > /dev/null || { echo "UNAVAILABLE python3 is not on PATH; install Python 3"; return 3; }
19
+ }
20
+
21
+ check_tlc_jar() {
22
+ local jar=$1 got
23
+ [ -f "$jar" ] || { echo "UNAVAILABLE $jar is missing; save tla2tools.jar from $TLA_RELEASE there, or set TLC_JAR"; return 3; }
24
+ if command -v sha256sum > /dev/null; then
25
+ got=$(sha256sum "$jar") || { echo "UNAVAILABLE cannot hash $jar with sha256sum; check its permissions"; return 3; }
26
+ elif command -v shasum > /dev/null; then
27
+ got=$(shasum -a 256 "$jar") || { echo "UNAVAILABLE cannot hash $jar with shasum; check its permissions"; return 3; }
28
+ else
29
+ echo "UNAVAILABLE install sha256sum or shasum to verify TLC"
30
+ return 3
31
+ fi
32
+ got=${got%% *}
33
+ [ "$got" = "$TLA_SHA256" ] || { echo "UNAVAILABLE $jar has sha256 $got, want $TLA_SHA256"; return 3; }
34
+ }
35
+
36
+ # A JAR the user supplies is trusted; the cached one must match the pin.
37
+ tlc_jar_ready() {
38
+ local probe
39
+ if [ -n "$TLC_JAR_SUPPLIED" ]; then
40
+ [ -f "$TLC_JAR" ] || { echo "UNAVAILABLE supplied TLC_JAR $TLC_JAR does not exist"; return 3; }
41
+ else
42
+ check_tlc_jar "$TLC_JAR" || return $?
43
+ fi
44
+ probe=$("$JAVA" -cp "$TLC_JAR" tlc2.TLC -help 2>&1) \
45
+ || [[ $probe == *"TLC - provides model checking"* ]] \
46
+ || { echo "UNAVAILABLE tlc2.TLC cannot run from $TLC_JAR; save a working JAR from $TLA_RELEASE or set TLC_JAR"; echo "$probe"; return 3; }
47
+ }
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env python3
2
+ # Copyright (c) 2026 Michael Denyer
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+ """Reduce the error trace in a TLC log to what each step changed.
5
+
6
+ Reads a TLC log (a file argument or stdin), finds the behaviour TLC prints
7
+ for an invariant violation, a deadlock or a temporal counter-example, and
8
+ prints one block per state: its number and action, then only the variables
9
+ whose value differs from the previous state. The first state prints every
10
+ variable. A "Back to state" line (a liveness lasso) is printed as is.
11
+ Exits 1 when the log holds no trace.
12
+
13
+ Usage: tlc-trace.py [tlc.log]
14
+ """
15
+ import re
16
+ import sys
17
+ from contextlib import nullcontext
18
+
19
+ START = ("The behavior up to this point is", "constitutes a counter-example")
20
+ STATE = re.compile(r"^State (\d+): <?(.*?)>?$")
21
+ LOCATION = re.compile(r" line \d+, col \d+ to line \d+, col \d+ of module \w+$")
22
+ # TLC omits the leading conjunction when the spec has one variable.
23
+ VAR = re.compile(r"^(?:/\\ )?(\w+) = (.*)$")
24
+
25
+
26
+ def states(lines):
27
+ """Yield (title, {var: value}) per state of the first error trace."""
28
+ lines = (line.rstrip("\n") for line in lines)
29
+ # any() stops on the marker line, so the loop below resumes just after it.
30
+ if not any(marker in line for line in lines for marker in START):
31
+ return
32
+ title, values, var = None, {}, None
33
+ for line in lines:
34
+ if m := STATE.match(line):
35
+ title, values, var = f"state {m[1]}: {LOCATION.sub('', m[2])}", {}, None
36
+ elif line.startswith("Back to state"):
37
+ yield line, {}
38
+ elif title and (m := VAR.match(line)):
39
+ var = m[1]
40
+ values[var] = m[2]
41
+ elif title and var and line.startswith(" "):
42
+ values[var] += "\n" + line
43
+ elif title:
44
+ yield title, values
45
+ title = None
46
+ if title:
47
+ yield title, values
48
+
49
+
50
+ def main():
51
+ with (open(sys.argv[1], encoding="utf-8") if len(sys.argv) > 1 else nullcontext(sys.stdin)) as src:
52
+ prev, found = {}, False
53
+ for title, values in states(src):
54
+ found = True
55
+ print(f"--- {title}")
56
+ for var, value in values.items():
57
+ if prev.get(var) != value:
58
+ print(f" {var} = {value}")
59
+ prev = values or prev
60
+ sys.exit(0 if found else 1)
61
+
62
+
63
+ if __name__ == "__main__":
64
+ main()
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: setup
3
+ description: "Prepare all shared tools for the agent-formal-verify plugin: Java, Python, TLC, elan, Lean, Rust and Kani. Use for initial installation, missing prerequisites, or /agent-formal-verify:setup."
4
+ ---
5
+
6
+ # Set up formal verification tools
7
+
8
+ Prepare the complete shared toolset once, independently of any repository. Verification selects TLA+, Lean and Kani from each repository's current targets. A global plugin installation uses the same tools across repositories.
9
+
10
+ 1. Read [tool setup](../formal-verify/references/setup.md) for pinned versions and environment settings. Inspect the operating system and existing runtimes. Reuse installed prerequisites and tool caches.
11
+ 2. Ensure a working JDK, Python 3, a SHA256 tool, elan, Rust, the pinned Kani and the pinned TLC JAR are available. Install missing prerequisites through the user's existing package manager or the upstream documented installer, following their installation constraints. Obtain `tla2tools.jar` from the TLA+ release that `scripts/tlc-tools.sh` pins and save it at the path the helper names; the helper verifies its SHA256 and downloads nothing itself. If no suitable installation method is available, or the user has not allowed installation, report the missing prerequisites and their installation instructions.
12
+
13
+ Install the Kani version from tool setup with Cargo, then run `cargo kani setup`. The plugin's helpers verify it without installing or downloading it.
14
+
15
+ 3. Check that `java -version` succeeds. If `java` is not on PATH or macOS resolves it to a stub, set `JAVA` to the working JDK's `java` binary for the commands below, and report that setting, because verification needs it too.
16
+ 4. Run the three commands below with the absolute path of the `formal-verify` skill, which is this skill's sibling directory, in place of `<skill-dir>`. Treat installed plugin files as read-only. The Lean template supplies the default pin without creating a model project or changing elan's global default.
17
+
18
+ ```shell
19
+ bash <skill-dir>/scripts/setup.sh rust
20
+ bash <skill-dir>/scripts/setup.sh tla
21
+ bash <skill-dir>/scripts/setup.sh lean <skill-dir>/examples/lean-template
22
+ ```
23
+
24
+ 5. Require a successful exit and a `READY` line from all three commands. Report the tool versions or pins, any environment settings required for verification, and anything still missing. Readiness confirms the tooling; the matrix runner and Lean checker check model correctness. Repeating setup reuses prepared tools. Verification handles repository-specific Lean pins when needed.