Skip to content

Settings and setup

Rennet discovers local tools and repository facts, then stores only preferences that have live consumers. Settings are split between machine-local files and the selected repository’s .rennet/ directory.

When a new client has no projects and no recorded welcome completion, Rennet opens a full-window welcome. Existing clients with no projects see the ordinary New Chat empty state instead; removing every project does not replay setup.

The welcome introduces Rennet, applies color scheme and theme-pack changes as soon as they are selected, reports the tools detected on the active environment, and configures the review harnesses. It then embeds the same Add Project flow as the rest of the app. Adding a project unlocks the final action, which records the welcome as complete and opens New Chat for that project.

Rennet discovers Claude and Codex executables by collecting candidate locations and running version probes. It does not depend on which. GUI applications often receive a different PATH from terminal sessions, so discovery also checks known install locations. On macOS, the Codex binary bundled with ChatGPT desktop is a candidate below a user-installed Codex CLI. Every selected Codex candidate must complete an app-server handshake.

The Claude adapter starts the user’s installed claude through @anthropic-ai/claude-agent-sdk. The Codex adapter starts the user’s installed codex app-server. Each harness owns its authentication; Rennet does not ask for provider credentials.

The tools step reports git, GitHub CLI and GitLab CLI availability and authentication, and Claude Code and Codex detection. Bitbucket is named as unsupported rather than presented as a detected integration. The welcome never asks the user to connect a forge account. GitHub operations prefer the environment’s authenticated gh CLI; see GitHub authentication. GitLab detection is the prerequisite for GitLab.com own-branch merge-request submission, which runs the repository environment’s authenticated glab. Merge-request intake, review posting, CI target attachment, and self-managed GitLab remain planned.

The review-setup step requires at least one detected Claude Code or Codex harness. With both available it asks which harness should own the orchestrator and enables Dual Harness by default. These controls write the same per-host harness enablement and Model Council assignments exposed later in Settings. They are setup shortcuts, not a second configuration model.

On macOS, the project step offers Grant Full Disk Access. The action opens System Settings → Privacy & Security → Full Disk Access; it cannot grant the setting itself. Access is optional and intended for protected or external project paths used by the in-app browser. Rennet does not scan unrelated files.

The welcome and the contextual onboarding tour have independent state. The welcome configures the client once; coach marks teach controls later and remain skippable and replayable from Help.

Choose either one repository or a directory containing several repositories. Rennet reads the selected path and returns an editable draft containing the detected repositories, worktrees, and primary branch. A workspace can include a subset of the repositories it contains.

Discovery does not fetch, check out, or create .rennet/. Processing starts only after the project draft is confirmed.

Global settings live in two machine-local files, split by who owns the value:

FileHoldsSettingValuesBehavior
~/.rennet/client-settings.jsonViewer preferences, outside the config ladderAppearancesystem, dark, lightApplies the selected color scheme to the app.
~/.rennet/client-settings.jsonTheme packaffineur, github, one-dark-pro, dracula, catppuccin-mochaApplies the selected application color pack and restores it across launches.
~/.rennet/client-settings.jsonKeybindingscommand ID to chord or explicit unbindOverrides the command catalogue on this machine.
~/.rennet/client-settings.jsonWelcome{ completedAt?: string; replayRequestedAt?: string }completedAt (written by the wizard’s Ready step) keeps the first-run welcome from replaying after setup. replayRequestedAt (Settings → Appearance → First Run → Replay the first-run welcome, or the same ⌘K action, through settings.resetWelcome) reopens it regardless of project count — first-run eligibility alone only ever elects a zero-project client, so the stamp is what makes a replay work on a real machine. The reset keeps any existing completedAt, because the client-settings version is still 1 and an older build requires that field; the startup gate reads the replay request first, so the pair means “completed, replay asked for”. Completing replaces the slice and drops the request. Independent of coach marks.
~/.rennet/client-settings.jsonCoachmarks{ seen: MarkId[]; skipAll: boolean }Remembers which onboarding coach marks you have seen and whether you skipped the tour; Replay Tour or a one-shot ?tour=reset route clears it through settings.setCoachmarks.
~/.rennet/client-settings.jsonNavigation{ lastProjectBySource: Record<string, string> }Remembers the last valid project per source so bare New Chat opens the real project picker directly.
~/.rennet/client-settings.jsonCouncil routingrouting.task[jobId][scenario] to model and effortOverrides one Model Council job’s assignment in one availability scenario. Written by the Environments Review section; absent until you change a mapping.
~/.rennet/client-settings.jsonBenchmark recording{ record: boolean }Whether measured pipelines archive their per-stage timings to ~/.rennet/benchmarks.jsonl. Default-on: an untouched install has no slice and records. Written by Settings → Benchmarks; observability configuration, never a gate on a review. See Benchmarks.
~/.rennet/daemon-settings.jsonThe global ladder rung as it exists on this hostDaemon listenerhost and optional portAllows a configured non-loopback listener for remote clients.
~/.rennet/daemon-settings.jsonWorktrees{ root?, pattern?, prPattern?, workspace? }This host’s worktree location, the two placement patterns, and whether Rennet works inside a checkout you already have open. Edited by hand; no screen writes this section. Any repository can override each of them on its own rung. See Worktrees.

Appearance and keybindings are personal, app-side choices — never a repo fact, never written into a working tree — so they sit outside the ladder in client-settings.json. The daemon listener is the host’s global rung and lives in daemon-settings.json; the settings surface lists every paired host’s daemon-settings section, not just the local one. The local host’s listener rung is read directly; a remote or WSL host is listed so it is visible, but its rung lives on that host and is not read from here.

Settings is a full-view takeover reached by route, not a set of tabs. The left nav lists five pages — Environments, Appearance, Keyboard Shortcuts, Projects, and Benchmarks — each its own route (/settings/:page); the active page is read from the URL, so a page deep-links and reloads directly. Archived is a sibling main-surface route (/archived), not a settings page. Appearance edits the color scheme, theme pack, and code theme, and carries the First Run row that replays the welcome; Keyboard Shortcuts edits keybindings; Benchmarks carries the recording toggle and the recorded history; Environments and Projects are described below.

The Benchmarks page renders the recorded runs with their stage breakdowns, split by the harness mode derived from each run’s own stage records — a run is labelled dual-model because two stages named two providers, never because a setting said so. Records live in ~/.rennet/benchmarks.jsonl and never leave the machine. Turning recording off writes nothing new and changes nothing else about how a review runs. The published numbers are in Benchmarks.

The history is paged, and the page says what it is not showing: how many runs are rendered, how many the archive holds, and any archive line the reader could not parse. A long history is served capped and rendered a page at a time, so both the cap and the page hide runs — a shorter list that announced neither would be indistinguishable from a shorter history.

The Keyboard Shortcuts page lists the app shortcuts that a single global key owner fires: Search (⌘P), Command Menu (⌘K), New Chat (⌘N), Toggle Sidebar (⌘B), Toggle Chat (⌘J), and Settings (⌘,). What the page advertises is exactly what fires — the page and the key owner read one table, so there is no advertised-but-dead shortcut. Remap a row and the action runs on the new chord right away (the remap invalidates the settings read the live key owner shares, so it rearms without a reload) and persists across launches. ⌘R is bound to nothing, so the reload chord stays the platform default.

The keybinding recorder needs a platform primary modifier or a bare key and rejects Shift or Alt combinations. It shows shortcut collisions but still stores them; the first matching command wins. An invalid stored shortcut falls back to the catalogue default or remains unbound. A filter narrows the list; Escape clears the filter before it can close Settings.

If either settings file is malformed, Rennet uses built-in values and disables writes that would replace the unreadable file. Fix or move the file, then reopen Settings.

Earlier Rennet stored these values in one ~/.rennet/config.json blob that mixed viewer preferences with the host’s daemon rung. On first read, Rennet migrates that legacy file mechanically and losslessly into the two split files — appearance and keybindings to client-settings.json, the daemon rung to daemon-settings.json. The migration is one-way and deterministic: every field lands in exactly one target and nothing is dropped. The legacy config.json is left in place, and the presence of either split file means the migration has already run, so it never repeats.

The Environments page is a card per machine. This Machine is always present and never removable — it is where Rennet runs. Remote and WSL hosts appear as their own cards and can be removed; removing a card forgets the environment and the projects and sessions Rennet tracked on it, and names those counts in the one sanctioned confirmation, while stating the machine itself is untouched. Each card header shows the OS glyph (a WSL chip for a WSL host), the environment name (rename inline — Enter commits, Escape cancels, an emptied name keeps the old one), and either the host address or a Local chip.

The daemon line is the resolver’s honest answer: the version when the daemon is reachable, “Not connected — last seen running Rennet daemon v” for a previously-seen host, or “Not connected — daemon unreachable, version unknown” otherwise, never an invented current version. Reconnect appears only when a host is unreachable; Update Daemon only when a reachable host has an update.

Reconnect performs a real re-handshake with that host’s daemon. It reads “Connecting…” and is disabled for exactly as long as the attempt is in flight, then either the card turns reachable — because the refreshed status says the host answered, never because the button was pressed — or the card stays unreachable and shows the reason the handshake failed. Reconnect re-attempts the connection; it does not install or start software that was not already there.

Update Daemon performs a real update of that host’s daemon, and appears only when the host reported an actual newer version to move to — where there is no update to make, or no mechanism to make it with, the button is simply absent. It reads “Updating the daemon…” while the update is genuinely in flight, then either the daemon line shows the version the host answered with afterwards, or the card shows the reason the update did not happen. The mechanism today is a WSL distribution: Rennet delivers its own server bundle into the distro and restarts the daemon on it. This machine’s daemon ships with the Rennet app, so updating Rennet updates it; a paired device runs its own Rennet and updates itself. Both say so plainly rather than offering a button that would do nothing.

Each card carries a Source Control and an Agents section. Agents on This Machine are live: Rennet lists the coding harnesses it discovered (Claude, Codex) with their versions, and disabling one rules it out of reviews on that host without uninstalling anything. Agent detection runs per host: the daemon asks each paired machine the only way it can be asked, so a card shows that machine’s own harnesses, and a host the daemon cannot interrogate reads its honest not-detected line rather than inheriting this machine’s answers. The enable decision is stored per host, so ruling an agent out survives a reload and leaves it running elsewhere.

Source Control reports GitHub / gh and GitLab.com / glab CLI readiness on each host. Detection runs per host exactly as agent detection does: the daemon runs the probes on each machine the only way it can, so a WSL distribution shows its own CLI versions and authentication state, while a host the daemon cannot interrogate reads its honest “Connect … to detect its tooling” line rather than inheriting this machine’s answer. GitHub’s enable toggle is stored per host. The GitLab health row has no toggle; it reports the prerequisite used for GitLab.com merge-request intake, pinned diff capture, CI status, review posting, and own-branch submission in that repository environment. A missing CLI keeps a Not installed row with a host-appropriate repair instruction and no invented version. A provider check that fails without explicit credential rejection reads Unreachable, not Not Authenticated. When one GitLab repository cannot use glab, its rows carry the repair while local and healthy-provider rows remain available. Self-managed GitLab hosting remains planned. Bitbucket remains unsupported. When at least one agent is enabled, a Review section exposes Model Mappings — see Model Mappings below. The source-control rows report the environment’s gh and glab states; the welcome does not contain a separate forge sign-in step.

Edit Mappings on a host card opens the Model Council’s role-to-model table for that machine. The dialog is honest-present: the council’s assignment tables are static and always available, so it lists Lens Drafters, Flagged Second Seat and Orchestrator with a real model and effort on a fresh install — never a blank waiting on a backend. Orchestrator is the row the welcome’s orchestrator choice writes; it routes the review’s chat thread. Values come from settings.get, which resolves the tables live rather than shipping the surface its own copy.

The column headers are the review-mode switch. Dual Harness needs both Claude and Codex enabled (its hover names the missing one); Single Harness shows whichever provider is enabled. A role that does not run in a scenario renders an em dash, never a fabricated model — the Flagged Second Seat, for one, exists only under Dual.

Changing a cell writes an override through settings.setRoleAssignment. Three things are true of that write:

  • It is model and effort only. The harness is never a stored field: it derives from the resolved model’s provider, so an override cannot pin an incoherent model/harness pair.
  • It is per (role, scenario) — Rai’s ruling of 2026-08-28. Editing a cell in Dual moves the dual scenario and nothing else; claudeOnly and codexOnly keep their own values, whether those are council defaults or their own overrides. Editing one scenario never moves a sibling.
  • It is a plain config write. An overridden cell carries an Overridden chip, and Reset to default clears the override so the council table answers again. Nothing is copied back on reset — the layer is dropped, so a later table change reaches the cell.

Overrides live in the viewer’s client-settings.json under routing.task, keyed by the council job id and then the scenario. An install that never changed a mapping has no routing key at all, and clearing the last override removes it again. A malformed config refuses the write rather than overwriting unreadable bytes.

Model Mappings changes which model carries a role, and the change reaches the turns that role runs. Every production dispatch reads the column its own probed availability answers to — dual on a host with both harnesses, claudeOnly or codexOnly on a host with one — on every dispatch, so the next round runs on the model you picked rather than the next daemon restart. See Where an override reaches for the sites that read it and for the one narrowing the Flagged lane applies.

It does not add council jobs, change the versioned default tables, or persist which providers are available — provider availability is detected, not configured.

Device pairing lives on the This Machine card, because a pairing bootstraps a connection to this machine’s daemon. See Device pairing.

Repository settings are the Projects page, scoped to one project through an inline picker grouped by environment and resolved from the ?project URL scope. Each included repository has its own local project config under ~/.rennet/projects/<escaped-absolute-path>/config.json.

SettingValuesBehavior
Map visibilitylocal, Git-visibleUpdates Rennet’s entry in .rennet/.gitignore.
Map promotionpromoted, not promotedReports whether a validated map is mirrored into the repository.
Runs ondetected host or named WSL distributionShows where Git and the harness run, detected from the repository path. Read-only.
Review guidancerules from .rennet/conventions.jsonShows the same catalogue review runners consume.
Issue trackergithub, jira, linear, noneNames the tracker whose referenced tickets are fetched for review agents.

Map visibility supports pin and reset operations. A pinned value is stored at the repository layer. Reset removes that layer’s value and returns to the inherited value. Map promotion and “Runs on” are read-only in Settings; promotion is a separate project action, and “Runs on” is a detected fact.

The Projects page also carries project identity (display name with the org/repo default, and the project mark) and the issue tracker’s fields — GitHub rides the host’s gh CLI and exposes no further fields; JIRA and Linear expose a project key, a base URL, and the name of the environment variable holding the token (never the token itself). These are wired to live commands. The display name writes through project.rename, which the sidebar’s own rename also calls, and an emptied name restores the org/repo identity host-side. The glyph, the mark and the issue-tracker fields write through settings.setProjectValue, which stores them on the repository rung — the project’s own config.json, the same layer map visibility uses — so a per-project answer beats the host’s global one, and an emptied field drops the entry and falls back down the ladder. Guidance rules write through settings.setGuidance into the repository’s own .rennet/conventions.json, the file the review runners read.

Project-scoped routes resolve a ?project= token by exact stable id first, then by an exact display name only when that name identifies one project. A duplicate display name is ambiguous and falls through to the remembered real project; renaming a project therefore cannot change route identity.

The per-project issue tracker reaches retrieval, not just the surface: the same repository rung is what related-context retrieval resolves through, so two projects on one machine can point at two different trackers. A rule the settings surface authors keeps its statement and severity; the rationale and anti-pattern already recorded for a rule survive an edit, and a newly authored rule takes its own statement as its reason (the catalogue reader requires one).

A daemon that does not serve the per-project rung — an older version on a remote or WSL host — returns rows without it, and those editors render DISABLED with a line naming the gap rather than accepting edits that would vanish.

Changing visibility never stages or commits files. Local visibility keeps the promoted map out of ordinary Git status through Rennet’s entry in .rennet/.gitignore. Git-visible removes only that Rennet-owned exclusion. At either visibility the managed block keeps ignoring Rennet’s own scratch: the per-session context/ directory, and the .gitignore file itself, which Rennet writes in the background after a capture and which must never read as a change to the branch under review.

Rennet detects where a repository runs from its path — a WSL locus from a WSL path, the host otherwise — and shows it as “Runs on”. It is a detected fact, not a setting: there is no override to choose the host or a distribution.

Malformed repository config resolves to defaults and disables writes for that row. Invalid entries in .rennet/conventions.json are dropped individually and reported in Settings; valid rules remain available to review runners.

The Worktrees section decides where a review works. It carries four controls and one list, and each control writes a registered key the binding reads — the location and layout a Rennet-made worktree is placed at, and whether Rennet may work inside a checkout the reviewer already has open.

ControlKeyGlobal rung, in daemon-settings.jsonRepository rung, in the project’s config.jsonBuiltin
LocationworktreeRootworktrees.rootworktreeBaseDirthe data directory’s worktrees/
Layout — branch worktreeworktreePatternworktrees.patternworktreePattern{repo}/{branch}
Layout — pull-request snapshotprWorktreePatternworktrees.prPatternprWorktreePattern{owner}/{name}/pr-{number}
Workspaceworkspaceworktrees.workspaceworkspaceshare

All four resolve on the ordinary ladder — builtin, then the global rung, then the repository’s own. None of the four has a producer on the detected rung.

The four controls write the repository rung, addressed by the row’s own repository path: ~/.rennet/projects/<escaped-absolute-path>/config.json, the same file the glyph, the mark and the issue-tracker fields write. That is what the section’s caption names, and it is why every row’s provenance chip, Pin and Reset speak about one entry in one file — a setting written there reads back as repo, and Reset drops it so the value falls back to the host’s answer or the builtin. Each of the four is a decision about one repository: the binding is per repository and so are its siblings, so the two repositories of a workspace project can differ.

A host-wide default for the same four keys lives in the daemon’s own ~/.rennet/daemon-settings.json under worktrees, and it is edited there, by hand. Rennet reads it — it is the global rung of these keys, and a value coming from it shows as global on the row — but no screen writes it, exactly as no screen writes the issue tracker’s global rung. Both layout builtins are the shapes the previous release hardcoded, so an install that has never written a rung places nothing differently.

Location takes a directory. The daemon expands ~ and makes the value absolute at the write, so the stored bytes name the same directory on every read; a relative value resolves against the data directory rather than against whatever directory the daemon was launched from. ~someone/trees is refused, because expanding another user’s home needs a lookup the daemon does not do.

Layout takes two patterns, both relative to the location.

  • The branch pattern substitutes {repo} (the repository’s real absolute path, escaped — the same key the project store files it under), {name} (the resolved remote’s repository name, falling back to the checkout’s own folder name only when no remote resolves), {owner} (the forge owner, or local), and {branch} (the branch, with its / kept as path separators).
  • The pull-request pattern substitutes {owner}, {name}, {repo}, and {number}. A snapshot has a number and no branch, which is why the two patterns are two grammars rather than one.

A pattern is refused at the write, with the file left byte-for-byte unchanged, when it carries an unknown token, renders to an absolute path, renders to the location itself, or can render outside the location. The refusal names the token or the escape rather than saying “invalid”.

Each pattern shows the path it resolves to for this repository, computed by the daemon from the same functions the binding calls: the repository’s own escaped key and resolved remote, its current branch as the sample, and 1 as the sample pull-request number. The client renders those two strings and derives no path of its own — a preview computed client-side is what made the old card name a folder Rennet never created.

Workspace is two segments.

  • share, the builtin: a review of a branch some worktree already has out binds to that checkout, and Rennet creates nothing.
  • own: Rennet works in a folder it made. A branch nothing has out and a pull request are placed exactly as under share — there is no conflict to avoid. A branch the reviewer already has out gets a Rennet worktree on a sibling branch, rennet/<branch>, forked from the branch’s head and placed at the branch pattern applied to the sibling’s own name. The reviewer’s checkout is left byte-for-byte as it stands, and the round’s commits land on the sibling. See One workspace per session for what that means for the round, the push, and the land action.

Workspaces lists every workspace Rennet knows for the scoped repository — the reviewer’s own checkout while a session is bound to it, each branch worktree, each sibling, each pull-request snapshot — with its path, its ref, the sessions bound to it, when it was made, when it was last used, and its size. A size that cannot be measured inside the time bound reads — rather than zero. A Rennet-made row with no live session carries a remove action: one click, no confirmation, and a refusal git returns is shown verbatim — capped, with an honest truncation marker — on the row, with the directory left as it was. Removing a sibling whose commits reach neither the reviewed branch nor any remote-tracking ref of it takes the worktree and keeps rennet/<branch>, and the outcome says so — the commits stay on a ref you can check out. A push puts those commits on the remote-tracking ref, so after one the branch goes with the worktree. The list is keyed by repository path, never by project id, so a workspace project’s repositories list separately.

A session binds once and keeps its recorded workspace for its whole life, so a changed location or layout applies to sessions created after the change. Nothing is relocated, and the old workspace stays on disk. It appears in this list only while a live session is still bound to it, or while the pull-request index holds it; once that session is archived, a worktree outside the resolved location is listed nowhere, and the daemon’s start sweep — which reaches only what sits under the resolved location — leaves it alone.

The project mark is the visual that identifies a project everywhere the shell shows it — the sidebar rows and switcher, the project picker, and the archived list. It is either a glyph from Rennet’s fixed symbol vocabulary or a logo: an image the project scout found in the repository, or one the reviewer uploaded. Identity shows all three choices together and exactly one reads as selected — the mark the shell is actually drawing.

ControlWhat it does
Glyph gridChooses a symbol. Writes both glyph (which symbol) and mark (that a symbol shows at all).
Repo logoShown only when the project holds a detected logo, with the repository-relative path the scout chose as its provenance line.
Upload an imageAccepts SVG, PNG, JPEG, or WebP. Any other type is refused with a line naming the formats, before anything is sent. There is no size limit.
Detect againRe-runs logo detection over the project’s repositories and reports what it found, or that it found nothing. Offered even when nothing was detected, so a repository that gains a logo can be re-scouted.

Logo bytes do not live in the checkout. A detected or uploaded logo is copied into the host’s per-project directory (ADR 0004) and served over project.logos as base64, which the client renders through a data: URL — one path for the desktop renderer and the served browser tab, with no static route. project.uploadLogo stores an upload and sets the mark to it in one write; project.detectLogo re-runs detection. A mark naming a logo whose bytes the host no longer holds falls back to the glyph rather than leaving an empty square.

The project questionnaire shown while a project is being built has a logo row for the same reason: it previews what the scout found and links here, rather than offering a path to retype.

Resolved settings carry the winning layer and every contribution. The current precedence is:

builtin < detected < global < repo

Appearance uses builtin < global. Map visibility uses builtin < repo. The per-project preferences resolve through the layers that actually have a producer today: the glyph is builtin < repo; the project mark is builtin < detected < repo, where the builtin is the glyph, the detected rung is offered once the project holds a copied repository logo, and the reviewer’s own choice sits on the repository rung — so a freshly added project wears its repository’s logo with no click, and an explicitly chosen glyph beats a later detection. There is no global rung for a mark: it is a fact about one project. The issue-tracker keys are builtin < detected < global < repo, and all four worktree keys are builtin < global < repo — both sections have a host-wide global rung in daemon-settings.json, the tracker because a token environment is a host fact and the worktree keys because a filesystem path is one. No worktree key has a producer on the detected rung: where a repository’s own worktrees already live is that repository’s convention rather than an instruction to Rennet, so nothing offers it and the questionnaire does not ask.

The tracker section resolves as a unit, not key by key. The layer that supplies the effective kind is the floor for that tracker’s project key, base URL, and token environment variable: an endpoint offered lower down described a different provider, so it is masked and the field reads honestly absent. A project that picks JIRA on its own rung therefore never inherits the host’s Linear URL and token — an incomplete endpoint surfaces as missing config and retrieval proceeds without it. An endpoint set at or above the kind’s layer is a refinement of the same choice and still applies. The settings surface and retrieval share that one resolution, so the values you see in project settings cannot disagree with the endpoint a review actually calls. The project settings sections show the resolved values and their controls; they do not badge the layer each value came from. “Runs on” (execution locus) is a detected fact with no ladder layer to override. The UI renders the resolver’s answer instead of recalculating precedence in React.

The Device Pairing section on the Environments This Machine card creates a single-use code that expires after five minutes. A remote device exchanges it for a device token and presents that token on future connections. The same section lists paired devices and revokes them.

A paired device connects directly to the configured daemon, normally over the user’s Tailscale network. There is no Rennet backend. Remote projections use repository references rather than host paths.

The daemon keeps everything in one directory. It is ~/.rennet by default, on every platform, and RENNET_USER_DATA or --data-dir moves the whole of it.

<daemon data directory>/ # ~/.rennet by default
├── daemon.json # discovery claim
├── daemon.log
├── github-token
├── rennet.sqlite # review database
├── projects.json # project registry
├── pr-worktrees.json
├── client-settings.json
├── daemon-settings.json
├── devices.json # paired devices
├── push-tokens.sqlite
├── sessions/
├── threads/
├── asks/
├── transcripts/
├── rounds/
├── generations/
├── reviews/ # `rennet review` documents, keyed by review id
├── board-meta/
├── worktrees/ # the builtin worktree location
└── projects/
└── <escaped-absolute-path>/
├── config.json
└── map/
<repo>/.rennet/
├── .gitignore
├── boards/
├── conventions.json
└── map/

boards/ is app-owned: capture, the repository watcher, and freshness exclude it by name, so board writes never change what a review is pinned to. The other entries are the user’s, and a change to one of them invalidates a review like any other tracked or non-ignored file. map/ is the exception under local visibility: the Rennet-managed block in the .gitignore beside it keeps derived map data out of git, so capture never sees a map rebuild and the review stays current.

The project-store key is the escaped real path of the checkout. Relocation records and aliases can move local state when a checkout moves. A worktree has its own local map entry.

The desktop app starts the daemon when necessary and connects over the same protocol used by other clients. Closing the last window leaves the app resident in the tray and the daemon running. Quitting completely stops the daemon owned by that app instance.

The daemon data directory holds every durable thing the daemon owns: its discovery claim, log, GitHub credential, project registry, pull-request worktree index, review database, settings, device and push-token stores, and the session-keyed stores behind sessions, threads, asks, transcripts, rounds, generations and boards. RENNET_USER_DATA or --data-dir selects that directory, and because every store honours it, a development or test daemon given one is fully self-contained — it reads and writes nothing under the real ~/.rennet.

The desktop launcher and rennet serve set UV_THREADPOOL_SIZE to 16 before the daemon starts when the variable is absent. An explicit operator value wins. The pool is shared by filesystem work and GitHub name resolution.

rennet serve
rennet status
rennet stop
rennet review <base>..<head> [path] | --pr <number> [path] [--out <file>] [--timeout <seconds>]
rennet map [path] [--base <ref>] [--json <file>] [--projects-dir <dir>]

review drives the daemon’s own session path, the same session.mint front door a New Chat row click uses, so a review it opens is a session the app can reopen by id, with its boards and transcript where the app expects them. Both forms are scoped to the repository the command runs in: in a workspace project holding several repositories, a branch name and a PR number are each unique only within one, so --pr <n> reviews the standing repository’s PR #n and refuses (naming the owner/name#n candidates) rather than guessing when the number collides across repositories and none is the standing one. The daemon resolves the checkout path to its canonical owner/name, so the CLI never spells the identity; a daemon older than that seam (Rennet server before 0.1.5) does not advertise the review-cli capability and is refused at connect, naming the minimum version, rather than reviewing against the wrong repository or base. It prints one progress line per capture step, lane transition and board write as the daemon reports them, then writes the settled boards to one JSON document at <data dir>/reviews/<reviewId>.json (or --out <file>) and prints that absolute path as its final stdout line, so a consumer can tail -1. Exit 0 means the boards settled; 1 means the daemon was absent, the preparation failed or was cancelled, or --timeout (default 1800s) elapsed, and the document is still written with the outcome and reason; 2 is a usage error.

serve, status, and stop operate on the daemon. stop asks the verified daemon to shut itself down over its own HTTP port (POST /shutdown, beside /healthz) and falls back to SIGTERM only when it gets no acknowledgement, then waits for the claim to clear. map runs without the daemon, builds the same deterministic Repo Map used by project processing, and stores it under the path-keyed local project directory. --json exports the map. It calls no model and needs no harness.

  • Run gh auth status in the project environment when GitHub operations cannot authenticate.
  • Run claude --version or codex --version to check a user-installed harness.
  • Fix malformed global or repository config before changing its settings.
  • Check the chosen directory when discovery returns no repositories; Rennet does not search outside it.
  • Use a separate RENNET_USER_DATA value for each development checkout’s daemon state.

See harness adapters for provider process boundaries and architecture contracts for project-map storage.