Architecture overview
Rennet runs a local review service and connects several clients to it. The service captures an immutable patchset, builds review artifacts, runs installed coding harnesses, and performs Git and GitHub operations. Clients display and control that work through the typed Rennet session protocol.
Process topology
Section titled “Process topology”The @rennet/server package is the composition root. It constructs stores,
adapters, harnesses, the command dispatcher, a WebSocket listener, and the
embedded board service (@wboard/server) whose event logs live under
.rennet/boards/ in the review project. Board writes route through the
adapters whiteboard-client, the only writer of board ops. The desktop app
supervises that server as a detached local daemon instead of running it inside
Electron.
The Electron main process owns windows, the static application menu, the
app:// protocol, auto-update, tray behavior, and daemon supervision. Its
preload exposes only the small set of native facts and effects the renderer
needs: platform, version, WebSocket port, installed WSL distros, daemon
resolution for a chosen path, the native folder dialog, update readiness, and
update application. Project browsing runs over the fs.listDir daemon RPC; the
native folder dialog is only a shortcut that jumps the in-app browser to the
chosen folder, and the browser offers it solely while listing this machine.
The daemon serves the browser client as well as the WebSocket protocol. It binds to loopback by default. A non-loopback bind is an explicit remote-access setup; there is no Rennet relay or hosted Rennet backend. See Remote access for the user-facing setup and egress boundary.
The daemon lifecycle
Section titled “The daemon lifecycle”The desktop reads the daemon claim, verifies it through /healthz, and reuses a
healthy daemon. Otherwise it starts the server as a detached child, keeps that
child handle, and records the new claim. The daemon removes its claim during
graceful shutdown.
Stopping is a command the daemon answers, not a signal sent into the dark.
POST /shutdown sits beside GET /healthz on the same HTTP port and behind the
same host guard. It also refuses a foreign browser Origin with a 403, since a
bodiless cross-site POST needs no CORS preflight; its launchers send no Origin.
The daemon acks with its pid, port, versions and claim path, and
only once that response has flushed does it run the same shutdown SIGTERM runs.
The desktop, the version-skew restart, and rennet stop all send that command.
The stopping side then escalates, in order: send the command; on an ack whose pid
matches the claim, wait for that process to be gone; if it never acked, or acked
and stayed, SIGTERM and wait again, then SIGKILL and wait again. “Gone” is the
spawned child’s own exit event when this app instance spawned the daemon — that
wait is also what makes Node reap it — and otherwise the claim clearing together
with the pid no longer being in a running state. The state test is deliberately
not kill(pid, 0): a process that has exited but has not been reaped answers
that as alive while holding no port, no descriptor and no app bundle. A stop that
runs the whole ladder without success names what it found — exited but unreaped,
still running, or the claim still naming that pid.
Startup does not wait for that. The desktop begins the daemon ensure and creates
its window immediately, so the shell paints while the daemon probes, spawns, and
comes up healthy. The window therefore cannot receive the WebSocket port as a
launch argument: the renderer asks for it over the rennet:ws-port IPC channel,
which ensures that data directory’s daemon on demand and answers with its port.
Until it answers, the renderer’s connection supervisor sits in connecting and
the app shows its ordinary pre-connection state. A daemon that never starts names
its cause and daemon.log, then the app quits. Every ask ensures afresh, so an
ask that follows a failed start re-probes instead of replaying that failure, and
the window recreated after update-apply recovery dials whichever daemon is
current. One exception: while a teardown is in flight — the tray’s complete quit,
or the update handoff — the channel refuses the ask instead of ensuring, so the
renderer’s reconnect cannot put a fresh daemon back on the bundle the installer
is about to replace. A failed apply restores the data directory, and the next ask
ensures again.
Starts and stops for one data directory are serialized. Concurrent ensures fold into a single probe and spawn, and a stop — the tray’s complete quit, or the update handoff below — never runs until the ensure ahead of it has spawned its daemon and verified it healthy, so the installer is never handed a daemon that started behind its back. Ensures fold only while no stop has been queued between them; one that arrives after a stop waits for that stop and probes again, rather than answering with a port the stop is about to kill.
Closing every desktop window leaves the tray process and daemon available. The desktop’s complete-quit action stops a daemon that the desktop owns.
Update application is the other intentional daemon-stop boundary. The packaged daemon executes Electron’s binary from inside the installed app bundle, so the desktop must await its verified graceful stop before Squirrel can replace that bundle. Both the renderer update action and the tray action share this one ordered handoff. A failed stop leaves the app open, restarts the daemon it just stopped, and surfaces the failure: the app staying open is only true if the thing that makes it work is back. If the native updater rejects after it has closed the windows, the same operation restarts the daemon and recreates the window before reporting the error. A download failure is also reported rather than discarded as updater noise. On macOS, an out-of-bundle helper waits for ShipIt to replace the bundle and opens the installed version, covering native installs that exit without relaunching.
Clients and reconnection
Section titled “Clients and reconnection”The desktop renderer and served browser client both mount the same
@rennet/app-ui application. @rennet/app-ui composes screens from the
@rennet/ui primitive kit, a vendored shadcn/ui component set built on Base UI.
Each client provides a connection factory to ConnectionHost; @rennet/app-ui
does not import a transport or Node APIs.
@rennet/client supplies WsRennetBridge and ConnectionSupervisor. The
supervisor owns the idle, connecting, online, offline, and error
states, reconnects with bounded backoff, and restores registered progress and
ask-stream subscriptions after a reconnect. Progress registrations cover both
long-running project processing and per-repository pull-request fetches for the
project detail view.
The Expo mobile app uses @rennet/client and the portable protocol directly. It
has its own native UI rather than mounting @rennet/app-ui. Remote clients receive
the projected protocol: server-side projection translates or redacts host-only
state and commands before they cross the connection. Shell-specific operations,
such as the desktop directory picker and updater, remain native-shell features.
Package boundaries
Section titled “Package boundaries”Repository architecture checks enforce these import directions:
@rennet/protocol and @rennet/theme have no in-repository dependencies. The
@rennet/ui kit imports only @rennet/protocol and @rennet/theme; @rennet/app-ui
adds the kit. Only the desktop app imports Electron.
scripts/check-boundaries.mjs runs three positive controls that prove forbidden
imports fail: @rennet/app-ui importing @rennet/core, the @rennet/ui kit
importing @rennet/core, and @rennet/server importing Electron.
Review flow
Section titled “Review flow”One review moves through the system as follows:
- A client asks the server to create or open a review.
- For a local review, the Git adapter resolves the primary base — the
merge-base of the reviewed head with the newest ref in the clone naming the
primary branch,
origin/mainormain— and captures the diff from that base to HEAD plus staged, unstaged, and untracked changes. The capture records complete byte counts even when the visible payload must be truncated. - The server records an immutable patchset and derives review artifacts against that identity.
- Deterministic analysis and drafting agents produce the Design, Sequence, Decisions, Flagged, and Noise lens boards for that generation.
- The reviewer reads the boards, asks questions, stages asks, and previews any outbound review or change-request result.
- The outbound action for a review submits the previewed review. The outbound action for an own-branch change pushes the named branch and opens or reuses its GitHub pull request or GitLab.com merge request, selected from the effective push remote.
Recapture creates a successor patchset. It does not mutate the patchset already under review: the current generation of boards freezes and a successor generation is minted. Board content whose element ids survive carries verbatim, along with the board-native data on them; content whose cited code changed is redrafted and stamped.
Server composition and dispatch
Section titled “Server composition and dispatch”packages/server/src/create-server.ts assembles the runtime. It supplies the
command implementations to the dispatch map in packages/server/src/dispatch/
(one module per command family, bound from the command registry), which validates
and routes protocol commands. This package also owns live orchestration, symbol
lookup, projected connections, the owned T3 Code sidecar, and daemon lifecycle.
@rennet/core contains portable review logic, document validation, scheduling,
lineage, and state folds. @rennet/adapters implements
Git capture, GitHub calls, filesystem stores, SQLite persistence, Repo Map
generation, and coding-harness execution. Keeping composition in the server
lets the desktop, browser, mobile, and CLI paths use the same behavior.
Persistence
Section titled “Persistence”Rennet stores different kinds of state at their natural scopes:
| Location | Contents |
|---|---|
Daemon data directory (~/.rennet) | rennet.sqlite, project registry, daemon claim, and every other daemon-owned store |
<data dir>/client-settings.json | Viewer preferences (appearance, keybindings) — the client rung |
<data dir>/daemon-settings.json | This host’s global settings rung (listener bind, tracker) |
~/.rennet/projects/<escaped-path>/ | Project snapshot, Repo Map shards and manifests, overlays, and context manifests |
~/.rennet/threads/<reviewId>.json | Durable review conversation |
Project .rennet/map/ | Optional promoted context mirror |
Promotion writes the optional project mirror but does not stage or commit it.
Rennet never stages or commits anything under the project’s .rennet/, and
never writes an ignore rule into the user’s repository to compensate. Only the
app-owned .rennet/boards/ prefix is excluded from capture, the repository
watcher, and freshness; everything else there is the user’s project content and
captures normally.
The repository watcher reads the repository’s own ignore rules — it asks git —
and on macOS and Windows holds a single recursive operating-system watch for the
whole tree, so its cost is one descriptor at any repository size. That is a
correctness rule: a watcher that cost one descriptor per file took every
descriptor the daemon had, and a daemon with none cannot start git, the
sidecar, or a chat turn. Linux has no recursive watch in the kernel and keeps a
pruning per-entry watcher. What the
repository watcher
watches has the
rule.
The SQLite review store persists commands and events transactionally. Reading a review folds its event history into the current projection. Conversation files are separate so completed messages and interrupted turns survive client or desktop restarts.
Live turns
Section titled “Live turns”A session’s conversation is a thread in the owned T3 Code sidecar, so the sidecar owns its persistence, its live tail and its reattachment. A reconnecting client re-subscribes to the thread through T3’s own client runtime; Rennet keeps no parallel transcript of it.
Closing a client or desktop window does not stop a daemon that remains resident. Daemon shutdown stops the sidecar it spawned.
Where to go next
Section titled “Where to go next”- Architecture contracts defines the correctness rules behind immutable capture, provenance, lineage, persistence, and posting.
- The lens pipeline describes the five lens boards and how they are drafted.
- Context assembly explains project context and selection-aware retrieval.
- The Model Council explains model assignment.