Skip to content

Architecture contracts

These contracts describe behavior that current code preserves. They protect the truth of a review and the reviewer’s authorship without restricting the coding agent’s ability to inspect, edit, test, commit, or push.

A review targets an immutable patchset. Local Git capture writes the complete reviewed working state—indexed, unstaged, and non-ignored untracked files—to a deterministic Git tree through a temporary index seeded from the real index. It pins that tree under refs/rennet/review-trees/<tree-oid> without moving the branch, HEAD, or the real index. The patchset keeps headOid as the actual branch commit and records the pinned tree separately as reviewedTreeOid.

The diff, file records, intent snapshots, repository inventory, and design artifact reads all derive from the merge base and that one reviewed tree. A file edited or deleted after capture therefore cannot change the review being drafted. Each file record keeps the complete byte count even when visible content is truncated. Binary files and submodules remain explicit capture states rather than disappearing from the review.

Recapture adds a successor patchset. It never rewrites the patchset that prior analysis, comments, and asks name.

Freshness compares the review’s recorded Git and project-context identities with the repository’s current state. Regeneration performs a new capture and derives a successor review state. The product does not present a mutated old artifact as fresh.

The watcher answers one question — has the tree moved since the review was pinned. On macOS and Windows it answers it with one recursive operating-system watch for the whole tree: one handle, one descriptor, at any repository size. macOS answers that with FSEvents and Windows with a recursive directory-change subscription. There is no per-file cost and so no descriptor bound to set.

That is a correctness rule, not a performance one, and it was learned twice. A per-file watcher costs one descriptor per file on macOS, because the system call underneath falls back to opening the file when the path is not a directory. The size of the repository then becomes the daemon’s supply of descriptors — and a daemon with no descriptors cannot start git, cannot start the coding-harness sidecar, and cannot answer a chat turn. Bounding the count did not save it: the bound has to come from the process’s real descriptor limit, and no API in the process reports that limit honestly. Node raises RLIMIT_NOFILE towards an unlimited hard limit at startup, so the soft limit a Mac reports afterwards is about a million while the kernel enforces something far smaller. The bound never fired, and the daemon drowned anyway. A watcher whose cost depends on the size of the tree cannot be made safe by choosing a better number.

Linux keeps a per-entry watcher, deliberately. The kernel has no recursive watch there; the runtime’s recursive option is implemented in user space by walking the tree and watching every entry, which costs the same as watching each file and additionally ignores this module’s ignore rules, so it would watch inside node_modules. Descriptors survive that — one shared kernel watch instance serves them all — but the per-user watch limit does not. It also cannot detect its own failure to arm, and a watcher that vouches for a tree it is not watching is a freshness lie, which is worse than the descriptor bug. So Linux uses the pruning per-entry backend, bounded at 32,768 entries, where trust waits on the initial walk finishing. Rennet ships no Linux desktop; this platform is the daemon running inside WSL, and continuous integration.

The repository’s ignore rules decide what counts as a change, and they come from the repository. A single git ls-files --others --ignored --exclude-standard --directory per watched root asks git which entries its ignore rules exclude, so nested .gitignore files, negations, the global excludes file, and .git/info/exclude all decide the answer, and the watcher agrees with capture — which excludes ignored files too — rather than approximating it. .git, .nx, and node_modules are excluded underneath that. The app-owned .rennet/boards/ prefix is excluded by the shared authority described below. Where the recursive backend runs, those rules filter events rather than pruning a walk, because the kernel watches the subtree whole; an .nx write still does not mark the tree dirty.

A WSL project reached over the \\wsl.localhost\… bridge polls, because inotify events do not cross that boundary. Polling holds no descriptors — it is a repeated stat, not a watch — so its bound is about the cost of those stat calls.

When a root is only partly watched, or no watch could be armed on it at all, the watcher says so once, naming the root, and then refuses to vouch for the tree: every freshness ask falls through to a real diff. A partial watcher’s silence means nothing, exactly as an unfinished walk’s silence means nothing, and neither is allowed to answer “unchanged”.

Two properties of the recursive backend are worth knowing. It arms in the same tick it is created, so no write that lands after the watcher starts is lost — the walk it replaced took most of a second on this repository and lost every edit that landed inside it. Arming is not delivery, though: when an event arrives is the operating system’s business, and on a loaded machine that has been measured in seconds, so a freshness ask inside that gap still answers “unchanged” for a tree that has moved. And it does not follow symlinks out of the tree, so a repository whose source is a symlink to somewhere else reports no changes for that content.

When a spawn does fail for want of descriptors, the message a reader sees says so rather than naming whichever subsystem happened to ask first.

The Repo Map is a deterministic project snapshot. It records what reading the tree proves and nothing else:

PartContainsIdentity
Project snapshotFiles, packages, entry points, exported symbols, identifier references, and dependenciesPinned Git OID and content hashes

Snapshots are composed by the live server. Multi-repository contexts refer to member maps and their pinned identities instead of flattening all content into one document.

Project processing writes its canonical state beneath ~/.rennet/projects/<escaped-path>/. Promotion to .rennet/map/ is explicit and never stages or commits those files. That directory also holds the project’s mark: mark-detected.<ext> is the logo the project scout found in the repository, copied in as bytes, and mark-upload.<ext> is one the user supplied. Each has a mark-<kind>.json sidecar carrying its source — the repository-relative path the scout chose, or the uploaded file’s name. A mark is a property of the project, not of whichever branch happens to be checked out, so it is never read out of the checkout on demand; the repository can move or delete the file without the project losing its mark.

An add-project run is one durable scout → structural-map sequence. Its stable command identity and per-repository checkpoints live in project-process.json beside the snapshot. The coordinator persists each phase before advancing, replays the latest state of each logical progress step, and resumes the first incomplete checkpoint after a daemon restart. Only the terminal done record carries the repo, file, and scope totals that the UI may call ready.

Model-produced review documents use the Rennet Structured Protocol. The envelope binds the document to its review, patchset, project context, instructions, generator, and model. Evidence references point back to reviewed material.

The inputDigest binds the patchset and the offered occurrence and lineage manifest used for that run. Validation rejects malformed envelopes or outputs that claim occurrences outside the offered set. A validated document can still express uncertainty; it cannot invent its input identity.

Deterministic artifacts record their source identities and generators too. The distinction is how the result was produced, not whether provenance is required.

Generations, carry, and the successor account

Section titled “Generations, carry, and the successor account”

One visit to a review’s boards over one patchset is a generation. The patchset id identifies immutable content; the generation id identifies the visit, so P0 → P1 → P0 produces two distinct P0 generations. Within a generation the boards are live append-only logs: re-running a lens appends, and board-native data on surviving element ids persists. When the code moves, the generation freezes immutable and a successor generation is minted against the successor patchset. Nothing is edited in place — append-then-freeze is the only change mechanic, and the frozen generation stays readable as drill-down.

What survives into the successor generation is decided by evidence, never by resemblance:

What movesRule
Board content on an element id a regenerated lens keepsCarried verbatim, with no delta stamp
Board-native data — marks, groupings, arrangement, notes — on a surviving element idCarried with its element
An ask, thread, or highlight anchored into board proseRe-anchored only by one exact quote match in the corresponding successor lens; zero or multiple matches preserve the thread as detached and suppress its stale highlight
A code ref whose cited bytes are identical, including through a Git-proven renameResolves against the successor patchset
A code ref whose cited content changedRedrafted, and the section carries a new or reworked stamp
A code ref whose source is goneOrphaned, kept with its reason rather than reattached nearby

The fuzzy lineage matcher can describe a successor relationship, but similarity never authorizes carry. This prevents a plausible match from impersonating reviewed identity.

The successor account bridges the two generations. It deterministically compares generation N with N+1 using the prior asks, carry result, patchsets, and rename evidence. It is not the reviewer-facing round report. A landed coding round produces that report through one separate classification turn over the durable dispatched asks and exact worker receipt, then the host verifies every claimed anchor against the measured diff. See Delta and generations.

The SQLite review store records commands and events in one transaction. A successful command appends its events and receipt together, so retrying the same command identity does not append the mutation twice. The current review is a fold over its stored event history.

Conversation state is stored separately under ~/.rennet/threads/<reviewId>.json. A live turn has both a durable placeholder and a server registry entry. Reattachment combines the stored thread with the registry so a connected client can resume the body currently being generated.

Board event logs persist under .rennet/boards/ in the review project — local, and never staged or committed by Rennet. An exclusion keeps them out of reviews, not an ignore rule: .rennet/boards/ is declared app-owned in one shared authority, and capture, the repository watcher, and freshness evaluation all consult it. Capture strips that prefix from the reviewed tree before deriving the patchset’s object ID, diff, file list, byte counts, intent, and identity from it, so a board Rennet writes cannot invalidate the review it belongs to — in any repository, whatever the user’s .gitignore says, and whether the board is untracked, staged, or committed. The rest of .rennet/ is the user’s project content and captures like any other file: tracked means intentional. Each board is a schema.json written once at creation plus an append-only log.jsonl with contiguous sequence numbers; a batch of ops lands contiguously or not at all. The embedded board service replays the log on restart, so a fresh process over the same directory serves the identical element state. The board-level document envelope and the validation results persist separately in the daemon’s board-meta store before the board is announced. board.read joins those two durable halves. All element writes route through the adapters whiteboard-client, the only writer of board ops.

A restarted round reuses a reserved report only when the exact report metadata and board state reconstruct and pass the same changed-line verification again. Partial lens boards are replaced as one attempt, not resumed element by element. Within one attempt a partial board IS resumed: a seat writes its board call by call and a turn that ends without finishing it leaves what it wrote in place, so the follow-up turn carries the last whole-board verdict and continues into the same board. The two are not in tension — the durable replacement unit is the attempt; the live unit inside it is the call. Recovery removes a partial board’s metadata before clearing its element log. A crash at either point therefore leaves the next retry able to repeat the cleanup; it cannot treat elements scheduled for replacement as a completed board.

Rennet uses the user’s installed coding harnesses. The Claude adapter invokes @anthropic-ai/claude-agent-sdk with the user’s installed Claude executable. The Codex adapter invokes the installed Codex executable. Rennet does not bundle its own harness binary or read provider credentials.

Model and harness traffic leaves the machine for the selected provider. Rennet has no hosted backend. The review patchset, Repo Map, and local state remain local except for the context deliberately supplied to an installed harness or external service as part of a requested operation.

Tool servers the daemon supplies to a harness turn are bound to the local interface, so a tool call is not egress. The board server a lens seat writes through is one of these: an HTTP MCP listener on 127.0.0.1 that the daemon owns, addressed per seat, described in T3 Code sidecar. Its credential reaches the harness child by environment variable and is on no argument list.

No output schema travels on a lens seat’s turn. A seat writes its board through that server and returns no document, so nothing binds its session, nothing is parsed back off it, and no structured payload appears as a message on its thread. A seat’s final assistant message is prose or nothing, and a turn that ends without one is not a failure on that ground. The turn-level structured-output contract still exists and is still sent exactly once, as the provider’s own output format rather than as prompt text — but the only board job that carries one now is the round-report seat, which still returns a document.

A session binds to exactly one workspace when it is created and keeps that binding for its whole life: the checkout that already has the reviewed branch out, when the repository resolves workspace: share or when that checkout is Rennet’s own branch worktree; a Rennet-created worktree on a rennet/<branch> sibling when the repository resolves own and a checkout Rennet did not place has the branch out; a Rennet-created worktree on the branch itself when nothing has it out, under either setting; the detached worktree at the reviewed head for a pull-request snapshot. Where a Rennet-created worktree goes is the location and layout resolved off the settings ladder for the reviewed repository, never a hardcoded shape. The session records the work branch its commits land on beside the bound root only when that branch is not the reviewed one, which is only a sibling bind. An absent field is not “unknown”: it is the reviewed branch, said once rather than copied onto every session, so every consumer that needs “the branch this session commits on” reads the recorded work branch, or the reviewed branch when there is none. The bound root is recorded on the session and is the working directory of every turn the session spawns — each lens seat, the chat thread, the handoff thread, and every cold utility turn — and the root their context files are written under, so a relative path in a prompt resolves in the tree the turn is actually standing in. The handoff exit captures its successor from that same workspace, because that is where the agent wrote. The binding is decided from the review target, never from the project: a workspace project holds many repositories and that mapping is not invertible. A workspace that cannot be created fails the bind rather than silently binding the session to the clone, which sits on another branch. Nothing re-decides a binding; a pull-request binding is re-pinned in place when the reviewed head moves. Worktrees earlier versions created per review are removed by a startup sweep that leaves any directory a live session is bound to, and nothing creates that layout again. A bind never prunes a worktree registration. Rennet prunes only from the daemon-start sweep, only registrations it placed, only when git reports them unreachable and no live session claims them — and because the prune verb is repository-wide, a repository holding any unreachable registration that fails those tests is not pruned at all on that pass. The coding round runs in the bound workspace like every other child of the session; no per-round worktree is created. A round runs on a sidecar thread of its own, keyed on the session and the round’s durable operation, created with that bound workspace — never on the session’s chat thread, which is the reviewer’s conversation.

Coding-agent handoff is an acting path. The agent receives a digest-bound bundle, works in the repository, and may write, test, commit, and push. Rennet then captures the resulting repository state as a successor and presents a deterministic successor account. The handoff does not grant model output permission to rewrite the identity of the review it started from.

For a branch review, that identity includes the exact selected branch. The coding worker runs as one turn in the session’s bound workspace and commits there, on that branch — Rennet creates no worktree per round and replays no delta afterwards. The successor patchset is captured from that bound workspace after the turn, pinned to the source patchset’s base OID and the head the round’s own commits reached; the branch names remain provenance, but a concurrent ref move cannot enter the round result. Rennet never stages untracked files on the reviewer’s behalf: the commits a round records are the commits its worker made, and the round’s own prompt is what asks for them — the review handoff forbids git because it recaptures a dirty tree, and a round says the opposite. The bound workspace must both be on the round’s branch and contain the reviewed commit; a branch name alone is not a repository identity, and a workspace failing either test is a refused round naming what it looked for, never a commit somewhere the reviewer cannot see. The sidecar’s per-turn checkpoint is the round’s receipt: the round account names the workspace root and that checkpoint — which identifies the round’s own thread — and restart recovery settles from it, honouring the checkpoint’s own status, since a failed turn checkpoints too. That checkpoint diffs the working tree, so a worker that committed leaves it clean: when the checkpoint’s diff is empty and the bound root’s commit range for the round is not, the receipt’s diff and changed paths come from that range, and a round that moved the branch is never reported as having changed nothing. That range is sourceHead..HEAD whole: every commit landing on the branch during the turn is attributed to the round’s worker, because the checkpoint is a working-tree snapshot and T3 exposes no per-commit worker identity to tell a concurrent human commit apart from the worker’s. This is the same range the commit count already reports, so nothing new enters scope — but the receipt cannot filter a commit another hand landed in the window, and does not claim to. The two reads that build that receipt — the changed-path list and the diff — are pinned to one resolved HEAD OID, so they always describe the same range even if a commit lands between them.

Restart recovery matches the exact worker start command. The attempt records its unique start identity before dispatch, and the sidecar durably associates that command with the provider’s returned turn ID. Replaying the same command uses T3’s durable command receipt and does not start another worker. Checkpoints carry that association independently of the bounded activity window, so an earlier attempt finishing late cannot become the current attempt’s receipt. Recovery waits briefly for its own checkpoint and distinguishes an absent checkpoint from an unreadable one. Legacy interrupted attempts with no start association remain unresolved; completed legacy history remains readable and explicit retry creates a new attempt. The round keeps its existing persistent thread and bound workspace.

Rennet does not run the repository’s check. A round has no gate step: after the turn settles, the next thing Rennet does is observe the commits. When the project scout has discovered a check command, the round’s work order tells the worker to run it before committing, to commit only when it passes, and to say why in its final message when it does not; when the scout found no command, the work order says nothing about one. Running the check in the bound worktree after the turn cost six and a half minutes and left an exit code as its only durable trace (Rai, 2026-09-04).

The first work-order round resolves one enabled installed Claude Code or Codex harness in the repository’s execution locus and pins that provider to the durable session. Later rounds resolve the same provider or fail explicitly; they do not silently switch harnesses. Every modern round receipt records the exact harness and version that executed its worker.

Loopback connections opened by Rennet receive the private session protocol: no Origin header (a non-browser client), the desktop renderer’s app://rennet, or the daemon’s own served UI on a loopback name at its port. A loopback socket opened by any other browser origin is classed as a network connection, because every page in the user’s browser can reach 127.0.0.1. Remote and mobile connections receive a projected protocol assembled by the server. Projection maps host paths and state into portable representations and restricts shell-specific commands to the shell that can perform them.

Projection is bidirectional. The server validates and translates incoming projected commands as well as outgoing state and events. Model-authored prose is displayed as authored and is not treated as a host-state transport.

Board events and board projections are wrapped surfaces. The boardEvent frame rides the existing push path: loopback connections receive raw frames, projected connections receive frames passed through the projection seam, which scrubs known-root and home-directory prefixes from every string the same way it scrubs other free text. Board prose attributes are model-authored and get only that blanket pass.

Live round report and lens-progress events have two protocol forms. Legacy unscoped events remain readable for older callers. Current durable events carry the operation id and operation revision together; a report event also carries the already-validated report projection. The client first selects the newest compatible operation revision, then the greatest sequence number inside that revision. A daemon restart may reset the transport sequence, so sequence alone cannot decide which attempt owns the screen.

The reviewer sees the exact outbound review or change-request payload before the external mutation.

For someone else’s pull or merge request, Rennet submits the previewed review. A deterministic marker and forge read-back make retries idempotent. GitHub posts one batched review. GitLab.com folds anchored comments into one review note and uses the native approval endpoint for an approval. A forge capability tells the core composer when the signed body must name the verdict. Each adapter then sends that exact reviewed body without adding provider-only prose after preview. An approving GitLab retry checks the current user’s approval state: an existing approval returns the reused marker receipt, while a note-only retry rechecks the immutable head and performs the missing approval once.

When that live head differs, the refused review remains unchanged and receives no publication receipt. Review latest revision first persists a new session for the same provider-qualified pull or merge request, then archives the old session’s target claim and routes to the new preparation progress. An interrupted transfer therefore leaves at least one live claimant. The fresh review owns a new patchset and a newly composed payload at the new head; the refused review remains readable at its original head and with its original bytes.

For the user’s own branch, posting pushes the named branch to the effective push remote and opens a GitHub pull request or GitLab.com merge request from the previewed title and body. If an exact open request already exists for that source and target branch in the repository, Rennet reuses it rather than opening a duplicate. Each CLI runs in the repository’s execution locus.

A retrospective review has no outbound post operation. Its findings and conversation remain local.

The architecture checker enforces the package dependency graph described in Architecture overview. It also runs positive controls that insert representative forbidden imports and require the checker to fail. This proves the rule is being exercised rather than merely configured.

Nx cacheable targets declare the files, shared configuration, environment inputs, and generated outputs that decide their results. Long-running and interactive targets are not cacheable. See Dependency standard for package and toolchain rules.