The Execution Substrate: Worktrees, Gates, and Two-Phase Commit
The Execution Substrate: Worktrees, Gates, and Two-Phase Commit#
A coding agent that can emit diffs is not an engineering system. An engineering system is a machine that can refuse a transition.
Nomos treats the shared trunk as a single-writer resource under contention. The LLM is a bounded heuristic subroutine. It does not own the working tree, does not hold the branch lock, and does not declare success. The compiled Go runtime owns those privileges, and it exercises them through four mechanisms that form the execution substrate:
- Ephemeral worktrees — physical isolation of mutation.
- Pre-commit trunk locks — physical exclusion of mutation on protected refs.
- Proof gates — non-negotiable, exit-code-enforced predicates on AST, imports, and tests.
- Two-Phase Commit with Ed25519 receipts — atomic publication plus an auditable, tree-independent proof object on
refs/notes/agent.
None of this makes the model smarter. It makes incorrect state transitions mechanically impossible.
Execution Is a Concurrency Problem#
The failure modes of 2024–2026 “vibe coding” were not primarily model failures. They were harness failures:
- Concurrent writers on
mainwith no isolation boundary. - “Done” declared in natural language while tests were red, skipped, or never run.
- Commits that mixed diagnostic scaffolding with production source.
- No durable, independently verifiable record of what was proven at release time.
Those are the same class of bugs distributed systems already named: lost updates, dirty reads, non-atomic commit, and missing write-ahead proof. The substrate therefore borrows the oldest reliable tools in that literature—isolation, exclusion, prepare/commit, signed receipts—and binds them to git’s object model rather than to a chat log.
The lifecycle the engine actually runs is not a prompt. It is a state machine:
TRIAGE → SPEC → PLAN → EDIT → REVIEW → SYNC → LEARNEDIT is allowed only inside a worktree. REVIEW is allowed only as gate evaluation. SYNC is allowed only as 2PC. The model is invoked with an ephemeral capability token and a step budget; when the budget or a gate fails, the transition does not occur.
stateDiagram-v2
[*] --> TRIAGE
TRIAGE --> SPEC: issue pinned
SPEC --> PLAN: contract accepted
PLAN --> EDIT: worktree scaffolded
EDIT --> REVIEW: patch staged
REVIEW --> EDIT: gate exit ≠ 0 / snap-back
REVIEW --> SYNC: all gates exit 0
SYNC --> LEARN: 2PC commit + receipt
SYNC --> EDIT: 2PC abort
LEARN --> [*]Hermetic Worktrees#
Git already provides the isolation primitive: linked working trees sharing an object store, each with its own index and HEAD. Nomos uses that primitive as a sandbox, not as a convenience.
Convention:
worktrees/<repo>-<task><task> is a substrate-issued identifier (not a model-invented branch name). The engine creates the worktree, binds it to the task’s capability token, and destroys it on commit or abort. The trunk working copy is never the EDIT cwd.
Scaffold is a single, engine-owned sequence:
git fetch origin
git worktree add -B agent/<task> worktrees/<repo>-<task> origin/<trunk>
# cwd for all subsequent agent syscalls:
# worktrees/<repo>-<task>Invariants the runtime enforces before any LLM-issued edit tool is enabled:
| Invariant | Enforcement |
|---|---|
| CWD is a linked worktree | git rev-parse --is-inside-work-tree and git worktree list --porcelain must name the task path |
HEAD is agent/<task> |
Symbolic ref match; detached HEAD on trunk is rejected |
| Worktree is not the primary checkout | Path inequality against the repository’s main worktree |
| Index starts clean relative to the pinned base | git status --porcelain empty after scaffold |
| Capability token is bound to this path | CapBAC resource = worktree path + task id; other paths return EPERM |
The model never receives a shell whose cwd is the trunk. File tools (replace_file_content, structured AST edits) resolve paths against the worktree root. Attempts to write ../ out of the worktree fail at the membrane, not as a polite request in the system prompt.
Teardown is equally mechanical:
git worktree remove --force worktrees/<repo>-<task>
git branch -D agent/<task> # if 2PC aborted
# object store retains blobs until ordinary git gcDiagnostic reproducers, scratch tests, and failed hypotheses die with the worktree. They are not an argument for lingering on main.
This is the Axiom of Ephemeral Sandboxing in operational form:
Scaffold → sandboxed TDD loop → DoD verification → atomic merge & teardown.
The worktree is the unit of mutation. The trunk is the unit of publication. They are not the same directory.
Pre-Commit Trunk Locks#
Isolation without exclusion is theater. A worktree that cannot reach main is useless if another process—human, agent, or CI—can still write main from the primary checkout while 2PC is in flight, or if an agent can cd to the primary tree and commit directly.
Nomos therefore treats protected refs as locked by construction. The lock has two layers: a hook that cannot be talked out of, and a prepare-phase lease used by 2PC.
Layer 1: Physical hook#
A pre-commit (and pre-push) hook is installed in the primary repository and inherited by worktrees via core.hooksPath. The hook does not inspect commit messages for obedience. It inspects refs:
var protected = map[string]struct{}{
"refs/heads/main": {},
"refs/heads/develop": {},
"refs/heads/substrate": {},
}
func denyTrunkMutation(ref string, cwd string, inWorktree bool) error {
if _, ok := protected[ref]; !ok {
return nil
}
// Trunk commits are legal only from the 2PC commit agent,
// holding a live prepare lease, executing outside EDIT.
if os.Getenv("NOMOS_2PC_LEASE") == "" || inWorktree {
return fmt.Errorf("exit 1: trunk mutation forbidden (ref=%s cwd=%s)", ref, cwd)
}
return verifyLease(os.Getenv("NOMOS_2PC_LEASE"), ref)
}If the hook exits 1, git aborts the commit. There is no retry prompt to the model. The state machine remains in EDIT or transitions to snap-back.
NOMOS_2PC_LEASE is not a string the LLM can forge in a commit message. It is set in the engine’s environment for the duration of phase 2, derived from a lease object on disk (see below), and cleared on abort.
Layer 2: Exclusive prepare lease#
Hooks prevent accidental and agentic trunk writes. They do not serialize two successful 2PC attempts. For that, the substrate takes an exclusive lease on the protected ref before phase 2:
$GIT_COMMON_DIR/nomos/locks/<ref-as-filename>The lease is a small, fsync’d record:
type TrunkLease struct {
Ref string `json:"ref"`
Task string `json:"task"`
Worktree string `json:"worktree"`
BaseSHA string `json:"base_sha"` // trunk tip at prepare
TreeSHA string `json:"tree_sha"` // proposed tree
Holder string `json:"holder"` // engine instance id
Expires time.Time `json:"expires"`
Nonce [16]byte `json:"nonce"`
}Acquisition is O_EXCL create (or flock on a well-known lockfile) plus a compare of BaseSHA to git rev-parse <ref>. If the tip moved, prepare fails. Stale leases expire; the holder is the engine, not the model.
The combination is mundane and sufficient: the agent cannot commit to trunk, and two engines cannot commit to trunk at once.
Gates: Non-Verbal Proof#
REVIEW is not a conversation about quality. It is a vector of predicates, each a process with an exit code. The engine runs them against the worktree. Any non-zero exit blocks SYNC.
The model is not asked whether the code works. The toolchain is asked.
Ground-truth gates (the minimum closed set; adapters may add language-specific predicates):
| Gate | Predicate | Failure |
|---|---|---|
| Compile | Native toolchain exit 0 | Transition blocked |
| Tests | 100% of the invoked suite green; reproducer (if any) now exits 0 | Transition blocked |
| Cyclomatic complexity | Per-function complexity < 15 (AST) |
Transition blocked |
| Docstring density | Documented public surface ≥ 10% (AST) |
Transition blocked |
| Import boundaries | No illegal edges; no cycles across declared packages | Transition blocked |
| Worktree hygiene | No untracked diagnostic files scheduled for trunk | Transition blocked |
These are the same idea as cargo clippy failing the build, or terraform plan disagreeing with apply: the substrate’s opinion is the exit code. Negotiation is not an input.
A sketch of the gate runner:
type Gate struct {
Name string
Run func(ctx context.Context, wt Worktree) error // nil iff exit 0
}
func Review(ctx context.Context, wt Worktree, gates []Gate) error {
for _, g := range gates {
if err := g.Run(ctx, wt); err != nil {
return fmt.Errorf("gate %s: %w", g.Name, err)
}
}
return nil
}On failure the engine does not “tell the agent to try harder” as a side channel. It:
- Records the failing gate name, stdout/stderr hash, and tree SHA in the task log.
- Restores the worktree to the last known good index state (snap-back).
- Leaves the FSM in
EDITwith a decremented step budget. - Re-invokes the model only as a heuristic to propose a new patch.
If the budget hits zero, the worktree is removed and the task fails. Unfinished work is not merged because a context window filled up.
This is Cognitive Inversion applied to verification: the Go runtime drives the loop; the LLM is a stochastic ALU that may be called again, or not.
Two-Phase Commit#
A green worktree is not a release. A release is a transaction over three resources that must commit or abort together:
- the trunk ref,
- the signed receipt,
- the lock/lease.
Git’s commit + merge is not a transaction over those three. Nomos therefore wraps publication in classical 2PC, with the engine as coordinator and the git refs as participants.
Participants#
| Participant | Prepare | Commit | Abort |
|---|---|---|---|
| Worktree / proposed tree | Gates green; tree SHA frozen | Merge or fast-forward trunk to frozen tree | git worktree remove; delete agent/<task> |
| Trunk lease | O_EXCL lease; BaseSHA still tip |
Lease released after ref update | Lease released |
Notes (refs/notes/agent) |
Receipt bytes signed; object written but not ref-tipped as published | Notes ref updated to include receipt for the release commit | Unsigned/prepared blob left unreachable |
Phase 1 — Prepare#
Coordinator algorithm:
Review(worktree)— all gates exit 0. FreezeTreeSHA = git write-tree.- Read
TrunkSHA = git rev-parse <trunk>. IfTrunkSHA ≠the SHA the worktree was based on, rebase-or-fail policy applies. Default: fail prepare (no silent rebase of an already-gated tree). - Acquire
TrunkLeasewithBaseSHA = TrunkSHA,TreeSHAfrozen. - Build the receipt (next section), sign with the engine’s Ed25519 key, write the blob to the object store. Do not yet move
refs/notes/agentto a published state the rest of the world treats as final. - Record a prepare log:
{task, tree, base, lease nonce, receipt sha}.
If any step fails, abort: drop lease, do not move trunk, do not publish notes.
Prepare is idempotent for a given task: repeating it with a dirty tree or a moved tip fails closed.
Phase 2 — Commit#
Only the coordinator holding a live lease, with NOMOS_2PC_LEASE in environment, may run:
# still inside engine, not inside EDIT tools
git -C <primary> merge --ff-only agent/<task>
# or: git update-ref refs/heads/<trunk> <new-commit> <BaseSHA>
git notes --ref=agent add -f -F <receipt.json> <release-commit>
git update-ref refs/notes/agent <new-notes-commit> <old-notes-commit>
release lease
git worktree remove worktrees/<repo>-<task>--ff-only (or update-ref with the expected old value) is the git encoding of “the world did not move.” If the compare-and-swap fails, this is an abort, not a retry from the model.
Abort#
Abort is the default. Any of: gate failure after a retry budget, lease expiry, tip movement, signature failure, notes CAS failure.
release lease if held
git worktree remove --force worktrees/<repo>-<task>
git update-ref -d refs/heads/agent/<task>
# trunk unchanged; notes unpublishedThere is no “partial merge with a comment.” Partial publication is the bug 2PC exists to prevent.
sequenceDiagram
participant E as Engine (coordinator)
participant W as Worktree
participant G as Gates
participant L as Trunk lease
participant T as refs/heads/trunk
participant N as refs/notes/agent
E->>W: freeze TreeSHA
E->>G: Review()
G-->>E: all exit 0
E->>L: O_EXCL acquire (BaseSHA)
L-->>E: lease
E->>E: sign Ed25519 receipt
alt commit
E->>T: CAS update-ref / ff-only merge
E->>N: notes add receipt
E->>L: release
E->>W: remove worktree
else abort
E->>L: release
E->>W: remove worktree
Note over T,N: unchanged
endThe protocol is deliberately boring. Boring is the point. Agents invent; publication must not.
Ed25519 Receipts on Git Notes#
A green CI log in a chat transcript is not evidence. Evidence is a signed statement bound to content-addressed objects, stored where git already knows how to replicate it, without mutating the source tree.
Nomos stores that statement as a Git note on refs/notes/agent (the machine subconscious; human contracts remain Markdown in the Intent plane). Notes are first-class refs. They travel with fetch/push of that ref. They do not change TreeSHA of the code.
Receipt payload#
type ReleaseReceipt struct {
Version int `json:"v"` // 1
Task string `json:"task"`
Repo string `json:"repo"` // origin URL or id
Trunk string `json:"trunk"` // e.g. refs/heads/main
BaseSHA string `json:"base_sha"`
CommitSHA string `json:"commit_sha"` // release commit
TreeSHA string `json:"tree_sha"`
Gates []GateResult `json:"gates"`
TestTrace string `json:"test_trace_sha256"`
Engine string `json:"engine"` // nomos binary version + goos/goarch
IssuedAt int64 `json:"iat"`
KeyID string `json:"key_id"` // fingerprint of Ed25519 public key
// Signature is over the canonical encoding of all fields above.
Sig []byte `json:"sig"` // Ed25519
}
type GateResult struct {
Name string `json:"name"`
Exit int `json:"exit"`
TraceSHA string `json:"trace_sha256"`
}Canonicalization is deterministic (sorted keys, no insignificant whitespace). The signature is Ed25519 over that byte string. The private key lives with the engine host, not with the model and not in the repository.
Verification is a pure function:
func VerifyReceipt(r ReleaseReceipt, pub ed25519.PublicKey) error {
if !ed25519.Verify(pub, canonical(r), r.Sig) {
return errBadSig
}
if r.TreeSHA != gitTree(r.CommitSHA) {
return errTreeMismatch
}
for _, g := range r.Gates {
if g.Exit != 0 {
return errGateNotGreen
}
}
return nil
}A replica that fetches refs/notes/agent can answer, without trusting the LLM or the operator’s memory: this commit was published by a substrate engine holding this key, from this base, with these gate traces. That is the audit object. Chat is not.
Notes are append-mostly. Force-updating a note on a commit is allowed only by the same 2PC path (the -f in git notes add -f is for idempotent recommit of the same receipt, not for rewriting history after the fact). Downstream policy may treat a notes ref as FF-only.
Why notes, not files in-tree#
In-tree receipts would couple proof to the source DAG, invite merge conflicts on every release, and tempt models to edit the proof. Notes keep the proof in the GitBrain: high-density, machine-oriented, invisible to language toolchains. The Intent plane (Markdown specs) remains human-readable. The two are transduced; they are not mixed.
Comparative Matrix: Cargo, Terraform, Substrate#
Cargo made a social rule (“don’t ship unsound Rust”) into a compile-time lock. Terraform made a social rule (“don’t mutate cloud state ad hoc”) into a plan/apply lock. Nomos makes a social rule (“don’t let the agent commit because it said it was done”) into a worktree/gate/2PC lock.
The domains differ. The shape does not: a deterministic engine owns state transitions; plugins extend; the core refuses to become a general-purpose script runner.
| Dimension | Cargo (Rust) | Terraform (HashiCorp) | Substrate (Nomos) |
|---|---|---|---|
| Primary domain | Crate compilation, feature resolution, dependency integrity | Infrastructure lifecycle, remote resource graphs | Agent SDLC: isolated mutation, proof, publication |
| State that must not tear | Cargo.lock + target artifacts vs. source |
Remote objects vs. terraform.tfstate |
Trunk ref vs. worktree tree vs. signed receipt |
| Core invariant | If it compiles and clippy/test agree, type and memory contracts hold | Divergence is computed in plan and applied only then |
Mutations live in transient worktrees; trunk moves only after gates + 2PC |
| Lock | Package graph lockfile; advisory file locks on target dir | State lock (remote backend) during plan/apply | Pre-commit denial on protected refs + exclusive prepare lease |
| Execution loop | check → test → build |
plan → apply |
PLAN (read-only) → EDIT (worktree) → REVIEW (gates) → SYNC (2PC) |
| Proof object | Compiler diagnostics; lockfile hashes | Plan file; state serial | Ed25519 receipt on refs/notes/agent |
| Failure closed form | non-zero exit; no binary | no apply; state unchanged | snap-back; lease drop; trunk unchanged |
| What the core must not do | Provision cloud, write product copy, own deployment topology | Author application logic, commit to git on behalf of an app | Embed company CD, CMS, or cloud credentials in the engine binary |
| Extensibility | cargo-<plugin> binaries |
Providers and modules | Agent adapters (nomos-code, …) and declarative workspace hooks |
| Untrusted component | Build scripts (heavily constrained) | Provider plugins (protocol-bounded) | The LLM (CapBAC-bounded heuristic) |
Read the middle column as a warning, not a compliment. Terraform’s original sin in the hands of agents is that apply is still a privileged mutation if the plan was produced by a stochastic process with no hermetic boundary. Cargo’s lesson is the useful one: make the lock a file the toolchain understands, and make the toolchain the only writer.
Nomos’s lock is not Cargo.lock. It is the tuple (worktree, lease, receipt). Losing any one of the three is an abort.
Mapping onto the Four-Plane Topology#
The execution substrate is the Substrate plane. It must not absorb the others.
| Plane | Role in this chapter |
|---|---|
| Intent | Markdown specs and DoD contracts. Read in SPEC/PLAN. Never executed. |
| Substrate | Go engine: worktrees, hooks, gates, 2PC, keys. Single writer of trunk. |
| Membrane | May display receipt verification and task state. Must not hold the Ed25519 private key or the lease. |
| Infra | NixOS/daemons may host the engine and replicate refs/notes/agent. Must not be compiled into the core as a cloud provider. |
SSoT transduction still applies: the human-readable description of these mechanics lives in Intent; this chapter is that description. Byte-parity projection to public membranes is a later concern. The runtime does not read this Markdown to decide whether a hook fires.
Swarm split follows the same cut. Tier 1 (orchestrator) may sign off PLAN and invoke SYNC. Tier 2 workers receive a worktree path and a token that cannot satisfy NOMOS_2PC_LEASE. Workers edit and run tests. They do not publish.
Failure Modes the Substrate Is Allowed to Have#
A honest substrate names what it does not solve.
- Semantic correctness beyond gates. Complexity
< 15and a green suite do not imply the spec was right. Spec error is an Intent-plane failure, caught—if at all—by humans or by later tasks, not by 2PC. - Rebase policy. Default fail-on-moved-tip is conservative. An automatic rebase would require re-running every gate on the new tree; that is a new prepare, not a continuation.
- Key management. Ed25519 receipts are only as strong as engine-key hygiene. Compromised keys forge history of proof, not of trees; trees remain content-addressed. Rotation is an ops problem, not an LLM problem.
- Notes replication. If
refs/notes/agentis not fetched, replicas see code without receipts. Absence of a note is not a signature of absence; policy must define whether unsigned trunk commits are admissible (default: no, for agent-published commits). - Hook bypass.
git commit --no-verifyexists. So does writing objects withgit hash-object. The substrate assumes control of the engine host and of CI that verifies receipts on the notes ref. It does not assume a hostile superuser on the same box.
What it does solve is the failure mode that actually dominated agentic coding: the model and the trunk sharing a working directory, with success defined in prose.
Invariants (Closed Form)#
- No
EDITsyscall whose resolved path is outsideworktrees/<repo>-<task>. - No commit to a protected ref unless a live 2PC lease is in the engine environment.
- No
SYNCunless every registered gate returned exit 0 against the frozen tree. - Trunk CAS, notes publication, and lease release succeed together or not at all.
- Every published agent commit has a verifiable Ed25519 receipt on
refs/notes/agentwhoseTreeSHAmatches the commit’s tree. - Worktrees are removed on commit and on abort. They are not a long-lived branch strategy.
- The LLM cannot set
NOMOS_2PC_LEASE, cannot sign receipts, and cannot disable hooks.
If an implementation violates any one of these, it is not this substrate. It is a chat wrapper with extra steps.
The next chapter concerns how Intent contracts are compiled into the predicates these gates evaluate. The mechanics above do not care what the predicates mean—only that they are processes, that they return 0 or 1, and that 1 is the end of the transaction.