# Row 217 (Case 2 C2-06): build notes Brief: docs/plans/2026-10-20_case2-rows.md, section C2-06, at base 45dca69f6. Builder: Launchpad until 2026-10-23 19:09Z. Then Mos moved coding to Codex seats (mos-1500, Q245) and the row went to Beakley on beakley/row217. ## State at handoff All code in this commit is UNTESTED. No node, no test and no mutant has run on it. No test file is written. `packages/harness/src/runner.mjs` is not started. Done, untested: - `packages/library/src/limits.mjs` (new, pure): `REPLY_LIMITS`, `resolveReplyLimits(read)` gives `{requireCitation, citationsPerReply, fallback, unreadable}`, `checkReplyLimits(value)` gives a clone or null. An unreadable business file forces `requireCitation` true. A key falls back to its default when `replyLimits` is not an object or the key holds an invalid value. - `packages/library/src/guard.mjs` (new): `GUARD_CODES` in design 5.2's table order, `FIXED_TEXTS`, `WEB_CITE_DEFINITIONS`, `REASON_MAX` (300), `GuardError`, `parseReply`, `checkReply`, `libraryReader`, `lastCommits`, `guardReply`. - `packages/library/src/index.mjs`: exports for both. - `packages/business/src/business.mjs`: `replyLimits` is a known top key. The validated output doesn't carry it. - `packages/business/src/organization.mjs`: the M11 fence `replyLimitsFence(old, document, actor)` compares against `old.human`, refuses the tester and any `.` of `old.roles`, and is called for both `document` and `written`. `setReplyLimits` and `readRaw` are exported. - `packages/bus/src/broker.mjs`: the guard call, the page store, the check event, `library.limits.set`, `library.status` gains `replyLimits`, `role.claim` gains `library`, `thread.messages` gains `check` and `last`. - `packages/bus/src/server.mjs`, `client.mjs`: `BusError.reason` (sage-3599). - `packages/bus/src/runtime.mjs`: guard deps and the `library.limits.set` dispatch. - `packages/cli/src/limits.mjs`: `limits replies --file LIMITS`. Left: - `packages/harness/src/runner.mjs`: the C2-07 runner work (see "Runner plan" below). - `packages/cli/README.md`: a line for `limits replies`. - Every test in the brief and the rulings below, then the mutants. - The rebases onto rows 283, 216 and 285, with the drift checks below. - This file's gate results and the manifest. ## Rulings in force sage-3595: 1. Supporting scope: a stub classify test now. Before in-review, rebase onto row 283's landing (honker/row283-build bd2c1c4ea when ruled) and add two real-fixture tests: a supporting cite with `as: "canon"` refuses `playground-cited-as-canon`, and the stored check result names `supporting`. 2. runtime.mjs is approved for the guard wiring and the one `library.limits.set` branch. List its hunks here, name it in the manifest, and drift-check it after the 216 and 285 rebases. 3. The check result is an `action.allowed` event with `action: "message.checked"`. The events kind CHECK at `packages/bus/src/migrations/3b.sql:9` has no `message.checked`, so a new kind would be a schema step. The event and the message INSERT share one transaction: a mutant that splits them must fail. 4. readLibrary at the cited commit stands in for the C2-05a index. On the 216 rebase, compare 216's index set with readLibrary's for the fixture and record the result here. 5. The retry is for a library business only. Tests: a JSON body between 32,768 characters and 64 KiB gets the retry; outside a library business nothing changes. 6. Old `role.claim` pins stay as they are. 7. `render.mjs:6` and `render.test.mjs:58` belong to row 225. Leave them. 8. `thread.messages` rows that carry `check` get `last` per path cite: the file's last commit at the served tip, one git call per distinct path per read. Test with a fixture file committed after the cite. sage-3599: `BusError` gets an optional `reason`. Only the citation guard's refusals set it. The envelope carries `reason` only when it is set. It is the guard's own text, never a quote, file text, url or a path other than the cited one, at most 300 characters. Tests: a guard refusal reaches the runner with code and reason; a refusal without a reason has an envelope byte-identical to today's, checked against an existing pin; a mutant that drops the client.mjs pass-through fails. sage-3602: store `file` per path cite from the same git log call as the trailer. Test with a cited commit that didn't touch the file, so `file` differs from the cited commit, and `last` equals `file`. ## The check-result contract with row 225 (launchpad-0118, launchpad-0120, morgana-0200, sage-3596) - Stored on the `message.checked` event: `{citations: [...]}` in body order. - A path cite: `{scope, agent, file}`. `scope` is the classify scope. `agent` is the cite's file's last commit's `Mosaic-Agent` trailer at the cited commit, or null. `file` is that commit's 40-hex id, or null. - A url cite: `{scope: "web", agent: null}`. - On `thread.messages`, each path cite gains `last`: the file's last commit at the served tip (40-hex or null). The Console compares `last` with `file`. - Rows without `check` are unchanged. No `changed` boolean. The webui passes rows through unchanged. ## Hunks outside the brief's file list - `packages/bus/src/runtime.mjs`: imports `setReplyLimits`; the library deps gain `guard`, `replyLimits` (organizationWriter `readRaw`, or `{readable: false}` with no organization), `last` and `GuardError`; the organization dispatch gains `library.limits.set`. - `packages/bus/src/server.mjs`: a `refusal(e, other)` helper above serve's doc comment builds every BusError envelope and adds `reason` only when set. The synchronous request path accepts a Promise result (the guard is async), with a 120 s timeout, and releases `own` when it settles. - `packages/bus/src/client.mjs`: `new BusError(r.error, reason)` with `reason` a string cut at 300. ## Decisions and their tradeoffs - The guard runs async outside the SQL transaction, then re-enters request() through `#guardPass`, so message.send's insert and the check event stay one synchronous transaction. - The guard runs before message.send's own checks, so a guard code can come before an authority code. - The check event id is derived from the message id (a UUID-shaped sha256), so `thread.messages` finds it through the unique index on `events.id`. - With no organization configured, `replyLimits` reads as unreadable, so it fails closed. - With more than one library project, the guard uses the first in the business file's order. - A library business without guard deps refuses guarded sends with `project-not-configured`. - The page store is per cap: 20 pages of 200 KB, kept only in a library business, dropped at session delete or disconnect and at run end. - The CLI `replies` verb reads the revision itself from `limits`. - Guard reasons never carry a url or a quote (sage-3599). ## Runner plan (not built) `role.claim`'s reply is discarded at `runner.mjs:462`; keep it and read `library === true`. In a library business, at the reply site (`runner.mjs:582-587`): - Fixed texts `(the session gave no answer)` and `(the turn failed)`; the reason stays in the run log. A fixed text goes with no note, and `unslop.sent()` is not called, so the note waits. - A body that parses as a JSON object with a string `text` gets `unslop.note()` appended to `text`, is serialized again and is never clipped. A plain body keeps `clip(body) + unslop.note()`. - On a refusal whose code is in `GUARD_CODES`, or `request-too-large` from the client, or the broker's `invalid-request` from `string(a.body, 32768)` for a body over 32,768 characters (ruling 5; the runner can tell it by the body length it sent), run one new turn in the same session naming the code and `err.reason`, or the 64 KiB limit. Bracket it with usage like other turns. - After the retry: a table code sends `(no answer passed the citation check)`, too large sends `(the answer was too long to send)`. One retry per answer, whatever the cause. - `runTurn`'s output buffer holds up to 4 * BODY_MAX characters, so a reply over 64 KiB can reach the send. - Outside a library business nothing changes. ## Mutants planned - The M11 writer check removed. - `document.human` compared instead of `old.human`. - The check event split from the insert transaction. - The client.mjs reason pass-through dropped.