Skip to content

The lens pipeline

A lens is one reading of the change — design, sequence, decisions, flagged, or noise — drafted by a review agent on a fixed prompt and read as its own board. This page describes how a draft is produced, what guarantees it before a human sees it, and the rules every board obeys.

Design, Sequence, Decisions, Flagged, Noise — in that display order, Design first. Each lens is a board of typed blocks drafted by a review agent on a fixed prompt.

The client projects the boards present in the selected generation into a centred rail in the session top bar, in that same order. The session URL owns the selection. Flagged is the address-free default; another lens uses ?lens=. When a completed frozen generation has no Flagged board, the client selects its first present lens and replaces the URL with that canonical address. A live generation drafts progressively: it may show the first board that arrives, but it keeps the requested address so a later Flagged arrival restores that reading. A typed absence or explicit failure remains selectable and renders its terminal state. Every lens has a segment from the moment the generation starts, whether or not it has a result yet, and each segment carries the state of its seat — waiting, working, settled, failed or absent. A lens whose seat is still running is selectable, never a disabled control, and a lens is never dropped from the rail as it settles: a tab that vanished under the reviewer would move their selection.

New Chat persists a session before it captures the selected target and navigates to that session immediately. The session’s durable preparation snapshot records capture, then folds the pipeline’s real per-lens events into five progress lanes. It is distinct from round progress: no coding round is fabricated for an initial generation. The client reads those lanes as the seats’ states; capture is reported in the workspace’s own header, over boards that are already on screen, rather than by a screen in front of them. Failure, cancellation, and daemon interruption remain explicit, retryable session states.

The prompts live in packages/prompts (@rennet/prompts), one markdown file per lens, the reviewer-voice file, the round-report classifier prompt, and three shared partials. The reader voice teaches explanation for someone with little time, product familiarity, and no prior reading of the changed code. Investigation requires reading the pinned change before making claims. Tool guidance covers writing, batching, and settlement. The pipeline expands all three from the prompt manifest. The package exports a typed manifest. Noise has two instruction sets on purpose: noise.md drives the Noise lens board seat, and the NOISE_CONTRACT prompt contract drives the RSP noise-document runner behind the noise index; they emit different shapes to different validators. The board schema is never prompt text, and since the board tool surface landed it is not the seat’s contract either: no output schema travels on a lens seat’s turn at all. A lens seat writes its board through tools 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. The landed-round report seat is the one board job still bound to a schema — a narrow classification shape, not the all-kind board schema — because it still returns a document for the host to build from.

The scheduler lives in packages/server/src/runtime/lens-pipeline.ts (runLensPipeline); the pure logic it drives — lint, the validation loop, composition mechanics — lives in packages/core/src/board/. The scheduler is pure over injected seams (the harness ports, a prompt-file reader, and the whiteboard writer), so the whole path runs in tests against a fake runTurn with no live model call.

runLensPipeline’s consuming turn is the rounds runtime. createRoundsRuntime (packages/server/src/runtime/rounds.ts) is the composition root that supplies the scheduler’s open seams — onBoardArrival to the board-event broadcast, persistBoardMeta to the durable BoardMeta store, composeTurn to the orchestrator’s authoring turn, readPrompt to the node prompt reader — and drives a generation visit: the round-report seat settles its sequencing boundary first, then the four core lens lanes draft concurrently and the Noise lane runs on their settlements. The per-board arrival events this scheduler emits drive the progressive reveal — each lane publishes its own settlement as it lands, so a slow Design lane never holds a finished core board back; the client reads each board through the same per-lens board.read seam while its seat is still writing, so a board is readable element by element rather than only at settle — and a PipelineStartGuard keyed on the session and exact generation visit makes a retry of that dispatch reattach rather than double-start. create-server owns the live trigger: the own-branch round loop dispatches the coding turn, captures its result against the active patchset, and calls board regeneration through this runtime.

  1. Round-report first (on landed rounds only). When a coding round returns with its exact worker receipt, the round-report seat makes one structured semantic-classification turn before any lens drafter starts. Its prompt names one file, evidence.json in the session’s context directory, holding only the successor patchset id, the durable dispatched asks, the worker’s changed paths and observed commit range, and the round’s evidence manifest (see the classifier evidence contract below); the seat reads it with its own tools. It does not receive the full DeltaPacket or the verbatim diff, nothing rides inline, and its session is bound to the narrow classification schema rather than the all-kind board schema. Each ask is reduced to its durable id, path, instruction, and optional source anchor, so stale prior-diff context cannot compete with the coding turn’s measured evidence. The host sorts the outcomes and builds the document, section, outcomes, and code refs deterministically — including every line anchor, which it derives from the cited evidence rather than reading from the model. It then verifies the whole partition and every derived anchor before persistence. Readback also requires the exact ask text and forbids change evidence on an untouched outcome. There is no model retry or generic post-process turn on this path. The resulting board is both the reviewer’s greeting and the lens drafters’ input. Here, legacy caller means an injected pipeline caller that supplies the older round context without an exact worker receipt. It retains the generic drafting path for compatibility. A live durable coding round always carries the receipt and never selects that path.

  2. Draft. Every lens board of a generation is created — empty, drafting, addressable — before its seat’s thread exists. One agent per lens then receives the delta context and its lens prompt and writes into that board, call by call, through a tool set scoped to the kinds its lens authors. It returns nothing. The board is not brought into existence by a seat’s return, and a lane whose seat writes nothing settles over the board that was already there rather than over a missing one.

    A seat settles in one of two ways, each an explicit call: finish, which either settles the board or comes back with a pointer list the seat answers with further calls in the same turn; or the one settle-absent verb its lens admits, whose reason is fixed by the lens and carries no field to name it with. Sequence has no such verb, because Sequence admits no seat absence, and neither does Noise, because the host settles a Noise lane’s absence from the derivation before any seat runs. One absence belongs to the host on every lane but Design: spec-only, settled from the packet’s file rows before any lane opens when the change is made of specification artifacts and nothing else (see A specification-only change dispatches Design alone below).

    The Noise seat also has write_board, which writes its whole board in one call, and it is the only seat that does. The payload is a JSON string carrying a list of that board’s own verbs with their own inputs — no second authoring format — applied in order through the same boundary tier, then finished. A payload names an element it creates with local_id and uses that name wherever an id goes; the host resolves it to the id it minted. The answer is per entry: it names any entry it would not take by position, keeps everything else on the board, and reports separately the entries that hung under a refused one and were therefore not applied. finish runs only when every entry landed.

    Why one seat and not five: batching pays when the writing is bulk and costs when it is thought. The Noise seat groups members the host placed and did not choose, and on the 95-file drive that was 961 one-at-a-time calls and 317.8 s — a third of the generation’s wall clock — against 4 calls and 108.7 s with the verb. The four reasoning lenses compose their boards while they write them, and the same measurement had them slightly slower with it, so they do not carry its 486 B of tool surface. The scoping is derived from the host-derived membership table rather than listed again (writesWholeBoard).

    Each drafting instruction requires a document envelope with an authored title, a short Markdown introduction, and a measure. The target owns the final measure: Design is structured; Sequence, Decisions, Flagged, and Noise are reading. The host constructs the landed-round report document with the reading measure. Every board write still lands through whiteboard-client, the sole op writer; what a drafter never calls is the five whiteboard protocol tools (create/schema/apply/describe/events), which stay host-only. A lens seat writes its own board through the board authoring verbs, and those calls reach whiteboard-client under the hood. The Flagged lens runs review then compile: two lane-less review seats (Claude and Codex) read the change independently and each writes a findings file with its own file tools — no board, no board tools, no lane address. A third compiler seat reads both files and writes the whole Flagged board in one turn, attributing each finding to the model that raised it and marking whether both did. The board has one author, the compiler, so its ids come from one mint counter and cannot collide.

    A branch that carries a specification in a format Rennet parses takes a deterministic fast path for its Design board and settles with no model turn at all: the assembler transforms the specification’s own files into the board. It reads the first format the patchset touches, in a fixed order — OpenSpec, Kiro, BMAD, Superpowers, then grill-with-docs (ADRs, CONTEXT.md glossaries, context maps) — so one specification is drafted, never a merge, and the same patchset always assembles the same board. The assembler renders by obligation kind rather than by format: requirements with their scenarios, stated decisions with their stated rationale, task groups, a bug fix’s behaviour sections, glossary terms and progress-ledger rows each land the same way whichever file they came from, under a section per source file. Every string it writes is the specification’s own text shipped verbatim or a fixed label, so it lints in the transcribed register described under Validate below. A specification it cannot settle falls back to the seat, which renders the same specification — and the daemon log carries a [seat] line naming the rule that refused it, so a Design seat the host could have avoided is visible as it runs rather than inferred afterwards from a round’s wall clock. That fallback seat does not start from nothing: the paths the host located travel to it as design-sources.md in the session’s context directory — the format and one line per artifact, never the text — named in the Design prompt alone, and the prompt tells the seat those files are the specification, so it renders them rather than searching for them. A branch with no specification in any of those formats writes no such file and runs the seat, which searches the checkout for itself and, when that search ends empty, drafts an overview from what the branch does say — described under The Design lens below.

    A verified report arrives before any lens turn starts and opens that boundary, after which all five lens lanes run independently rather than waiting for the preceding display-order lens. A required report that fails or proves unavailable ends the round at report drafting with its exact reason; no lens turn starts behind an unusable greeting. Report arrival is an awaited handoff. The durable consumer must verify, read back, and record the report before the pipeline starts any lens seat; a rejected handoff ends the round with zero lens turns. Every Council seat inherits the user’s own harness configuration. Claude seats always load the user’s filesystem settings — authentication routing, such as a settings-env ANTHROPIC_BASE_URL credential proxy, lives there, and a seat that skipped user settings would reach the API on the wrong credential. That inheritance includes the user’s hooks, plugins, and configured MCP servers, which start per seat. Every board seat runs as a persistent thread in the T3 Code sidecar, so its MCP servers are the ones the sidecar’s own turn command carries — the daemon’s per-seat loopback board server among them — rather than anything a one-shot leg narrowed for it. A clean generation makes one drafting turn for Design, Sequence, Decisions, and Noise, one review turn for each installed Flagged model, and one compile turn that assembles the Flagged board from those reviews. It does not run a separate board editor after those turns, and no lens spends a second turn accounting for what it did not cite.

  3. Compile (Flagged only). The two review files are not a board; the compiler turns them into one. It writes each finding once through add_finding, carrying two flat authored enums beside the element: origin (claude or codex), the model that raised it, and agreement (concur, diverge, or solo), whether both reviews raised it and how they landed. The host expands those two enums at write time into the finding’s board-native author, concurrence tally ({ model, agree, total } per model), and accord stamp: solo writes one tally and accord split; concur writes both tallies and accord concur; diverge writes both tallies and accord conflict. The enums are input-only — they ride the add_finding call, never the persisted board, because author already encodes the model and accord the agreement, so keeping them would store one fact twice. The accord stamp is load-bearing, because a concurrence and a conflict fold to the identical tally pair — without it a reader cannot tell agreement from disagreement. With only one harness available the lens degrades to a single review and the compiler marks every finding solo from that model. When one of two review seats fails, the compiler assembles the survivor’s file alone.

  4. Validate. Validation is two-tier, and both tiers run inside the seat’s own turn. A rule decidable from the element a call carries is enforced at the tool boundary: the call is refused, the element is not created, and the refusal names the field and says what would be admissible. A rule that can only be answered over the whole board runs when the seat calls finish, which comes back as a pointer list — a rule id, an element ref, one sentence each — that the seat fixes with further calls before calling finish again. Neither costs anything: a refusal and a finish verdict are both results inside a live turn.

    The tiers split the rules by when a rule can be decided. A third question is who wrote the text, and it is orthogonal to both. A seat’s board is authored — a model chose every sentence, so every rule has a writer to address. The Design assembler’s board is transcribed: the host is quoting the project’s own artifacts, and a rule that tells a writer to choose different words has no subject. So the transcribed register drops exactly the voice rules — process-vocabulary, no-dialogue, no-remainder-narration. Citation rules treat path-shaped strings in transcribed prose as text, preserving the original wording and inline formatting without creating source-navigation controls. Explicit code_ref elements still validate their immutable patchset, side and range. Authored prose keeps automatic citation validation and links. The host stamps proseRegister on the board document so saved boards retain this distinction in introductions, prose, decisions and quote highlighting. Model tools cannot set that field; older boards without it keep their existing behavior. Code carried as bytes instead of a code_ref, and other integrity or finish failures, still take the existing fallback path.

    The distinction is load-bearing rather than tidy. process-vocabulary exists to stop a model writing about the review machinery instead of the change under review; run over a quoted spec it refuses the author’s own words for describing the author’s own system, which happens by construction in any repository whose specs discuss their own pipeline. Each such refusal threw away a board the host already had and bought a model seat to render the same quoted text.

    An attempt is spent by exactly one event: a turn that ENDS with the board neither finished nor declared absent — the context ran out, the harness died, the seat stopped. The board it wrote is KEPT, marked unsettled with its reason, and the follow-up turn carries the last finish verdict and nothing else: never the lens prompt, never the board, never a draft. That only means something to a session that already holds the conversation and a board that survived the turn — and every board seat is one, because a board job runs on a sidecar seat thread and nowhere else. A generation with no sidecar drafts no board at all: each lane settles as a typed failure naming the missing sidecar. A lane whose attempts are spent while its board is unfinished settles as a typed terminal failure naming the lens, the attempts spent and what the last verdict said — not as an empty board and not as an absence — and the elements the seat did write stay on the lane’s board. Anything the host finds after the seats settle ships as a labelled blemish: visible, never blocking.

    Two things the document path had are simply gone from a seat’s life, because the states they accounted for cannot occur. There is no honest-omission exit: an element the old ladder would have dropped is one the boundary tier refuses before it exists, so there is nothing to drop and nothing to account for. And there is no inferred absence: an empty board is never read as a claim, because a seat that means “there is nothing here” has a verb for saying so.

    After the seats settle, the host checks the material the served board actually needs. finish has already refused to settle a board holding no order_step, decision or finding for its lens, so what this catches is the gap that rule names in its own comment: Decisions and Flagged have no reachability rule, so finish accepts material no served root reaches. Noise is not among them: its membership is derived rather than authored (see The Noise board is the complement below), so its no-noise absence is settled by the host from an empty complement before any Noise seat runs. Unreachable core material becomes a precise failure; it never starts a second full drafting session and never lands as an empty successful board.

    The kind palette is enforced structurally at parse time: the frozen DraftBoardSchema has no thread, message, or code kind, so an out-of-palette kind is rejected with ZodError issues before any lint rule runs. The lint rules the seat runs then enforce what parse cannot:

    • kind allowlist per lens — each lens admits only its own element kinds;
    • no code bytes — code inside legal prose is a lint error; code on a board is a code_ref (path plus line span) the surface hydrates, so numbering cannot drift from the file it claims to show (backticked identifiers and patchset ids are exempt);
    • citation well formed — a path:line in prose is read as a citation only when its left side ends in a plausible source, config or markup extension, so a host and port (127.0.0.1:0), a pinned version or an ordinal stay prose rather than being refused as a citation of a file named 127.0.0.1. What IS a citation must then be repo-relative: an absolute or ~-rooted path, a bare basename (app.tsx:551), or the GitHub path#L12 form is refused, because a reader cannot resolve any of them;
    • citation resolves — every citation is a repository path plus a 1-based line range on the new or the old side, and the daemon resolves it against the changed regions of the captured patchset with the same predicate the citation reader uses: every cited line must sit inside a captured region on the named side, so a range one line past a hunk, or spanning the gap between two, is refused by the citing call itself — in the turn that made it, carrying the path, the range and the nearest changed range, with no element created (a rename’s base side answers to either name; a truncated capture’s tail counts as changed rather than being claimed outside; a range past the end of the file is the citation-resolves overrun pointer alone; a citation naming a side that is neither base nor head is the board schema’s own pointer, and lint answers nothing about it rather than checking it against head);
    • element references resolve — every schema-declared element reference names an element in that exact board, and the reference graph is acyclic so the host can create each target before its citer;
    • decision-grounded — a decision carries non-empty evidence and alternatives. alternatives is PLAIN TEXT — one sentence per road not taken — and not element references: every reader of the field treats it as text, and the Decisions target has no verb that creates an “alternative-option element”, so an input asking for ids asked for something the seat could not obtain;
    • a process-vocabulary screen flags structural-field prose that names lenses, boards, agents, seats, or drafts.

    Which kinds a lens may author is one table, LENS_TYPED_KINDS in packages/protocol/src/board/kind-tables.ts, beside the shared authoring kinds every lens gets. The kind allowlist reads it, and so does the board tool surface below, so a kind reassigned between lenses cannot mean one thing to the rules and another to the verbs a seat is given.

    Every rule above screens the board’s document as well as its elements. The document is not an element, so a fenced code block in the board’s opening prose used to pass a lint that rejects the same bytes one line below it; document violations report at a board-level pointer (/document/introMarkdown).

    The rule registry is partitioned into two tiers by what each rule reads. A rule decidable from a single element plus the daemon’s knowledge of the patchset is a boundary rule: code bytes and dialogue in prose, malformed and unresolvable citations, machinery vocabulary, an ungrounded decision, an unknown source. A rule that can only be decided over the whole board is a finish rule: report coherence, requirement order and verbatim quoting, and the Design artifact-set checks. The partition is asserted over the rules the tool path actually receives, not over the authored lists, so a rule cannot be in the registry and in neither tier.

    Two settlement rules sit in that registry and in the finish tier, moved down from the drafting runtime where they were lane failures a seat could not answer: every Sequence step must be reachable from a top-level section, and the board must hold material at all. lint — the document path’s entry point — does not run them, and that exclusion is declared as its own named subset rather than achieved by keeping a rule out of the registry. The reason is cost: the Noise prompt asks for an empty board when nothing in the change is skip-safe, and the runtime settles that as a typed no-noise absence, so asking “does this board hold material?” during document validation would spend a model repair turn arguing with a board that was right.

    The two settlement rules ask one question each. Material presence counts what exists; reachability answers for itself and names the step to re-parent. They used to overlap, and a Sequence board holding one orphaned step got both — an “the board is empty” pointer over a board with a step on it.

    The core validator retains its typed-data immutability result for callers that provide a deterministic transform; the production lens scheduler supplies no model-backed post-process transform. Immutability is its own gate rather than a lint rule, and a handful of designed rules reference fields the frozen schema deliberately does not carry (they wait on a schema follow-up rather than being enforced against absent data). The reviewer-voice authored prose is screened by a separate, narrower register. A turn that writes nothing at all is not a settlement. It ended unsettled like any other, so it spends the lane’s one attempt and the seat is re-asked rather than the lane failing at attempt zero — and the re-ask says exactly that, because there is no verdict to carry when finish was never called. Only a ladder that runs out with the board still unfinished settles a failure.

    The document path — parse the return, lint it, hand ZodError-shaped pointers back on a retry channel, freeze the passers — survives for exactly one caller: the legacy round-report leg, which supplies no evidence manifest and is still bound to the full board schema. lint, validateDraft and the pointer-only renderRepairPrompt are that caller’s loop and nothing else’s.

  5. Freeze. The validated draft board becomes the lens board without a second model rewrite. Host-owned Design projections, the Flagged compiler’s origin/agreement expansion, round composition, delta stamps, and metadata persistence remain deterministic.

    Those host-owned passes run after lint, so the board that gets written is not the board lint last saw. A reference-admission pass at the write boundary checks every element reference against the exact document being written, because the board service validates references in batch order and rejects the whole write as bad-ref when one names an element the document does not contain. An inadmissible reference is repaired only when its unique intended target is provable: exactly one element of that document shares the reference’s identity, it is not the citing element itself, and it is the kind the field is declared to hold — an order_step.span may only land on a code_ref. Identity folds case and the separator set (-_./\ and space) and nothing else; two ids that differ in a letter, ASCII or not, are two ids. A code_ref must cite the captured patchset this generation reads, and that test applies to a reference spelled exactly as much as to a repaired one, because an id that happens to exist is not a licence to cite another patchset’s code. The one exception is host-carried round history: a prior round’s addressed chapter is about an earlier generation, so its orchestrator-authored anchors keep that generation’s patchset. Every repair is recorded on the board’s durable metadata. An ambiguous or absent target is not proof: the lane settles a typed failure instead. An element is never dropped to make the rest of a board acceptable — an accepted board that silently sheds produced material is the quiet lie the complete-coverage ruling forbids. The board service stays authoritative and keeps rejecting; repairs happen producer-side.

  6. Compose. A frozen draft board is the lens board the human reads; there is no separate composed surface. Composition is split. The mechanical part lives in core/board/: verbatim carry on stable element ids (a carried element is byte-identical across generations), new/reworked delta stamps on sections, and the host-owned finding lifecycle for a returned round. That lifecycle matches each generation-and-board-scoped finding against the prior and freshly drafted Flagged boards. It removes only a stable-id or unique semantic match for an addressed ask or durable dismissal. It leaves an ambiguous match or a reference to an abandoned draft attempt visible and returns a detached resolution instead of applying the old disposition. A uniquely reattached dismissal is cloned onto the successor board before Flagged persists, while the frozen predecessor keeps its own disposition. On Sequence, the same pass preserves earlier host chapters and appends Round N · Addressed from the report’s exact dispatched-ask ids. The pass is pure and produces the successor boards without changing either input board or any frozen generation.

    The authored part is the orchestrator’s connective review prose, written write-through on the versioned reviewer-voice prompt (src/prompts/review-draft-voice.md) in the reviewer’s first-person register. The authoring prompt carries path references only: the voice rules are written to review-draft-voice.md in the session’s context directory, each frozen board to boards/<lens>.json, and any curation feedback from the prior generation to curation-feedback.md; the seat reads them there. The authored prose is screened by a narrower register lint (citations plus the machinery screen) — visible, never blocking.

Each successful board persists its metadata as soon as its lane settles, so live progress follows completion order. Successful typed absences, per-board arrival events, and lens failures are all published in that same settlement order through one serialized callback, which keeps cumulative generation snapshots monotonic. A lane’s arrival is emitted the moment its board is written — no global barrier over the five lanes — and a lane whose attempts are exhausted is emitted the moment they are, for the same reason: a failure only visible in the returned outcomes is a failure the surface cannot show until the slowest sibling finishes, which is how a seat that died at 33 s went on reading “quiet for 320 s” until the reveal. The returned outcomes still use the canonical Design, Sequence, Decisions, Flagged, Noise order regardless of which drafter finished first, because that array is completion bookkeeping rather than the reveal. The rounds machinery consumes the arrival events to drive the reveal; the pipeline only emits them.

A board is published as it is written, not only when it arrives. Every write the board writer accepts — a seat’s call, and the members the host places on a derived board before its seat’s first turn — reaches an observer the lane was opened with, and goes out as one frame carrying the elements that call touched and the position each holds. A lane’s opened frame is the empty board, ahead of any seat thread; its closed frame says nothing more will land. Every frame carries its generation, so a superseded drafting attempt cannot paint over the live one, and a revision monotonic within (generation, lens), so a reader can tell a duplicate from a gap. A lane can carry more than one watcher — it is never deleted, and a content-addressed generation is shared by two reviews of identical content — so every opener hears every write and the opened frame carries the board the lane already holds. The stream and the durable board are two surfaces: the elements travel live without patchset stamps or round-delta marks, because both are stamped where the board is persisted at settle. See the T3 Code sidecar for the wire and its catch-up read.

Coverage is a projection, never a gate. No step checks that every changed region was taught or accounted for: a board carries no skip list, composition runs no coverage assertion, and no reveal is blocked, failed, or annotated on coverage. Which regions the boards cite is derivable on the daemon from the citations themselves, so a coverage view is a read over what was cited rather than an obligation on a seat.

Per-phase timings are durable and versioned. One record per phase — report (the whole report gate) and report-classification (the provider turn inside it), each lane’s lens-draft / lens-repair / lens-post-process, plus reveal, first-core-board and first-element — carries the wall-clock start and the measured duration. They live on the generation under a versioned timings record, so no label can absorb another phase’s time and the benchmark archive reads this one spine rather than measuring anything a second time. lens is discriminated on the record: the four lane-scoped phases require it, the generation-wide ones forbid it.

Spend is durable beside the timings. Every seat turn the pipeline runs (board, report, repair, on either harness) records one metric into the generation’s collector: tokens, its wall-clock duration, the number of board tool calls it made — refusals included, because the seat made them and the provider billed them — and, when the provider gave one, its price. A board turn that dropped any of the three would be spend the reviewer cannot see; a turn with no board to call carries no count rather than a zero, because zero is a real measurement of a seat that wrote nothing. The orchestrator’s compose turn is not yet counted. The sum rides the lens progress frame while the generation drafts and lands on the generation as usage when it settles; a repeat drafting attempt adds to the prior attempt’s total rather than replacing it. The round shows it as one line under the lane rows, naming any turns that produced no usage record so a partial sum is never read as the whole. A price appears only when every turn was metered and priced; a subscription session shows tokens and no invented dollar figure. Retries are counted like any other turn, and a repair is always a further turn on the seat’s own thread.

Three of those records are measured from a boundary the pipeline does not own. first-core-board starts from the moment the reviewer’s wait began — the captured input becoming ready on an initial generation, the round landing and its report verifying on a returned one — which the caller supplies, because measuring from the drafting runtime’s own entry would silently exclude board minting, partial-state cleanup and provider resolution. first-element shares that origin and stops at the first element any board published on its element stream, which is the first thing the reviewer could see; a lane that settles absent writes nothing and so is never what it names, and a generation on which no lens ever wrote an element records no such figure at all. reveal ends at the last lane that actually revealed something; a lane that failed revealed nothing and does not extend the window.

Every stage record names what ran it, one record per seat. A single-seat lane emits one lens-draft record carrying that seat’s harness and model. The Flagged lane runs a review seat per installed model plus the compiler, so it emits one record per seat that ran — each with its own provenance and its own wall-clock span, and the lane’s aggregate span is min-start to max-end across them. The two review records name their harnesses, which is what makes “this run was dual-model” derivable from the stages rather than assumed from settings; one merged record could name no harness at all, which answered the question with silence.

Repair budgets are per lane and per whole-board attempt. The first drafting run over a generation spends the lane’s full ladder; every repeat whole-board attempt — the redraft a restart’s partial-state recovery starts — draws the same repeat entry, so one restart costs one draft plus that budget rather than a silently refreshed ladder. The repeat entry is reduced but never zero: a zero budget ends a lane on one malformed output, and the restart recovery that exists to re-draft a retryable lens could then never produce a board for it.

The classified report path also emits content-free diagnostics. Its fixed milestones distinguish provider time from session cleanup, schema parsing, evidence verification, and board persistence. Each milestone carries only enums and a nonnegative integer elapsedMs; the first also names the resolved harness, model, and effort. Prompts, provider prose, model output, diffs, paths, evidence, and classification notes never enter these events. Legacy report drafting and the five lens seats emit none.

A retry after process loss reserves the same board identities. The runtime reconstructs an exact landed-round report only when its metadata and element log agree, then re-runs the same changed-line verification before reuse. That path makes no second classifier call. It replaces every other partial board as one attempt: remove its metadata first, clear its board state second, then draft. The order is repeatable after a crash and prevents stale metadata from presenting a partially cleared board as complete. A malformed or semantically invalid report is scrubbed the same way before one fresh classification turn.

Writing a board through tools is the live flow: every lens seat authors its board by calling verbs on the daemon’s per-seat loopback board MCP server, and returns no document. The only document parse left is the legacy round-report leg (Validate, step 3). This section covers the verb surface and the writer behind it.

A seat’s tool set is derived, never listed per lens. buildBoardTools in packages/protocol/src/board/tool-schemas.ts walks the shared authoring kinds plus that target’s typed kinds over the authored board schema, the way the orchestrator’s app_* tools walk the command registry: each kind yields an add verb and an update verb, plus set_document, cite, remove_element, finish, and — only where the lens admits an absence — a settle_absent whose reason is fixed in its description with no field to name another. Sequence admits no absence and gets no such verb.

Every input is one flat object of scalars, string enums and arrays of scalars. No nested object, no array of objects, no union at any depth: a source reference is source_path / source_candidate / source_line, and a citation is an id cite returns rather than an object. This is the shape rule that closes #810, where a union rendered as a bare anyOf with no top-level type and the API refused the turn before the model saw it. flatInputViolations renders each input to JSON Schema and reports any of those four shapes, naming the tool and the field.

A structured value that is a LIST is flattened into parallel arrays the seat aligns by index, so it carries only the parts worth that cost: a section’s sources are source_paths alone, while a requirement’s single source keeps its candidate and line, where there is no index to align. A scenario’s scenario_clauses is single-valued the same way and keeps both halves — scenario_condition and scenario_response, the WHEN and the THEN — because a trigger with no outcome is not a scenario. Where both parts of a list are load-bearing — a stat is a label and a value — the writer refuses a companion given without its spine and refuses arrays of different lengths, naming the field. Silently rebuilding the shorter list is how a partial update came to wipe a field the call never mentioned, which is the opposite of what update_* tells the model it does.

The fields the host owns are on no input at all: an element’s author, a code_ref’s patchset id, a noise verdict’s judge, a finding’s draft status and its cross-seat concurrence and accord, a section’s round-delta stamp, and the document’s reading measure.

BoardWriter in packages/core/src/board/board-writer.ts applies one call at a time, purely. The host mints every id and returns it, and a child names its parent — the host keeps the parent’s children in step. So the parent graph is a forest and every other reference names an element minted earlier, which is what makes a dangling reference and a reference cycle unconstructible rather than checked. A reference argument naming something the board does not hold is refused with what it does hold. The boundary does not constrain what kind a reference names, so any reference field can point at an ancestor and close a loop through the one edge that runs forward, parenting; every mutation therefore re-runs the boundary tier over the board the call would produce and refuses whatever that call introduced, which is what actually holds the two structural guarantees. A refusal names the field and says what would be admissible in the lint layer’s own words. finish runs the finish tier and answers with pointers only — a rule id, an element reference and one sentence — or settles the board. Writing after a finish is not refused; it takes the board back to drafting, because a settlement describes the board that finished and not the one that moved.

Every result is bounded, because the provider bills a tool result like a prompt. A refusal that names what the board holds, a receipt that names what a removal took, and a finish verdict all interpolate a collection, and on a large host-derived board those collections are large: the Noise complement of a 95-file branch is 1,252 elements. Worse than a prompt, a result then sits in the conversation prefix and is re-read on every remaining round trip of that turn. So each one declares a cap and carries an honest truncation marker — twenty held ids, twenty finish pointers, ten rule violations or schema issues, twenty removed ids — and board-tool-surface.measure.test.ts builds the 1,252-element board and fails when any result exceeds 4 kB or grows more than tenfold with the board. The counts are the load-bearing half of each message; the sample is there so a seat can see the shape.

The round-report classification turn is bounded on both sides, locally, with the limits declared once in @rennet/protocol’s round-evidence module. The numbers below are those constants; if they disagree with the code, the code is right and this page is a bug.

What goes in. The host parses the coding turn’s measured diff into a canonically ordered evidence manifest. Each unit carries a content-derived id (ev- plus 16 hex characters of a SHA-256 over the unit’s kind, path, and identity — hunk text, mode pair, previous path, or change status). The order is path (compared by code unit, never a locale-dependent comparator — one shared comparator serves the manifest, the report builder, and the report verifier), then kind (rename, mode-change, binary, text-hunk), then position within the file.

Read the id’s stability precisely, because the contract is narrower than “stable”:

  • Rebuilding the same measured diff yields the same ids. That is the recovery path — a report recovered after a crash is re-verified against ids rebuilt from the same diff — and it is the property the system relies on.
  • A change in an unrelated file never renumbers a surviving unit, the way an ordinal position would.
  • An unrelated change in the same file, above a hunk, re-keys that hunk. A text hunk’s identity includes its @@ header, which carries line numbers. Dropping the header would remove that shift and collide two byte-identical hunks in one file onto a single id — ids must be unique within a manifest before stability across manifests means anything. Uniqueness is enforced: a repeated id (a shared identity or a 16-hex collision) is a typed local failure, not a silently merged bucket.

Evidence is a discriminated union, and no variant invents a line anchor:

VariantCarriesAnchor
text-hunkThe verbatim @@ header and bodyThe hunk’s first added line, or its first deleted line when it only removes
renameThe head path and the previous pathNone
mode-changeThe old and new file modesNone
binaryThe path and the change statusNone

A file that both moved and changed contributes a rename unit and its text-hunk units, so a mixed change stays lossless across variants.

The input budget. The complete serialized manifest is measured in UTF-8 bytes by one serializer: at or under 262,144 bytes and 400 entries it is sent intact; over either limit produces a typed local failure with zero provider calls, routed to the durable round-failure path. Nothing is truncated, split, or summarized to fit — a manifest that fits by omission would classify a change that did not happen.

What comes back. The classifier returns outcomes and beyond entries that cite manifest ids in evidenceIds; it never writes a path, a side, or a line number. The provider’s raw response is capped at 131,072 UTF-8 bytes, enforced at the harness transport boundary in both the Claude and the Codex adapter before structured-output decoding — core only ever sees decoded values, so a core-side check would already be too late. On the Claude leg the SDK hands the structured output back already decoded, so both carriers (the result text and the decoded object) are measured and the larger governs. Decoded cardinality limits then apply before persistence: at most 100 beyond entries, and exactly one outcome per dispatched ask.

The turn also asks the provider for at most 32,768 output tokens, so an over-long classification stops at the source rather than being paid for and then rejected. That cap is asymmetric by transport, and honestly so: the Claude harness takes it through its child environment, while codex exposes no model-output-token parameter or config override at all, so a Codex classification is bounded by the byte cap alone. The byte cap is the enforced backstop on both legs.

The partition. Every manifest id appears in exactly one ask outcome or the beyond asks bucket. Unknown, duplicated, and omitted ids are all rejected before anything is persisted, and the accepted ids are stored on the board’s round_outcome elements, so a recovered report is re-verified against the measured diff rather than trusted.

When it fails. Every limit above fails typed to the durable round-failure path and spawns no further turn — a cap failure never becomes another classification attempt. Because the classifier is side-effect-free before durable projection, recovery after a crash MAY repeat the provider call; what is guaranteed is exactly one durable report projection per round, not exactly-once remote invocation.

A drafted board lands in two durable places. Its elements go to the whiteboard event log. Its document envelope, blemishes, omissions, and immutability result go to the board-meta store before the board arrival is announced. The client reads both halves through board.read, keyed by review, generation, and lens. The handler resolves the review’s session, finds that triple’s board-meta record, projects the element state, and assembles one LensBoard with the persisted document, the element pool in creation order, and one fold line per top-level section. On the Flagged lens a top-level section that reaches no finding (through nested sections too) is dropped rather than folded, and its fold line counts those reachable findings — the #927 orphan-section guard, shared with the live-draft reader so both agree.

Rai’s ruling, 2026-09-04: anything not covered by one of the other boards is noise. Noise is a POSITION, not a property of a hunk — a changed region that Design, Sequence, Decisions and Flagged all passed over — so its membership is set subtraction rather than a model’s judgement about reading effort. Every changed region of a change is in exactly one of two sets, and the partition is total by construction rather than by diligence.

The unit is the hunk, not the side. A changed region is one SIDE of one hunk, because that is what a citation names: a modified hunk offers a base-side region and a head-side region, and they are genuinely different text. The complement asks a different question of them — did any lens read this change — and for that the hunk is the unit. So a citation on either side cancels the hunk, and an uncited hunk becomes one member: its head side where it has one, its base side when it is a pure deletion. changedRegions stamps each region with the hunk it came from, and nothing in citation geometry reads that stamp.

Subtracting per side instead made no-noise unreachable for any change containing a modification, and put the exact regions the other lenses had just read back on the Noise board — the misfiled-noise harm the ruling exists to prevent, arriving through the derivation rather than a seat’s judgement. Filing per side also doubled the member list: a renamed file’s two base names are one change and were filed twice under two names.

deriveNoiseMembers in packages/core/src/board/noise-complement.ts takes the subtraction. It is handed what each of the four core lanes SAID, and the three cases are not two:

  • A lane that settled a board stated its citations, and they subtract.
  • A lane that declared an admissible absence stated that it cites nothing. An absence is an empty citation set and subtracts safely.
  • A lane that failed stated nothing, and nothing is not an empty set.

The rule that makes “stated nothing” operational is that the complement subtracts only what a restart could re-read, because it has to be reconstructible from the same durable evidence the reveal is. A board clears that the moment the draft returns: its elements are on the whiteboard and its metadata is already persisted, so a lane that then throws on a later durable write has plainly said what it cites, and reading it as silent because no row survived the unwind would be matching on the absence of a record rather than on a positive contradiction. An absence is different, because nothing but its own durable write records it: if that write throws, no restart can re-read the declaration and the lane reads as one whose citations are unknown. A draft that threw before producing an outcome at all leaves nothing to subtract either way.

When any core lane failed, the Noise lane does not settle a board at all. It settles as a typed failure naming the lanes whose citations are unknown. That failure’s classification is the named lanes’, not an assumption about Noise. It is retryable while any named lane still has attempts left — the sibling’s retry is what makes this lane runnable again — and terminal when every named lane has exhausted its own ladder, because a derived lane whose cause is settled has no retry of its own to reach. The distinction is load-bearing on the restart path: a retryable account is read as evidence the generation must re-draft, so calling a derived failure retryable when its cause is terminal makes every generation carrying a terminal core failure re-draft all five lanes on every restart, spending a model to rebuild boards already on disk and arrive back at the same failure. A lane that threw without an account at all stays retryable: an unknown ladder is not an exhausted one.

A complement taken over a partial set of siblings would present un-reviewed regions as safely skippable, which is the failure the lens exists to avoid; a partial complement is worse than no Noise board, because the reviewer cannot see which part of it is guesswork.

The lane runs last, and nothing waits on it. The complement of boards that have not settled is not knowable, so the four core lanes fan out together and Noise starts on their settlements. That is a sequencing fact rather than a barrier: every core board still reveals the moment it lands. The cost is a tail on the generation’s wall clock, accepted by ruling and measured rather than argued.

And the lane says so. A lens lane carries a waiting status distinct from both queued (the generation has not kicked off) and running (a seat is writing). The lens kickoff promotes the four core lanes to running and the derived lane to waiting; the derived lane leaves waiting only when the pipeline really opens it, which is after its siblings settle. The test is the same HOST_DERIVED_MEMBER_KIND row that takes the target’s member-creating verb away, so which lens waits is one fact read where it applies rather than a second list.

The client keeps Noise unselectable while its siblings run, with a spinner and an explanation available on hover or keyboard focus. The tab itself anchors its activity popover, with no separate activity control. The active generating lens opens automatically; hovering or focusing another tab temporarily replaces it. Completion shows a brief status animation and dismisses the automatic popover after 900 ms. A panel the reader is hovering or focusing stays open until they leave or dismiss it. Close and Escape return focus to the trigger tab without reopening the panel. Hovering a settled tab keeps its transcript accessible. Its transcript actions remain visible and are disabled only until their agent threads exist. The observed history and elapsed time stay with the review, generation and seat thread across navigation and reconnection. A new generation or thread starts a fresh observation.

No header narrates the preparation. The frame’s Rennet mark — the sphere in the sidebar lockup, or the floating orb in the corner slot when the sidebar is collapsed — animates while any review is being prepared or any round is regenerating, and a floating Cancel chip in the bottom-right corner is the running generation’s only other chrome. A header returns for a terminal state, carrying the reason and Retry. Continue does not exist while the boards are being written: the Cancel chip stands in its corner, and Continue appears there once the review is ready. The same session’s sidebar row stays animated when the user navigates away. Completion becomes a check and then an unread dot until opened; failure retains its reason. Initial preparation and post-round regeneration project their durable state into this presentation rather than relying on the currently mounted board.

An empty complement is settled without a seat. When the four lanes between them cited every changed region, the host knows the remainder is empty before any turn, and the lane settles no-noise with no Noise seat dispatched — the cheapest turn in the change. The reader-facing wording changed with the meaning: no-noise used to say that nothing here was safely skippable, and now says that every changed region is on another board, which is a different and much rarer claim.

A specification-only change dispatches Design alone. Before any lane opens, the pipeline asks the packet’s file rows one deterministic question: is every changed path a specification artifact? The roots are the ones the Design readers select on — openspec/**, .kiro/**, .bmad/** and .bmad-core/**, docs/superpowers/** and .superpowers/**, a Markdown ADR under any docs/adr/ or docs/decisions/, and a CONTEXT.md or CONTEXT-MAP.md — and a rename’s old side counts too, so a file moved out of a spec directory is a code change. When the answer is yes, Sequence, Decisions, Flagged and Noise settle spec-only with no seat dispatched, the lanes close and their addresses are revoked exactly as after a seat, and Design runs alone: on a format the assembler reads it renders the specification on the host with no model turn at all, so the whole review of an OpenSpec change proposed ahead of its code costs nothing. spec-only is the one absence more than one lens admits, because it is one fact about the change rather than four facts about four boards; no seat’s settle-absent verb offers it, and an empty board never derives it. A docs-only or README-only branch is not spec-only — the Design lens does not read that prose — and neither is an empty inventory.

The OpenSpec reader that feeds the assembler selects a change by directory, and a directory only: a file that sits directly under openspec/changes/ — the README.md index OpenSpec keeps there — selects nothing, so a spec-only branch that touches the index beside its change still assembles on the host rather than falling through to the seat.

The seat makes no judgement of any kind. A member’s verdict and its judge mark are host-stamped constants — noise and deterministic — and appear on no tool input, because each has exactly one admissible value once membership is derived, and a one-valued field offered to a seat states a choice that does not exist. The Noise seat has no verb that creates a member, none that removes one, and no settle-absent verb. Its update verb is how it parents a member into a group and writes that group’s reason, and the grouping is the only thing it can get wrong: finish refuses to settle while any member is unparented or any group carries no reason.

The assurance that nothing worth reading is filed as skippable now rests on the other four lenses’ citation coverage, not on a Noise seat’s second opinion. A hunk that matters and that no lens cited is a defect in the lens that missed it, and is answered there. The failure mode did not disappear; it moved from an invisible one — a hunk quietly filed as noise — to a visible one a reviewer can see, name and route.

Fold counts are reader-facing domain objects, not raw element-kind tallies. The projection emits findings, decisions, requirements, steps, outcomes, groups, files, and comments from each section’s direct children. The stored groups count counts Noise members, so the UI labels it as changed regions within that group. Repeated code refs for one path count as one file, and structural prose does not inflate the count. The Flagged lens is the exception: its fold line counts the findings a section reaches through nested sections, and a code_ref is never counted as a file (it is a finding’s own code citation, not a section child), matching the orphan-section guard that drops a finding-less Flagged section at both readers. A pair with no persisted board answers null. A successful empty result is typed instead of persisted as a zero-element board: Design uses no-spec, Decisions uses no-decisions, Flagged uses no-findings, and Noise uses no-noise — which the host settles, not the seat. Sequence, Decisions, Flagged and Noise all record spec-only when the host settles them together on a specification-only change. An empty Design board is never an absence — only the seat’s own no-spec declaration is. For the three core review lenses, material follows the topology the client serves, not the flat element pool. Sequence needs a reachable order_step, Decisions a reachable decision, and Flagged a reachable finding. Their finish checks name detached elements while the drafting turn can still attach them, so a successful settlement does not become a failed review later. Prose-only boards, empty sections, and detached typed elements do not satisfy those core lenses. Flagged persists any round finding-resolution migration before settling that typed absence. The client treats the absence as settled, keeps its segment selectable with explicit empty-state copy, and stops polling. Sequence requires a reading result; a semantically empty return gets one explicit retry, then becomes a retryable lens failure rather than an arrival.

Which absence each lens may settle with is the protocol’s LENS_ADMISSIBLE_ABSENCES table, and it is enforced where an outcome becomes durable rather than merely advised: a lens settling an absence its own row does not admit is a producer defect that persists as a typed failure, never as a clean result. That failure is retryable at attempt zero — nothing has been retried, and another drafting attempt is exactly what answers it. The durable GenerationSchema stays permissive on purpose — sessions written before a field existed must keep parsing — so the boundary that refuses a wrong pairing is the write, never the read.

A failure persists as the drafter’s own words plus a typed account: which attempt failed, and whether another attempt could plausibly succeed (retryable / terminal). The account is durable beside the message, survives a daemon restart, and rides board.read to the client, so a lens whose seat simply did not draw is not presented as beyond another attempt. A failure with no account means the classification is unknown — which is not the same as terminal. For a multi-seat lens the account aggregates: retryable if any seat is, since the lens needs only one seat to draw a board. The account also decides what a RESTART does with the failure: a retryable one is not complete evidence for its lens, so a fresh runtime over that durable state redrafts the generation instead of reconstructing the same failure forever. A terminal one is settled, and its generation reconstructs without a model. A landed-round report gets exactly one classification turn and fails honestly when that classification omits an ask, cites evidence outside the measured coding-turn diff, or fails to partition that evidence exactly once. A plain null remains missing because the board may still be in flight. No board is assembled from another generation’s elements.

The client addresses a frozen board with ?generation=<id> and treats an absent generation parameter as the live generation. Both the generation and lens selections come from the session URL rather than component-local state, so reload and direct navigation resolve the same (generation, lens) pair. An absent lens parameter resolves Flagged when it is present, then the first board present in canonical order.

The client overlays reviewer-owned finding state instead of editing the board bytes it reads. Request and Dismiss bind to (generation, Flagged board id, finding id) in the durable ask projection; Undo applies the inverse event. A failed draft attempt therefore cannot leak its action onto a retry that reuses the same model-authored finding id. Discuss stores a quote thread at the same generation anchor and sends one live anchored ask through the existing chat dock. The Flagged segment derives its open count from the board’s finding statuses, staged finding asks, and dispositions, so reload reconstructs the same number without a stored counter.

The drafting scheduler seeds every seat with its lens prompt, the reviewed range and the exact diff command for this capture, and a path reference to the session’s context directory. Nothing derived from the change rides in the prompt: the seat’s working directory is the reviewed checkout, it runs the diff itself, and it opens the files the directory’s README.md indexes. The DeltaPacket stays on the daemon, where lint, persistence and the delta renderer read it. The pipeline consumes the related-context dossier described below, it does not build the retrieval.

The related-context dossier holds the change’s referenced issue-tracker tickets, the PR description and comments, and one-hop links, retrieved per patchset generation by a light-tier Model Council seat (related-context-retrieval) after a deterministic pass extracts issue refs from the branch name, commit messages, and PR body. GitHub is first-class via gh; JIRA and Linear work from per-project config (base URL plus a token environment variable). The bounded dossier is persisted and referenced by path, and the round workers can reach it too. The landed-round report classifier intentionally receives only the successor patchset id, durable asks, the worker’s identity, and the round’s evidence manifest; full raw payloads stay behind a context tool. Items are structured (id, tracker, title, state, bounded body, acceptance criteria, URL, provenance, fetched-at) and cited by id, which is how ticket citations reach boards. Like every other input, it reaches a seat as a file the prompt names, never as an interpolation — for the Design lane, as the related-context.md described under The Design lens. Standing project background is not fetched for the drafter: a drafter that wants it reads the repository it is standing in. Cosmetic project facts (the logo) never enter agent context. When no tracker is configured, the dossier carries what the forge itself supplies and the review proceeds; the tracker is named in project settings, and nothing blocks on it.

A turn grounded on an anchor gets the hunk containing that span, under an 8,000-byte ceiling. A path-only note gets a bounded diff of its file instead. When the file or span cannot be found, the model works from the note and its metadata and says so — it never substitutes a different code location that looks close enough.

Every other interpolation into a seat prompt declares its bound at the call site too: the round-report evidence manifest is measured against its 256 KiB ceiling before any seat runs and then written to evidence.json rather than sent. The RSP noise seat’s hunk payload is gone: the offer is written to noise-offer.json — one entry per changed region, each a path, a side and a 1-based line range, with no line bodies and no hunk ids — and the seat reads the lines from git diff, so a re-ask re-sends nothing of the change. The seat cites a region back the same way; the daemon resolves that citation against the offered regions and mints the rennet:hunk/<id> anchor the stored document carries, so no hunk id travels in either direction. Unbounded interpolation is a bug.

A tripwire keeps the drafter prompt itself honest. The lens-pipeline prompt-budget test assembles every lens’s drafter prompt against the real capture fixture and asserts its UTF-8 size under a per-lens budget: the size measured when the test was pinned, plus ten percent. There is no per-file or per-hunk term any more, and the test says so by rendering the same prompt against a synthetic 74-file, 292-hunk packet and asserting the bytes are IDENTICAL — an inventory creeping back into any layer reddens every lens at once. A prompt that grows on purpose raises its budget in the same change and says so in the pull request; one that grows by accident reddens the test instead of waiting for the next audit.

The schema makes good structure the only expressible structure (a finding’s kind, severity, cited code, and concurrence are typed fields, so a claim in the wrong shape fails to parse, not merely reads badly); the lint makes the mechanical rules guarantees; the prompts carry what only judgment can check. A rule that lives in a prompt alone is a wish.

The tool surface is the same argument taken one step further. A rule the schema cannot express and the lint has to catch after the fact — a lens authoring another lens’s kind, a reference to an element that does not exist — becomes a verb that was never offered or an argument that cannot resolve. What is left for the lint is what a call genuinely cannot decide on its own.

A board says what it does not know as plainly as what it does.

  • A drafting seat that fails renders as failed, never as empty. The surface distinguishes a lens that ran and found nothing from a lens that did not run.
  • A Design seat that looked for this branch’s specification, found none, and had no pull request description, documentation change or related issue to draft an overview from returns no-spec. That is a successful absent lane, not a failed drafter and not an empty board; the other four lenses continue normally, and the finished board views carry no Design tab.
  • An element the validation loop could not make pass leaves a trace, never a silent hole. If it was dropped, the omission names it with a reason (the honest-omission exit); unresolved board-level or schema violations ride along as labelled blemishes — shown to the reviewer, never blocking the board.
  • Capture limits stay visible. Truncated files, binary files, and submodule blocking states keep their state on the board, so a review never implies it inspected bytes no runner ever saw.
  • CI signals are informational evidence shown beside findings. They are never presented as model-authored findings, and a green pipeline never hides unread code.
  • Rationale the Decisions lens reconstructs is labelled model analysis. It is never presented as an author’s quote or as a fact read out of the repository.

When a change touches the interface and the Claude adapter is available, one separate verification turn runs after the first Flagged result. It mounts the changed surface using whatever the project already provides — its tests, Storybook, dev server, or browser tools — takes bounded screenshots into the review evidence store, runs the project’s own accessibility tooling, and compares what it sees against the pull-request title and body and the captured spec artifacts.

Its observations return as ordinary anchored findings, with no privileged status. The pass reports completed, non-UI, pending, or unavailable; a mount failure is inconclusive and never an all-clear.

Each lens owns a lane, and material in another lens’s lane is omitted, never narrated. The branch’s specification and its requirements belong to Design; the reading walk to Sequence; judgment calls to Decisions; defects to Flagged; skip-safe mechanical hunks to Noise. Generated scaffold stamps (OpenSpec’s .openspec.yaml and the like) are noise, not specification documents.

A lens accounts for what it cites and for nothing else. There is no skip list to fill and no remainder to declare: material another lens owns is simply absent from this board. Boards never carry remainder essays about what is not on them.

Authored explanations lead with what happens and why it matters. They introduce components by their job before naming code and explain essential technical terms where they appear. Titles are concise labels. Every section needs a distinct folded preview: authors supply descriptive child subheadings or a self-contained opening paragraph. The host displays those subheadings, otherwise truncates the opening paragraph to two lines, and calculates counts. Expanded text adds the cause or example. Each card should make sense on its own, with citations available for checking the claim.

Openings state the user-visible change in two short sentences, about 35 words. They explain the change without an itinerary for reading the board. Sequence sections supply shared context in one short sentence; their steps trace the state or data through the code to its outcome. The explanation precedes the step citation; code excerpts remain expanded. Step citations are displayed through the step’s span, without also attaching the same code excerpt to the parent section. Independent changes stay separate; their presence in one diff is not a causal link. Decisions carry a viable alternative rather than padding the list with broken choices.

Readers may know the product without having traced its code. Explanations cover the technical mechanism in plain language, naming relevant state, functions, or data flow and connecting each to its effect. Citations support that explanation.

Distinct mechanisms, effects, cases, or choices use short, flat bullet lists with one point per item. A single explanation stays in prose. Review text renders -, *, and + bullets with hanging indentation and space between items, including a list immediately after a lead-in and indented continuation lines. Inline code, citations, and reviewer highlights retain their behavior inside list items.

The target is about 40 words per explanation, including a decision’s statement and rationale together. The rationale explains the benefit or tradeoff rather than restating the implementation. There is room for the trigger, consequence, and evidence. This is drafting guidance, not a word-count validator. Design retains its verbatim source obligations and quotations. Its deterministic rendering does not pass through these authoring instructions.

  • Boards narrate in third person about the change, never as its author.
  • Board prose never names lenses, boards, agents, or the review process; cross-lens connection happens through anchors and composition.
  • Threads and messages are records of real exchanges. A drafting agent runs before any exchange exists, so a draft board can never contain one. A real question from the change’s history renders as an annotation or callout citing its source.
  • A decision stated in a specification renders on both the Design board (as source content) and the Decisions board (as a call citing that document). Each board stands alone.

The Design seat finds the specification itself. Nothing is discovered for it and no artifact bundle rides in its prompt: it stands in the reviewed checkout and looks where specifications live — openspec/changes/** and openspec/specs/**, .kiro/**, .bmad/**, docs/superpowers/specs/** and docs/superpowers/plans/**, docs/adr/** and docs/decisions/**, grill-me documents and CONTEXT.md context maps. Their exact shapes are surveyed in the spec-format reference. The clue is the change’s own history: the commit messages of the reviewed range and the pull request body name the change directory, the story, or the ADR.

The board must cite the evidence that ties the specification to the branch — the commit message, pull request text, or task line that connects them — so a reader can check the link instead of trusting it. One specification per board; a neighbouring change that merely sorts first is not this branch’s.

Four of those endings are a board and the fifth is a stated absence. The branch that carries no specification is the common case, and it still has material: what the author wrote on the pull request, the documentation the branch itself changes, and the issues it links.

So when the search ends empty the seat drafts an overview from three sources, in this order: the pull request’s title and description as the host wrote them to pr.md, the documentation the branch adds or modifies (every .md, .mdx, .rst or .txt file and every file under a docs/ directory the change index lists, read at the reviewed tree), and the related issues in related-context.md. The board says what it is: its stats read Format: Overview and Specification: none found, its intro opens with the sentence “No specification was found for this branch; this overview is drafted from” and the sources it used, and each section’s source chip names the file it was read from. A decision appears only where a source states one, marked inferred: false and carrying that source; an issue’s acceptance criteria appear as requirement rows whose shall is the tracker’s own text and whose source is the item’s id. The overview carries no capability, requirement or task count — those count a specification, and there is none — and it infers nothing from code. It is model-drafted only: there is no host assembler for a pull request body, because a description is prose rather than a document with obligations. A one-line body makes a one-section overview, which is the honest board for a one-line body.

related-context.md is how the issues reach that seat. Related-context retrieval already runs at review open and stores a bounded dossier for the review target and patchset; the host renders that dossier into the session’s context directory as one region per item in dossier order — id, tracker, title, state, URL, provenance, bounded body, and acceptance criteria when the tracker carries them — under declared bounds of 20 items and 64 KiB, ending on a line naming the dropped count when either bound is hit. No items, no file. Like pr.md and design-sources.md, it is named in the Design prompt alone.

The Design lane waits for that retrieval, bounded, and only on the path that needs it: the host located no specification and the assembler produced no board. It continues the moment retrieval settles, which on an ordinary branch is a few gh fetches and one light-tier council turn. The ceiling is 120 seconds; past it the file carries the deterministically extracted refs with their URLs and a line saying retrieval had not finished, so the seat can fetch a GitHub ref itself. While the lane waits its latest event reads “waiting for related issues”, so the delay is visible on the preparation surface rather than silent. Noise starts on the four core settlements, so the same ceiling bounds its start. The host-located and assembler paths never wait.

The residual absence is the branch that has none of the three — no pull request paper, no documentation change, no related issue. Then the seat returns { "absence": "no-spec" } and drafts nothing, and its note names the three sources it looked for. The lane settles absent, not failed: a branch without a spec workflow is an ordinary branch. Design keeps its place on the rail and its board says “No spec found for this branch.” — a stated result rather than a gap, and rather than an empty board, which would be a lie about what the repository holds. The tab stays because a lens that vanished as it settled would move the reviewer’s selection out from under them. Design’s older no-material absence stays readable for generations recorded before Rennet stopped settling it; nothing settles it now.

The spec-backed board is a structured composition, not a Markdown viewer. Its header names the source set, displays the format, and reports capability, requirement, and task counts read from those files. Each stat appears once. Header source chips list every rendered file exactly once in reading order, and their first named source regions preserve that order. Header chips jump to their rendered regions; section and requirement source chips open the repo-relative file in the project editor. A proposal renders source-grounded Why as the document’s intro, then its remaining headings in the file’s own order: What Changes as one row per listed change (a row wears a tag only when its author gave it one), Impact beside it, and any other heading as a nested section whose deeper headings nest again. A fenced code block in a proposal is left out and its place stated with a fixed label, since code on a board is a code_ref and a proposal’s fence is illustration. Capabilities render as counted jump cards, and task groups keep their source’s own - [x] / - [ ] marks. Requirements preserve their normative text and source order, and every scenario and task remains its own canonical element so later dispositions can address it. A scenario is owned only through its requirement’s scenarios list, never repeated in section children.

Format-specific display fields are stamped by the host assembler, which holds the parsed source structure, onto the elements it built once the board settles: Kiro requirement_refs on task prose; BMAD status on a story requirement and acceptance_criteria on task prose; Superpowers task_manifest file, interface, and verification arrays on a task-group section; task_progress on the top-level source section and each task-group section; source_cells on a matched tech-stack or architecture decision; and grill glossary_term on the glossary-entry prose. They are not authored fields: the tool surface carries no input for them (#889 is why they will not gain seven), and a model seat runs only when the host found no parseable specification, so it has nothing to transcribe them from. The one projection a seat writes is scenario_clauses, split from a scenario’s own WHEN/THEN words as the two flat inputs scenario_condition and scenario_response; the assembler writes the same pair from the parser’s split. The surface reads the scenario’s own text first: a Scenario: <name> prefix becomes the row’s heading and each - **WHEN** / **THEN** / **AND** / **GIVEN** item becomes one clause row under its own keyword, so a nine- scenario requirement reads as nine named cases. Text with no such rows renders the host’s pair as Trigger/Outcome, and a scenario that names neither still renders, as the prose it was written as. Every array preserves source order, and the surface renders each projection once at its owner. A field whose shape does not match is not rendered. Stated decisions continue to use their canonical statement, rationale, alternatives, and evidence fields.

A requirement cites the code that implements it through trace — code_ref elements by path and line range — and names implementing paths in related_files. Those citations resolve against the patchset like any other; a requirement with no implementing code in the change carries an empty trace rather than a guess. There is no met/partial/gap coverage chip: nothing derives one, and no lens accounts for requirements it did not cite.

spec_delta and the round delta marker are independent: the first records the specification’s added, modified, removed, or renamed state; the second records whether the rendered section changed since the prior review generation. A capability file has one source-linked capability root. When it contains several delta headers, exact operation sections sit beneath that root in source order, and each requirement row and its nearest operation section carry the source spec_delta. The capability card rolls those operations up as ordered unique badges without duplicating the capability.

For Superpowers, the seat leaves plan checkbox bytes untouched and reports task completion from a progress ledger only when its exact first line binds the selected plan path. Only Task N: complete (...) completes a group. Fix-round, minor, and ruling lines remain visible in the progress region but never count as completion.

  • The lens name opens the document, with its authored title as a subtitle and introduction beneath it. A reading measure keeps prose narrow; structured gives artifact-heavy content a wider column.
  • Folded sections list their current child headings in document order. Selecting an entry opens the section and focuses that element. Without titled children, the first substantive paragraph is clamped to two lines. Counts remain secondary, and a section never repeats its own title as its preview.
  • Decisions have a concise heading above their complete statement, rationale, alternatives and evidence. Existing decisions without a separate title remain readable without regenerating the board.
  • Code is cited, never copied: the code block card renders a citation with path and line span. In board prose, backticked terms render monospace and path:line citations are interactive — clicking one reveals the real cited lines inline.
  • A code-card filename opens the captured file in Diff. View test and View implementation use relationships from the reviewed tree, including relative imports and naming conventions. An unchanged test can be inspected inline; several matches offer a chooser, and Back restores the previous code context and scroll position. Full-file inspection uses a bounded scrolling viewport and renders only the visible rows.
  • A revealed citation displays the relevant captured diff hunks, with old/new line numbers and addition/deletion markers. The citation positions the code; it does not paint a reviewer selection. Syntax and diff colours remain distinct from the reviewer’s comments and selections.
  • Context expansion and full-file inspection read the immutable base and reviewed-tree/head objects on demand. Later working-tree changes cannot alter those bytes. Captured hunks remain readable when additional context is unavailable, and the code surface names that limitation.
  • patchset.readEvidence returns the captured diff first. Full sources and the cached test relationship index are separate requests. Selections carry the patchset, file, side and line range into comments, explanations, change requests and replies, including deleted lines. A selection spanning different files or diff sides asks the reviewer to select a single source range.
  • Older daemons retain the existing patchset.readSpan response. Its single-side excerpt fallback reads captured lines and, for a truncated capture’s tail, the recorded immutable object when available. Existing citations remain valid.
  • Multi-site evidence (a decision’s excerpts) renders as one tabbed code viewer: quiet pill tabs, one visible code block card.
  • A finding is document flow, not a boxed card: severity and claim title, a short body, one subheaded part per member of the failure, the proposed remedy as its own actionable callout, and the anchor.
  • Prose citations use full repo-relative paths so every citation resolves.
  • Blast radius is an explainable overlay: it names the dependency, reference, or ownership evidence behind every reachable site it marks. It annotates the substrate the reviewer is already reading — it never reorders or duplicates it.
  • Hand off and the exits — what the reviewer does with a board once it is drafted, and how a round mints the next generation.
  • Delta and generations — what carries from one generation of boards to the next.
  • The Model Council — how seats are assigned to the drafting, reconciliation, and classification jobs.