Files
stack/docs/plans/2026-10-20_case2-rows.md

130 KiB

Case 2 slice rows: the creative writing hub (#416, T2)

Gosalyn, 2026-10-20 (from date -u), for sage-1329. Every section follows docs/plans/BRIEF-TEMPLATE.md. The design they build is agents/gosalyn/work/case2/DESIGN.md on gosalyn/case2-design. Webby approved r5 at e1635308b in webby-0021, with notes at dba33aaf6. r6 and r7 change only sections 9 and 10 and C2-13's row (mos-1057, Q183, webby-0023), and that diff is with Webby. Below, "the design" means that file, with its section number. A builder follows the design and doesn't reopen a decision in it. A departure goes to Sage before the build continues.

These are the slice rows from design 9.2, plus C2-11a, which the design starts in the first wave. The Q226 ruling's rows, C2-01d, C2-02, C2-03b, C2-08b, C2-11b, C2-12b and C2-21, have briefs in the section "Q226 rows" near the end. C2-07 folds into C2-06 and C2-17 drops. C2-16, C2-18 and C2-19 get briefs when a seat is free for them. Owners and models come from design section 8 (Q177, Q178). Where the design names only a model, Sage picks the seat. The reviewers are my proposals, and Sage can change any of them with queue set. Sage owns every gate.

Base: refactor at ae3c36f24. The design cites 9ae8c73ac. git diff --stat 9ae8c73ac ae3c36f24 -- packages adapters roles scripts touches only scripts/stack-release.sh and its test, and no cited line is in either.

Order (design 8, "Parallel waves"):

  1. Start now: C2-01, C2-08, C2-11a, C2-13a.
  2. On C2-01's candidate: C2-04, C2-05a, C2-06, C2-12. On C2-08's: C2-09a, C2-10a, C2-20.
  3. Then C2-03a. Then C2-05b, C2-10b and C2-13b.
  4. Last: C2-14.

The Q226 section gives the order after design 8 at af3349f00. C2-10b and C2-14 now wait on C2-02 too.

A row whose parent hasn't landed builds on the parent's candidate and rebases onto its landing (Q177).

Rules every row keeps:

  • No test touches ~/.mosaic-dev, ~/.config/mosaic-dev, a live unit, a slot file, Melanie's books repo, git.mosaicstack.dev or any Discord server. Tests use scratch roots, scratch config dirs and a local bare repo the test builds in her layout's shape: spaces and capitals in folder names, a .canvas file and an unmapped top-level note.
  • No row reads her text. Fixtures are made up. A marker string in a fixture body stands in for her text wherever a test checks that output holds none.
  • No credential value in any output, test, log or message (CONSTITUTION gate 13). Tests use a fixture token file with a made-up value.
  • No live stack change. Nothing here re-pins, restarts or activates anything. C2-13a and C2-13b's runs on the host needs Jason's go (design 10).
  • Before in-review, grep packages/*/tests and scripts/test-*.sh for each changed path and for any 64-hex pin of it, and run every changed or touched test file on its own. Suites need Sage's slot.
  • A refusal code named in the design is the code the row ships. A new code goes to Sage first.

C2-01 packages/library: mapping, scope, reader, resolver, R-rules, renderItems, survey

Problem

Nothing in the stack reads a writing repo by a mapping of folders to books, types, canon and playground. packages/project/src/read.mjs:19-26 reads blobs from a record repo but accepts executables (:21) and knows no scope. Design 1.1 to 1.3 and 7.4 set out what Case 2 needs, and every later row builds on it.

Owner and reviewer

  • Owner: Gosalyn, Opus 5.5.
  • Reviewer: Webby, who reviewed the design.
  • After: none.

Files owned

  • packages/library/ (new): package.json, src/mapping.mjs, src/scope.mjs, src/reader.mjs, src/links.mjs, src/problems.mjs, src/render.mjs, src/survey.mjs, src/index.mjs, tests/*.test.mjs, tests/fixtures/, README.md.
  • scripts/stack-copy.sh: add packages/library/src to COPY_PATHS. Without it the live copy has no library code, and no test catches the gap today.
  • packages/cli/tests/stack-copy.test.mjs: add packages/library/src/index.mjs to the list at :37.
  • scripts/mosaic or packages/cli/src: only the dispatch line for library survey.

What ships

  • The mapping schema of design 1.1, including library-playground-outside-book and the rule that no playground prefix holds another. Business file loading wires it in C2-04, so here it is a pure validator.
  • The scope rule of design 1.2. Only .md files are playground or canon.
  • The commit reader of design 1.3: git ls-tree -r -z and cat-file on a commit, never a working tree, through the same git() and gitEnv() as packages/project. R1 to R7 as problems with path and line.
  • The link resolver, in the order 1.3 gives: repo root, vaultPrefix/ stripped, relative to the linking file, then shortest path. appears_in and relationships wikilinks are read as links.
  • renderItems(body) (7.4), one pure function for the item kinds and the three notices.
  • library survey <repo> [--mapping <file>], which prints folder names, file counts, front matter key sets and link-form counts, and never text.

Tests: one case per R-rule and per resolver step. A link into another repo resolves to nothing. An appears_in wikilink is indexed. renderItems has a snapshot for each kind and each notice. The survey prints names, counts, key sets and link-form counts, and a marker string in a fixture body never appears in its output. The stack-copy test lists the new path. The library tests and packages/cli/tests/stack-copy.test.mjs pass.

Out of scope

Business file loading and consumer skips (C2-04), sync (C2-03a), the index and queries (C2-05a), the guard (C2-06). The survey run on the host is design 10 step 7, after the token slot exists.

Gate

Webby approves the manifest. The library tests and the cli suite are green on Sage's gate, along with every scripts/test-*.sh.


C2-01b survey escapes, list item lines and attachment embeds

Problem

Webby's row 212 review (webby-0041, agents/webby/work/queue-212/REVIEW-r1.md, approve on bdf18272a) left three notes that change C2-01's code. Row 212 lands as approved, and these come after it.

  • N1. The comment at packages/library/src/survey.mjs:76 is false. JSON.stringify leaves C1 and bidi controls raw, so a folder name can carry them into the survey output.
  • N2. A front matter link reports its key's line, not its list item's (reader.mjs:156, :162-165). Two links under relationships: both point at the key, and so do their R4, R5 and R6 problems.
  • N3. links.mjs:2 reads ![[x]] embeds as links, so an embedded image that is unmapped gives R4 "a link to no file". Design 1.3 says nothing about embeds.

Owner and reviewer

  • Owner: Gosalyn.
  • Reviewer: Webby.
  • After: C2-01.

Files owned

packages/library/src/survey.mjs, src/reader.mjs, src/links.mjs, packages/library/README.md where it describes these, and packages/library/tests/.

What ships

  • N1. The survey prints U+0080 to U+009F, U+200E, U+200F, U+202A to U+202E and U+2066 to U+2069 in any name as \uXXXX escapes, and the comment says what the code does.
  • N2. A link in a front matter block list reports its list item's line. A link in a scalar or flow value keeps the key's line.
  • N3 (sage-1517, Gosalyn's proposal). An embed whose target has an extension other than .md is an attachment. It is neither resolved nor indexed, and it never gives R4. The survey still counts it as an embed. An embed with no extension or with .md resolves as a link, so an unresolved one is R4.

Tests: one fixture folder name per escaped range prints escaped. Two links under one block list key report two different lines, and their problems carry those lines. ![[cover.png]] gives no problem and counts as an embed, ![[missing-note]] gives R4, and ![[note.md]] resolves.

Out of scope

The index and queries (C2-05a), and webby-0041 N4 and N5, which go to C2-12 and C2-10b.

Gate

Webby approves the manifest. The library tests are green on Sage's gate, along with every scripts/test-*.sh.


C2-08 creative roles, persona file and block, M1, M2, M6, M10, prompt passed by file

Problem

The stack has no role that may only read and answer in cited items, no way to carry Bean's and Chet's personas, and several paths by which a non-human actor could change a creative instance (design 4, B3). Both adapters pass the whole prompt on the command line (adapters/claude/adapter.sh:79, :82, adapters/pi/adapter.sh:106, :121), so any local user can read it in /proc/<pid>/cmdline.

Owner and reviewer

  • Owner: an Opus 5.5 seat that Sage picks.
  • Reviewer: Webby.
  • After: none.

Files owned

  • roles/companion.json, .md, roles/ideas.*, roles/research.*, roles/proofreader.*, roles/fact-checker.*, roles/continuity.* (new).
  • packages/business/src/vocabulary.mjs: THREAD_DEFINITIONS, END_PROTECTED as THREAD_DEFINITIONS, CREATIVE_DEFINITIONS, CREATIVE_KINDS, CREATIVE_TOOLS.
  • packages/business/src/role.mjs (M1), vars.mjs (persona.name, persona.sha256 in REGISTRY), launch-settings.mjs (the instances and models checks and the setRoutes branch), instance-settings.mjs (the creative branch).
  • packages/bus/src/broker.mjs: M6 decision-not-allowed only.
  • packages/harness/src/bundle.mjs: the persona file read and checks, the fenced block in prompt(), and the webPlan refusal.
  • packages/harness/src/tools.mjs: raise_decision left out for creative definitions.
  • adapters/claude/adapter.sh, adapters/pi/adapter.sh, adapters/README.md (:89, :107).
  • Tests in packages/business/tests, packages/bus/tests, packages/harness/tests.

What ships

Design 4, 5.1 (M1, M2, M6, M10) and 5.3:

  • The six role files, version 2, with the tools, authority, messaging and network of the design 4 table.
  • The constants. THREAD_DEFINITIONS replaces THREAD_DEFINITION only where it means "may hold a thread", and that use is C2-09a's. The assistants lines at launch-settings.mjs:140, :186 and :191, and tools.mjs:186, stay THREAD_DEFINITION.
  • The persona file at <configDir>/personas/<business>/<instance>.md, read with O_NOFOLLOW. The bundle refuses a non-regular file, another uid's, any group or other bit, more than 8,000 characters, a digest off the var (persona-mismatch), and </persona-file> or a line starting # or ## (persona-invalid). The block goes between the contract and "## This session", fenced as in design 4.
  • One actor rule on all five write paths (design 4): a write that touches a CREATIVE_DEFINITIONS instance is accepted only when its actor is the business file's human, otherwise organization-creative-human-only. The instances key of limits.set and organization.set refuses a creative instance for every actor with organization-creative-instance.
  • Both adapters pass the prompt file, not its text. Claude uses --system-prompt-file. Pi gets the path as --system-prompt, and its adapter checks [ -r ] first and refuses otherwise, because pi 0.85.1 uses the path string itself as the prompt when the file is missing or unreadable (resource-loader.js:21, :24-27, :30).

Tests, all named in the design 8 C2-08 row:

  • Role, gate and messaging tests. A creative raise is refused and no decision row appears. harness-builtin is refused for research and companion. scripts/test-config.sh passes.
  • Persona vars are refused at other layers, when malformed and on a non-creative definition. Each persona file refusal above has a test. A prompt snapshot pins contract, persona, session, in that order.
  • M10:
    • Each path refuses with the file unchanged, and the human's instances.set and routes.set on bean work.
    • The human's limits.set change to Bean's launch.models key works, next to the chief of staff's organization.set refusal of the same change.
    • Tester cases run through the broker with a bound tester session, and chief of staff cases with a role session.
    • A mutant without the models check fails.
    • No instances read, changes.jsonl line or adapter argv holds a marker string from a fixture persona.
  • Adapter tests in claude-session.test.mjs and pi-session.test.mjs: argv holds the prompt path and no marker from the prompt. Pi's adapter refuses a missing prompt file and an unreadable one.
  • Business file, .history and changes.jsonl modes after an instances.set write.

Out of scope

Thread creation per instance and the launcher (C2-09a), the lookup tools (C2-05b), the cited-reply guard (C2-06). Checking the host's claude for --system-prompt-file is C2-13a's precheck. If the C2-20 dry run finds heading lines in a real persona, the fence rule changes in a follow-up to this row, reviewed, before the import (design 9.1).

Gate

Webby approves the manifest. The business, bus and harness suites, scripts/test-config.sh and every scripts/test-*.sh are green on Sage's gate, and the models mutant is killed.


Problem

The Console's editor opens a file in visual mode only if roundTrip(text) === text (packages/webui/src/public/bus.js:919-920). Its commonmark rules (packages/webui/editor/model.mjs:119-120) have no wikilink rule, and nobody has measured whether the serializer escapes [[. If it does, every note with a wikilink opens in Source.

Owner and reviewer

  • Owner: Rocko, gpt-6.1-sol.
  • Reviewer: Filbert.
  • After: none.

Files owned

packages/webui/editor/model.mjs, the editor's bundle output if the build commits one, and packages/webui/tests/ editor test files.

What ships

First a test that measures today's behaviour on [[a]], [[a|b]], [[a#h]], in a list item, and next to front matter, with the result recorded in the manifest. Then a wikilink node that keeps each form byte for byte. roundTrip tests cover plain, labelled, heading, in a list, and next to front matter.

Out of scope

Library pages and saving (C2-11b), link clicking to open a target (C2-11b), any documentPath change.

Gate

Filbert approves the manifest. The webui suite is green on Sage's gate, along with every scripts/test-*.sh.


C2-04 the library entry in the business file, consumer skips, M9, mirror, library.file and library.status

Problem

The business file accepts only root and repo per project (packages/business/src/business.mjs:186-187), and several consumers assume every project has a root (design 2.1). Nothing stops an organization write from changing projects, and nothing creates a mirror.

Owner and reviewer

  • Owner: Herb, gpt-6.1-sol. Herb replaces Rocko, who holds five other rows (sage-1482).
  • Reviewer: Filbert.
  • After: C2-01.

Files owned

  • packages/business/src/business.mjs, organization.mjs (M9 in organizationWriter.write, :177), project.mjs, cli.mjs, resolve.mjs, instance-settings.mjs (only the skip at :142).
  • packages/seat/src/checkout.mjs (:310, :319), packages/cli/src/launcher.mjs (:353).
  • packages/project/src/paths.mjs (the overlap loop at :41-44, and the mirror path).
  • packages/bus/src/broker.mjs: library.file and library.status only.
  • packages/webui/src/cli.mjs: dispatch lines for the two verbs.
  • Tests in the matching tests/ directories.

What ships

The kind: "library" entry of design 1.1, validated by C2-01's schema. Each consumer in design 2.1 skips it, with its own test. M9: organization-projects-protected for any organization write whose projects differ from the file's in canonical JSON. The bare mirror is created at the projectRepo path with no remote in its config. library.file (read at the served commit) and library.status (reads the sync state, empty until C2-03a) are human only, refused from an agent session with human-required (M4).

Tests: business file tests. One test per consumer skip. A launcher test that the workspace has no clone. The M9 test calls write directly, once with a changed playground prefix and once with a changed hybrid entry, and the file and changes.jsonl are unchanged. Each verb from an agent session refuses.

Out of scope

Fetching (C2-03a). Bus writes (C2-02). The Console's library pages (C2-11b).

Gate

Filbert approves the manifest. The business, seat, cli, project, bus and webui suites are green on Sage's gate, along with every scripts/test-*.sh.


C2-05a lookup index and queries

Problem

Agents need typed lookups over her repo that keep universes apart and default to canon (design 3). The reader from C2-01 has no index or query layer.

Owner and reviewer

  • Owner: a Haiku 5.5 subagent at high effort, dispatched by Sage (lead decision 129).
  • Reviewer: Filbert.
  • After: C2-01.

Files owned

packages/library/src/index-build.mjs, packages/library/src/query.mjs, their exports in packages/library/src/index.mjs, and packages/library/tests/ query test files.

What ships

An index per commit (types, names, links, backlinks), cached in memory by commit id. Queries for find, get, search, manuscript and universe, with the arguments and result shapes in the design 3 table. universe is required. Results never leave it, except a hybrid's declared sources minus exclusions. Every result carries scope. Unmapped files never appear. Playground files can have book: null and an entry can be a draft with no books (design ruling after r11, three levels, built in C2-01c): a lookup that names a book leaves book-null files out, library_manuscript never reads them, a draft has no canon so its lookups return files only with scope: "playground", and library_universe on a draft returns its kind, name and prefixes. One test each. links_out and links_in drop other-universe and unmapped targets. Results page under REPLY_CAP (packages/harness/src/tools.mjs:271-280) and are never cut.

Tests: isolation, scope labels on every query, unmapped files never returned, paging under REPLY_CAP, and a rebuild time on a 3,000-file fixture recorded in the manifest.

Out of scope

Broker verbs and typed tools (C2-05b). Hybrid entries in the business file (C2-15). The query code takes hybrids as a shape, and its tests use a hand-built hybrid mapping.

Gate

Filbert approves the manifest. The library suite is green on Sage's gate, along with every scripts/test-*.sh.


C2-06 the citation guard, the page store, the runner texts and the retry

Problem

Today a creative agent's reply reaches her as a stored message with nothing tying it to her files. Jason's rulings say agents always refer to the source (Q176), and that chat is free text with citations as a setting (Q223, Q226). Design 5.2 puts one guard in the broker before every agent-to-human message in a library business. When message.send refuses, the runner logs it and marks the message read, so the answer is lost (packages/harness/src/runner.mjs:570-583 at aca16404c). Its unslop note and its clip would turn a cited body into plain text (design 5.2, "The unslop note and the clip").

Owner and reviewer

  • Owner: an Opus 5.5 seat that Sage picks.
  • Reviewer: Webby.
  • After: C2-05a, and C2-04, since both change organizationWriter.write. It takes in C2-07's runner work (design ruling after r11, Q226).

Files owned

  • packages/library/src/guard.mjs and its export.
  • packages/business/src/business.mjs: replyLimits in TOP_KEYS (:24), any value accepted.
  • packages/business/src/organization.mjs: the M11 check in write (:180), next to M9 (:191-193, :199-200).
  • packages/cli/src/limits.mjs: the mosaic limits replies --file <json> verb, which calls library.limits.set.
  • packages/bus/src/broker.mjs: the pre-check on message.send to human, the stored check result, the page store beside the web verbs (:1516, :1521), the human verb library.limits.set, and a library boolean in the role.claim reply (:4807-4830).
  • packages/bus/src/web.mjs, only if the page store needs the fetched text passed out.
  • packages/harness/src/runner.mjs: the fixed texts, the retry, and the note and clip rule in a library business.
  • Tests in packages/library/tests, packages/bus/tests, packages/business/tests, packages/cli/tests and packages/harness/tests/runner.test.mjs.

Line cites are at aca16404c. The builder re-reads them on its base.

What ships

The guard as design 5.2 sets it out. A body is free text, or a JSON object {text, citations} that must fit the schema, with library cites {path, commit, lines, quote, as?} and web cites {url, quote}. The seven checks and codes of the 5.2 table: reply-invalid, citation-missing, citations-over-limit, citation-invalid, playground-cited-as-canon, citation-cross-universe and citation-unfetched. A supporting path is citable and isn't canon. The guard runs before the insert, outside the SQL transaction, as #requestProject does (broker.mjs:1378-1430). It doesn't look at the sender's definition to decide whether to run. The check result stored with the message names each library cite's scope and whether the file's last commit at commit carries a Mosaic-Agent trailer, read with git log -1 --format=%(trailers:key=Mosaic-Agent,valueonly) <commit> -- <path>. No agent field sets it. The page store keeps the last 20 broker fetches per session, 200 KB each, in memory, dropped when the session ends.

The settings are design's ruling after r11, agents never write story. replyLimits holds two keys:

Key Default Valid value
requireCitation false true or false
citationsPerReply 20 an integer from 1 to 1,000

The guard reads the business file on each check, not the broker's copy from start. If the file is missing, unreadable, not JSON or not an object at check time, the guard checks at the defaults, except that requireCitation resolves to true, so a setting she turned on doesn't fail open (webby-0151 R2). library.status shows reply-limits-unreadable. It never skips the check. A key is read only if Object.hasOwn finds it. requireCitation is used only when typeof gives boolean, and citationsPerReply only when Number.isInteger is true and the value is in range. Anything else uses its default, and library.status names the key. library.status shows the limits in force and the keys that fell back. When a reply carries citations, all the cite checks run whatever requireCitation says.

M11. Only the human's library.limits.set changes replyLimits. The verb refuses an unknown key or an invalid value with invalid-request. organizationWriter.write compares against the on-disk file, old.replyLimits and old.human, on document and on written as M9 does. It refuses a changed replyLimits with organization-reply-limits-human-only unless the actor is old.human and is neither tester nor <definition>.<role> for a role in old.roles. A refusal reason names the key and its value in force. A human id that equals a role's <definition>.<role> gets that same code from the writer. The business validator doesn't refuse such an id, since that would be a schema change outside this row (webby-0144 N7).

The runner, which was C2-07. role.claim's reply gains library, true when the business has a project of kind: "library", and the runner reads it. In a library business:

  • The two fixed texts at runner.mjs:572 become (the session gave no answer) and (the turn failed). The failure reason stays in the run log.
  • When message.send refuses with a code from the 5.2 table, the runner starts one new turn in the same session with the code and the reason. If that reply also refuses, it sends (no answer passed the citation check).
  • A send that refuses request-too-large at the client (packages/bus/src/client.mjs:19-20), plain or JSON, gets the same one retry, and the new turn names the 64 KiB limit. If that reply is still too long, the runner sends (the answer was too long to send). If it refuses with a code from the table, the runner sends the citation text. One retry per answer, whatever the cause (webby-0151 R1).
  • The guard accepts exactly these four bodies when requireCitation is true.
  • A body that parses as a JSON object gets unslop.note() added to the end of its text, is serialized again and is never clipped. A body too long for the 64 KiB request limit gets the retry above. A fixed text goes out with no note, and the note waits for the next reply. A plain body keeps today's clip and note.

Outside a library business the runner does what it does today.

Tests: one pass and one refusal for each check in the 5.2 table, each through the runner's RESULT path and through send_message, and once with a non-creative sender in a library business. The messages count is unchanged on each refusal. Plain text passes with requireCitation false and refuses citation-missing with it true. 20 citations pass and 21 refuse citations-over-limit at the default. A supporting cite with "as": "canon" refuses playground-cited-as-canon. The stored check result names a supporting scope and the agent-written flag for a fixture file whose last commit has the trailer.

Runner tests: a reply over 64 KiB and then a valid one stores the second. Two replies over 64 KiB store (the answer was too long to send), and the guard accepts it with requireCitation true. A refused reply and then a valid one stores the second, and two refused replies store (no answer passed the citation check). Each case runs one retry turn, never two. Outside a library business a refused send does what it does today.

Settings tests: a changed value takes effect on the next reply with no restart, in both directions. Each invalid value uses its default, and the business file still loads. The invalid values are "true", 1, null, [true] and {} for requireCitation, and 0, 1001, 2.5, true and "5" for citationsPerReply, plus a non-object replyLimits, an array and a missing one. With the business file unreadable at check time, a reply with 21 citations refuses, an uncited reply refuses citation-missing, the four fixed texts pass, and messages is unchanged on each refusal.

M11 tests: the human's library.limits.set works, and an out-of-range value refuses invalid-request with the file unchanged. library.limits.set from an agent session refuses human-required, and from the tester tester-refused. The chief of staff's organization.set, the tester's instances.set and a direct write with a changed replyLimits refuse. A direct write that changes both human, to its own actor, and replyLimits refuses. With a dotted human id equal to a role's <definition>.<role>, a direct write with that role as actor refuses organization-reply-limits-human-only (webby-0144 N8). library.limits.set with an unknown key refuses invalid-request. In each refusal the file and changes.jsonl are unchanged. A mutant without the writer check fails, and so does one that compares document.human instead of old.human.

Runner tests, with a fake harness: a bad cite and then a valid reply store one message. Two refusals store the retry's fixed text. With requireCitation true, an empty and a failed turn store their fixed texts. With a flagged unslop event, a cited reply is stored with its citations whole and the note at the end of text, a fixed text is stored with no note, and the next reply carries it. A cited reply over 32,000 characters isn't cut. Outside a library business, the old texts, clip and note hold.

Out of scope

The Console's rendering (C2-10b). Agent writes and their notice (C2-02). Any classifier of prose. The setup sheet's reply settings paragraph moves to C2-12b, which lands after this row so the sheet never names a setting the bus doesn't have (webby-0139 N1).

Gate

Webby approves the manifest. The library, bus, business, cli and harness suites are green on Sage's gate, along with every scripts/test-*.sh. Before in-review, grep packages and docs/case2 for cited-reply-required, reply-kind-not-allowed, correction-unbound, no-cited-answer and the seven old replyLimits keys. A hit in a file this row owns goes. A hit elsewhere goes to Sage.

Handoffs

Every seat hands off directly (DECISIONS Q210, mos-1221). When the owner pushes a build or a new round, or the reviewer posts a verdict, that seat messages the next seat's T3 thread and Sage's with mcp__t3__t3_send_message. Copy Mos's thread only when Mos has a step.


C2-12 the setup sheet for Melanie and the survey run sheet

Problem

Melanie keeps writing in Obsidian, and she needs to know what the agents read, how a playground folder works, what changes for Bean and Chet, and two surprises (design 7.1, N2, N4). Whoever runs the survey on the host needs exact steps.

Owner and reviewer

  • Owner: gpt-6-luna.
  • Reviewer: Webby.
  • After: C2-01.

Files owned

docs/case2/SETUP-SHEET.md and docs/case2/SURVEY-RUN.md (new).

What ships

The setup sheet in plain words:

  • Which folders the agents read, and that a folder named in playground is her playground.
  • How to add a graph colour group for that path in her own vault.
  • That moving a note into a book folder makes it canon, so agents may cite it as fact.
  • That a short link to a name that also exists in another of her repos can point somewhere else for the agents, and that a full-path link avoids this.
  • What changes for Bean and Chet: short cited items, no passages (design 4).
  • That a list of links in a note's properties uses the quoted form Obsidian's property editor writes, appears_in: ["[[A]]", "[[B]]"]. The unquoted [[[A]], [[B]]] is R3 (webby-0041 N4).

The survey run sheet gives the exact library survey command with C2-01's real flags, and states what the output holds: names and counts only.

Out of scope

Sending it to her. Design 10 step 7 makes that Jason's call.

Gate

Doc only. Webby approves it against C2-01's real flags. Before landing, grep packages/*/tests and scripts/test-*.sh to confirm no test reads either file.


C2-09a one thread per creative instance

Problem

thread.create picks the single assistant instance and refuses more than one (packages/bus/src/broker.mjs:2331-2334). The same rule sits in #threaded (:290-291) and the launcher (packages/cli/src/launcher.mjs:135, :236-238). So she can't open a thread with Bean beside one with Chet.

Owner and reviewer

  • Owner: Rocko, gpt-6.1-sol.
  • Reviewer: Dewey.
  • After: C2-08.

Files owned

packages/bus/src/broker.mjs (#threaded, thread.create), packages/cli/src/launcher.mjs, packages/webui/src/public/bus.js and serve.mjs (the new-thread control only), and tests in packages/bus/tests, packages/cli/tests and packages/webui/tests.

What ships

Design 6, C2-09a: #threaded and the launcher read THREAD_DEFINITIONS. thread.create takes instance, which must be a THREAD_DEFINITIONS instance. With no instance and exactly one such instance, today's behaviour holds. With more than one, it refuses thread-instance-ambiguous. The Console's new-thread control lists thread instances by persona.name or instance name and passes the one she picks. No schema change.

Tests: a creative instance's thread starts a run, thread.create with instance binds it, and two thread instances with no instance still refuse.

Out of scope

Rooms and @tags (C2-09b, C2-10c).

Gate

Dewey approves the manifest. The bus, cli and webui suites are green on Sage's gate, along with every scripts/test-*.sh. Watch for hash pins on bus.js and broker.mjs (packages/bus/tests/channel-proof.test.mjs pins a webui file).


C2-10a no raw session view (M7) and no raw Discord engine (M8)

Problem

The Console's conversation view shows a model's raw text from session files (packages/conversation/src/reader.mjs:225-240, events.mjs:58, :82), so a refused answer would still show. The Discord connector posts its engine's output and the broker never sees it (packages/discord/src/connector.mjs:381-387).

Owner and reviewer

  • Owner: a Haiku 5.5 subagent at high effort, dispatched by Sage.
  • Reviewer: Filbert.
  • After: C2-08.

Files owned

packages/conversation/src/reader.mjs, packages/discord/src/cli.mjs (check and run only), and tests in packages/conversation/tests and packages/discord/tests.

What ships

M7 and M8 as design 5.1 states them. On a business with a library project, catalogue() leaves creative sessions out and open() refuses them with conversation-not-listed. discord check and discord run refuse a binding whose engine isn't mosaic-bus with discord-engine-not-allowed, and also refuse a mosaic-bus binding with tools.

Tests: a creative session file under the root isn't listed and won't open by id, and an assistant session still lists. Both Discord verbs refuse on a library business, and a pi binding on a business without a library still checks clean.

Out of scope

The relay engine itself (C2-18).

Gate

Filbert approves the manifest. The conversation and discord suites and scripts/test-discord.sh are green on Sage's gate, along with every scripts/test-*.sh. No test reaches a Discord server.


C2-20 Bean and Chet persona import, with a count-only dry run

Problem

Bean's and Chet's personas live in her two Hermes profiles on 10.1.1.54. The hub needs each one as a 0600 persona file with its digest in the business file (design 4), and nothing should read her memory, sessions or messages to get them (design 9.1).

Owner and reviewer

  • Owner: Gosalyn, Opus 5.5. The script runs only under Jason's go, by whoever he names.
  • Reviewer: Webby.
  • After: C2-08.

Files owned

scripts/case2-persona-import.mjs (new), packages/cli/tests/case2-persona-import.test.mjs (new) and its fixture tree.

What ships

A script that reads only the persona definition of each of the two Hermes profiles. It confirms profile to name from each profile's own configured name, prints the pairing, and stops if a profile names neither Bean nor Chet. Per-channel prompts are listed by name and size only. It writes each persona to a 0600 staging file. A second step, run as <user>, installs it at <configDir>/personas/<business>/<instance>.md and prints persona.name and persona.sha256. Otherwise it prints names, byte counts and sha256 only. --dry-run writes nothing and prints only counts of files, lines, and lines starting # or ## .

Build note (gosalyn-0030, mos-1087). The dry run also counts lines that name a Hermes tool or channel, so Jason knows before the import whether a persona refers to things Bean and Chet won't have in Mosaic. The words come from a fixed list in the script: the tool names in the hermes-agent checkout at df5700ebe, read from its code and not her profiles, plus Hermes and Discord. It prints one count per persona file and never the line, its number or the matched word. A nonzero count changes nothing in the import. It goes to Jason with the other counts.

Tests, on fixture Hermes profile trees:

  • The right fields are taken.
  • Memory and session files are never opened, shown by a read trace on the fixture.
  • A profile with no persona field refuses, and a persona that fails C2-08's fence check refuses.
  • Output holds no persona text. Each written file is 0600 and its digest matches.
  • --dry-run writes nothing, and prints the right counts and no marker string for a fixture with two heading lines.
  • --dry-run counts three lines that name a listed Hermes tool or channel in a fixture. Its output holds none of the matched words.

Out of scope

Running it. Design 10 step 6 and 9.1 make the dry run, the import and the pairing check Jason's go. If the dry run counts heading lines, the C2-08 follow-up changes the fence rule before the import, and her text is rewritten only if she says so through Jason.

Gate

Webby approves the manifest. The test file runs green on its own and the cli suite is green on Sage's gate, along with every scripts/test-*.sh.


C2-20b persona import follow-up: printed keys, the dry run fence check and the mkdir race

Problem

Webby's round 1 review of row 221 (webby-0073, REVIEW-r1 a0a00630e on webby/reviews) approved it with four notes that don't block. Three are in scripts/case2-persona-import.mjs and go in before Jason's import run, because he runs the script once (sage-1941):

  • N1: lines :147 and :237 print channel and platform keys from config.yaml as they are. A key with a newline or a control character could forge an output line.
  • N2: the dry run counts but doesn't apply C2-08's 8000-char fence. A persona over the fence and under 32000 bytes passes the dry run and then refuses at staging.
  • N4: privateDir runs lstat, then mkdir. A directory made between the two gives a raw EEXIST. It still fails closed, so only the error text is wrong.

N3 (Morgana's 213 mutants.py:48 B3 pattern no longer matches bundle.mjs) isn't in this row. It's in DEFERRED.

Owner and reviewer

  • Owner: Gosalyn.
  • Reviewer: Webby.
  • After: row 221 (C2-20) lands.

Files owned

scripts/case2-persona-import.mjs, packages/cli/tests/case2-persona-import.test.mjs and its fixture tree.

What ships

  • N1: every printed config key goes through JSON.stringify.
  • N2: the dry run calls personaTextFault on each persona and prints the fault's name. It still prints no persona text, no line and no line number.
  • N4: an EEXIST from the mkdir refuses with a named reason, not a raw EEXIST.

Tests:

  • A fixture key with a newline prints as one quoted line.
  • A persona over the fence makes the dry run print the fault, write nothing and exit nonzero.
  • The mkdir race refuses with the named reason. A stubbed mkdir that throws EEXIST is enough.
  • One mutant for each of the three fixes, each killed by its test.

Gate

Webby approves the manifest. The test file runs green on its own, and the three mutants are killed on Sage's slot. The cli suite and every scripts/test-*.sh are green on Sage's gate.


C2-03a fetch-only sync

Problem

The mirror from C2-04 holds nothing until something fetches her repo, safely and read-only, without the token ever reaching argv, config, logs or replies (design 7.2, CONSTITUTION gate 13).

Owner and reviewer

  • Owner: an Opus 5.5 seat that Sage picks.
  • Reviewer: Dewey.
  • After: C2-04.

Files owned

packages/library/src/sync.mjs, packages/library/bin/askpass (new), packages/bus/src/broker.mjs (library.sync, the 60-second timer and the library lock), packages/webui/src/cli.mjs (dispatch line), and tests in packages/library/tests and packages/bus/tests. If the helper sits outside packages/library/src, add its path to COPY_PATHS in scripts/stack-copy.sh.

What ships

Design 7.2's slice:

  • syncCommand(kind, url): argv is SAFE, then -c protocol.https.allow=always, -c http.followRedirects=false, -c credential.helper=, then -C <mirror>, then the command. Its env is gitEnv() plus LC_ALL=C and GIT_ASKPASS naming a helper outside the mirror, which reads the library-gitea slot. The helper answers only the exact Username and Password prompts for the configured host and user, checked with row 199's HOST and NAME patterns, and exits 1 on anything else. The URL carries no userinfo. (Sage, 2026-10-20, ruling the askpass departure: megavolt-0060 to -0063, dewey-0765, gosalyn-0068. It closes the redirect, host-check and username gaps that row 199's helper closes, and keeps GIT_ASKPASS with the slot path, never the value, per sage-1487.)
  • An https:// URL only.
  • The served ref refs/heads/served, and library-diverged, held until a human library.sync --accept <commit>.
  • The mirror's HEAD is a symbolic ref to refs/heads/served, so mosaic library survey <mirror> reads the served commit. The survey reads HEAD and has no ref flag, and design 10 step 7 surveys this mirror (C2-12 run sheet; Sage, 2026-10-21). Test: after the first sync, git symbolic-ref HEAD in the mirror prints refs/heads/served and the survey's commit line equals the served commit.
  • The sync state file, written atomically inside the mirror, and the 60-second timer.
  • library.sync, human only. A test reaches a local bare repo only through the module parameter that replaces syncCommand.

Tests: the C2-03a tests in design 7.2, with the argv test checking the fixed sequence above rather than "exactly one -c". Also dewey-0765's tests: the username prompt, a wrong host, a wrong port, another user, a 302, and the slot path against the value. Also gosalyn-0068's test that the env holds LC_ALL=C. A mutant that sets protocol.allow=always fails the argv test, and dewey-0765's four mutants are killed.

Out of scope

Push and every bus write (C2-03b, C2-02). Creating the token slot, which is design 10 step 4.

Gate

Dewey approves the manifest. The library and bus suites are green on Sage's gate, along with every scripts/test-*.sh, and the mutants are killed. A search of the test run's output for the fixture token finds nothing.


C2-07 folded into C2-06

The design's ruling after r11, agents never write story (Q226), folds this row into C2-06. The runner retry, the fixed texts and the note rule ship there, with Webby as reviewer. Sage doesn't queue C2-07.


C2-05b broker lookup verbs and typed tools

Problem

The queries from C2-05a aren't reachable from an agent session. Design 3 and M3 call for typed tools that the broker re-checks on every call.

Owner and reviewer

  • Owner: Rocko, gpt-6.1-sol.
  • Reviewer: Dewey.
  • After: C2-05a, C2-03a.

Row 224 built this brief at 23816cf28, and it lands as built (sage-2833). The supporting scope and blob that Q226 adds are row C2-05c, below in the Q226 rows.

Files owned

packages/bus/src/broker.mjs (the lookup verbs), packages/harness/src/tools.mjs (the five typed tools), and tests in packages/bus/tests and packages/harness/tests.

What ships

The five lookup tools of design 3, for every agent session in a business with a library project. Each read is at the served commit, and a read whose last fetch is older than 60 seconds syncs first. The verbs refuse library-not-configured in a business without a library and refuse a missing universe. The creative tool set is exact: the lookups and send_message, with no write tool and no raise_decision.

Tests: broker verb tests and a snapshot of typed tool names per creative role (M3).

Out of scope

The guard (C2-06). Write verbs (C2-02, C2-19).

Gate

Dewey approves the manifest. The bus and harness suites are green on Sage's gate, along with every scripts/test-*.sh.


C2-10b Console chat: replies, citations, write notices and the status line

Problem

The Console would show a cited reply as raw JSON, she can't see when the hub falls behind her repo, and an agent's write needs a line she can act on (design 7.4).

Owner and reviewer

  • Owner: a Haiku 5.5 subagent at high effort, dispatched by Sage.
  • Reviewer: Dewey.
  • After: C2-06, C2-09a, C2-03a, C2-02.

Files owned

packages/library/src/render.mjs (renderReply, which replaces renderItems), its test, packages/webui/src/public/bus.js, its stylesheet, packages/webui/src/serve.mjs (the library.status and library.revert routes only), and tests in packages/webui/tests.

What ships

Design 7.4:

  • renderReply(body), one pure function shared with the later Discord relay. Free text renders as text. A JSON body renders its text, then its citations as path, lines and quote, each with a scope badge: canon, playground, supporting or web. renderItems and its tests go.
  • A cite whose stored check result marks the file agent-written gets an agent-written label. A cite is marked "changed since" when the file's last commit is newer than the cited one.
  • A library-write notice shows the fixed line " wrote " or " deleted ", with the commit and a Revert button that calls library.revert {commit}. A refusal shows its code.
  • A status line reads library.status: the served commit and its age, library-diverged, a fetch error, the limits in force, and the count of R-problems with a link to the list.
  • A web cite's url renders as a link only when its scheme is https, and as plain text otherwise (webby-0041 N5).

Tests: webui tests for free text, a cited reply, each scope badge, the agent-written label, the notice line and its Revert call, a refused revert, "changed since" and the status line, and a web cite with an http: and a javascript: url that each render as text. renderReply tests in packages/library/tests.

Out of scope

The room view (C2-10c), library pages (C2-11b). Her sign-in from her machine waits on SSO (#412) or Jason's interim ruling (design 10 step 5).

Gate

Dewey approves the manifest. The library and webui suites are green on Sage's gate, along with every scripts/test-*.sh. Grep for hash pins on bus.js, serve.mjs and render.mjs, and for renderItems in packages, before in-review.

Handoffs

Every seat hands off directly (DECISIONS Q210, mos-1221). When the owner pushes a build or a new round, or the reviewer posts a verdict, that seat messages the next seat's T3 thread and Sage's with mcp__t3__t3_send_message. Copy Mos's thread only when Mos has a step.


C2-13a unit render and host precheck

Gitea #1732. Installer I2 with C2-13b, Vikunja P19 #432 (mos-1118). Re-briefed 2026-10-20 for Q189 (the Console starts at install and at boot) and row 190's landing.

Problem

The hub runs on 10.1.1.54 as mosaic under /srv/mosaic, as two system units, mosaic-bus@<business>.service and mosaic-webui.service (design 9 and 10 step 2c, r11). Nothing renders those units for a system install, and nothing checks the host before the start. Row 195 needs the same render and precheck for Case 1 on 10.1.1.53, so this part comes first and has no library dependency (sage-1480, Q177).

Owner and reviewer

  • Owner: Gosalyn, Opus 5.5. The scripts run on a host only under Jason's go, by him or his delegate (design 10).
  • Reviewer: Webby.
  • After: nothing. Row 195 chains on this row's candidate.

Files owned

scripts/case2-unit.sh and scripts/case2-precheck.sh (new), packages/cli/tests/case2-unit.test.mjs (new), and one line of packages/cli/README.md: at :713, "nothing in the repository creates or reads it" changes to say that nothing creates it, that only the Case 2 precheck reads it, and that the precheck prints only variable names, never values (webby-0034 N2, webby-0035).

What ships: case2-unit.sh

case2-unit.sh is the 2c step. Root runs it from a root-owned checkout of the reviewed commit outside /srv/mosaic, never from the release, because mosaic can write the release.

Inputs. --profile factory|library (default library), --user, --home, --business, --copy ID, --node PATH, with factory only --pm and --claude PATH, and the Console's serve flags after --. Row 195 calls it with those names. library refuses --pm and --claude.

  • --node must be an absolute path to a root-owned regular file outside <home>.
  • --claude follows the --node rule: an absolute path to a root-owned regular file outside <home>, so nothing running as <user> can replace the binary the bus runs (sage-1517, darkwing on gosalyn-0057). Row 195 installs claude under a root-owned prefix to match.
  • The copy id must be exactly 40 lowercase hex characters naming a directory that holds scripts/mosaic, directly under <home>/.local/share/mosaic-stack/copies. That is the rule copy_root uses (scripts/stack-copy.sh:8, :22). Anything else refuses before the render.
  • The serve flags. The script copies the checks from serve_flags (scripts/webui-service.sh:201-264) into its own function, with its own tests. It doesn't source or change that script, because its proof-file check compares the owner to id -u (:257), which is root here, and its render finds node with command -v (:294), not --node (webby-0036 N3). The library profile takes exactly these flags, each once, and refuses any other:
    • --port N, with N in 1..1023 (design 9.3).
    • --business ID, equal to --business.
    • --public-host NAME, --auth-header HEADER and --allow-user ID, checked as webui-service.sh checks them. Their values come from design 10 step 5.
    • --send, because C2-10b's chat needs it.
    • It refuses --sso. For 10-27 the library Console signs people in through the proxy, with the preview set and the proof pair above. webui-service.sh refuses --sso with either (:221-225). Taking --sso here waits for Q175 phase 2 (row 200 and its window), as a follow-up (gosalyn-0065 G2).
    • --proof-header NAME and --proof-file PATH. The proof is required, not optional (design 9.3). The file must be a regular file, not a symlink, owned by <user>'s uid from getent passwd, mode 0600 or 0400, under <home>/.config/mosaic-dev/secrets/. That keeps it out of the data root, where a session's run directory is. The script stats it and never opens it.
    • It refuses --tester-port. The tester listener takes instance, route, name and chat writes from loopback with no sign-in (packages/webui/src/serve.mjs:482, :552-553).
    • It refuses --checkout. Her host has no canonical checkout, and the queue store refuses a read from the copy, so the queue panel isn't part of Case 2 (webby-0036 N4).
    • It refuses --board. Case 2 runs no control board, and design 9.3 says why the default stays. The factory profile (gosalyn-0057 items 1-3, sage-1517) takes --port N in 1..1023 and --business ID, both required, and an optional --tester-port N in 1024..65535, never equal to --port, because Case 1's acceptance uses the tester. .53 has no proxy, so factory refuses --public-host, --auth-header, --allow-user, --proof-header, --proof-file and --send. It refuses --checkout and --board as library does, and any other flag.

The render. It renders two system units from the checkout's existing templates and changes neither: [email protected] from packages/cli/systemd/mosaic-bus.service.in, enabled as mosaic-bus@<business>.service, and mosaic-webui.service from packages/webui/systemd/mosaic-webui.service.in. Every token has one value (webby-0036 B2):

  • @REPO@ is the copy directory.
  • @NODE@ is --node.
  • @PATH@ is the directory of --node first, then whichever of /usr/local/bin, /usr/bin and /bin it isn't, in that order. The bus's ExecStart= is scripts/mosaic, which runs exec node from PATH (scripts/mosaic:22-40), so this line picks the bus's node. It differs from scripts/webui-service.sh:303-305 in one case on purpose. That script leaves the list alone when the node's directory is already in it, so with --node /usr/bin/node a node in /usr/local/bin would come first and the bus would run it.
  • @FLAGS@ is the checked serve flags, in the order given.
  • @PM_FLAGS@ is --pm when factory is given --pm, as scripts/bus-service.sh:82 renders it, and empty otherwise.
  • @CLAUDE_ENV@ is the line Environment=MOSAIC_CLAUDE_BIN=<--claude> when factory is given --claude, and empty otherwise.
  • @CONFIG_ENV@ and @SESSION_ENV@, which row 190 added to the templates (landed 00d4cc1f6). The render refuses an unknown token, and row 190 landed first, so this row renders both, with tests (gosalyn-0057 item 4). Fill them as scripts/instance.sh:85-107 does: for the default instance dev the @CONFIG_ENV@ line is deleted and @SESSION_ENV@ is the session env path under %h, which the %h rule below then makes literal. This row renders the default instance only, with the unsuffixed unit names below, and takes no --instance. A named instance on a Case 2 host is a follow-up (gosalyn-0065 G1). The bus template's EnvironmentFile= line is now mosaic-bus.service.in:25, EnvironmentFile=-@SESSION_ENV@, and the checks below match it there.
  • %h becomes the literal --home, because in a system unit %h is root's home.
  • After the render it refuses any @[A-Z_]+@ or %h left in either unit.

Lines it adds or replaces. At the end of [Service] in both units it adds User=<user>, Group=<user's primary group> and these four lines, in this order (webby-0036 B3):

ProtectSystem=strict
ReadWritePaths=<home>
ProtectHome=yes
PrivateTmp=yes

The templates' own NoNewPrivileges=yes stays. The Console unit also gets AmbientCapabilities=CAP_NET_BIND_SERVICE and CapabilityBoundingSet=CAP_NET_BIND_SERVICE, so it can bind its port below 1024 (design 9.3). The bus unit gets no capability line. It adds nothing else. No line restricts namespaces or system calls. A session's bwrap needs a user, PID and IPC namespace (packages/seat/src/sandbox.mjs:67) and a mount namespace, and RestrictNamespaces=, PrivateUsers= or SystemCallFilter= can each take one away, so it refuses a template or render that holds any of them.

  • [Install]. Each unit renders WantedBy=multi-user.target as its one WantedBy= line. In the bus unit it replaces WantedBy=default.target, not followed by a second line (webby-0036 N1). The Console unit gets the same line, replacing WantedBy=default.target at mosaic-webui.service.in:41 (Q189, mos-1122, mos-1123, gosalyn-0064 item 1). That reverses mos-1097 item 1 and webby-0036 B1's "no [Install] on the Console".
  • Root's 2c step enables both units, then starts the bus and then the Console (gosalyn-0064 item 2, design ruling after r11 at 8614ba87c). Both come back after a boot on their own. Design 10 step 9 has no hand start (item 3), and nothing in I1 or I3 starts the Console by hand either.
  • No After= or Wants=. serve calls the bus per request, so the Console starts without the bus (webby-0036 N2), and a boot that brings the Console up first is safe. The port below 1024, its capability lines and its precheck stay (mos-1110).

Checks. It refuses unless User=, Group=, ExecStart=, the bus unit's EnvironmentFile=, the hardening lines and the Console's two capability lines match fixed values. EnvironmentFile= is the line in packages/cli/systemd/mosaic-bus.service.in:25 (:24 before row 190), with the literal home. The Console unit has none, and it refuses one. It also refuses:

  • any PermissionsStartOnly=, and any Exec*= value whose leading run of prefix characters (@, -, :, +, !) holds a + or !. systemd takes those prefixes in any order, so -+/srv/mosaic/x runs as root even with User=mosaic. The named-line check doesn't read these lines (webby-0026 N1, webby-0027, defence in depth).
  • on a re-install, an installed pair whose two units name different copy ids (webby-0036 N5). The bus accepts only its own human-cli.mjs path, so a split pair breaks the human proof (scripts/webui-service.sh:324-348).
  • on a re-install, an installed unit that differs from the render in anything but the copy id. So the node path and serve flags must equal the installed ones. The copy id is the one value that may change, so a re-pin or a rollback to another copy re-renders both units together (row 195). With factory, --pm and --claude must also match the installed bus unit.

It writes both units under /etc/systemd/system/ as root:root 0644. It never uses systemctl link, and it refuses a symlink at either unit path. It opens no file under <home>. It only stats the copy directory, its scripts/mosaic and the proof file.

What ships: case2-precheck.sh

It runs as <user> and takes --profile factory|library, default library. C2-13a ships the host checks below. C2-13b adds the library checks to the same file. It refuses unless all of these hold:

  • The config validates.
  • sudo -n true fails as <user>, and id -G prints exactly one entry, its primary group. A system user always has one, so the check is for supplementary groups.
  • bus.tester: library refuses it on. factory requires it on, because Case 1's acceptance uses the tester. factory refuses a business file that has a kind: "library" project, with profile-library-business, so Case 2 can't reach a tester-on check through a wrong flag.
  • The run root is 0700.
  • XDG_DATA_HOME is unset or is <home>/.local/share. copy_root reads the copies dir from it (scripts/stack-copy.sh:8), and the unit fixes <home>/.local/share, so any other value would put the copy where the unit refuses it (webby-0032 N2).
  • node --version is 24 or later, because every package's engines field says >=24 (row 195: .53 has v22.22.2).
  • bwrap --unshare-all --ro-bind / / true exits 0 as <user>. Without it no session starts. Row 195 found no bwrap on .53, and kernel.apparmor_restrict_unprivileged_userns=1, and .54 may be the same. On a refusal, the precheck prints the sysctl's value and whether bwrap is on PATH. This runs in <user>'s shell, outside the unit's hardening, so it can't show that the unit lets bwrap run. Design 10 step 9 checks that.
  • net.ipv4.ip_unprivileged_port_start is above the Console's --port, read from the installed Console unit. It refuses when the floor is at or below the port, because a lowered floor lets any mosaic process bind the port, a session included, and makes the Console's capability pointless (design 9.3, mos-1100).
  • When some instance's harness is claude-code, one marker prompt run through the host's claude with --system-prompt-file shows that it replaces the default prompt. The claude version is printed. The host's claude is the installed bus unit's MOSAIC_CLAUDE_BIN when the unit sets one, as factory with --claude does, and claude on PATH otherwise. With the design's model path, Bean and Chet run pi, so this check is skipped unless she adds a claude-code specialist.
  • When an instance's provider is zai, <configDir>/secrets/claude-session.env exists, is a regular file owned by <user> at 0600, and sets a nonempty ZAI_API_KEY (design 10 step 2d). It parses the file the way systemd does, so the last assignment of a name wins (webby-0032 N3). A pi launch has no credential check, so without this the first turn is where a missing key shows. The precheck prints the name and never the value.

It also prints free memory and verifies that HOME is <home>.

Tests

case2-unit.sh runs against a scratch unit directory. A clean render writes 0644 files, and these hold:

  • the bus unit's Environment=PATH= starts with the --node directory, both for a --node outside the default list and for /usr/bin/node, which gives /usr/bin:/usr/local/bin:/bin. Its ExecStart= is the copy's scripts/mosaic
  • the Console unit's ExecStart= starts with --node and holds the flags in order
  • each unit has exactly one WantedBy=, multi-user.target
  • against a fake systemctl that records its argv, a clean run enables both units, then starts the bus, then the Console, in that order. A failed bus start refuses with the Console not started
  • both units hold the four hardening lines once, in order, and only the Console holds the two capability lines
  • a check of the script for systemctl link finds nothing
  • with factory, --pm renders an ExecStart= ending in bus start <business> --pm with one --pm, and --claude renders exactly one Environment=MOSAIC_CLAUDE_BIN= line with that path. Without them both tokens render empty

These cases refuse:

  • an installed unit edited to User=root
  • an edited ExecStart=
  • an installed unit with a hardening line removed, and a template with RestrictNamespaces=, PrivateUsers= or SystemCallFilter= added
  • a bus template with the EnvironmentFile= line removed or pointing elsewhere, and a Console template with one added
  • a symlink at either unit path
  • a --node that is relative, not root-owned, or under <home>
  • a re-install with a different --node or different serve flags
  • an installed pair whose units name different copy ids
  • a template holding an unknown @TOKEN@, and an unrendered %h left in either unit
  • a template with ExecStartPre=+/srv/mosaic/x, and one with ExecStartPre=-+/srv/mosaic/x
  • a copy id that is short, uppercase, holds / or .., or names no directory
  • with library: --port 7332, a missing proof pair, a proof file owned by root, at 0644, a symlink, or under the data root, --tester-port, --checkout, --board, a --business different from the unit's, --pm, --claude, and an unknown flag. With factory: --tester-port equal to --port, a missing --port or --business, each of --public-host, --auth-header, --allow-user, --proof-header, --proof-file and --send, a --claude that is relative, not root-owned or under <home>, and a re-install with a different --pm or --claude.

The precheck runs against a scratch data root. Each refusal has a case. A fake node printing v22 refuses, a fake bwrap that exits 1 refuses, and an unprivileged port start at or below the Console's port refuses. The claude check runs against a fake claude that appends rather than replaces, and that case refuses. With no claude-code instance the claude check doesn't run, and a test shows a missing claude passes. When the bus unit sets MOSAIC_CLAUDE_BIN, the check runs that binary: a replacing fake there passes while an appending fake sits first on PATH, and the swap refuses. The key file refuses when missing, at 0644, a symlink, with ZAI_API_KEY= empty, and with ZAI_API_KEY=x followed by ZAI_API_KEY=. A search of the test output for the fixture key value finds nothing. factory passes with the tester on and refuses a business file with a library project. library refuses with the tester on.

The bind test (mos-1100). A session can't take the Console's port. The test builds the argv a real session gets, with binds() and sandboxArgv() from packages/seat/src/sandbox.mjs, around a node one-liner that binds 127.0.0.1 on a port below 1024 and prints the error code. Inside bwrap the bind fails with EACCES. The control runs the same one-liner in the same sandbox on a free port of 1024 or above, and that bind succeeds, so the failure comes from the port floor and not from a broken sandbox. The test reads net.ipv4.ip_unprivileged_port_start first. If the test host's floor is at or below the chosen port, the test fails and says so, rather than skipping.

Out of scope

Running either script on a host. Design 10 steps 2 to 9 are Jason's, and the root step 2c is ops-01's over ssh as localadmin. The library checks and the install are C2-13b.

Gate

Webby approves the manifest. The test file and the cli suite are green on Sage's gate, along with every scripts/test-*.sh. A run on a host is a separate window with Jason's go.


C2-13b install script and library precheck

Gitea #1727. Installer I2 with C2-13a, Vikunja P19 #432 (mos-1118).

Problem

Nothing installs a library business on her host, and C2-13a's precheck doesn't yet check the library parts.

Owner and reviewer

  • Owner: Gosalyn, Opus 5.5. The script runs only under Jason's go, by him or his delegate (design 10).
  • Reviewer: Webby.
  • After: C2-13a, C2-04, C2-03a, C2-20.

Files owned

scripts/case2-install.sh (new), scripts/case2-precheck.sh (the library checks only) and packages/cli/tests/case2-install.test.mjs (new).

What ships

The install does: bootstrap, the stack copy (its npm ci installs the pinned harness), the business file with the mapping, bean and chet with their persona vars and files, any specialists she picks, her human id, the token slot path. It runs no release.sh package or activate. Both need Docker, and the hub runs no containers (design 10 step 2b, D8, webby-0085 B1, sage-2189). It runs as <user> through sudo -u mosaic -H from localadmin, and mosaic gets no key. It doesn't create <user> and writes no unit file. Root does those in design 10 steps 2a and 2c. Every host value is an input, so a later host move changes only inputs.

Row 195 runs the install by input for Case 1 on 10.1.1.53 (darkwing to gosalyn, design at agents/darkwing/work/queue-195/DESIGN.md section 2). It doesn't copy it. So the install takes --profile factory|library, default library, as the precheck does:

  • library is everything this brief lists.
  • factory skips the library mapping, the persona files and digests, the token slot, and the --library-status check.

The precheck's library profile adds these checks to C2-13a's:

  • The business file, .history, changes.jsonl and persona files have the modes in design 4, and each persona digest matches.
  • --library-status, run as <user> once the bus is up and step 4's token is in place (design 10 step 4a). It reads the bus's recorded sync state only and syncs nothing. It refuses when the mirror is missing, when lastError is set or when lastFetch is null. It prints each R-problem as a problem line and then a count, never file text (sage-2189).
  • No Discord binding exists.

Tests

They run against a scratch data root and a local bare repo. Each new precheck refusal has a case: a persona file at 0644, a digest mismatch, a business file at the wrong mode, a Discord binding, and for --library-status a missing mirror, a mirror never synced and a recorded lastError. The install's factory profile writes no library mapping, persona file or token slot.

Out of scope

Running it on the host. Design 10 steps 2 to 7 are Jason's. Step 1 no longer waits on #421 (Q183), and Mos lifted its hold (mos-1074). The root steps 2a and 2c are ops-01's, over ssh as localadmin, so the install waits on #423 step 1 and on nothing else in #423. The Gitea bot account and the token slot are steps 3 and 4. Stopping Hermes is step 9 and her choice.

Gate

Webby approves the manifest. The test file and the cli suite are green on Sage's gate, along with every scripts/test-*.sh. The run on the host is a separate window with Jason's go.


C2-14 end-to-end acceptance on a scratch root

Problem

Each slice row tests its own part. Nothing yet shows the whole path from her push to a checked reply in the Console.

Owner and reviewer

  • Owner: an Opus 5.5 seat that Sage picks.
  • Reviewer: Dewey, who also runs the script.
  • After: every other slice row, the Q226 rows included.

Files owned

scripts/test-case2.sh (new) and its fixtures.

What ships

One script on a scratch data root and a bare repo in her layout, with a supporting folder. It syncs, asks a creative agent in its thread through a fake harness, and stores a reply whose cites verify. It refuses a playground cite with "as": "canon" and holds a force-push as library-diverged. A seat with a fixture grant writes a supporting doc: the bare repo gets one commit with the trailer, and her thread gets the notice. A write to a story path refuses library-write-story-path with the bare repo's ref unchanged. Her revert of the write lands as one commit, and her library.file.save lands as one commit with her editor as author. The Console renders the reply and the notice.

Out of scope

Any live host or real model call.

Gate

Dewey runs the script and approves. It is green on Sage's gate with every scripts/test-*.sh.


v1 rows past the slice: rooms and hybrid universes (mos-1233)

Gosalyn, 2026-10-21 (from date -u), for mos-1233 (Vikunja 464). Mos reads Q211 to put rooms with @tags (TARGET.md:99) and hybrid universes (TARGET.md:82) in v1. Design 9.2's "Not ready by 10-27" is a capacity estimate, not a cut. If one misses 10-27 it is reported late. C2-01c, the playground levels of the design ruling after r11, comes first because C2-05a and C2-06 read its classify (mos-1240). These four briefs follow the form above and keep every rule listed at the top of this file. The editor rows stayed as they were here, since Q195 makes the editor an add-on. Q226 later brought C2-02, C2-03b, C2-11a and C2-11b into the slice, and their briefs are in the Q226 section below. C2-19 waits. C2-16 to C2-18 stay later. Owners follow design 8 and Q177. Where a brief names only a model, Sage picks the seat.

Base: refactor at cca206cdc. Each row builds on its parents' candidates and rebases onto their landings (Q177). C2-09a is row 219, C2-05a row 216, C2-05b row 224, C2-06 row 217 and C2-10b row 225, all briefed and not landed. Line cites are at cca206cdc. A builder re-reads them on its base.

New refusal codes below go to Sage with these briefs, as the rule at the top asks: library-playground-other-universe (C2-01c, replacing library-playground-outside-book), room-not-open and addressee-ambiguous (C2-09b), and the problem code R8 (C2-15).


C2-01c playground levels: universe-level prefixes and draft universes

Problem

TARGET.md:92-95 asks for a playground "where new universes can be created" and one "for books in a universe", and says "universe-level and book-level playgrounds must be provided". C2-01 puts every playground prefix inside one of its universe's book folders and refuses anything else with library-playground-outside-book (packages/library/src/mapping.mjs:81-86). classify looks for a book first and only then for a prefix inside it (scope.mjs:10-20), so a prefix outside every book can't be playground. The design ruling after r11, "playgrounds at three levels" (gosalyn/case2-design 51fdfb7e2, Webby approved in webby-0089), sets out the fix, and this row builds it.

Owner and reviewer

  • Owner: an Opus 5.5 seat that Sage picks (design 8). Gosalyn wrote C2-01 and the ruling.
  • Reviewer: Webby, who reviewed C2-01 and the ruling.
  • After: C2-01 (row 212, landed ee7b2f137). Before C2-05a (row 216) and C2-06 (row 217), which read classify. If either has a candidate by then, this row's change goes into that candidate as well, and its owner reruns its tests on the new classify. C2-05a's brief gains one What ships line for the null book and drafts in this same commit. C2-06's brief needs none: its guard takes universe from classify, so a draft is its own universe and citation-cross-universe covers it unchanged.

Files owned

  • packages/library/src/mapping.mjs (the universe entry checks) and src/scope.mjs (classify).
  • packages/library/README.md: the universe keys at :36 and the refusal table at :44-45.
  • packages/library/tests/mapping.test.mjs (:16-20 and :72 pin the old code) and a scope test file.
  • packages/business/tests/business.test.mjs:24-25. Its Elsewhere/Playground/ prefix becomes a valid universe-level playground, so the case moves to a prefix inside another universe's book and the new code.
  • docs/case2/SETUP-SHEET.md ("Your playground", :23-27): one line per level and the draft entry.
  • docs/case2/SURVEY-RUN.md:114: the refusal line names the new code and text.

What ships

From the ruling, which wins if this brief and it disagree:

  • kind is absent for a plain universe, "hybrid" or "draft". Any other value refuses library-mapping-invalid. A draft takes only kind, name and playground, and needs at least one prefix. A draft with books refuses library-mapping-invalid. A plain universe and a hybrid keep their key lists. checkMapping returns a draft frozen as {kind: 'draft', name, books: [], playground}, so every reader of books, classify included (scope.mjs:11), gets a list.
  • Three levels, set by where a prefix sits. Book level is a prefix inside one of the entry's own book folders, as now. Universe level is a prefix in a plain universe's or a hybrid's playground that lies outside every book folder of every entry. A hybrid may have books: [] and still have universe-level prefixes, and their files take the hybrid's id as universe. A draft's prefixes lie outside every book folder.
  • Refusals. A prefix inside, or holding, another entry's book folder refuses library-playground-other-universe. A prefix holding one of its own entry's book folders, or equal to a book folder, refuses library-mapping-invalid. The rules that no prefix holds another and none is listed twice cover all levels and entries (:123-124). A hybrid's sources naming a draft refuses library-mapping-invalid (:115-120). library-playground-outside-book is gone.
  • classify checks prefixes first. A .md file under any entry's prefix is playground, with that entry's id as universe, the book folder that holds the prefix as book, or null at universe level and in a draft. type and folder come from the first folder under the prefix, as now. Otherwise a .md file in a book folder is canon, and everything else is unmapped. The comment at scope.mjs:5-7 says so.
  • Readers at cca206cdc that see the change: reader.mjs:182 and :194 copy book into each file record, and :206-209 gives R6 for a canon link into a draft, since a draft is never a source. survey.mjs:66 and mirror.mjs:246 read only scope. A builder re-reads these on its base, and checks C2-05a's and C2-06's candidates, if any exist, for code that assumes a non-null book or a non-empty books.

Tests:

  • One case per level. A book-level file has its universe and book. A universe-level file has its universe and book: null, in a plain universe and in a hybrid. A draft file has the draft's id and book: null. A .canvas under a prefix stays unmapped.
  • One case per refusal: a prefix inside another entry's book, a prefix holding another entry's book, a prefix holding its own book, a prefix equal to a book folder (mapping.test.mjs:20's 'Book One/' moves to library-mapping-invalid), a draft with books, a draft with no prefix, an unknown kind, a hybrid source naming a draft, and two overlapping prefixes in different entries.
  • A draft comes back from checkMapping frozen with books: [].
  • Prefixes first: a canon file in a book that holds a prefix, outside that prefix, stays canon, and a file under the prefix is playground.
  • A reader case: a canon file linking into a draft gives R6, and linking into a universe-level prefix of its own universe gives R5.
  • business.test.mjs refuses the moved prefix with the new code through validateBusinessDocument.

Out of scope

The lookups' handling of a null book and of drafts: library_find's book filter, library_manuscript and the library_universe reply belong to C2-05a (its brief's What ships line), and the guard's universe check to C2-06. This row only gives them the right classify. Promotion of a draft is her hand edit under M9 (C2-04), with no verb. Hybrid exclude and pin (C2-15).

Gate

Webby approves the manifest. The library and business suites are green on Sage's gate, along with every scripts/test-*.sh. Before in-review, grep packages (src, tests and README.md files) and docs/case2 for library-playground-outside-book: no hit may remain. At 6ba8a582e the hits are mapping.mjs:84, packages/library/README.md:44, mapping.test.mjs:16-20 and :72, business.test.mjs:25 and SURVEY-RUN.md:114, all owned above. The plans and agents/ keep the old code as history.

Handoffs

Every seat hands off directly (DECISIONS Q210, mos-1221). When the owner pushes a build or a new round, or the reviewer posts a verdict, that seat messages the next seat's T3 thread and Sage's with mcp__t3__t3_send_message. Copy Mos's thread only when Mos has a step.


C2-09b rooms: room.open, room.send with @tags, room.read

Problem

TARGET.md:99 asks that she "converse with N agents in one chat session with @tagged messages to specify only one addressee where desired". After C2-09a she can hold one thread per creative instance, but each thread is a separate chat. thread.send reaches one thread's instance (packages/bus/src/broker.mjs:3511-3536), and nothing sends one message to several threads or reads them as one timeline. Design 6 sets out the room verbs.

Owner and reviewer

  • Owner: Rocko, gpt-6.1-sol (design 8).
  • Reviewer: Dewey.
  • After: C2-09a (row 219).

Files owned

packages/bus/src/broker.mjs (the three room verbs only), a new pure module packages/bus/src/address.mjs with the @tag parse, its export, packages/bus/README.md (the verb table rows), and tests in packages/bus/tests.

What ships

Design 6, C2-09b. A room is a project. Its members are the project's thread instances, the THREAD_DEFINITIONS instances that C2-09a's thread.create accepts.

  • room.open {project}. Human only. For each thread instance it creates a thread named after the instance key, owned by the caller, through the same insert as thread.create. A same-name thread that the caller owns on the same instance is reused. A same-name thread on another instance, or another owner's, makes room.open refuse room-thread-name-taken and create nothing. Returns {project, threads: [{id, instance, name}]}, sorted by instance key.
  • room.send {project, body}. Human only. parseAddress(body) in address.mjs reads leading @name tokens, each ended by whitespace, up to the first token that doesn't start with @. Names resolve through #named (broker.mjs:1480-1488) or as instance keys, and must be members. A tag that is one member's current name and another member's instance key refuses addressee-ambiguous and inserts nothing. A tag that names the same member both ways resolves to it. With no tag every member gets the message. Repeated tags count once. An unknown tag refuses unknown-addressee and inserts nothing. A body of tags only refuses invalid-request. A room whose member threads aren't all there refuses room-not-open. The body is stored as written, tags included. Each target gets what thread.send writes today: #checkMessaging(business, 'human', instance), one message row, #route and one human.input event. All inserts use one #now() and run inside the dispatch transaction, so one refused target leaves nothing inserted. Returns {ids: [...], to: [...]}.
  • room.read {project, after?, limit?}. Human only. The caller's member threads' messages, oldest first by messages.seq, each with its thread and instance. One room.send inserts its rows in one transaction, so their seqs are consecutive. room.read groups only a run of consecutive seqs from the human with the same at and body into one entry, with to listing the instances once each. Two sends of the same body in one millisecond, such as a double click, stay two entries. Paging follows thread.messages (THREAD_PAGE, :260): after is a message id in one of the room's threads, else message-not-found.
  • The tester gets none of the three verbs, so they aren't added to TESTER_VERBS (:117-119). A tester session refuses as for any verb outside that list. An agent session refuses human-required.
  • No schema change. Thread names stay unique per project (packages/bus/schema.sql:249).

Tests, from design 6: @continuity x makes exactly one message, in continuity's thread. No tag makes one per member. @nobody x refuses with the messages count unchanged. A tag after the first word is plain text. room.open against a same-name thread on another instance refuses and creates nothing, and a second room.open reuses the threads. Also: a #named display name resolves, repeated tags send once, a tag that is one member's name and another's key refuses addressee-ambiguous with the messages count unchanged, two identical sends at one at read back as two entries, a tags-only body refuses, a member whose messaging list refuses human makes the whole send insert nothing, room.read merges one send into one entry, pages with after, and refuses an after from another project. parseAddress has a table test, which C2-10c's parity test reuses.

Out of scope

The Console room view (C2-10c). Agents reading each other's replies (C2-16). A Discord channel bound to a room (C2-18). A CLI verb for rooms.

Gate

Dewey approves the manifest. The bus suite is green on Sage's gate, along with every scripts/test-*.sh. Grep packages/*/tests for broker.mjs and TESTER_VERBS before in-review, since packages/bus/tests/tester-writes.test.mjs reads the tester list.

Handoffs

Every seat hands off directly (DECISIONS Q210, mos-1221). When the owner pushes a build or a new round, or the reviewer posts a verdict, that seat messages the next seat's T3 thread and Sage's with mcp__t3__t3_send_message. Copy Mos's thread only when Mos has a step.


C2-10c Console room view: merged timeline and @name completion

Problem

The Console's chat lists threads one at a time. Its seam calls thread.* only (packages/webui/src/chat.mjs:19-27), and the webui channel plugin allows only those verbs (packages/plugins/channel/webui/index.mjs:4-12). So she can't see a room or send to it from the Console (design 6, 7.4).

Owner and reviewer

  • Owner: a Haiku 5.5 subagent at high effort, dispatched by Sage (design 8, lead decision 129).
  • Reviewer: Dewey.
  • After: C2-09b, C2-10b (row 225).

Files owned

packages/webui/src/chat.mjs (three room calls), packages/webui/src/chat-http.mjs (the room routes and their codes), packages/plugins/channel/webui/index.mjs (the three room verbs in VERBS), packages/webui/src/public/chat.js and chat.css (the room view), and tests in packages/webui/tests and packages/plugins/tests.

What ships

  • A room view per project: an "Open room" control calls room.open. The timeline reads room.read and shows every member's messages in one list, each marked with its agent's persona.name or instance name. A human room message shows once with its addressees. Agent replies render through C2-10b's cited-item renderer, with its scope badges and notices.
  • The composer completes @ against the room's members. The broker stays the authority: the browser sends the body as typed through room.send, and shows the broker's unknown-addressee refusal as a fixed line.
  • chat-http.mjs maps all four room codes, unknown-addressee, addressee-ambiguous, room-thread-name-taken and room-not-open, to 409, with fixed text for each, as it does for the thread codes (:15-23).

Tests: webui tests for the merged timeline, one entry per room send, the reply renderer in the room, completion against members, and each refusal's fixed line. A parity test runs C2-09b's parseAddress case table through the browser's tag reader and asserts the same addressees for every case. The plugin test asserts the webui channel allows the three room verbs and still refuses others.

Amendment 1 (Sage sage-4156, 2026-10-24, from Fethry's fethry-0027 seam check). room.read (broker.mjs, the room.read case) returns each message row with ids and to. It does not return the stored citation-guard result that thread.messages adds, the check read from the message's action.allowed event. It also skips #withLast, which adds last to each path cite. C2-10b's renderer needs check for its scope badges and its forged-notice safety. Without it, an agent reply in the room would not render the way it does in the thread. So:

  • packages/bus/src/broker.mjs joins the files owned, for this change only. room.read attaches check to each entry by the same event lookup thread.messages uses, and dispatches through #withLast on the same condition (a library project with last configured). Move the lookup into one helper that both verbs call, so the two can't drift. No other verb, field or refusal changes, and room.open and room.send stay as they are.
  • New bus tests: a guard-checked agent reply read through room.read carries the same check, and the same last on its path cites, as the same message read through thread.messages. An unchecked row carries no check. A human row merged across two threads still shows once with both addressees.
  • The seam keeps exactly the three room calls. It makes no extra thread.messages calls.
  • The bus suite joins the gate. No old test changes for this amendment.

Out of scope

The broker verbs (C2-09b), apart from amendment 1's room.read fields. Agents seeing each other's replies (C2-16). Library pages (C2-11b). Her sign-in from her machine waits on SSO (#412) or Jason's interim ruling (design 10 step 5).

Gate

Dewey approves the manifest. The webui and plugins suites are green on Sage's gate, along with every scripts/test-*.sh. Grep for hash pins on chat.js, chat-http.mjs and the channel plugin before in-review.

Handoffs

Every seat hands off directly (DECISIONS Q210, mos-1221). When the owner pushes a build or a new round, or the reviewer posts a verdict, that seat messages the next seat's T3 thread and Sage's with mcp__t3__t3_send_message. Copy Mos's thread only when Mos has a step.


C2-15 hybrid universes: exclusions, pins and the end-to-end crossover

Problem

TARGET.md:79-84 allows a crossover only as a deliberate, user-sanctioned hybrid universe that reads the character info of its sources and doesn't break her constraints. The parts are spread out. C2-01's validator reads a hybrid entry (packages/library/src/mapping.mjs:69-120), and its reader skips R6 for a link into a source (reader.mjs:206-209). C2-05a's queries and C2-06's guard take hybrids as a shape. M9 in C2-04 keeps agents off the mapping, so only her hand edit makes a hybrid. Three things are missing: an exclude isn't checked against the sources, a link into an excluded path isn't reported, and nothing checks a pin (design 1.4). C2-05a's index drops "exclusions" (its brief above, What ships) without a rule for reading them. Design 8 lists C2-15 with no model.

Owner and reviewer

  • Owner: an Opus 5.5 seat that Sage picks. Under Q177 Opus and gpt-6.1-sol share the harder builds. I name Opus because the row crosses three rows' code and the universe boundary that keeps her worlds apart.
  • Reviewer: Filbert, who reviews C2-05a.
  • After: C2-05a (row 216), C2-05b (row 224), C2-06 (row 217).

Files owned

packages/library/src/mapping.mjs (the exclude and pin checks), packages/library/src/scope.mjs (an excluded helper), the exclude read in C2-05a's packages/library/src/index-build.mjs or query.mjs, packages/library/src/problems.mjs (R8), packages/library/src/reader.mjs (the excluded-link R6 and the pin check), the guard file from C2-06 (packages/library/src/guard.mjs, the excluded-path cite only), docs/case2/SETUP-SHEET.md from C2-12 (one hybrid section), and tests in packages/library/tests and packages/bus/tests.

What ships

  • exclude keeps both forms of design 1.4. It is the type form when its first segment is one of the hybrid's sources and the rest is exactly one key of types, as second/Characters is in mapping.test.mjs:57. That form excludes the type folder in each of that source's books. Otherwise it is the path form, and it must lie inside, or equal, a book folder of one of the sources. If both readings fit, the validator refuses library-mapping-invalid. A path-form exclude or a pin.entity that lies inside no source book folder, or under one of a source's playground prefixes, refuses library-mapping-invalid, since the hybrid never reads it (REQ-LIB-3). Whether the path exists at a commit is the reader's to check.
  • One helper decides exclusion: excluded(mapping, hybrid, path) in scope.mjs, true when either form covers the path. C2-05a's index, the reader's R6 and the guard all call it. If C2-05a has landed with its own reading of exclude, this row replaces that reading with the helper, and C2-05a's query tests stay green.
  • The reader adds R6 for a link from a hybrid file into an excluded path. The index already drops it from the hybrid's results (C2-05a).
  • R8, "pinned value changed", reported only, like R5 and R6. At each commit the reader reports R8 on the pinned entity when the file is missing, or when its front matter field doesn't equal value. It also reports R8, with the line, on a hybrid file that is a version of the entity and sets field to another value in its front matter. A hybrid file is a version of the entity when classify gives it the entity's type and its front matter name equals the entity's. When either file has no name, the file names compare instead. A link alone doesn't count: a hybrid character that links [[Mara]] in relationships and has its own age is someone else, and so is a place file with age: 300. Values compare as the reader's parsed text against String(value). A hybrid can't edit a source's file, so this is how this brief reads design 1.4's "when a hybrid file changes it" (Webby d1 N1 agreed with the reading and asked for the narrowing).
  • The guard refuses citation-cross-universe for an item that cites a hybrid file together with a source path that excluded covers, in either form. An item that cites only one source universe is that universe's item, as now.
  • The setup sheet gets one section on making a hybrid by hand: the entry's keys, exclude, pin and notes, and that the continuity role cites the notes.

Tests, through a business-file hybrid on a bare repo in her layout's shape, and not only a hand-built mapping: library_find and library_search in the hybrid return its own files and its sources' canon, each with its source universe, and never a source's playground or an excluded path. library_get keeps links_out into sources and drops excluded targets. A source universe's lookups never return hybrid files. A cite to source canon in a hybrid item passes the guard, and one beside an excluded path refuses with the messages count unchanged. library_universe returns sources, constraints and notes. Each exclude form gets its own case on the lookups, R6 and the guard, and second/Characters from mapping.test.mjs:57 still checks. One case per validator refusal, including both readings fitting and a pin or exclude under a source playground. For R8: a missing entity, a changed entity, a same-name hybrid version with no link, and as non-R8 a place file with age: 300 that links the entity and a different character that links it. Also the excluded-link R6, and an agent's organization write that adds a hybrid refuses organization-projects-protected (M9).

Out of scope

Promotion and every write path (C2-02, C2-19). The Console's crossover button (C2-11b). Any check of a plain-word constraint in code: notes stay for the continuity role to read and cite (design 1.4). Draft universes as hybrid sources are refused by C2-01c (design ruling after r11, three levels).

Gate

Filbert approves the manifest. The library and bus suites are green on Sage's gate, along with every scripts/test-*.sh.

Handoffs

Every seat hands off directly (DECISIONS Q210, mos-1221). When the owner pushes a build or a new round, or the reviewer posts a verdict, that seat messages the next seat's T3 thread and Sage's with mcp__t3__t3_send_message. Copy Mos's thread only when Mos has a step.


Q226 rows: agents write supporting docs, her saves commit (mos-1309)

Gosalyn, 2026-10-22 (from date -u), for sage-2817 and mos-1309. The design is agents/gosalyn/work/case2/DESIGN.md on gosalyn/case2-design at b68d79663, the ruling after r11 "agents never write story (Q223, Q226)". It replaces the item list and its caps with free text and optional citations, lets agents write .md files in folders she marks supporting, and makes each of her Console saves a commit. Every write goes through the writing seat's own Gitea account from the per-seat grant path (F40). There is no shared bot account. These briefs follow the form above and keep every rule listed at the top of this file.

Base: refactor at aca16404c. Line cites are at that commit, and a builder re-reads them on its base. Each row builds on its parents' candidates and rebases onto their landings (Q177). C2-13b is row 226, in progress, and these rows leave it as built.

Order (design 8 at b68d79663): 0. First: the PRD row, REQ-LIB-6 rewritten (mos-1320 item 8). It lands before C2-01d and C2-02.

  1. Side by side: C2-01d, C2-03b, C2-08b, C2-11a, C2-12b. C2-05a, C2-09a and C2-10a also run in this wave.
  2. Then C2-05b, which is row 224 and lands as built, C2-05c, C2-06, C2-21 and C2-02.
  3. Then C2-10b and C2-11b.
  4. Last: C2-14.

C2-11a and C2-11b are slice rows that don't gate 10-27 (Q195).

New refusal codes, for Sage as the rule at the top asks: library-supporting-other-universe (C2-01d), library-write-story-path (C2-01d, C2-02), library-write-refused, library-write-no-grant and library-revert-not-agent (C2-02), library-human-missing (C2-02, C2-03b), library-fetch-no-grant (C2-03b), and reply-invalid, citation-missing and citations-over-limit (C2-06). library-push-failed, library-remote-invalid and stale-base were named before. Gone with the item list: cited-reply-required, reply-kind-not-allowed and correction-unbound.

C2-08b is a role change: library.write in the creative authority, and the gitea credential on six creative definitions and the chief of staff. Mos authorized it in mos-1309 item 5. It still lands only through a reviewed commit (invariant 7).


C2-01d supporting folders, the supporting scope, the editor field and supportingPath (M12)

Problem

The mapping has playground and no place an agent may write (packages/library/src/mapping.mjs, scope.mjs:12 at aca16404c). The ruling adds a supporting prefix list per universe entry and a supporting scope, and M12 needs one pure function that every agent write passes. Her Console saves need an author name and email (F41).

Owner and reviewer

  • Owner: an Opus 5.5 seat that Sage picks.
  • Reviewer: Webby, who reviewed C2-01c and the ruling.
  • After: C2-01c.

Files owned

packages/library/src/mapping.mjs, scope.mjs, a new packages/library/src/supporting.mjs with supportingPath, packages/library/src/index.mjs (exports), packages/library/README.md, and tests in packages/library/tests (mapping.test.mjs, scope.test.mjs, a new supporting.test.mjs) and packages/business/tests/business.test.mjs if it pins mapping codes.

What ships

Design 1.1, 1.2 and the ruling's M12 paragraph:

  • supporting, a list of path prefixes in each universe entry, drafts included. The prefix rules are the playground's: a prefix inside a book folder is book level, one outside every book folder is universe level, no supporting prefix holds another or holds a book folder, and one inside or holding another universe's book folder refuses library-supporting-other-universe. A prefix may also be in playground. Other malformed values refuse library-mapping-invalid.
  • editor, an object {name, email} on the library entry, both non-empty strings with no control character. It is optional for the mapping. C2-02's save refuses without it.
  • classify checks supporting prefixes first, then playground, then canon (design 1.2). A .canvas or .base file under a supporting prefix stays unmapped. A supporting file's universe and book follow the playground's rule.
  • supportingPath(mapping, universe, path, tree), pure, where tree is a lookup of the tip's entries the caller passes in. It returns the path or refuses library-write-story-path for a path that isn't a .md file under a supporting prefix of universe. That covers an unmapped path, a .canvas or .base file, an attachment, a story or canon path, a playground path that isn't also supporting, and a path under another universe's prefix. It also refuses an absolute path, a .., . or empty segment, a backslash, a control character, a path any of whose prefixes in tree is a symlink or a gitlink, a path that differs only in case from a tree entry it doesn't equal, and a path over 1,024 bytes.

Tests: one test per prefix rule and per refusal, a folder in both lists classified supporting, a playground-only folder classified playground, a supporting .canvas unmapped. supportingPath: one refusal per path class against a fixture tree with a symlink, a gitlink and a case variant, and one pass at book level, universe level and in a draft. editor refused when not two non-empty strings. A mutant that checks playground before supporting fails the scope test.

Out of scope

The broker calls and every git write (C2-02). Lookups taking scope: "supporting" (C2-05b). The setup sheet (C2-12b).

Gate

Webby approves the manifest. The library and business suites are green on Sage's gate, along with every scripts/test-*.sh.

Handoffs

Every seat hands off directly (DECISIONS Q210, mos-1221). When the owner pushes a build or a new round, or the reviewer posts a verdict, that seat messages the next seat's T3 thread and Sage's with mcp__t3__t3_send_message. Copy Mos's thread only when Mos has a step.


C2-05c the supporting scope and blob in the lookups (sage-2833)

Problem

Row 224 built C2-05b to its brief before Q226, so its lookups take scope of canon or playground only (packages/harness/src/tools.mjs and packages/harness/tests/library-tools.test.mjs:49 at 23816cf28), and library_get returns no blob id. Agents need to find and read supporting docs, and C2-02's writes take the blob id as base (design 1.2, 3, ruling after r11).

Owner and reviewer

  • Owner: a seat Sage picks.
  • Reviewer: Dewey, who reviews C2-05b.
  • After: C2-05b, C2-01d.

Files owned

packages/library/src/query.mjs (scope and the blob id), packages/bus/src/broker.mjs (the lookup verbs only), packages/harness/src/tools.mjs (the lookup tools' scope enum and descriptions), and tests in packages/library/tests (query.test.mjs, query-fixture.mjs), packages/bus/tests (library-lookups.test.mjs) and packages/harness/tests (library-tools.test.mjs).

What ships

  • scope takes supporting beside canon and playground in library_find, library_search and library_get, read from C2-01d's classify(). The default stays canon, so a lookup with no scope leaves supporting and playground files out. Where results from more than one scope are listed together, the order is supporting, then playground, then canon (design 1.2).
  • library_get returns blob, the file's blob id at the served commit.
  • The creative tool set doesn't change. The M3 snapshot stays at six names, or eight for companion and research. C2-02 adds the two write tools.

Tests: a supporting lookup in find, search and get. A lookup with no scope returns no supporting file. blob equals git rev-parse <served>:<path> for a canon, a playground and a supporting file. A supporting scope in a business whose entry lists no supporting prefix returns nothing and refuses nothing. The M3 snapshot is unchanged.

Out of scope

The write verbs (C2-02). The guard's scope checks (C2-06). The mapping and classify() (C2-01d).

Gate

Dewey approves the manifest. The library, bus and harness suites are green on Sage's gate, along with every scripts/test-*.sh. Grep packages/*/tests for the scope enum pin (library-tools.test.mjs:49 at 23816cf28) and for ['canon', 'playground'] before in-review, and update each one in this row.

Handoffs

Every seat hands off directly (DECISIONS Q210, mos-1221). When the owner pushes a build or a new round, or the reviewer posts a verdict, that seat messages the next seat's T3 thread and Sage's with mcp__t3__t3_send_message. Copy Mos's thread only when Mos has a step.


C2-08b role changes for agent writes and the chief's read grant (mos-1309 item 5)

Problem

C2-08 landed the six creative roles with authority: {withinRole: ["message.send"], crossRole: []} and credentials: []. checkCreative refuses any other action (packages/business/src/role.mjs:162-166 at aca16404c). An agent write needs the action library.write and a Gitea grant, and the grant path needs a definition credential for gitea, an instance identity and publish (business.mjs:132-167). The chief of staff needs the same credential for the read grant the mirror fetches with. CREATIVE_KINDS (vocabulary.mjs:209) belongs to the item list, which is gone.

Owner and reviewer

  • Owner: an Opus 5.5 seat that Sage picks. From 2026-10-24 that is Magica on Codex (mos-1542, mos-1576).
  • Reviewer: Webby, who reviewed C2-08.
  • After: C2-08.

Files owned

roles/companion.json, ideas.json, research.json, proofreader.json, fact-checker.json, continuity.json and chief-of-staff.json, their .md contracts where they name what the role may do, packages/business/src/role.mjs, packages/business/src/vocabulary.mjs, any reader of CREATIVE_KINDS, and tests in packages/business/tests (role.test.mjs, chief-of-staff.test.mjs, business.test.mjs). Amendment 1 adds packages/business/src/business.mjs (the chief's library condition only) and one new test file in packages/business/tests.

What ships

Design 4:

  • library.write joins the action vocabulary. Each creative role's withinRole becomes ["message.send", "library.write"], and checkCreative allows exactly those two.
  • Each creative role's credentials becomes [{"service": "gitea"}]. roles/chief-of-staff.json gets the same credential and no library.write.
  • A creative instance in a business file may carry identity and publish, which the grant path reads, under the existing rules at business.mjs:132-167.
  • CREATIVE_KINDS goes, with every reader of it.

The chief's credential widens nothing by itself. A grant also needs identity and publish on the instance, which only the human sets, and a mint the human approves. Without those the credential reaches nothing (webby-0151 N5).

The contracts say what the role may write: .md files in supporting folders, through the two write tools, never her story. A contract change is a role change too, so it lands with this row.

Tests: a creative role with library.write validates. One with any other added action, and one with library.write and a non-gitea credential, refuses with its message. The chief holds the credential and not library.write, and library.write added to the chief refuses if the chief's validator has an allowlist. A creative instance with identity and publish loads. scripts/test-config.sh.

Amendment 1 (Mos mos-1580, 2026-10-24, after the r3 gate went red on row 323's case1-business-file.test.mjs). Row 323's Case 1 document (docs/guides/case1-business.mosaic-stack.json) has no library and no credentials. The exact-match rule at business.mjs:51 would make every business's chief carry a gitea reference it never uses. Q247 (docs/plans/2026-10-23_optional-tracker-brief.md) made the tracker optional because a business without one should carry nothing it doesn't use. The same reasoning extends to the library. So:

  • The chief's gitea reference is required only when the business declares a library. A business with no library validates with chief credentials {}.
  • A business that declares a library but has no chief gitea reference refuses with a named message.
  • A business with no library and a chief gitea reference also refuses, so "exactly" still holds.
  • Every other role keeps the exact-match rule unchanged.
  • Row 323's document and case1-business-file.test.mjs stay byte-identical. No old test changes for this amendment. Its three cases above are new tests in a new file.

Out of scope

The verbs that use library.write (C2-02). The grant executor (C2-21). Any business file change on a live host.

Gate

Webby approves the manifest. The business suite and scripts/test-config.sh are green on Sage's gate, along with every scripts/test-*.sh. Grep packages, roles and scripts for CREATIVE_KINDS and for 64-hex pins of each changed role file before in-review. Sage confirms the role change against mos-1309 item 5 before the landing.

Handoffs

Every seat hands off directly (DECISIONS Q210, mos-1221). When the owner pushes a build or a new round, or the reviewer posts a verdict, that seat messages the next seat's T3 thread and Sage's with mcp__t3__t3_send_message. Copy Mos's thread only when Mos has a step.


C2-03b push and per-seat credentials for the library mirror

Problem

C2-03a's sync fetches only, with the bot slot library-gitea (packages/library/src/sync.mjs, packages/library/bin/askpass at aca16404c). The ruling drops the shared bot. Writes push with the writing seat's own grant token, the fetch uses the chief of staff's read grant, and her saves push with her own token from a new slot library-human (design 7.2, ruling after r11).

Owner and reviewer

  • Owner: an Opus 5.5 seat that Sage picks.
  • Reviewer: Dewey, who reviewed C2-03a.
  • After: C2-03a.

Files owned

packages/library/src/sync.mjs, packages/library/bin/askpass, packages/bus/src/broker.mjs (the sync credential choice and the push call only), and tests in packages/library/tests and packages/bus/tests (library-sync.test.mjs).

What ships

Design 7.2's C2-03b paragraph:

  • syncCommand('push', url) with the same SAFE prefix, -c keys and env as the fetch. The push argument is <commit>:refs/heads/<branch> with no +, so it is fast-forward only (D6).
  • A push rejected because the tip moved fetches once and returns a result that tells the caller to re-apply once (C2-02 owns the re-apply). A second rejection refuses stale-base. Any other failure refuses library-push-failed. A remote that doesn't pass C2-04's URL rules refuses a write with library-remote-invalid (B9).
  • The askpass helper answers with one credential per call, named by its env: a seat's grant store file at <storeRoot>/<business>/<agent>/gitea with that seat's Gitea user, the chief's store file and user for the fetch, or slot library-human with her Gitea user for her save. The helper reads the file with the same checks as readSecretFile in gitea-grant.mjs: no symlink, the bus's uid, mode 0600. It still answers only the exact Username and Password prompts for the configured host and that one user.
  • The fetch falls back to slot library-gitea only while the bus has never recorded a gitea grant event for the chief. The check reads the events table for any event matching GRANT_EVIDENCE (broker.mjs:246 at aca16404c) with method gitea and agent equal to the chief, of any action. It never reads gitea_grants. A revoke deletes that row (broker.mjs:2785), so does grant-lost (:2609), and the table has no state column (packages/bus/schema.sql:469-480). The events table is append-only (schema.sql:131-132), so the check survives a revoke, a grant-lost and a fire. It names no project, since grant.applied and grant.revoked carry none (:2469, :2787). A chief grant on any project ends the fallback, which fails safe (webby-0156 R4). An install that runs before C2-21 still syncs. Once such an event exists, a revoked or held grant, or a fired chief, refuses the fetch with library-fetch-no-grant and the slot is never read. A write, a revert and a save never fall back to the slot. library.status names which credential the last fetch used, by kind and user, never the value, and shows library-fetch-no-grant when the fetch refuses (webby-0151 R3).
  • Her save and revert read slot library-human. A missing slot, or one failing the helper's file checks, refuses library-human-missing before any git call.

Tests: against a local bare repo through the module parameter, a fast-forward push lands, a push over a moved tip reports the race, a second race refuses stale-base, and a dead remote refuses library-push-failed. The helper answers with the named seat's user and file and refuses another seat's user, a symlinked file, a 0644 file and another uid's file. The fetch uses the chief's file when a live grant exists and the slot only when the events table holds no gitea grant event for the chief. The revoked case revokes the chief's grant through grants.revoke and the fired case fires the chief through instance.fire, the broker's real paths, and each asserts the gitea_grants row is gone before the fetch runs. With that revoked grant, the fired chief and a held grant, each with a valid slot file present, the fetch refuses library-fetch-no-grant. A chief grant on a project other than the library project also ends the fallback. The same three cases with the slot file at mode 0000 still give library-fetch-no-grant, not a read error, which shows the slot is never opened. A missing library-human refuses library-human-missing and runs no git. A search of argv, the mirror's config, the run log and test output finds no fixture token. A mutant that adds + to the push refspec fails.

Out of scope

The write verbs and the commit (C2-02). Granting tokens (C2-21). Creating slot library-human on the host, which is design 10 step 4, and deleting slot library-gitea, which is step 4b.

Gate

Dewey approves the manifest. The library and bus suites are green on Sage's gate, along with every scripts/test-*.sh, and the mutant is killed. A search of the test run's output for the fixture token finds nothing.

Handoffs

Every seat hands off directly (DECISIONS Q210, mos-1221). When the owner pushes a build or a new round, or the reviewer posts a verdict, that seat messages the next seat's T3 thread and Sage's with mcp__t3__t3_send_message. Copy Mos's thread only when Mos has a step.


C2-21 the grant executor for a library project

Problem

The Gitea grant path forks each project and refuses any other reach (#436b steps 4 and 5, packages/bus/src/gitea-grant.mjs at aca16404c). A library write must land in her repository, so a fork won't do. #giteaRole also refuses every creative role, whatever its definition declares (broker.mjs:2122-2135, mos-1163). F40 needs a creative seat to hold a write grant on her repository and the chief a read grant (design ruling after r11, "Who writes, with what").

Owner and reviewer

  • Owner: an Opus 5.5 seat that Sage picks.
  • Reviewer: Dewey.
  • After: C2-08b, C2-13b.

Files owned

packages/bus/src/gitea-grant.mjs, packages/bus/src/broker.mjs (#giteaRole, #giteaTargets and the rendered block, :2122-2200), scripts/case2-install.sh (the chief's identity and publish only), and tests in packages/bus/tests (gitea-grant.test.mjs, fake-gitea-api.mjs, and gitea-grant-docker.test.mjs for the two facts).

What ships

First, before any code, the two Gitea facts on a scratch Gitea 1.27.1 in the Docker test, as F1 to F11 were proven: the admin token's scopes (436b 3b) can add a collaborator to a repository the admin account doesn't own, and /user/repos with the seat token shows that repository with the granted permissions. If either fails, the row stops and goes to Mos with the output.

The same run records two more facts, which don't gate the row: whether a protected file pattern on her branch refuses a seat user's push to a story path while her own push still passes (mos-1320 item 5), and whether branch protection refuses a seat user's force-push while a normal push passes (webby-0151 N4). The owner puts both results in BUILD.md and sends them to Mos, who files a task if the first works.

Then, for a project of kind: "library" only:

  • #giteaRole lets a creative role pass when every project the raise names is a library project. A creative raise naming any other project still refuses gitea-not-declared. The other checks stay. Mos ruled this in mos-1320 item 2: mos-1163 still covers every gitea project except kind: "library" and covers host grants whole. The comment above #giteaRole (broker.mjs:2118-2121 at aca16404c) cites mos-1320. The host grant writer, the executor's step 1 recheck and host.run keep refusing a creative role with host-creative-role (broker.mjs:1096, :1244), and their tests stay as they are: grant-host.test.mjs:125, grant-executor.test.mjs:1035 and :1046.
  • Step 4 adds the seat's user as a collaborator on the project's own repository with the admin token, PUT /repos/{owner}/{repo}/collaborators/{user}, in place of a fork. The permission is write for a CREATIVE_DEFINITIONS instance and read for any other, the chief included. The definition decides it. The raise can't name one.
  • Step 5 accepts that repository when its permissions show exactly that level with admin false. Anything wider refuses gitea-reach-wider.
  • The rendered block names the repository and the level in place of a fork URL, and adds the line "The token can write every path of this repository. Only the bus's path check limits it to supporting folders."
  • A revoke deletes the token as today. The collaborator entry stays, as forks do. A fire's delete sets prohibit_login as today.
  • scripts/case2-install.sh gives the chief of staff identity and publish: ["books"], so the chief can hold the read grant. The chief raises its own grant and the other seats' through the existing credential.mint step. If #grantableAgent refuses the chief as its own target, the row stops and goes to Sage rather than adding a path.

Whether each such grant also needs Jason's Approve is Q31. This row adds no step for it.

Tests: the two facts, and the two recorded ones. Executor tests against the fake API: write for a creative definition, read for the chief and any other, a wider permission or admin true refuses gitea-reach-wider, a revoke deletes the token, a non-library project still forks, and a creative raise naming a non-library project refuses gitea-not-declared. The rendered block holds the repository, the level and the line above. The install test's business file gives the chief identity and publish. A mutant that drops the library-only condition from #giteaRole fails.

Row 286's T7 and T9 (C2-03b) seed the chief's read grant as fixture data, because row 285's #giteaRole refuses the chief and no executor mints a read grant before this row (sage-4100). This row replaces those seeds with the real chief read grant through decision.raise, the human's yes and the executor, and keeps their revoke and fire assertions unchanged.

Out of scope

The askpass use of the token (C2-03b). The write verbs (C2-02). Running a grant on a live host, which needs Jason's go.

Gate

Dewey approves the manifest. The bus suite, the Gitea Docker test and the install test are green on Sage's gate, along with every scripts/test-*.sh. A search of the test run's output for the fixture admin and seat tokens finds nothing.

Handoffs

Every seat hands off directly (DECISIONS Q210, mos-1221). When the owner pushes a build or a new round, or the reviewer posts a verdict, that seat messages the next seat's T3 thread and Sage's with mcp__t3__t3_send_message. Copy Mos's thread only when Mos has a step.


C2-02 agent writes, the notice, revert and her Console saves

Problem

Agents can read her repo but not write a note for her, and the Console can't save a library file (LIBRARY_VERBS at broker.mjs:99 is library.file, library.status and library.sync at aca16404c). The ruling lets an agent create, edit and delete .md files under a supporting prefix, each as one commit by its own Gitea account with a notice she can revert, and makes each of her Console saves one commit (F40, F41).

Owner and reviewer

  • Owner: an Opus 5.5 seat that Sage picks.
  • Reviewer: Webby, who reviewed the design.
  • After: C2-01d, C2-03b, C2-04, C2-05c, C2-08b. C2-21 supplies live grants. Until it lands, tests use fixture grant store files.

Files owned

packages/library/src/mirror.mjs and a new packages/library/src/write.mjs (the plumbing commit), packages/bus/src/broker.mjs (the verbs, the notice, LIBRARY_VERBS), packages/bus/src/server.mjs and packages/bus/src/client.mjs (the large request exception for library.file.save), packages/harness/src/tools.mjs (the two typed tools), packages/webui/src/cli.mjs (dispatch lines), and tests in packages/library/tests, packages/bus/tests and packages/harness/tests.

What ships

The ruling's write path:

  • library.doc.write {universe, path, base, content} and library.doc.delete {universe, path, base}, tools library_doc_write and library_doc_delete. base is the blob id from library_get, or null to create. Only a session of a CREATIVE_DEFINITIONS instance holding library.write may call them (library-write-refused), and only with a live, not held, Gitea grant on the library project (library-write-no-grant). The tester gets tester-refused. supportingPath runs before any git call. content is UTF-8 with no NUL, at most 1 MiB. An agent request over the 64 KiB socket limit refuses request-too-large before the broker (runner.mjs:294, server.mjs:62).
  • Under the library lock: fetch, read the path at the remote tip, compare with base. Equal, or both absent for a create: write the blob, build the tree with GIT_INDEX_FILE pointing at a temporary index outside the mirror, and commit-tree with the tip as the one parent. Author and committer are the seat's identity. The message is Mosaic: write <path> or Mosaic: delete <path> and the trailer Mosaic-Agent: <business>/<instance>. The agent supplies no message text. Not equal refuses stale-base.
  • Push through C2-03b with the seat's own credential. A race re-applies once on the new tip when the blob there still equals base, and refuses stale-base otherwise. Another failure refuses library-push-failed and leaves the commit unreferenced. Nothing is kept to push later.
  • After the push, move refs/remotes/library/<branch> and the served ref, rebuild the index, record lastPush, and store the notice: a message from the writing instance to human in its thread, body {"notice": "library-write", "op", "path", "commit"}, built by the broker with no agent text. The broker inserts it directly, so the citation guard, which checks agent sends, doesn't see it.
  • library.revert {commit}, human only. It refuses library-revert-not-agent unless commit carries this business's Mosaic-Agent trailer and is reachable from the served commit. For the one path the commit touched, it puts back the parent's blob, or deletes the path if the parent had none, as a new commit by the same steps, with base the commit's own blob. A later change to the path refuses stale-base. The author and committer are her editor, and the push uses slot library-human. It works after the agent's grant is revoked or the seat is fired.
  • library.file.save {path, base, content}, human only, one commit per save by the same steps. The author is the library entry's editor, and a missing editor refuses library-mapping-invalid. The push uses slot library-human, and a missing or invalid slot refuses library-human-missing before any git call (C2-03b). The same two refusals hold for library.revert. Any mapped .md file is allowed, and M12's other path rules hold. It joins the project.file.save exception at client.mjs:19 and server.mjs:9 and :62, with PROJECT_SAVE_REQUEST_MAX.
  • The tool snapshot from C2-05b and C2-05c gains the two write tools: eight names, or ten for companion and research (M3).

Tests: the M12 broker tests, each refusal leaving the remote ref, the served ref and the mirror's refs unchanged. A write and a delete land against a bare repo, each with one parent, the seat's identity and the trailer. A moved tip with an unchanged blob re-applies, and a changed blob refuses stale-base. Each agent verb from the human, the tester, the chief of staff, a creative session without library.write and a creative session with a revoked or held grant refuses with its code. The notice body is exact. A revert lands with her editor as author, after the agent's grant is revoked too. A revert or a save with no editor refuses library-mapping-invalid, and one with no slot library-human refuses library-human-missing, each with no git run. A revert of a commit without the trailer refuses library-revert-not-agent, and a revert after a later change refuses stale-base. Her save lands with her editor as author, a save over 64 KiB and under 1 MiB lands, and a save over a moved file refuses stale-base. Each human verb from an agent session refuses human-required (M4). A mutant without the supporting check fails, and so does one that pushes with +.

Out of scope

The Console's notice line and Revert button (C2-10b). The library pages (C2-11b). Promotion (C2-19). Granting tokens (C2-21).

Gate

Webby approves the manifest. The library, bus and harness suites are green on Sage's gate, along with every scripts/test-*.sh, and the mutants are killed. A search of the test run's output for the fixture tokens finds nothing. The six creative .md contracts already name library_doc_write and library_doc_delete (row 285, for example roles/companion.md:44). The builder checks that the typed tools this row ships carry exactly those names, or changes the contracts in this row as a role change (webby-0282 N1).

Handoffs

Every seat hands off directly (DECISIONS Q210, mos-1221). When the owner pushes a build or a new round, or the reviewer posts a verdict, that seat messages the next seat's T3 thread and Sage's with mcp__t3__t3_send_message. Copy Mos's thread only when Mos has a step.


C2-11b Console library pages and her saves

Problem

She can't browse or edit her library in the Console. The ruling makes the Console her editor beside Obsidian, with each save one commit through library.file.save (F41, design 7.3).

Owner and reviewer

  • Owner: a Haiku 5.5 subagent at high effort, dispatched by Sage.
  • Reviewer: Filbert, who reviews C2-11a.
  • After: C2-02, C2-11a.

Files owned

packages/webui/src/public/bus.js or a new library page module beside it, its stylesheet, packages/webui/src/serve.mjs (the library.file and library.file.save routes only), and tests in packages/webui/tests.

What ships

Design 7.3:

  • A tree of universes, books, type folders, manuscript, playground and supporting folders. Playground and supporting each get their own section and colour. Unmapped files aren't listed.
  • A file opens in the existing editor through library.file, in visual mode when C2-11a's roundTrip holds and in Source otherwise. A wikilink click opens its target when it resolves.
  • Each save calls library.file.save {path, base, content} with the blob the page opened. One save is one commit. A stale-base refusal keeps her text in the page and offers a reload, and nothing is lost.
  • The page shows who last changed the file: her, Obsidian, or an agent by the trailer.

Tests: a save makes one commit with her editor as author, a moved file shows stale-base and keeps her text, an unmapped path offers no save, and a supporting folder shows in its own section. The route test holds that the Console sends no editor or author field of its own.

Out of scope

Promote and crossover buttons (C2-19, C2-15). The chat (C2-10b).

Gate

Filbert approves the manifest. The webui suite is green on Sage's gate, along with every scripts/test-*.sh. Grep for hash pins on bus.js and serve.mjs before in-review.

Handoffs

Every seat hands off directly (DECISIONS Q210, mos-1221). When the owner pushes a build or a new round, or the reviewer posts a verdict, that seat messages the next seat's T3 thread and Sage's with mcp__t3__t3_send_message. Copy Mos's thread only when Mos has a step.


C2-12b the setup sheet rewrite for free chat, supporting folders and saves

Problem

docs/case2/SETUP-SHEET.md describes short cited items and a reply limits paragraph that no longer hold (Q223, Q226). She needs to know how chat works now, how to mark a supporting folder, what an agent write looks like and how to undo it, and what a Console save does to her repo.

Owner and reviewer

  • Owner: gpt-6-luna.
  • Reviewer: Webby, who reviewed C2-12.
  • After: C2-12. It lands after C2-06, so the sheet never names a setting the bus doesn't have (webby-0139 N1).

Files owned

docs/case2/SETUP-SHEET.md.

What ships

The sheet in plain words, from the design's ruling after r11:

  • Chat is free text of any length. An agent may cite her files, and the Console shows where each cite points.
  • The two settings, requireCitation and citationsPerReply, their defaults, and that she or Jason can change them.
  • A folder named in supporting is where agents may write .md notes, and nowhere else. A folder she hasn't marked is story, and agents never write there. How to ask Jason to mark one, and that a folder can be in both playground and supporting.
  • Each agent write is a commit with a notice in the Console and a Revert button. Obsidian sees it on its next pull.
  • Each Console save is a commit under the name and email she gives for editor. A save over a file that moved is refused and keeps her text.
  • The residual: a supporting note can hold any text, and every one is visible and revertible.
  • What changes for Bean and Chet: free answers that can cite her books.

Out of scope

Sending it to her. Design 10 step 7 makes that Jason's call. SURVEY-RUN.md is unchanged.

Gate

Doc only. Webby approves it against the ruling. Before landing, grep packages/*/tests and scripts/test-*.sh to confirm no test reads the file.

Handoffs

Every seat hands off directly (DECISIONS Q210, mos-1221). When the owner pushes a build or a new round, or the reviewer posts a verdict, that seat messages the next seat's T3 thread and Sage's with mcp__t3__t3_send_message. Copy Mos's thread only when Mos has a step.


PRD REQ-LIB-6 rewritten to Q223 and Q226 (mos-1320 item 8)

Problem

REQ-LIB-6 (docs/prd/mosaic-stack.md:1230 at aca16404c) still says no creative agent can write to her repository and every message is a short list of cited items. Jason's Q223 and Q226 rulings replaced both. The residual at :1594 is the item list's, which is gone.

Owner and reviewer

  • Owner: Gosalyn.
  • Reviewer: Webby.
  • After: nothing. It lands before C2-01d and C2-02.

Files owned

docs/prd/mosaic-stack.md, packages/tasks/requirements.json (the version and the changed REQ-LIB titles) and packages/tasks/tests/requirements.test.mjs (the version pin at :24 only).

What ships

REQ-LIB-6 says agents never write story. They write .md files only in supporting folders a human marks. Replies are free text, and citations are checked when present. requireCitation is a setting only a human changes, default off. It cites Q223 and Q226. The id stays, and the count stays 85. The :1594 residual becomes the design's new one. The other REQ-LIB ids are checked, and the note lists the unchanged ones. The version is the next one at landing, after Ludwig's mail PRD if that lands first.

Out of scope

Any code. Requirements outside REQ-LIB.

Gate

Webby approves it against the ruling. packages/tasks/tests/requirements.test.mjs is green on Sage's gate, along with every scripts/test-*.sh that reads the PRD or requirements.json. Jason gets a report.

Handoffs

Every seat hands off directly (DECISIONS Q210, mos-1221). When the owner pushes a build or a new round, or the reviewer posts a verdict, that seat messages the next seat's T3 thread and Sage's with mcp__t3__t3_send_message. Copy Mos's thread only when Mos has a step.


Brief: row A, ZAI_API_KEY in the session env allowlist

Problem

For 10-27, Bean and Chet both run pi with provider zai on glm-5.3 (Q186, mos-1090, sage-1469). Pi reads the zai key from ZAI_API_KEY (pi-ai env-api-keys.js:89, with glm-5.3 in dist/providers/data/zai.json). ENV_ALLOW (packages/seat/src/session.mjs:83-88) doesn't hold that name, so sessionEnv (:97-99) drops it. A pi launch has no credential check (packages/plugins/harness/pi/index.mjs:42 countsAccount: false, packages/cli/src/launcher.mjs:299), so today a zai role launches and fails at its first turn, as packages/cli/README.md:439-444 says.

Owner and reviewer

  • Owner: a Haiku 5.5 subagent at high effort, dispatched by Sage (lead decision 129). Size XS.
  • Reviewer: Webby.
  • After: nothing. It doesn't wait on any Case 2 row.

Files owned

packages/seat/src/session.mjs, packages/seat/tests/session.test.mjs and packages/cli/README.md.

What ships

  • "ZAI_API_KEY" joins the provider line of ENV_ALLOW (session.mjs:86), after OPENROUTER_API_KEY. Nothing else in the list changes. SESSION_CREDENTIALS and hasSessionCredential (:94-95) don't change. They are Claude Code's login sets, and a zai key isn't one.
  • packages/cli/README.md, after "A Pi session gets the same variables through the allowlist." (:435), one sentence: a Pi session on provider zai logs in with ZAI_API_KEY, which the allowlist passes. Like every allowlist variable, it reaches every session the host starts, Claude Code ones included, and a shell inside any of them can read it. Where the README names claude-session.env ("Unit", :711), "The file holds one line" becomes "The file holds one line per variable", followed by a sentence saying it holds every session provider key, not only Claude's (webby-0032, webby-0034 N1). Leave :713, "nothing in the repository creates or reads it", alone. C2-13a owns that line.
  • One test in session.test.mjs: ENV_ALLOW includes ZAI_API_KEY, and sessionEnv({ ZAI_API_KEY: "fixture-zai", ZAI_TOKEN: "t" }, run) returns ZAI_API_KEY with the fixture value and no ZAI_TOKEN. The existing allowlist test (:51-59) stays as it is.
  • One mutant: the same file with "ZAI_API_KEY" removed from ENV_ALLOW. The new test fails on it.

Out of scope

Placing a key anywhere. The hub's own key on the Z.ai account is design 10 step 2d, and it is Jason's. Any pi launch or live check: running a pi role waits on #383. Chet's Codex login (row B, #428). A credential check for pi launches.

Gate

Webby approves the manifest. The mutant is killed. The seat suite and the cli suite are green on Sage's gate with every scripts/test-*.sh.


Brief: row B, a per-instance pi agent dir for Chet's Codex login (#428)

Problem

After 10-27, Chet moves from Bean's Z.ai account to his own ChatGPT/Codex login (Q186). Pi's openai-codex provider logs in with OAuth and a refresh token, and keeps both in auth.json under PI_CODING_AGENT_DIR (pi config.js:405-438). A role session's HOME is the run's own, so pi never sees an agent dir from the host (packages/cli/README.md:439-441), and ENV_ALLOW doesn't pass PI_CODING_AGENT_DIR. So no env variable can carry this login. Each instance needs its own agent dir that survives runs, because a refresh rewrites the file.

Chet's Hermes profile uses the same login today. Until Hermes stops, two clients would hold one refresh token. If a refresh by one revokes the other's, one of them drops out mid-session.

Owner and reviewer

  • Design: Gosalyn owns it (sage-1469). The build owner and reviewer are named when the design is approved.
  • Starts after 10-27. Nothing in this row is built or tested before then.

Files owned

None yet. The design names them.

What ships

The design first. It settles where the dir lives and who can read it, how the launcher mounts it into the sandbox writable for one instance only, how a refresh write survives the run, what a shell in the session can read, and how a launch with no login refuses. Build rows follow from it.

Out of scope

Bean's model path. Any change to row A. Any change to Hermes or her server.

Gate

A live check, run with Jason's or Mos's go, showing that two clients on one ChatGPT login don't revoke each other's refresh tokens. Until that check passes, no build row for this design starts.