AI agents · Formal verification · Developer tools
Invariant
You decide what must be true. The factory checks the code against it.
A code factory that turns GitHub issues into merged pull requests. The code is checked against statements a person ratified on the issue: proved with Gobra or Nagini, or tested against the model, and every receipt says which.
Watch it liveThe factory’s first real issue, replayed from what GitHub recorded, in UTC on September 25, 2026. Open any step to read what was said.
- Invariant reads the issue and asks 42s
- Joseph decides 3m 28s
- Invariant proposes and checks with TLC 1m 15s
- Joseph reviews and ratifies 6m 50s
- Invariant writes and proves the code 2m 5s
- CI’s gate, then the merge 11m 56s
+0sJoseph opened issue #1Add a bounded buffer for log shipping
“Add a bounded buffer for log shipping”, opened by Joseph as @gitdek.
+42sInvariant asked two questions instead of guessing42 seconds after the issue opened
◉ Invariant
Before I write down what must be true for this issue, I need you to decide:
F1. When a producer writes a line and the buffer is already at capacity, what should happen?
F2. When the shipper fails to send a line, what should happen to that line?
+4m 10sJoseph decidedF1 A and F2 A
Joseph
/invariant choose F1 A /invariant choose F2 A
+5m 25sInvariant proposed 13 statements6 invariants, 3 witnesses and 3 known bugs, already checked by TLC
◉ Invariant
Here's what I propose must always be true for bounded log shipping buffer. Once you ratify it, these statements are pinned by hash, and I can't change them.
The spec
SpecThe system starts in Init, and every step is a Next step.
Invariants: what must never go wrong
TypeOKThe buffer, the shipped lines and the write history are lists of log lines, each producer has written a bounded number of lines, and the shipper is either retrying or not.BoundedCapacityThe buffer never holds more lines than its capacity.NoLineLostEvery line a producer has written is either still in the buffer or has been shipped.ShippedOnceNo line is shipped more than once.ShippedInOrderThe shipped lines are exactly the oldest written lines, in the order they were written.BufferInOrderThe buffer holds exactly the written lines not yet shipped, oldest first.
Witnesses: outcomes that must be reachable
BufferFullThe buffer can fill up to its capacity.RetryPendingA send can fail and leave its line in the buffer to be retried.AllShippedEvery producer can finish writing and every line can be shipped.
Known bugs: mistakes the invariants must catch
WriteOverCapacityA producer adds a line to a buffer that is already full. It must breakBoundedCapacity.DropOnFailureWhen a send fails, the line is thrown away instead of retried. It must breakNoLineLost.ResendDeliveredA send that actually went through is treated as failed and retried, so the line is shipped twice. It must breakShippedOnce.
Already checked against a draft model, within
Capacity = 2,MaxLines = 2,Producers = {p1, p2}: TLC explored 87 states and found no violation and no deadlock, all 3 witnesses were reached, and the known bugsWriteOverCapacity,DropOnFailure,ResendDeliveredwere caught.The statements in TLA+
vars == <<buf, sent, log, written, retrying>> Lines == Producers \X (1..MaxLines) Range(s) == {s[i] : i \in DOMAIN s} TypeOK == /\ buf \in Seq(Lines) /\ sent \in Seq(Lines) /\ log \in Seq(Lines) /\ written \in [Producers -> 0..MaxLines] /\ retrying \in BOOLEAN BoundedCapacity == Len(buf) <= Capacity NoLineLost == \A i \in 1..Len(log) : log[i] \in Range(sent) \cup Range(buf) ShippedOnce == \A i, j \in 1..Len(sent) : i # j => sent[i] # sent[j] ShippedInOrder == /\ Len(sent) <= Len(log) /\ sent = SubSeq(log, 1, Len(sent)) BufferInOrder == /\ Len(sent) <= Len(log) /\ buf = SubSeq(log, Len(sent) + 1, Len(log)) BufferFull == Len(buf) = Capacity RetryPending == retrying /\ buf # <<>> AllShipped == /\ \A p \in Producers : written[p] = MaxLines /\ buf = <<>> /\ Len(sent) = Len(log) /\ Len(log) > 0 WriteOverCapacity == \E p \in Producers : /\ written[p] < MaxLines /\ Len(buf) = Capacity /\ LET line == <<p, written[p] + 1>> IN /\ buf' = Append(buf, line) /\ log' = Append(log, line) /\ written' = [written EXCEPT ![p] = @ + 1] /\ UNCHANGED <<sent, retrying>> DropOnFailure == /\ buf # <<>> /\ buf' = Tail(buf) /\ retrying' = FALSE /\ UNCHANGED <<sent, log, written>> ResendDelivered == /\ buf # <<>> /\ sent' = sent \o <<Head(buf), Head(buf)>> /\ buf' = Tail(buf) /\ retrying' = FALSE /\ UNCHANGED <<log, written>> Spec == Init /\ [][Next]_vars+12m 15sJoseph ratifiedproposal e5dca8da9f15
Joseph
/invariant ratify e5dca8da9f15
◉ Invariant
Ratified by @gitdek. I pinned 13 statements in
examples/03-log-bufferon branchinvariant/issue-1-log-buffer(proposalsha256:e5dca8da9f15). Writing the code now.From here the 13 statements are pinned by hash. The factory can’t change them, and CI checks that a person with write access ratified exactly this proposal.
+14m 20sInvariant opened #2: provedGobra: 5 of 5 functions · the code reaches all 87 states
◉ Invariant
The code is written, and it passed the gate here: it's proved. I opened #2, and I'll merge it once CI's
invariant/gatepasses on it.An AI coding agent wrote the code in 8 turns, confined to its workspace, and the gate passed on run 1 of 1.
+26m 16sInvariant merged #2 once CI’s gate passedas 3ce3ba3
◉ Invariant
CI's
invariant/gatepassed on #2, and I merged it as 3ce3ba3a5ca07acd2bb422902c945fb43a80de59.The pull request closed issue #1 at 18:25:02.
People decide, the factory checks
AI coding agents don’t ask clarifying questions. They pick an interpretation and ship it confidently. A proof alone doesn’t fix that: if the agent also wrote the property, the proof only certifies the agent’s guess.
Invariant splits the work. People decide what must be true, on the issue, and the factory checks the code against it: proved with Gobra in Go or Nagini in Python, or tested against the model in TypeScript and Python. Every receipt says which.
- Formalize. An agent reads the issue and drafts formal statements in TLA+: invariants, what must never go wrong; witnesses, outcomes that must be reachable; and known bugs, plausible mistakes the invariants must catch.
- Ask. When the issue allows materially different behaviors, the factory doesn’t pick. It posts the question on the issue and waits.
- Ratify. The factory checks its draft with the TLC model checker before anyone sees it. A person with write access ratifies it with a comment, and the statements are pinned by hash.
- Synthesize. An agent writes the code in Go, TypeScript or Python against the ratified statements, confined to its workspace. The gate checks it, and the agent gets up to three repairs.
- Gate and merge. CI re-runs every check. The factory merges its own pull request only when the gate passes on that exact commit.
One morning, 17 pull requests
On Sunday, September 27, from 7 AM to noon Eastern, 17 pull requests merged into Invariant. Three of them went from a plain-language issue to a merge on their own, written and merged by the factory’s bot: a connection pool proved at every size, the factory’s own crash recovery, and a rate limiter with its model bounds taken out of the code. The rest changed Invariant itself: its gate, its watcher and its dashboard.
“Proved at every size” is new that morning. The code carries no model bounds, and Gobra proves it against its contracts at every size. The design’s rules are claimed only within the sizes TLC checked: the bounds a person ratified, and one size past them.

#18 to #20 20 minutes of factory time, CI included, and 1 comment from a person
Merged A connection pool that never hands out more than it has #20
invariant/gate· proved at every size · 13 states, and 73 one size larger · merged by the factory✓ Pass. Every check passed. The code is proved at every size. Fingerprint
sha256:b432097d509bCheck Result Evidence Pinned statements ✓ 9 of 9 match ratified by @gitdek on #18 Design · TLC ✓ no violations, no deadlock 13 distinct states (55 generated), depth 3 Reachability ✓ 2 of 2 witnesses reached PoolFullin 2 steps,SomeoneHoldsin 1 stepKnown bugs ✓ 2 of 2 caught acquire-when-full: NeverOverSizeviolated after 3 steps, acquire-twice:AtMostOnePerClientviolated after 2 stepsAgreement ✓ code reaches the model's states 13 states, depth 3 One size larger ✓ code reaches the model's states 73 states, depth 4, within Clients = {c1, c2, c3, c4},Size = 3Code · Gobra ✓ proved: 3 of 3 functions verified 3 with contracts, overflow checked, not verified: Init, load, store, Successors Build ✓ go vet, go test sandboxed, no network Checked within
Clients = {c1, c2, c3},Size = 2, and again one size larger, withinClients = {c1, c2, c3, c4},Size = 3. Within each, TLC's search is exhaustive. Gobra proves the code against its contracts at every size, and the design's rules are claimed only within the sizes checked.An AI coding agent wrote the code in 9 turns, and the gate passed on run 2.
#23 to #25 31 minutes of factory time, CI included, and 1 comment from a person
Merged Prove the factory recovers from a crash at any point, and from a second watcher #25
invariant/gate· proved at every size · 5,188 states, and 39,550 one size larger · merged by the factory✓ Pass. Every check passed. The code is proved at every size. Fingerprint
sha256:01e74cf660f9Check Result Evidence Pinned statements ✓ 23 of 23 match ratified by @gitdek on #23 Design · TLC ✓ no violations, no deadlock 5,188 distinct states (16,312 generated), depth 28 Reachability ✓ 5 of 5 witnesses reached Mergedin 15 steps,ReportedUnmergeablein 14 steps,StoppedRunReportedin 12 steps,RetryBuiltin 18 steps,RecoveredAfterCrashesin 17 stepsLiveness · TLC ✓ 2 of 2 properties hold CommandsAnsweredholds,GatedPullRequestsSettledholds, underFairAcquire,FairExpire,FairAct,FairFinishRun,FairDropRunFairness ✓ 5 of 5 on steps the model takes FairAcquire: weak, onAcquire(w),FairExpire: weak, onExpire,FairAct: weak, onAct(w),FairFinishRun: weak, onFinishRun(w),FairDropRun: weak, onDropRun(w)Known bugs ✓ 3 of 3 caught post-without-looking: NoDoubleAnswerviolated after 4 steps, post-without-lease:OnlyHolderActsviolated after 2 steps, rerun-stopped-run:NoRunTwiceviolated after 6 stepsAgreement ✓ code reaches the model's states 5,188 states, depth 28 One size larger ✓ code reaches the model's states 39,550 states, depth 30, within MaxCrashes = 3,Watchers = {w1, w2, w3}Code · Gobra ✓ proved: 31 of 31 functions verified 31 with contracts, overflow checked, not verified: b2i, runIndex, repoOf, withRepo, watcherOf, Init, Successors Build ✓ go vet, go test sandboxed, no network Checked within
MaxCrashes = 2,Watchers = {w1, w2}, and again one size larger, withinMaxCrashes = 3,Watchers = {w1, w2, w3}. Within each, TLC's search is exhaustive. Gobra proves the code against its contracts at every size, and the design's rules are claimed only within the sizes checked.An AI coding agent wrote the code in 8 turns, and the gate passed on run 1.
#28 to #30 42 minutes of factory time, CI included, and 1 comment from a person
Merged Take the rate limiter's model bounds out of its code #30
invariant/gate· tested against the model · 19,489 states · merged by the factory✓ Pass. Every check passed. The code is tested against the model. Fingerprint
sha256:2e143ead1b0fCheck Result Evidence Pinned statements ✓ 17 of 17 match ratified by @gitdek on #28, amending #5 Design · TLC ✓ no violations, no deadlock 19,489 distinct states (29,000 generated), depth 14 Reachability ✓ 5 of 5 witnesses reached BurstGoesOutin 2 steps,CallWaitsin 3 steps,WaitingCallGoesOutin 4 steps,BucketRefillsin 2 steps,CallRefusedin 5 stepsKnown bugs ✓ 4 of 4 caught send-without-token: WithinRateviolated after 3 steps, jump-queue:FirstInFirstOutviolated after 5 steps, queue-past-limit:BoundedWaitviolated after 5 steps, refuse-early:NoNeedlessRefusalviolated after 3 stepsOne size larger not claimed TLC didn't finish one size larger within 3m0s: context deadline exceeded Code · conformance ✓ tested against the model: every reachable state, no step outside it 28999 runs, 322,028 steps, 19489 of 19,489 model states visited Build ✓ node --test sandboxed, no network Checked within
Apis = {a1, a2},Capacity = 2,MaxCalls = 5,MaxTime = 3,MaxWaiting = 2. Within these bounds TLC's search is exhaustive. Nothing is claimed outside them.An AI coding agent wrote the code in 9 turns, and the gate passed on run 1.
It proved its own crash recovery
The factory’s watcher takes each step on an issue as a few effects on GitHub, and a crash can come between any two of them. Issue #23 asked the factory to model how it recovers, with a second watcher running at the same time, and to prove the core it would run on.
It proposed 23 statements: the spec, 7 invariants, such as no effect ever happening twice; 5 witnesses; 2 liveness properties, such as every command eventually being answered, under 5 fairness assumptions a person ratifies; and 3 known bugs. TLC checked them before anyone saw them, and one comment ratified them.
The factory wrote factory/recovery in Go, and the gate passed on run 1. Gobra proves 31 of 31 functions at every size, and the code agrees with TLC at 5,188 states, and at 39,550 one size larger.
Then the watcher started running on it. It holds a lease in a Git ref, so only one watcher acts at a time, and it records each agent run and merge before it happens. A test stops it just before each of its 20 effects, then restarts it on the same machine or another: all 40 stops recover, with every effect done once. Before the change, stopping before 8 of its 15 effects broke the flow, 14 of 30 stops.
A planted bug, caught
A model can pass its checks because nothing ever happens in it. So every proposal also carries witnesses, outcomes that must be reachable, and known bugs: plausible mistakes planted in the model, each of which TLC must catch. That’s how you know the invariants have teeth.
These are the real counterexamples TLC found. Step through the early-commit bug in two-phase commit: 6 states, from every resource manager working to a violation of TCConsistent.
Planted bug EarlyCommit The coordinator commits once any resource manager has prepared, instead of waiting for all of them.
Step 1 of 6
All three resource managers are working, and the coordinator hasn’t decided.
- Coordinator
tmState - init
- Resource managers
rmState - r1 workingr2 workingr3 working
- Messages
msgs - none
- Votes recorded
tmPrepared - none
msgs = {} rmState = (r1 :> "working" @@ r2 :> "working" @@ r3 :> "working") tmPrepared = {} tmState = "init"- Coordinator
Step 2 of 6 ·
RMPreparer1 prepares, and its Prepared message goes out.
- Coordinator
tmState - init
- Resource managers
rmStatechanged - r1 preparedr2 workingr3 working
- Messages
msgschanged - Prepared r1
- Votes recorded
tmPrepared - none
msgs = {[type |-> "Prepared", rm |-> r1]} rmState = (r1 :> "prepared" @@ r2 :> "working" @@ r3 :> "working") tmPrepared = {} tmState = "init"- Coordinator
Step 3 of 6 ·
TMRcvPreparedThe coordinator records r1’s vote.
- Coordinator
tmState - init
- Resource managers
rmState - r1 preparedr2 workingr3 working
- Messages
msgs - Prepared r1
- Votes recorded
tmPreparedchanged - r1
msgs = {[type |-> "Prepared", rm |-> r1]} rmState = (r1 :> "prepared" @@ r2 :> "working" @@ r3 :> "working") tmPrepared = {r1} tmState = "init"- Coordinator
Step 4 of 6 ·
RMChooseToAbortr2 aborts on its own.
- Coordinator
tmState - init
- Resource managers
rmStatechanged - r1 preparedr2 abortedr3 working
- Messages
msgs - Prepared r1
- Votes recorded
tmPrepared - r1
msgs = {[type |-> "Prepared", rm |-> r1]} rmState = (r1 :> "prepared" @@ r2 :> "aborted" @@ r3 :> "working") tmPrepared = {r1} tmState = "init"- Coordinator
Step 5 of 6 ·
EarlyCommitThe buggy coordinator commits on r1’s vote alone.
- Coordinator
tmStatechanged - done
- Resource managers
rmState - r1 preparedr2 abortedr3 working
- Messages
msgschanged - CommitPrepared r1
- Votes recorded
tmPrepared - r1
msgs = {[type |-> "Commit"], [type |-> "Prepared", rm |-> r1]} rmState = (r1 :> "prepared" @@ r2 :> "aborted" @@ r3 :> "working") tmPrepared = {r1} tmState = "done"- Coordinator
Step 6 of 6 ·
RMRcvCommitMsgr1 receives the Commit message and commits.
- Coordinator
tmState - done
- Resource managers
rmStatechanged - r1 committedr2 abortedr3 working
- Messages
msgs - CommitPrepared r1
- Votes recorded
tmPrepared - r1
msgs = {[type |-> "Commit"], [type |-> "Prepared", rm |-> r1]} rmState = (r1 :> "committed" @@ r2 :> "aborted" @@ r3 :> "working") tmPrepared = {r1} tmState = "done"TCConsistentviolated: r1 has committed while r2 has aborted. The invariant says: No resource manager commits while another aborts.- Coordinator
Planted bug WriteOverCapacity A producer adds a line to a buffer that is already full.
Step 1 of 4
The buffer is empty.
- Buffer
buf - emptycapacity 2
- Written
log - none
- Shipped
sent - none
- Lines per producer
written - p1 0p2 0
- Retrying
retrying - no
buf = <<>> log = <<>> retrying = FALSE sent = <<>> written = (p1 :> 0 @@ p2 :> 0)
- Buffer
Step 2 of 4 ·
Writep1 writes its first line.
- Buffer
bufchanged - p1·1capacity 2
- Written
logchanged - p1·1
- Shipped
sent - none
- Lines per producer
writtenchanged - p1 1p2 0
- Retrying
retrying - no
buf = <<<<p1, 1>>>> log = <<<<p1, 1>>>> retrying = FALSE sent = <<>> written = (p1 :> 1 @@ p2 :> 0)
- Buffer
Step 3 of 4 ·
Writep1 writes its second line, and the buffer is full.
- Buffer
bufchanged - p1·1p1·2capacity 2
- Written
logchanged - p1·1p1·2
- Shipped
sent - none
- Lines per producer
writtenchanged - p1 2p2 0
- Retrying
retrying - no
buf = <<<<p1, 1>>, <<p1, 2>>>> log = <<<<p1, 1>>, <<p1, 2>>>> retrying = FALSE sent = <<>> written = (p1 :> 2 @@ p2 :> 0)
- Buffer
Step 4 of 4 ·
WriteOverCapacityThe buggy producer p2 writes anyway.
- Buffer
bufchanged - p1·1p1·2p2·1capacity 2
- Written
logchanged - p1·1p1·2p2·1
- Shipped
sent - none
- Lines per producer
writtenchanged - p1 2p2 1
- Retrying
retrying - no
buf = <<<<p1, 1>>, <<p1, 2>>, <<p2, 1>>>> log = <<<<p1, 1>>, <<p1, 2>>, <<p2, 1>>>> retrying = FALSE sent = <<>> written = (p1 :> 2 @@ p2 :> 1)
BoundedCapacityviolated: the buffer holds 3 lines, and its capacity is 2. The invariant says: The buffer never holds more lines than its capacity.- Buffer
Planted bug DropOnFailure When a send fails, the line is thrown away instead of retried.
Step 1 of 3
The buffer is empty.
- Buffer
buf - emptycapacity 2
- Written
log - none
- Shipped
sent - none
- Lines per producer
written - p1 0p2 0
- Retrying
retrying - no
buf = <<>> log = <<>> retrying = FALSE sent = <<>> written = (p1 :> 0 @@ p2 :> 0)
- Buffer
Step 2 of 3 ·
Writep1 writes a line.
- Buffer
bufchanged - p1·1capacity 2
- Written
logchanged - p1·1
- Shipped
sent - none
- Lines per producer
writtenchanged - p1 1p2 0
- Retrying
retrying - no
buf = <<<<p1, 1>>>> log = <<<<p1, 1>>>> retrying = FALSE sent = <<>> written = (p1 :> 1 @@ p2 :> 0)
- Buffer
Step 3 of 3 ·
DropOnFailureA send fails, and the buggy shipper throws the line away.
- Buffer
bufchanged - emptycapacity 2
- Written
log - p1·1
- Shipped
sent - none
- Lines per producer
written - p1 1p2 0
- Retrying
retrying - no
buf = <<>> log = <<<<p1, 1>>>> retrying = FALSE sent = <<>> written = (p1 :> 1 @@ p2 :> 0)
NoLineLostviolated: line p1·1 was written, but it is neither in the buffer nor shipped. The invariant says: Every line a producer has written is either still in the buffer or has been shipped.- Buffer
Planted bug ResendDelivered A send that actually went through is treated as failed and retried, so the line is shipped twice.
Step 1 of 3
The buffer is empty.
- Buffer
buf - emptycapacity 2
- Written
log - none
- Shipped
sent - none
- Lines per producer
written - p1 0p2 0
- Retrying
retrying - no
buf = <<>> log = <<>> retrying = FALSE sent = <<>> written = (p1 :> 0 @@ p2 :> 0)
- Buffer
Step 2 of 3 ·
Writep1 writes a line.
- Buffer
bufchanged - p1·1capacity 2
- Written
logchanged - p1·1
- Shipped
sent - none
- Lines per producer
writtenchanged - p1 1p2 0
- Retrying
retrying - no
buf = <<<<p1, 1>>>> log = <<<<p1, 1>>>> retrying = FALSE sent = <<>> written = (p1 :> 1 @@ p2 :> 0)
- Buffer
Step 3 of 3 ·
ResendDeliveredThe send goes through, but the buggy shipper treats it as failed and sends it again.
- Buffer
bufchanged - emptycapacity 2
- Written
log - p1·1
- Shipped
sentchanged - p1·1p1·1
- Lines per producer
written - p1 1p2 0
- Retrying
retrying - no
buf = <<>> log = <<<<p1, 1>>>> retrying = FALSE sent = <<<<p1, 1>>, <<p1, 1>>>> written = (p1 :> 1 @@ p2 :> 0)
ShippedOnceviolated: line p1·1 was shipped twice. The invariant says: No line is shipped more than once.- Buffer
One contract per action
Each Go function carries a Gobra contract that restates exactly one TLA+ action. The action’s enabling condition becomes the contract’s precondition, and its effect, including everything it leaves unchanged, becomes the postconditions.
In two-phase commit, Gobra verified all 10 functions against their contracts, with integer overflow checked, and the Go code reaches exactly the model’s 288 states. Choose a clause to see its match.
TwoPhase.tla, lines 72 to 76RMPrepare(r) == /\ rmState[r] = "working" /\ rmState' = [rmState EXCEPT ![r] = "prepared"] /\ msgs' = msgs \cup {[type |-> "Prepared", rm |-> r]} /\ UNCHANGED <<tmState, tmPrepared>>twophase.go, lines 96 to 109// RMPrepare mirrors RMPrepare(r): a working resource manager prepares and// tells the transaction manager.// @ requires 0 <= r && r < N// @ requires s.RM[r] == Working// @ ensures t.RM[r] == Prepared && t.PreparedMsg[r]// @ ensures forall i int :: 0 <= i && i < N && i != r ==> t.RM[i] == s.RM[i] && t.PreparedMsg[i] == s.PreparedMsg[i]// @ ensures t.TM == s.TM && t.TMPrepared == s.TMPrepared// @ ensures t.CommitMsg == s.CommitMsg && t.AbortMsg == s.AbortMsgfunc RMPrepare(s State, r int) (t State) { t = s t.RM[r] = Prepared t.PreparedMsg[r] = true return t}Choose a clause, in either language, to see its match.
The action takes one resource manager, r. The contract requires r to be one of the resource managers.
The action can only happen while r is working. The TLA+ guard is the contract’s requires.
r becomes prepared and its Prepared message goes out. The effect is the first ensures.
UNCHANGED keeps the coordinator’s state and its recorded votes. The frame lines in the contract also spell out what EXCEPT and the set union leave alone: the other resource managers and messages.
go run ./cmd/invariant verify -out out/02-twophase-commit examples/02-twophase-commit
Pinned statements ✓ 6 of 6 match · recorded in D-0027
Design · TLC ✓ no violations, no deadlock · 288 distinct states (1,146 generated), depth 11
Reachability ✓ 2 of 2 witnesses reached · AllCommitted in 10 steps, AllAborted in 3 steps
Known bugs ✓ 1 of 1 caught · early-commit: TCConsistent violated after 5 steps
Agreement ✓ code reaches the model's states · 288 states, depth 11
Code · Gobra ✓ proved: 10 of 10 functions verified · 10 with contracts, overflow checked, not verified: Successors
Build ✓ go vet, go test · sandboxed, no network
Checked within RM = {r1, r2, r3}. Within these bounds TLC’s search is exhaustive. Nothing is claimed outside them.
✓ Pass. Every check passed. The code is proved. Fingerprint sha256:2a0189e69e1f
The receipt on the pull request
The factory opened #2 once the code passed the gate on Joseph’s machine, and merged it only after CI’s invariant/gate passed on that exact commit.
In CI, the gate also checks that a factory pull request touches only its own project, and confirms on GitHub that a person with write access ratified exactly the proposal in the lock.
Merged Add a bounded buffer for log shipping #2
invariant/gate · proved · 87 states, exhaustive within bounds · merged by the factory
◉ Invariant receipt · bounded log shipping buffer
✓ Pass. Every check passed. The code is proved.
| Check | Result | Evidence |
|---|---|---|
| Pinned statements | ✓ 13 of 13 match | ratified by @gitdek on #1 at 18:10:59 |
| Design · TLC | ✓ no violations, no deadlock | 87 distinct states (159 generated), depth 9 |
| Reachability | ✓ 3 of 3 witnesses reached | BufferFull in 2 steps, RetryPending in 2 steps, AllShipped in 8 steps |
| Known bugs | ✓ 3 of 3 caught | write-over-capacity: BoundedCapacity violated after 3 steps; drop-on-failure: NoLineLost violated after 2 steps; resend-delivered: ShippedOnce violated after 2 steps |
| Agreement | ✓ code reaches the model’s states | 87 states, depth 9 |
| Code · Gobra | ✓ proved: 5 of 5 functions verified | 5 with contracts, overflow checked, not verified: Successors |
| Build | ✓ go vet, go test | sandboxed, no network · go test found no test files |
Checked within Capacity = 2, MaxLines = 2, Producers = {p1, p2}. Within these bounds TLC’s search is exhaustive. Nothing is claimed outside them.
Fingerprint sha256:dfaf31c67a3f78c8909ce205f89823204aa20237a2b2b69553cba0817064d31f
The same fingerprint came from the run that opened #2, at 18:13:02, and from a later re-run at 18:46:09.
Receipts claim only what was checked
Issue #28 asked the factory to take the TypeScript rate limiter’s model bounds out of its code. Its amendment changed none of the 17 statements ratified earlier, so only the code changed: the factory rewrote the limiter as a class whose capacity and queue size are passed in.
Its receipt, on #30, says the code is tested against the model in all 19,489 states. It marks one size larger as not claimed, because TLC didn’t finish the bigger model within its three minutes.
The same morning, the live dashboard’s count called every merge proved. The TypeScript merges are tested against the model, so #32 changed the label to “merged through the gate”, which every merge is, so the dashboard claims no more than the receipts do.
Two coding agents
Invariant runs Claude Code or Codex. An issue’s Agent: line picks one, or its project’s manifest does, and every record of the work names the agent that did it.
Codex’s commands run under a permission profile of their own: they read only the workspace and their tools, write only in the workspace, reach no network, and can’t read the home directory, where Codex keeps its sign-in.
Codex’s first build, on September 29, was two-phase commit: 28 steps in about 10 minutes, and the code is proved at every size.
That evening it built two parts of Invariant’s own decision journal, both proved at every size too: the store’s journal only grows, and a decision’s state is the fold of its journal’s lines. All 145 of Invariant’s decisions fold on the proved core as they are. Wiring the first part in found a real bug: the store kept lines unique by their hash alone, so a line another project had written word for word, in the same second, was dropped.
codex Two-phase commit its first build
invariant/gate · proved at every size · 288 states, and 1,568 one size larger
✓ Pass. Every check passed. The code is proved at every size. Fingerprint sha256:1ec3ecccde20
Coding agent: codex. The fingerprint leaves it out.
| Check | Result | Evidence |
|---|---|---|
| Pinned statements | ✓ 6 of 6 match | ratified by @gitdek on #65, amending D-0027 |
| Design · TLC | ✓ no violations, no deadlock | 288 distinct states (1,146 generated), depth 11 |
| Reachability | ✓ 2 of 2 witnesses reached | AllCommitted in 10 steps, AllAborted in 3 steps |
| Known bugs | ✓ 1 of 1 caught | early-commit: TCConsistent violated after 5 steps |
| Agreement | ✓ code reaches the model's states | 288 states, depth 11 |
| One size larger | ✓ code reaches the model's states | 1,568 states, depth 14, within RM = {r1, r2, r3, r4} |
| Code · Gobra | ✓ proved: 9 of 9 functions verified | 9 with contracts, overflow checked, not verified: Init, transactionManager, withTransactionManager, Try, modelValue, phaseName, Abstract |
| Code · conformance | ✓ tested against the model: every reachable state, no step outside it | 4,896 steps recorded, 288 of 288 model states visited |
| Every step tried | ✓ in every state reached, but where only the environment's bounds rule a step out | 4,896 attempts in 288 states, refusals included |
| Build | ✓ go vet, go test | sandboxed, no network |
Checked within RM = {r1, r2, r3}, and again one size larger, within RM = {r1, r2, r3, r4}. Within each, TLC's search is exhaustive. Gobra proves the code against its contracts at every size, and the design's rules are claimed only within the sizes checked.
codex Prove the store's journal only grows (#177, part 4) #199
invariant/gate · proved at every size · 144 states, and 200,000 one size larger
✓ Pass. Every check passed. The code is proved at every size. Fingerprint sha256:1f484c2b2a9e
Coding agent: codex. The fingerprint leaves it out.
| Check | Result | Evidence |
|---|---|---|
| Pinned statements | ✓ 9 of 9 match | ratified by @gitdek on #195 |
| Design · TLC | ✓ no violations, no deadlock | 144 distinct states (397 generated), depth 9 |
| Reachability | ✓ 2 of 2 witnesses reached | KeptAbandoned in 2 steps, RebuiltFromOther in 3 steps |
| Known bugs | ✓ 3 of 3 caught | rebuild-replaces: StoreOnlyGrows violated after 2 steps, abandon-drops: StoreOnlyGrows violated after 2 steps, write-not-journaled: KeepsEveryLine violated after 1 step |
| Agreement | ✓ code reaches the model's states | 144 states, depth 9 |
| One size larger | ✓ code reaches the model's states | 200,000 states, depth 19, within Checkouts = {c1, c2, c3}, MaxLines = 3 |
| Code · Gobra | ✓ proved: 6 of 6 functions verified | 6 with contracts, overflow checked, not verified: Init, restore, readSystem, Try, checkoutValue, lineSet, Abstract |
| Code · conformance | ✓ tested against the model: every reachable state, no step outside it | 1,584 steps recorded, 144 of 144 model states visited |
| Every step tried | ✓ in every state reached, but where only the environment's bounds rule a step out | 1,584 attempts in 144 states, refusals included |
| Build | ✓ go vet, go test | sandboxed, no network |
Checked within Checkouts = {c1, c2}, MaxLines = 2, and again one size larger, within Checkouts = {c1, c2, c3}, MaxLines = 3. Within each, TLC's search is exhaustive. Gobra proves the code against its contracts at every size, and the design's rules are claimed only within the sizes checked.
Codex built it in 31 turns and 15 minutes, and the gate passed on its fourth run.
codex Prove a decision's state is the fold of its lines (#177, part 2) #200
invariant/gate · proved at every size · 43 states, and 43 one size larger
✓ Pass. Every check passed. The code is proved at every size. Fingerprint sha256:5f0f06e76fa1
Coding agent: codex. The fingerprint leaves it out.
| Check | Result | Evidence |
|---|---|---|
| Pinned statements | ✓ 14 of 14 match | ratified by @gitdek on #193 |
| Design · TLC | ✓ no violations, no deadlock | 43 distinct states (83 generated), depth 4 |
| Reachability | ✓ 3 of 3 witnesses reached | AgentProposedPersonRatified in 2 steps, TwoWayDecidedByAgent in 1 step, Superseded in 2 steps |
| Known bugs | ✓ 4 of 4 caught | agent-ratifies: OnlyPeopleRatify violated after 2 steps, agent-decides-one-way: OneWayNeverDecided violated after 1 step, ratify-again: RatifiedOnce violated after 2 steps, decide-again: FirstLineOnly violated after 2 steps |
| Agreement | ✓ code reaches the model's states | 43 states, depth 4 |
| One size larger | ✓ code reaches the model's states | 43 states, depth 4, within Agents = {agent}, MaxLines = 4, People = {alice} |
| Code · Gobra | ✓ proved: 12 of 12 functions verified | 12 with contracts, overflow checked, not verified: Init, restore, snapshot, Try, abstractWriter, abstractDoor, abstractStatus, abstractLine, Abstract |
| Code · conformance | ✓ tested against the model: every reachable state, no step outside it | 1,032 steps recorded, 43 of 43 model states visited |
| Every step tried | ✓ in every state reached, but where only the environment's bounds rule a step out | 1,032 attempts in 43 states, refusals included |
| Build | ✓ go vet, go test | sandboxed, no network |
Checked within Agents = {agent}, MaxLines = 3, People = {alice}, and again one size larger, within Agents = {agent}, MaxLines = 4, People = {alice}. Within each, TLC's search is exhaustive. Gobra proves the code against its contracts at every size, and the design's rules are claimed only within the sizes checked.
Codex built it in 23 turns and 11 minutes, and the gate passed on its first run. An earlier build, before the gate limited Gobra, looped for most of an hour.
What it doesn’t do yet
Invariant is pre-alpha. Model checking is exhaustive only within the bounds each receipt states, and one size past them where a receipt says so; nothing is claimed outside them. TypeScript is tested against the model, not proved.
The log buffer’s model assumes the shipper knows whether a send succeeded. And the work on crashes isn’t finished: what’s left is two watchers running live for a day.
Current status
The factory works as its own GitHub bot. A watcher polls GitHub through the gh command-line tool, holds the repository’s lease while it acts, and hands the writing to AI coding agents. Invariant itself is written in Go.
Since September 29 the factory has been paused, by Joseph’s call, until it’s ready to run for real: it was making drafts, plans and reviews faster than he could take them. Until then, coding agents build changes by hand, through pull requests that still pass the gate.
Its live dashboard is public: the factory’s orbit, the issues it’s working on, what it’s waiting for from a person, and its decision graph, every decision in the journal and what rests on what.

