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.
The five lenses
Section titled “The five lenses”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 drafting flow
Section titled “The drafting flow”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.
-
Round-report first (on landed rounds only). When a coding round returns with its exact worker receipt, the
round-reportseat makes one structured semantic-classification turn before any lens drafter starts. Its prompt names one file,evidence.jsonin 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 anuntouchedoutcome. 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. -
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 withlocal_idand 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.finishruns 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 arereading. The host constructs the landed-round report document with thereadingmeasure. Every board write still lands throughwhiteboard-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 reachwhiteboard-clientunder 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.mdglossaries, 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 asdesign-sources.mdin 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_URLcredential 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. -
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(claudeorcodex), the model that raised it, andagreement(concur,diverge, orsolo), whether both reviews raised it and how they landed. The host expands those two enums at write time into the finding’s board-nativeauthor,concurrencetally ({ model, agree, total }per model), andaccordstamp:solowrites one tally and accordsplit;concurwrites both tallies and accordconcur;divergewrites both tallies and accordconflict. The enums are input-only — they ride theadd_findingcall, never the persisted board, becauseauthoralready encodes the model andaccordthe agreement, so keeping them would store one fact twice. Theaccordstamp 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 findingsolofrom that model. When one of two review seats fails, the compiler assembles the survivor’s file alone. -
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 callingfinishagain. Neither costs anything: a refusal and afinishverdict 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. Explicitcode_refelements still validate their immutable patchset, side and range. Authored prose keeps automatic citation validation and links. The host stampsproseRegisteron 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 acode_ref, and other integrity or finish failures, still take the existing fallback path.The distinction is load-bearing rather than tidy.
process-vocabularyexists 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
finishverdict 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 labelledblemish: 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.
finishhas already refused to settle a board holding noorder_step,decisionorfindingfor its lens, so what this catches is the gap that rule names in its own comment: Decisions and Flagged have no reachability rule, sofinishaccepts 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 itsno-noiseabsence 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
DraftBoardSchemahas nothread,message, orcodekind, 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:linein 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 named127.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 GitHubpath#L12form 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
basenorheadis 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.
alternativesis 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_KINDSinpackages/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 typedno-noiseabsence, 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
finishwas 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,validateDraftand the pointer-onlyrenderRepairPromptare that caller’s loop and nothing else’s. -
Freeze. The validated draft board becomes the lens board without a second model rewrite. Host-owned Design projections, the Flagged compiler’s
origin/agreementexpansion, 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-refwhen 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 — anorder_step.spanmay only land on acode_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. Acode_refmust 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. -
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/reworkeddelta 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 toreview-draft-voice.mdin the session’s context directory, each frozen board toboards/<lens>.json, and any curation feedback from the prior generation tocuration-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 with verbs
Section titled “Writing a board with verbs”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 classifier evidence contract
Section titled “The classifier evidence contract”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:
| Variant | Carries | Anchor |
|---|---|---|
text-hunk | The verbatim @@ header and body | The hunk’s first added line, or its first deleted line when it only removes |
rename | The head path and the previous path | None |
mode-change | The old and new file modes | None |
binary | The path and the change status | None |
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.
Reading a board back
Section titled “Reading a board back”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.
The Noise board is the complement
Section titled “The Noise board is the complement”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.
Related context in the delta
Section titled “Related context in the delta”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.
Bounding an anchored turn
Section titled “Bounding an anchored turn”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.
Three layers carry every rule
Section titled “Three layers carry every rule”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.
Honest states
Section titled “Honest states”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.
The UI-verification pass
Section titled “The UI-verification pass”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.
Lane discipline
Section titled “Lane discipline”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.
Voice rules
Section titled “Voice rules”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 lens
Section titled “The Design lens”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.
How the Design lane ends
Section titled “How the Design lane ends”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 board it renders
Section titled “The board it renders”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.
Reading affordances every board shares
Section titled “Reading affordances every board shares”- The lens name opens the document, with its authored title as a subtitle and
introduction beneath it. A
readingmeasure keeps prose narrow;structuredgives 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:linecitations 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.readEvidencereturns 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.readSpanresponse. 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.
Where to go next
Section titled “Where to go next”- 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.