Published August 23, 2026 · one of the RedMirror role playbooks
"It passes the tests and it looks right" is not the same as "it cannot reach a bad state." This playbook shows how to use RedMirror Reflection as a pre-merge gate: you state the invariant your change must keep, a compiled kernel searches every reachable state for a way to break it, and CI fails the build on a proven bug, not a style nit.
Everything below is a real run. We pointed Reflection at a small wallet service, driven by a cheap model through a coding agent, and it found, proved, and fixed a money-creation bug, then re-verified that the fix holds.
You are the one who has to be right before the merge button. Reviews catch style and intent; tests catch the cases you thought of. Neither one searches the whole state space for the case you didn't. Reflection does, and it sits exactly where it helps most: at the test and deploy gates of the AI-native SDLC, so a change that can reach a bad state never reaches production.
Here is the transfer function on the branch. It reads fine at a glance, which is the point.
// wallet.js — balances in cents function transfer(from, to, amt) { if (amt <= 0) throw new Error('amount must be positive'); // Credit the recipient, then debit the sender down to zero if short. balances[to] = (balances[to] || 0) + amt; // credit — unconditional balances[from] = Math.max(0, (balances[from] || 0) - amt); // debit — floored at 0 return { from: balances[from], to: balances[to] }; }
The recipient is credited before anyone checks the sender can cover it, and the debit is floored at zero with Math.max. Nothing here is obviously wrong, and every happy-path test passes.
The agent modelled the wallet as a state machine and stated the invariant a money system must keep: the sum of all balances is constant across any sequence of transfers. The kernel refuted it.
$ docker run -e ROLE=software-engineer ... reflection-playbooks == PHASE 2 — reflect: find + prove (deepseek/deepseek-v4-flash) == Finding: money creation via insufficient-funds transfer (CWE-682) grounded by kernel stamp 8d35488569f94f29 attack: transfer('alice','bob',200) alice 100 -> 0, bob 0 -> 200, total 100 -> 200 (100c created) [kernel] money-conservation REFUTED # fix applied, then re-verified: [kernel] re-verify PROVED (stamp 130bdda87149033e; 3 states, frontier empty) == PHASE 3 — gate == reflection: 3 finding(s) grounded, nothing left open. audit exit: 0 --- .redmirror-reflect/session.jsonl --- {"note":"claim REFUTED and answer matches","status":"GROUNDED","stamp":"8d35488569f94f29"} {"note":"claim PROVED (of the model) and answer matches","status":"GROUNDED","stamp":"130bdda87149033e"}
The raw gate records. First the kernel refutes the invariant; after the fix it proves the invariant holds across every reachable state.
The attack is a single call. Alice has 100; transfer('alice','bob',200) credits Bob the full 200 but floors Alice's debit at 0 with Math.max, so the system's total rises from 100 to 200 — a hundred cents created from nothing. Repeat it and you mint arbitrary money. No test in the suite exercised an overdraw, so nothing else would have caught it.
With the attack path in hand, the fix is obvious: check the sender can cover the amount, then move exactly what you debit.
- balances[to] = (balances[to] || 0) + amt; - balances[from] = Math.max(0, (balances[from] || 0) - amt); + const senderBal = balances[from] || 0; + if (senderBal < amt) throw new Error('insufficient funds'); + balances[from] = senderBal - amt; // debit first + balances[to] = (balances[to] || 0) + amt;
Re-running the gate on the patched file, the kernel searched every reachable state and this time proved the invariant holds (stamp 130bdda87149033e), and redmirror-reflect audit exits 0. That is the state you want a merge to require: not "looks fixed", but "the kernel could no longer break it".
| Step | What happened |
|---|---|
| Find | Model states the money-conservation invariant over transfer. |
| Prove | Kernel refutes it: transfer('alice','bob',200) raises the total 100 → 200 (CWE-682), stamp 8d35488569f94f29. |
| Fix | Check funds up front; debit then credit in equal measure. |
| Gate | PROVED after the fix (stamp 130bdda87149033e), audit exit 0 — safe to merge. |
Reflection installs as one binary and is driven by the coding agent you already use. The gate is a single command your pipeline can block on:
# local, on your own machine and your own model redmirror-reflect init claude # or cursor, codex, pi, ... # in CI: fail the build if any proven finding is left open redmirror-reflect audit # exits non-zero on an un-grounded / unfixed finding
The kernel runs on your runner and uses no tokens, so the structural search is free; you spend model tokens only when your agent proposes what to check. See the reference on gating pull requests on proven bugs in CI.
It is a pre-merge gate. Before a pull request lands, you state the invariant the change must keep and let a compiled kernel search every reachable state for a way to break it. If it finds one, you get the exact call sequence and fix it; if it does not, you get a bounded proof. Wire the gate into CI so a change that can reach a bad state fails the build.
That a wallet service could create money. The invariant was that the sum of all balances stays constant across transfers. The kernel refuted it: transfer('alice','bob',200) credited Bob in full but floored Alice's debit at zero, so the total went from 100 to 200. That is an incorrect calculation, CWE-682. After the fix, the kernel proved the same invariant held across every reachable state and the gate passed.
A unit test checks the cases you thought of. The kernel checks every reachable state against the invariant, so it finds the overdraw case you did not write a test for and returns the exact sequence that triggers it. It complements tests: you keep your tests and add a proof that the invariant holds across the whole state space.
Yes. The gate (redmirror-reflect audit) exits non-zero while any finding is left un-grounded or unfixed, so it fails the build on a proven bug rather than on a style warning. It runs on your own machine or runner and the kernel search uses no tokens, so the structural half of the check is free.
Gate the merge, not the vibe. Install the binary, point it at your code and your own model, and let CI block on proven bugs. First month free, then $4.99/month, cancel any time.