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.
Review identity and capture
Section titled “Review identity and capture”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.
What the repository watcher watches
Section titled “What the repository watcher watches”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.
Repo Map and project context
Section titled “Repo Map and project context”The Repo Map is a deterministic project snapshot. It records what reading the tree proves and nothing else:
| Part | Contains | Identity |
|---|---|---|
| Project snapshot | Files, packages, entry points, exported symbols, identifier references, and dependencies | Pinned 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.
Provenance
Section titled “Provenance”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 moves | Rule |
|---|---|
| Board content on an element id a regenerated lens keeps | Carried verbatim, with no delta stamp |
| Board-native data — marks, groupings, arrangement, notes — on a surviving element id | Carried with its element |
| An ask, thread, or highlight anchored into board prose | Re-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 rename | Resolves against the successor patchset |
| A code ref whose cited content changed | Redrafted, and the section carries a new or reworked stamp |
| A code ref whose source is gone | Orphaned, 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.
Review state and command persistence
Section titled “Review state and command persistence”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.
Harness boundary
Section titled “Harness boundary”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.
Client projection
Section titled “Client projection”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.
Outbound forge actions
Section titled “Outbound forge actions”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.
Package enforcement
Section titled “Package enforcement”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.