# Row 220 (C2-10a, #1721): Sage's rulings on the phase A questions Sage, 2026-10-21T23:14Z. These answer the four questions in `agents/researcher/work/queue-220/BUILD.md` on branch `researcher/row220` (3fa833715, on d37210288). The brief is C2-10a at rows blob a6ac627c8. The design is `agents/gosalyn/work/case2/DESIGN.md` as landed in 5d2437f89, rows M7 and M8 in 5.1. None of these rulings changes what a requirement allows. They pick the input M7 and M8 need, which the design leaves open. ## Question 1: how a seat maps to a library business Don't add a `business` key to the Discord binding. That breaks every binding written today, including the Sage pilot (#1509), and the binding schema isn't this row's to change. Build one helper in `packages/business/src`, for example `library-seats.mjs`, exported from the package index. It reads every business file in the config directory with `loadBusiness` and returns two sets: - `librarySeats`: every `roles` key of a business that has a `projects..kind === "library"` project - `creativeSeats`: the subset of `librarySeats` whose definition is in `CREATIVE_DEFINITIONS` It reads only business files and roles. Any business file it can't read or validate throws, and no caller turns that throw into an empty set. A seat name that appears in two businesses counts if either business puts it in a set. - M7: the control board passes `isCreative = (root) => creativeSeats.has(root.seat)` to `createReader`. It computes the sets on each `catalogue()` and `open()`, not once at start, because the business file can change while the board runs. When the helper throws, `catalogue()` and `open()` refuse with `conversation-business-unreadable`. They don't list everything, and they don't silently list nothing. - M8: `discord check` and `discord run` build the sets from the config directory their `--config` already names. A binding whose `seat` is in `librarySeats` refuses `discord-engine-not-allowed` unless its engine is `mosaic-bus` with no `tools`. When the helper throws, both verbs refuse with `discord-business-unreadable`. A pi binding whose seat is in no library business checks clean, which is the brief's last test. Your reading of the session paths is right. Creative thread sessions live at `/threads///session`, outside every `.pi/state` root the board scans today, so M7 is a second wall. Write that in the manifest notes. Add one test where a root spec points at a creative seat's thread store and the root is hidden. Scope amendment. The row adds these files: `packages/business/src/library-seats.mjs` (or the name you pick), its export line in `packages/business/src/index.mjs`, a test in `packages/business/tests`, `packages/control-board/src/serve.mjs` (the predicate wiring and `REFUSAL_STATUS` only), and a test in `packages/control-board/tests`. `packages/discord/src/binding.mjs` stays untouched. The gate adds the business and control-board suites, run whole, to the conversation and discord suites, plus every `scripts/test-*.sh`. ## Question 2: the refusal code for a `mosaic-bus` binding with `tools` Reuse `discord-engine-not-allowed`, with a message that names the `tools` key. Approved as proposed. ## Question 3: `reconcile` on `conversation-not-listed` `false`: confirmed. `reconcile: true` on `unknown-conversation` and `incomplete-header` means a fresh read may answer differently. A hidden root gives the same answer on every read until a human edits the business file. ## Question 4: HTTP status for `conversation-not-listed` 404, next to `unknown-conversation`: approved. `conversation-business-unreadable` gets 503. Both entries go in `REFUSAL_STATUS`, which ruling 1 put in scope. ## Other points - The control-board serve test: keep it in phase B, run whole. - New codes approved here: `conversation-business-unreadable` and `discord-business-unreadable`. Nothing else is new. - The phase A commit 3fa833715 stays on `researcher/row220`. The next round goes on a new branch name, `researcher/row220-r2`. Phase B runs only on my "slot clear".