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"):
- Start now: C2-01, C2-08, C2-11a, C2-13a.
- On C2-01's candidate: C2-04, C2-05a, C2-06, C2-12. On C2-08's: C2-09a, C2-10a, C2-20.
- Then C2-03a. Then C2-05b, C2-10b and C2-13b.
- 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.canvasfile 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/*/testsandscripts/test-*.shfor 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: addpackages/library/srctoCOPY_PATHS. Without it the live copy has no library code, and no test catches the gap today.packages/cli/tests/stack-copy.test.mjs: addpackages/library/src/index.mjsto the list at:37.scripts/mosaicorpackages/cli/src: only the dispatch line forlibrary survey.
What ships
- The mapping schema of design 1.1, including
library-playground-outside-bookand 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
.mdfiles are playground or canon. - The commit reader of design 1.3:
git ls-tree -r -zandcat-fileon a commit, never a working tree, through the samegit()andgitEnv()aspackages/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_inandrelationshipswikilinks 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:76is false.JSON.stringifyleaves 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 underrelationships:both point at the key, and so do their R4, R5 and R6 problems. - N3.
links.mjs:2reads![[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
\uXXXXescapes, 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
.mdis 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.mdresolves 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_PROTECTEDasTHREAD_DEFINITIONS,CREATIVE_DEFINITIONS,CREATIVE_KINDS,CREATIVE_TOOLS.packages/business/src/role.mjs(M1),vars.mjs(persona.name,persona.sha256inREGISTRY),launch-settings.mjs(theinstancesandmodelschecks and thesetRoutesbranch),instance-settings.mjs(the creative branch).packages/bus/src/broker.mjs: M6decision-not-allowedonly.packages/harness/src/bundle.mjs: the persona file read and checks, the fenced block inprompt(), and thewebPlanrefusal.packages/harness/src/tools.mjs:raise_decisionleft 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_DEFINITIONSreplacesTHREAD_DEFINITIONonly where it means "may hold a thread", and that use is C2-09a's. Theassistantslines atlaunch-settings.mjs:140,:186and:191, andtools.mjs:186, stayTHREAD_DEFINITION. - The persona file at
<configDir>/personas/<business>/<instance>.md, read withO_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_DEFINITIONSinstance is accepted only when its actor is the business file'shuman, otherwiseorganization-creative-human-only. Theinstanceskey oflimits.setandorganization.setrefuses a creative instance for every actor withorganization-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-builtinis refused forresearchandcompanion.scripts/test-config.shpasses. - 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.setandroutes.setonbeanwork. - The human's
limits.setchange to Bean'slaunch.modelskey works, next to the chief of staff'sorganization.setrefusal 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
modelscheck fails. - No
instancesread,changes.jsonlline or adapter argv holds a marker string from a fixture persona.
- Each path refuses with the file unchanged, and the human's
- Adapter tests in
claude-session.test.mjsandpi-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,
.historyandchanges.jsonlmodes after aninstances.setwrite.
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.
C2-11a editor wikilink node, after measuring whether [[ round-trips
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 inorganizationWriter.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.fileandlibrary.statusonly.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.mjsand its export.packages/business/src/business.mjs:replyLimitsinTOP_KEYS(:24), any value accepted.packages/business/src/organization.mjs: the M11 check inwrite(:180), next to M9 (:191-193,:199-200).packages/cli/src/limits.mjs: themosaic limits replies --file <json>verb, which callslibrary.limits.set.packages/bus/src/broker.mjs: the pre-check onmessage.sendtohuman, the stored check result, the page store beside the web verbs (:1516,:1521), the human verblibrary.limits.set, and alibraryboolean in therole.claimreply (: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/testsandpackages/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:572become(the session gave no answer)and(the turn failed). The failure reason stays in the run log. - When
message.sendrefuses 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-largeat 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
requireCitationis true. - A body that parses as a JSON object gets
unslop.note()added to the end of itstext, 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
playgroundis 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-runwrites nothing, and prints the right counts and no marker string for a fixture with two heading lines.--dry-runcounts 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.yamlas 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:
privateDirruns 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
personaTextFaulton 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 isSAFE, then-c protocol.https.allow=always,-c http.followRedirects=false,-c credential.helper=, then-C <mirror>, then the command. Its env isgitEnv()plusLC_ALL=CandGIT_ASKPASSnaming a helper outside the mirror, which reads thelibrary-giteaslot. 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 keepsGIT_ASKPASSwith the slot path, never the value, per sage-1487.)- An
https://URL only. - The served ref
refs/heads/served, andlibrary-diverged, held until a humanlibrary.sync --accept <commit>. - The mirror's
HEADis a symbolic ref torefs/heads/served, somosaic library survey <mirror>reads the served commit. The survey readsHEADand 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 HEADin the mirror printsrefs/heads/servedand the survey'scommitline 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 replacessyncCommand.
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 itstext, then its citations as path, lines and quote, each with a scope badge: canon, playground, supporting or web.renderItemsand 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-writenotice shows the fixed line " wrote " or " deleted ", with the commit and a Revert button that callslibrary.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
urlrenders as a link only when its scheme ishttps, 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.
--nodemust be an absolute path to a root-owned regular file outside<home>.--claudefollows the--noderule: 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 rulecopy_rootuses (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 toid -u(:257), which is root here, and its render finds node withcommand -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 HEADERand--allow-user ID, checked aswebui-service.shchecks 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.shrefuses--ssowith either (:221-225). Taking--ssohere waits for Q175 phase 2 (row 200 and its window), as a follow-up (gosalyn-0065 G2). --proof-header NAMEand--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 fromgetent 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 Nin 1..1023 and--business ID, both required, and an optional--tester-port Nin 1024..65535, never equal to--port, because Case 1's acceptance uses the tester. .53 has no proxy, sofactoryrefuses--public-host,--auth-header,--allow-user,--proof-header,--proof-fileand--send. It refuses--checkoutand--boardaslibrarydoes, 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--nodefirst, then whichever of/usr/local/bin,/usr/binand/binit isn't, in that order. The bus'sExecStart=isscripts/mosaic, which runsexec nodefrom PATH (scripts/mosaic:22-40), so this line picks the bus's node. It differs fromscripts/webui-service.sh:303-305in one case on purpose. That script leaves the list alone when the node's directory is already in it, so with--node /usr/bin/nodea node in/usr/local/binwould come first and the bus would run it.@FLAGS@is the checked serve flags, in the order given.@PM_FLAGS@is--pmwhenfactoryis given--pm, asscripts/bus-service.sh:82renders it, and empty otherwise.@CLAUDE_ENV@is the lineEnvironment=MOSAIC_CLAUDE_BIN=<--claude>whenfactoryis given--claude, and empty otherwise.@CONFIG_ENV@and@SESSION_ENV@, which row 190 added to the templates (landed00d4cc1f6). The render refuses an unknown token, and row 190 landed first, so this row renders both, with tests (gosalyn-0057 item 4). Fill them asscripts/instance.sh:85-107does: for the default instancedevthe@CONFIG_ENV@line is deleted and@SESSION_ENV@is the session env path under%h, which the%hrule 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'sEnvironmentFile=line is nowmosaic-bus.service.in:25,EnvironmentFile=-@SESSION_ENV@, and the checks below match it there.%hbecomes the literal--home, because in a system unit%his root's home.- After the render it refuses any
@[A-Z_]+@or%hleft 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 rendersWantedBy=multi-user.targetas its oneWantedBy=line. In the bus unit it replacesWantedBy=default.target, not followed by a second line (webby-0036 N1). The Console unit gets the same line, replacingWantedBy=default.targetatmosaic-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=orWants=. 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 anyExec*=value whose leading run of prefix characters (@,-,:,+,!) holds a+or!. systemd takes those prefixes in any order, so-+/srv/mosaic/xruns as root even withUser=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.mjspath, 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,--pmand--claudemust 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 truefails as<user>, andid -Gprints exactly one entry, its primary group. A system user always has one, so the check is for supplementary groups.bus.tester:libraryrefuses it on.factoryrequires it on, because Case 1's acceptance uses the tester.factoryrefuses a business file that has akind: "library"project, withprofile-library-business, so Case 2 can't reach a tester-on check through a wrong flag.- The run root is 0700.
XDG_DATA_HOMEis unset or is<home>/.local/share.copy_rootreads 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 --versionis 24 or later, because every package'senginesfield says>=24(row 195: .53 has v22.22.2).bwrap --unshare-all --ro-bind / / trueexits 0 as<user>. Without it no session starts. Row 195 found no bwrap on .53, andkernel.apparmor_restrict_unprivileged_userns=1, and .54 may be the same. On a refusal, the precheck prints the sysctl's value and whetherbwrapis 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_startis 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 anymosaicprocess 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-fileshows that it replaces the default prompt. The claude version is printed. The host's claude is the installed bus unit'sMOSAIC_CLAUDE_BINwhen the unit sets one, asfactorywith--claudedoes, andclaudeon 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.envexists, is a regular file owned by<user>at 0600, and sets a nonemptyZAI_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--nodedirectory, both for a--nodeoutside the default list and for/usr/bin/node, which gives/usr/bin:/usr/local/bin:/bin. ItsExecStart=is the copy'sscripts/mosaic - the Console unit's
ExecStart=starts with--nodeand holds the flags in order - each unit has exactly one
WantedBy=,multi-user.target - against a fake
systemctlthat 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 linkfinds nothing - with
factory,--pmrenders anExecStart=ending inbus start <business> --pmwith one--pm, and--clauderenders exactly oneEnvironment=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=orSystemCallFilter=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
--nodethat is relative, not root-owned, or under<home> - a re-install with a different
--nodeor different serve flags - an installed pair whose units name different copy ids
- a template holding an unknown
@TOKEN@, and an unrendered%hleft in either unit - a template with
ExecStartPre=+/srv/mosaic/x, and one withExecStartPre=-+/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--businessdifferent from the unit's,--pm,--claude, and an unknown flag. Withfactory:--tester-portequal to--port, a missing--portor--business, each of--public-host,--auth-header,--allow-user,--proof-header,--proof-fileand--send, a--claudethat is relative, not root-owned or under<home>, and a re-install with a different--pmor--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:
libraryis everything this brief lists.factoryskips the library mapping, the persona files and digests, the token slot, and the--library-statuscheck.
The precheck's library profile adds these checks to C2-13a's:
- The business file,
.history,changes.jsonland 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, whenlastErroris set or whenlastFetchis null. It prints each R-problem as aproblemline 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 readclassify. 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 newclassify. 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 takesuniversefromclassify, so a draft is its own universe andcitation-cross-universecovers it unchanged.
Files owned
packages/library/src/mapping.mjs(the universe entry checks) andsrc/scope.mjs(classify).packages/library/README.md: the universe keys at:36and the refusal table at:44-45.packages/library/tests/mapping.test.mjs(:16-20and:72pin the old code) and a scope test file.packages/business/tests/business.test.mjs:24-25. ItsElsewhere/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:
kindis absent for a plain universe,"hybrid"or"draft". Any other value refuseslibrary-mapping-invalid. A draft takes onlykind,nameandplayground, and needs at least one prefix. A draft withbooksrefuseslibrary-mapping-invalid. A plain universe and a hybrid keep their key lists.checkMappingreturns a draft frozen as{kind: 'draft', name, books: [], playground}, so every reader ofbooks,classifyincluded (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
playgroundthat lies outside every book folder of every entry. A hybrid may havebooks: []and still have universe-level prefixes, and their files take the hybrid's id asuniverse. 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, refuseslibrary-mapping-invalid. The rules that no prefix holds another and none is listed twice cover all levels and entries (:123-124). A hybrid'ssourcesnaming a draft refuseslibrary-mapping-invalid(:115-120).library-playground-outside-bookis gone. classifychecks prefixes first. A.mdfile under any entry's prefix isplayground, with that entry's id asuniverse, the book folder that holds the prefix asbook, ornullat universe level and in a draft.typeandfoldercome from the first folder under the prefix, as now. Otherwise a.mdfile in a book folder iscanon, and everything else isunmapped. The comment atscope.mjs:5-7says so.- Readers at
cca206cdcthat see the change:reader.mjs:182and:194copybookinto each file record, and:206-209gives R6 for a canon link into a draft, since a draft is never a source.survey.mjs:66andmirror.mjs:246read onlyscope. 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-nullbookor a non-emptybooks.
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 andbook: null. A.canvasunder a prefix staysunmapped. - 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 tolibrary-mapping-invalid), a draft withbooks, a draft with no prefix, an unknownkind, a hybrid source naming a draft, and two overlapping prefixes in different entries. - A draft comes back from
checkMappingfrozen withbooks: []. - Prefixes first: a canon file in a book that holds a prefix, outside that prefix, stays
canon, and a file under the prefix isplayground. - 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.mjsrefuses the moved prefix with the new code throughvalidateBusinessDocument.
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 asthread.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, makesroom.openrefuseroom-thread-name-takenand create nothing. Returns{project, threads: [{id, instance, name}]}, sorted by instance key.room.send {project, body}. Human only.parseAddress(body)inaddress.mjsreads leading@nametokens, 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 refusesaddressee-ambiguousand 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 refusesunknown-addresseeand inserts nothing. A body of tags only refusesinvalid-request. A room whose member threads aren't all there refusesroom-not-open. The body is stored as written, tags included. Each target gets whatthread.sendwrites today:#checkMessaging(business, 'human', instance), one message row,#routeand onehuman.inputevent. 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 bymessages.seq, each with itsthreadandinstance. Oneroom.sendinserts its rows in one transaction, so their seqs are consecutive.room.readgroups only a run of consecutive seqs from the human with the sameatand body into one entry, withtolisting the instances once each. Two sends of the same body in one millisecond, such as a double click, stay two entries. Paging followsthread.messages(THREAD_PAGE,:260):afteris a message id in one of the room's threads, elsemessage-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 refuseshuman-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 readsroom.readand shows every member's messages in one list, each marked with its agent'spersona.nameor 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 throughroom.send, and shows the broker'sunknown-addresseerefusal as a fixed line. chat-http.mjsmaps all four room codes,unknown-addressee,addressee-ambiguous,room-thread-name-takenandroom-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.mjsjoins the files owned, for this change only.room.readattachescheckto each entry by the same event lookupthread.messagesuses, and dispatches through#withLaston the same condition (a library project withlastconfigured). Move the lookup into one helper that both verbs call, so the two can't drift. No other verb, field or refusal changes, androom.openandroom.sendstay as they are.- New bus tests: a guard-checked agent reply read through
room.readcarries the samecheck, and the samelaston its path cites, as the same message read throughthread.messages. An unchecked row carries nocheck. 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.messagescalls. - 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
excludekeeps 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 oftypes, assecond/Charactersis inmapping.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 refuseslibrary-mapping-invalid. A path-formexcludeor apin.entitythat lies inside no source book folder, or under one of a source's playground prefixes, refuseslibrary-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)inscope.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 ofexclude, 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
fielddoesn't equalvalue. It also reports R8, with the line, on a hybrid file that is a version of the entity and setsfieldto another value in its front matter. A hybrid file is a version of the entity whenclassifygives it the entity's type and its front matternameequals the entity's. When either file has noname, the file names compare instead. A link alone doesn't count: a hybrid character that links[[Mara]]inrelationshipsand has its ownageis someone else, and so is a place file withage: 300. Values compare as the reader's parsed text againstString(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-universefor an item that cites a hybrid file together with a source path thatexcludedcovers, 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,pinandnotes, 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.
- 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.
- Then C2-05b, which is row 224 and lands as built, C2-05c, C2-06, C2-21 and C2-02.
- Then C2-10b and C2-11b.
- 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 refuseslibrary-supporting-other-universe. A prefix may also be inplayground. Other malformed values refuselibrary-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.classifychecks supporting prefixes first, then playground, then canon (design 1.2). A.canvasor.basefile under a supporting prefix stays unmapped. A supporting file's universe andbookfollow the playground's rule.supportingPath(mapping, universe, path, tree), pure, wheretreeis a lookup of the tip's entries the caller passes in. It returns the path or refuseslibrary-write-story-pathfor a path that isn't a.mdfile under a supporting prefix ofuniverse. That covers an unmapped path, a.canvasor.basefile, 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 intreeis 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
scopetakessupportingbesidecanonandplaygroundinlibrary_find,library_searchandlibrary_get, read from C2-01d'sclassify(). The default stayscanon, 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_getreturnsblob, 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
companionandresearch. 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.writejoins the action vocabulary. Each creative role'swithinRolebecomes["message.send", "library.write"], andcheckCreativeallows exactly those two.- Each creative role's
credentialsbecomes[{"service": "gitea"}].roles/chief-of-staff.jsongets the same credential and nolibrary.write. - A creative instance in a business file may carry
identityandpublish, which the grant path reads, under the existing rules atbusiness.mjs:132-167. CREATIVE_KINDSgoes, 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.mjsstay 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 sameSAFEprefix,-ckeys 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 refuseslibrary-push-failed. A remote that doesn't pass C2-04's URL rules refuses a write withlibrary-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>/giteawith that seat's Gitea user, the chief's store file and user for the fetch, or slotlibrary-humanwith her Gitea user for her save. The helper reads the file with the same checks asreadSecretFileingitea-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-giteaonly while the bus has never recorded a gitea grant event for the chief. The check reads the events table for any event matchingGRANT_EVIDENCE(broker.mjs:246ataca16404c) withmethodgiteaandagentequal to the chief, of any action. It never readsgitea_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, sincegrant.appliedandgrant.revokedcarry 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 orheldgrant, or a fired chief, refuses the fetch withlibrary-fetch-no-grantand the slot is never read. A write, a revert and a save never fall back to the slot.library.statusnames which credential the last fetch used, by kind and user, never the value, and showslibrary-fetch-no-grantwhen 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, refuseslibrary-human-missingbefore 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:
#giteaRolelets a creative role pass when every project the raise names is a library project. A creative raise naming any other project still refusesgitea-not-declared. The other checks stay. Mos ruled this in mos-1320 item 2: mos-1163 still covers every gitea project exceptkind: "library"and covers host grants whole. The comment above#giteaRole(broker.mjs:2118-2121ataca16404c) cites mos-1320. The host grant writer, the executor's step 1 recheck andhost.runkeep refusing a creative role withhost-creative-role(broker.mjs:1096,:1244), and their tests stay as they are:grant-host.test.mjs:125,grant-executor.test.mjs:1035and: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 iswritefor aCREATIVE_DEFINITIONSinstance andreadfor any other, the chief included. The definition decides it. The raise can't name one. - Step 5 accepts that repository when its
permissionsshow exactly that level withadminfalse. Anything wider refusesgitea-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_loginas today. scripts/case2-install.shgives the chief of staffidentityandpublish: ["books"], so the chief can hold the read grant. The chief raises its own grant and the other seats' through the existingcredential.mintstep. If#grantableAgentrefuses 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}andlibrary.doc.delete {universe, path, base}, toolslibrary_doc_writeandlibrary_doc_delete.baseis the blob id fromlibrary_get, ornullto create. Only a session of aCREATIVE_DEFINITIONSinstance holdinglibrary.writemay call them (library-write-refused), and only with a live, notheld, Gitea grant on the library project (library-write-no-grant). The tester getstester-refused.supportingPathruns before any git call.contentis UTF-8 with no NUL, at most 1 MiB. An agent request over the 64 KiB socket limit refusesrequest-too-largebefore 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 withGIT_INDEX_FILEpointing at a temporary index outside the mirror, andcommit-treewith the tip as the one parent. Author and committer are the seat'sidentity. The message isMosaic: write <path>orMosaic: delete <path>and the trailerMosaic-Agent: <business>/<instance>. The agent supplies no message text. Not equal refusesstale-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 refusesstale-baseotherwise. Another failure refuseslibrary-push-failedand 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, recordlastPush, and store the notice: a message from the writing instance tohumanin 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 refuseslibrary-revert-not-agentunlesscommitcarries this business'sMosaic-Agenttrailer 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, withbasethe commit's own blob. A later change to the path refusesstale-base. The author and committer are hereditor, and the push uses slotlibrary-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'seditor, and a missingeditorrefuseslibrary-mapping-invalid. The push uses slotlibrary-human, and a missing or invalid slot refuseslibrary-human-missingbefore any git call (C2-03b). The same two refusals hold forlibrary.revert. Any mapped.mdfile is allowed, and M12's other path rules hold. It joins theproject.file.saveexception atclient.mjs:19andserver.mjs:9and:62, withPROJECT_SAVE_REQUEST_MAX.- The tool snapshot from C2-05b and C2-05c gains the two write tools: eight names, or ten for
companionandresearch(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'sroundTripholds 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. Astale-baserefusal 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,
requireCitationandcitationsPerReply, their defaults, and that she or Jason can change them. - A folder named in
supportingis where agents may write.mdnotes, 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 bothplaygroundandsupporting. - 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 ofENV_ALLOW(session.mjs:86), afterOPENROUTER_API_KEY. Nothing else in the list changes.SESSION_CREDENTIALSandhasSessionCredential(: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 providerzailogs in withZAI_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 namesclaude-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_ALLOWincludesZAI_API_KEY, andsessionEnv({ ZAI_API_KEY: "fixture-zai", ZAI_TOKEN: "t" }, run)returnsZAI_API_KEYwith the fixture value and noZAI_TOKEN. The existing allowlist test (:51-59) stays as it is. - One mutant: the same file with
"ZAI_API_KEY"removed fromENV_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.