Compare commits

...
Author SHA1 Message Date
jason.woltje 16e809457e row 283 (#1788): design draft 2 folds webby-0169 R1 (editor refuses < and >, T3, M6b) and N1 (check 8 compares NFC and case, NFD case in T6, M4b); Q2-Q4 accepted, Q4 handoff to C2-02 in BUILD.md; doc only 2026-10-22 13:48:12 -05:00
jason.woltje 0761e603c0 row 283 (#1788): design draft 1. supporting lists with the playground's prefix rules and library-supporting-other-universe, cross-list rules, the optional editor, supporting first in classify, supportingPath over the listTree array; survey.mjs departure Q1 for Sage, Q2-Q5 for Webby; doc only (sage-2906) 2026-10-22 13:43:35 -05:00
jason.woltje 94c33a43c0 docs: row 275 landing line; Researcher session line 2026-10-22 13:13:47 -05:00
jason.woltje a870fd85bb docs: seat log lines for rows 281, 224, 219, 214, 211 and 258, the row 253 landing line, Sage's SESSIONS line, and DEFERRED for the 224 residual and the 206 sharee list 2026-10-22 13:10:27 -05:00
jason.woltje 5b1f1dc885 docs: DEFERRED gains row 253 N1 (Vikunja 532, mos-1297), row 198 N2 (/api/seen 400, sage-2867) and the stale row 203 text in access.mjs (Quackerjack) 2026-10-22 13:10:27 -05:00
jason.woltje 8c638b6f66 agents: register Della, Scrooge, Huey, Louie and Ludwig as seats so the queue can name them as row owners (Q220, mos-1301) 2026-10-22 13:10:27 -05:00
jason.woltje a700992cd4 docs: 193b2 brief, the live --migrate window needs Mos's go under Q173, not Jason (mos-1310 item 4) 2026-10-22 13:10:27 -05:00
jason.woltje d30f35e292 docs: SESSIONS line for Gizmoduck, rows 264a and 268 after-landing docs and the row 269 plan 2026-10-22 13:10:27 -05:00
jason.woltje e5b7d1d083 row 268 (#1773): BUILD.md Runs records the r2 gate on ecd9b35eb and the r3 and r4 carry reruns with every count and skip; doc only (sage-2455) 2026-10-22 13:10:27 -05:00
jason.woltje 836b2f25ca docs: preserve email hard-wait approval and tune parser limits 2026-10-22 13:10:27 -05:00
jason.woltje 73516da853 docs: apply Q217 and Q222 to email source settings 2026-10-22 13:10:27 -05:00
jason.woltje 95e3f71661 docs: address Q195 editor design review findings 2026-10-22 13:10:26 -05:00
jason.woltje cde0cd0832 docs: Q195 writing editor design and Q196 options 2026-10-22 13:10:26 -05:00
jason.woltje 100f018af3 gosalyn: row 229 design round 2 for Webby webby-0148: the settings.json allowlist is three keys a fresh /login writes, written in Mosaic code with httpProxy and the path and command keys refused by name and a sha256 bump test on pi's setter names, models-store.json and keybindings.json now refuse, auth.json.lock is the only allowed dir, pi-login runs --offline and --no-session in an empty temporary cwd with a 229b allowlist test, the contents check runs before preflight, and creativeHumanOnly covers the fallback (R1, R2, N1-N5) 2026-10-22 13:10:26 -05:00
jason.woltje 4a8215e3a1 gosalyn: row 229 design draft 1 after mos-1306: Chet's route has no fallback by default with a human-only fallback still possible through routes, and the dates note records that the live check gates every build row, not the design 2026-10-22 13:10:26 -05:00
jason.woltje 669e680368 gosalyn: row 229 design draft 1, a per-instance pi agent dir for Chet's Codex login: the host-made 0700 store per instance, a same-path rw bind with PI_CODING_AGENT_DIR through the pi module env, a contents allowlist since pi reads models.json and settings.json there, a refusal for roles with bash, a no-refresh auth check preflight, no fallback for Chet, a fresh-grant mosaic pi-login, the live check that gates the build, and rows 229a and 229b (#428, #1730) 2026-10-22 13:10:26 -05:00
jason.woltje c06ccc9ba4 gosalyn: Case 2 C2-03b round 3 for Webby webby-0156 R4: the fetch fallback reads the append-only events table for any GRANT_EVIDENCE event with method gitea and agent the chief, never gitea_grants, whose row a revoke and a grant-lost delete. It names no project, since grant.applied and grant.revoked carry none, so a chief grant on any project ends the fallback and fails safe. The revoked and fired tests go through grants.revoke and instance.fire and assert the row is gone before the fetch 2026-10-22 13:10:26 -05:00
jason.woltje 136e5c1ca3 gosalyn: Case 2 Q226 row briefs with Webby's round webby-0151, Mos's mos-1320 and Sage's sage-2833 folded in. C2-06 rewritten and taking in C2-07: an unreadable business file resolves requireCitation to true (R2), a send over 64 KiB gets the one retry and then a fourth fixed text (R1). C2-03b falls back to slot library-gitea only while no chief grant record has ever existed and refuses library-fetch-no-grant after (R3), with library-human-missing for her save and revert. C2-02 after C2-05c, revert and save by her editor (N1). C2-21 cites mos-1320 item 2 and keeps the host-creative-role refusals, and records two Gitea facts that don't gate it (N4, item 5). C2-08b names what the chief's credential needs (N5). C2-05b restored to the brief row 224 built, which lands as built, and new row C2-05c carries the supporting scope and blob. A PRD row brief for REQ-LIB-6 (mos-1320 item 8). New briefs C2-01d, 05c, 08b, 03b, 21, 02, 11b and 12b 2026-10-22 13:10:26 -05:00
jason.woltje cf17ab387b gosalyn: C2-06 brief Q222 round 2 for Webby webby-0144: M11 compares against the on-disk human and replyLimits on document and written and refuses agent actor shapes, an unreadable business file checks at the defaults, keys read by Object.hasOwn and Number.isInteger, the verb refuses unknown keys, the new tests, the residual line, cites at aca16404c, and the hold moves to Q30 (mos-1305, design f164dcef8) 2026-10-22 13:10:26 -05:00
jason.woltje 09948da187 gosalyn: C2-06 brief makes every M5 limit a replyLimits setting with Q219's numbers as defaults, the human's library.limits.set and mosaic limits replies, the M11 writer check, read at check time with per-key fallback to the default, the setup sheet's settings line, the settings and M11 tests, After C2-04, and the build held for Q29 (Q222, mos-1302, design df29550c9) 2026-10-22 13:10:26 -05:00
jason.woltje 35615b01b7 gosalyn: C2-06 brief cites the setup sheet's reply limits paragraph at :115-118 (webby-0140 N4) 2026-10-22 13:10:26 -05:00
jason.woltje 3f1f4e44fa gosalyn: C2-06 brief owns the setup sheet's reply limits paragraph, which says 3 ideas of up to 120 characters and drops the lower-limits offer, and the tests paragraph is rewrapped (webby-0139 N1, N2, Q219) 2026-10-22 13:10:26 -05:00
jason.woltje 65b8c8a9bd gosalyn: C2-06 brief takes Jason's idea caps, 3 items of 120 characters per reply, tested one past each boundary, and the idea-text residual line reads 360 characters closed in Q219 (mos-1299, design 2852348d2) 2026-10-22 13:10:26 -05:00
jason.woltje 271cef205c gosalyn: Case 2 design round 3 for Webby webby-0156 R4: the fetch fallback reads GRANT_EVIDENCE events with method gitea and agent the chief in the append-only events table, never gitea_grants, and names no project, so it survives a revoke, a grant-lost and a fire and fails safe. The ruling paragraph, 7.2 and section 10 steps 4 and 4b say so, cited at aca16404c 2026-10-22 13:10:26 -05:00
jason.woltje 3e5db651b7 gosalyn: Case 2 design ruling after r11, agents never write story (Q223, Q226, mos-1309), with Webby's round webby-0151 and Mos's mos-1320 folded in. Supporting prefix lists agents may write .md files under through library.doc.write and library.doc.delete, fenced by M12 supportingPath, plumbing commits pushed fast-forward only with a Mosaic-Agent trailer, a notice and a human revert authored by her editor on slot library-human. Chat is free text with optional citations, replyLimits keys requireCitation and citationsPerReply, and an unreadable business file resolves requireCitation to true (R2). A send over 64 KiB gets the one retry and then a fixed text (R1). The fetch falls back to slot library-gitea only while no chief grant record has ever existed, and Jason deletes the slot once status shows the chief's grant (R3). A missing editor or slot refuses before any git call (N1), runner and tools cites fixed at aca16404c (N2, N3), two record-only Gitea facts at C2-21 (N4, mos-1320 item 5), mos-1163 narrowed to gitea library projects (mos-1320 item 2), M3 at eight or ten (mos-1320 item 4). C2-05b is row 224 as built and C2-05c carries the supporting scope and blob (sage-2833). New rows C2-01d, C2-05c, C2-08b, C2-12b and C2-21 2026-10-22 13:10:26 -05:00
jason.woltje e1c9c09935 gosalyn: Case 2 Q222 round 2 for Webby webby-0144: the M11 fence compares against the on-disk human and refuses agent actor shapes, checks document and written as M9 does, a failed business file read checks at the defaults and never skips, the fallback reads keys by Object.hasOwn and Number.isInteger with booleans, arrays and objects named, the verb refuses unknown keys, the ceilings residual line, and cites at aca16404c (R1-R3, N1-N3, N5, N6) 2026-10-22 13:10:26 -05:00
jason.woltje aaaf45576a gosalyn: Case 2 design ruling after r11, reply limits are settings: every M5 limit is a replyLimits key in the business file with Q219's numbers as defaults, changed only by the human's library.limits.set and fenced in organizationWriter.write as M11, read at check time, with a missing or invalid value using its default. C2-06 carries it and waits on Q29 (Q222, mos-1302) 2026-10-22 13:10:26 -05:00
jason.woltje cf6abca9c0 gosalyn: Case 2 design ruling after r11, idea items capped at 3 of 120 characters: M5, the B5 note, the 5.1 residual (now 360 characters per reply) and 10 step 8 closed by Jason; the rest of M5 unchanged, C2-06 builds and tests the new boundaries (Q219, mos-1299, Vikunja 465, ludwig-0014 Q4) 2026-10-22 13:10:26 -05:00
17 changed files with 2908 additions and 297 deletions

No files matched your search

+20
View File
@@ -5322,3 +5322,23 @@ Gander: after Duckworth-0100 design approval with C1-C5, prepared the two host-s
2026-10-22 14:45 UTC | Gander row253 focused27 on finalaca16404c base | Fresh0604d91fc / manifest573c51cf:27 whole files604/0/0, no timeout/retry, case134/0 in99.281s, case243/0 in66.418s, stack-release includingT46221/0 in583.453s. N9 sandbox bind ran and passed, no focused skips. Seven source blobs unchanged; sweep0 run-created leftovers, pre-existing infrastructure receipts retained. Whole gate and M1-M40 await separate Sage CLEAR after Duckworth rebase check. No live action.
2026-10-22 14:46 UTC | Gander correction to the row253 focused27 entry immediately above | stack-release whole was218/0/0, not221/0. The27-file sum604/0/0 and duration583.453s are unchanged; raw files/results.json and SUMMARY.json give the exact per-file totals.
2026-10-21T16:04:23Z | Q195/Q196 writing editor design, #1752 / Vikunja #448 | Before: row 154 edits Markdown in a separate Docs view, with no selected-word thesaurus or document pins. After: design draft on gyro/editor-design compares three editor approaches, records WordNet source/license/size, folds the pane/pin model and specifies guarded context/proposal APIs, DesignCraft handoff and seven proposed briefs. Q196 changes the basis to a Word-like interface. Verification so far: editor/provenance checks 10 pass, 1 explicit rebuild skip (pinned esbuild absent); brief fields, unique headings and 11-line checklist checked. Product, queue and live state unchanged. WordCraft scratch evaluation is being incorporated before publication. Review and Jason choices remain open.
2026-10-21T16:22:36Z | Q195/Q196 design verification, #1752 / Vikunja #448 | Folded Fenton 7caf8008b and Louie 880ef19e0. Recommend bounded O3: Word-like DOM editor plus isolated headless DOCX export, with explicit feature cut and semantic loss checks. WordCraft web 44,179,239 bytes, identical twice; measured comment/footnote conversion limits recorded. Seven briefs and 12-line checklist validated. Fresh editor/provenance: 10 pass, 0 fail, 1 explicit rebuild skip (esbuild 0.28.2 absent). CI queue guard identity gyro/purpose push returned documented degraded-pass (exit 0), CANNOT_ASSERT credential-unresolvable, scratch audit gyro-q195-ci-queue-audit.jsonl; no fallback or CI-green claim. Documentation-only candidate for Mos/Fenton review, no source, queue or live changes.
2026-10-22T16:06:34Z | Q195/Q196 design review fixes, #1752 / Vikunja #448, mos-1300 / fenton-0004 | Before: draft 105811400 had owner-only thread wording, stale source positions, two mismatched seat/model assignments, an assumed library save boundary and 12 owner questions. After: grant-aware pin validation and stale/revoked access redaction; references refreshed against aca16404c, including public/bus.js and README offsets; Sage assigns model-only seats; unimplemented library.file.save/C2-11b brief blocks book editing; tabs explicitly cover 62-76rem; J7 stricter context choice recorded; pronouns removed; six owner questions. Static docs/source-anchor checks only, no builds, product suites, MVP gate slot or live action. Fenton re-check pending; O3 and both companion folds retained.
2026-10-22T16:09:56Z | Della mos-1303 / Vikunja 460 Q217/Q222 revision | Before: Q1/Q2 open and Q3-Q7 phrased as Jason questions. After: Jason-created InMotion/woltje.com ingress recorded; per-source mailboxScope defaults none with human-only CLI then Console writer and named default/change/agent-refusal tests. Q3-Q7 settings/defaults/owners documented; no new external decision required for default development, conditional provider/spend/exposure list retained. PRD/grant/data.delete/live gates preserved. Docs only, no product suite or live operation. Earlier uncommitted append-only PRD review session receipts are preserved in this docs commit. Ludwig reviews before Sage landing.
2026-10-22T16:12:26Z | Della ludwig-0035 / mos-1303 / Vikunja 460 focused corrections | Verified REQ-WAIT-2/3 at db1e7732a: Jason approval is required to route mail.access out of hard waits. Restored explicit Jason-approved PRD gate; no Q211 waiver inferred. Named tunable parser/attachment settings, defaults and schema/test review ownership of resource bounds; E04 checks changed values and bound refusals. Clarified Q8 extraction stays Jason choice. Docs-only checks, no product/live action; focused review before Sage handoff.
2026-10-22 13:06 UTC | Megavolt | Row 281 (#1786) build. Before: a fire or delete left host_grants/gitea_grants rows, the host line and the Gitea token live. After: step G in broker.mjs revokes them in a detached job per agent (the host line first, then Gitea local-first with an admin preflight); a refusal is grant.revoke.refused plus a human message; open grant decisions are in the set; a raise for a fired agent refuses instance-fired. Tests T1-T14 (bus fire-revokes-grants, lifecycle-revoke, grant-executor, gitea-grant; cli lifecycle). Gate on 7979fb41e: base 121/121 + 24/24, 29 anchors, M1 exit check rc1/17, M1-M22 22/22 killed, dewey-0840 files 76+14+23, bus 819, business 231, cli 837, ten shells rc 0 under the mos-1275 limits; skips: test-task row 244 live-provider pair, test-queue canonical-root check. Corrections: the first base run on c174f37ac went red on five first-run test defects (b3, test only); the first mutant run on 06ee5ed6b hung on M1 because T6c's held loop never ended (b4a fixed the teardown, and the runner now has timeout -k 10 180 and an exit check without --test-force-exit).
2026-10-22T16:36:30Z | Della row224 C2-05b #1725 preparation | Before: C2-05a queries exist at 4f3fbc25a but no broker lookup verbs or typed tools. Scope: broker/tools plus Sage-authorized runtime/bundle integration, tests and docs. Existing human-only library reads retained; five agent queries require a live holder and trusted configured source. Code/docs only under row253 gate; no Node/test/push/claim. Runtime ownership coordinated with Rocko219/Huey272.
2026-10-22T16:52:55Z | Rocko | Row219 prepared under sage2793/2807; explicit creative thread/default-session selection, narrow metadata route/picker U1–U5,22 static mutant sites; unrun while253 holds.
2026-10-22T17:42:16Z | Sage | Row 253 (#1758, Gander, Duckworth) LANDED 48e450ce8. Before: refactor at aca16404c; case-sudo calls had no supervised session check and the M21 survivor hid behind a fast-exit fixture (mos-1297). After: a fast-forward of 22 commits; manifest 591020a1 rechecked with 0 mismatches; run 5 whole gate green with M1-M40 killed, the M38 PID equals SID assertion added under mos-1298; Duckworth APPROVE rev 3252, note rev 3253, done rev 3254; #1758 comment 30701 and closed; P19 416 comment 719. N1 (fast child reported as exit 85) is DEFERRED as Vikunja 532.
2026-10-22T18:11:57Z | Sage | Row 275 (#1780, Morgana, Duckworth) LANDED 3410df7c3. Before: refactor at 48e450ce8; viewers had no verb list of their own and board reads were not refused to users. After: a fast-forward of 3 commits; manifest 39847b15 (4 paths) blobs match; gate 17:52:12Z-18:05:00Z green (whole files 7/22/6, bus 834, cli 861, harness 151, business 231, seat 167, 10 scripts with 3 named skips), M6 and M10 killed; Duckworth APPROVE rev 3260 (comment 30717); note and done by sage-2897; #1780 comment 30728 and closed. N1 (board refusal test as a loop over BOARD_VERBS) is non-blocking.
+5
View File
@@ -27,6 +27,11 @@ not fleet seats. Keep changes in `/mnt/storage/src/mosaic-stack`; do not modify
| Gizmoduck | Federation design (#414) and builder of FED R1 | T3 session (Claude, Opus 5.5, thread 9312a896) | none, T3 only |
| Webby | Reviewer: Case 2 designs, rows 192 and 194 | T3 session (Claude, Opus 5.5, thread 03086006), started by Mos (mos-0981) | none, T3 only |
| Duckworth | Reviewer: federation designs, row 193 | T3 session (Claude, Opus 5.5, thread ffceafec), started by Mos (mos-0977) | none, T3 only |
| Della | Builder, email plan (Vikunja 460) and row 224 (C2-05b) | T3 session (Codex, gpt-6.1-sol, thread 66735a7f), started by Mos | none, T3 only |
| Scrooge | Builder of row 252 and reviewer of row 272 | T3 session (Claude, Opus 5.5, thread 629b6cd3), started by Mos | none, T3 only |
| Huey | Builder, row 272 (T1 sessions) | T3 session (Codex, gpt-6.1-sol, thread cabd45ca), started by Mos | none, T3 only |
| Louie | Builder and research, editor evaluation and row 174 | T3 session (Codex, gpt-6-luna, thread bfd13d49), started by Mos | none, T3 only |
| Ludwig | Builder and reviewer, post-v1 email PRD and row 258 (E01) | T3 session (Claude, Opus 5.5, thread 8615b2a6), started by Mos | none, T3 only |
| Sage | Project lead: coordination, review, integration; earlier DYOR strategy records retained | T3 session (Claude Code); Pi launcher `zai/glm-5.3:high` retained | `agents/sage/launch.sh` |
Each script supports `--check` and `--fresh`. Normal launches resume the agent's
+8
View File
@@ -0,0 +1,8 @@
# Della
Builder seat, Codex gpt-6.1-sol, started by Mos for the email plan (Vikunja 460). Owns row 224 (C2-05b) under Q220.
No launcher in this directory. The seat runs as a T3 thread (thread 66735a7f) and works in its own worktree. It has no
credentials of its own; it pushes only its own `della/*` branches through the repository credential helper (mos-0981),
never `refactor`, `main` or another seat's branch. Registered by Sage on 2026-10-22 so the queue can name the seat as
row owner under Q220 (mos-1301).
+16 -1
View File
@@ -77,4 +77,19 @@ from overlapping, so the first pass finds at most one.
## Runs
None yet. Every run waits for Sage's CLEAR.
- r2 ecd9b35eb on dcd8d4dda, Sage's CLEAR sage-2449, one pass with the first red stopping it
(run-268-c.sh), 2026-10-22 04:40:22Z to 04:42:17Z, 115 s. M1, M2 and M3 all killed (scope.test
2 of 3 failed, mapping.test 2 of 11, mapping.test 1 of 11), and each file's sha256 matched after the restore.
Brief files 14/14, library 83/83, business 209/209. test-auth 15, test-conductor 17, test-config
31, test-discord 78, test-extension-package 18, test-foundation 44, test-land-work 86, test-queue
28 plus 156 node, test-release 14, test-task 91, with 0 failures. Node skipped 0. The scripts
skipped three cases: test-queue's verify and render check (not the canonical root) and
test-task's live provider cases twice (row 244). Neither test-task skip is a dead-Docker skip.
- r3 aca53c4f0 on 0fa7bf421, after row 191 landed. The cherry-pick of r2 has range-diff `=` and
stable patch-id 2942acc8e6e2b0171b96ebbf033ab5c08c0f0eac. Rerun on CLEAR sage-2461, 04:50:29Z to
04:50:38Z: brief files 14/14, library 83/83 (survey.test on 191's scripts/mosaic), business
209/209, 0 skipped. Webby approved r3 as a carry (webby-0112).
- r4 c27207f6e on ce6a96ad6, after row 205 landed (sage-2466: 205's gate went first). Cherry-pick
of r3 with range-diff `=` and the same patch-id. Rerun on CLEAR sage-2480, 05:04:50Z to
05:04:58Z: brief files 14/14, library 83/83, business 209/209, 0 skipped.
- Every sweep found the worktree clean and no node process in it or in its temp dir.
+522 -145
View File
@@ -114,6 +114,291 @@ Webby's round d1 on this ruling (webby-0088, REVIEW-d1 at webby/reviews 35130c26
refusal for a prefix that holds a book folder (B1), and asked for the `unmapped` row, the `kind` values,
the readers and the section 8 line (N1-N4). All five are in the text above, in 1.2 and in 8.
## Ruling after r11: idea items capped at 3 of 120 characters (Q219)
Jason answered Q26 with "yes" (DECISIONS Q219, mos-1299, Vikunja 465, ludwig-0014 Q4). An `idea` item
is at most 120 characters, at most 3 per reply. The rest of M5 is unchanged: 10 items and 1,200
characters of item text per reply, 300 for `fact`, `finding`, `correction` and `question`, and the
correction edit distance. The residual in 5.1 is now 360 characters of free suggestion text per reply,
and 10 step 8 is closed by Jason. C2-06 hasn't landed, so it builds with the new numbers, and its tests
refuse one past each new boundary and accept at it.
## Ruling after r11: reply limits are settings (Q222)
Jason ruled Q222 (DECISIONS Q222, mos-1302): "Things should NOT be hardcoded." Q219 stands for now, and its
numbers become the defaults. Every M5 limit is a setting in a new top-level `replyLimits` object of the
business file:
| Key | Default | Valid value |
|---|---|---|
| `itemsPerReply` | 10 | an integer from 1 to 1,000 |
| `itemTextPerReply` | 1,200 | an integer from 1 to 100,000 |
| `ideasPerReply` | 3 | an integer from 0 to 1,000 |
| `ideaLength` | 120 | an integer from 1 to 100,000 |
| `itemLength` | 300, for `fact`, `finding`, `correction` and `question` | an integer from 1 to 100,000 |
| `correctionMinDistance` | 12 | an integer from 0 to 100,000 |
| `correctionPercent` | 25 | an integer from 0 to 100 |
A `correction`'s `to` is within edit distance max(`correctionMinDistance`, `correctionPercent`% of
`from`) of its `from`.
Where. The object sits beside `projects`, not inside the library entry, because M9 keeps `projects` to
hand edits and Mos asked for a CLI verb. `TOP_KEYS` (`packages/business/src/business.mjs:24`) gains `replyLimits`, and
`validateBusinessDocument` accepts any JSON value under it. A bad hand edit then never stops her business
from loading.
Who changes them (M11). Only the business file's `human`, through a new human verb `library.limits.set`
with `revision` and `replyLimits`. She runs it as `mosaic limits replies --file <json>`, a new verb in
`packages/cli/src/limits.mjs` beside `mosaic limits set`, and later from the Console. The verb replaces the whole object, and a key left out means its default. It sits outside
`ORGANIZATION_KEYS` and `TESTER_ORGANIZATION_VERBS`, as `instances.set` does (`broker.mjs:219`, `:139`,
sage-0918). An agent session gets `human-required` and the tester gets `tester-refused`. A value outside the
table refuses `invalid-request`, and so does a key not in the table. Nothing is written.
The fence that covers every other path sits in `organizationWriter.write`
(`packages/business/src/organization.mjs:180`), next to M9. It compares against the file on disk, `old`
in `write`, never against the incoming document. A write whose `replyLimits` differs from
`old.replyLimits`, compared in canonical JSON, refuses `organization-reply-limits-human-only` unless both
hold:
- `actor` equals `old.human`. A write that sets `human` to its own actor in the same document still
compares against the old `human`, so it refuses.
- `actor` isn't `tester` and isn't `<definition>.<role>` for any role in `old.roles`, the two shapes of
an agent actor (`broker.mjs:4201`, `:4206`, `:4306`). `ID_PATTERN` (`vocabulary.mjs:199`) allows a dotted
`human` id, and the broker refuses only `tester` (`broker.mjs:328`), so without this a human id such as
`assistant.bean` would let that agent through.
As M9 does (`organization.mjs:191-193`, `:199-200`), the fence checks `document` and then `written`, the
JSON round trip, so a value with a `toJSON` can't differ between the check and the disk. That stops the
chief of staff's `organization.set`, any actor's `limits.set`, `instances.set`, `routes.set` and
`plugins.set`, the tester, and a direct call to `write`. Every setter builds its document with
`structuredClone` of the file, so none of them changes `replyLimits` today and the fence refuses no
ordinary write.
Read at check time. The guard reads `replyLimits` from the business file on each check, through the
organization reader, not from the broker's copy loaded at start (`broker.mjs:419`, read by `#business`
at `:570`). A change applies to the next reply with no restart.
A failed read never skips the check. If the business file is missing, can't be read, isn't JSON or isn't
an object at check time, the guard checks the reply at the defaults, except `requireCitation`, which
resolves to `true` (webby-0151 R2, ruling after r11), and `library.status` shows
`reply-limits-unreadable`. The reply is neither refused for that alone nor let through unchecked.
Fallback. Each key resolves on its own. The guard reads a key only if `Object.hasOwn(replyLimits, key)`,
and uses it only if `Number.isInteger(value)` is true and the value is within the table's range. Anything
else resolves to its default: a missing key, `null`, a boolean, a string such as `"5"`, an array such as
`[7]`, an object, a fraction, `NaN`, a negative number, a value below the minimum or above the ceiling.
Every key resolves to its default when `replyLimits` is missing or isn't a plain object, an array
included. The guard never compares a value it hasn't checked with `Number.isInteger`, so JavaScript's
coercion of `true` or `[7]` to a number can't reach a range check. Unknown keys are ignored when read. The
guard has no path without a number, so no value reads as "no limit". `library.status` shows the limits in
force and names each key that fell back.
The ceilings are validity bounds, not policy: they keep a value from reading as unlimited. 100,000
characters is far past any reply, and Mos can change them. At the ceilings the human can make the guard
loose. The idea residual is `ideasPerReply` times `ideaLength`, up to 100,000,000 characters at both
ceilings, and `correctionPercent` 100 lets a correction replace its whole `from`. Cites stay required at
every value. `library.status` shows the residual, and the setup sheet says so in one line.
What agents see. The contracts don't state numbers. A refusal reason names the key and its value in
force, so the runner's retry (C2-07) tells the agent the limit it broke.
Hold, now lifted. Q29 is answered (Q223, Q226). The next ruling replaces this table's seven keys with
`requireCitation` and `citationsPerReply`, and C2-06 builds that.
## Ruling after r11: agents never write story (Q223, Q226)
Jason answered Q29, and Mos's reading of it, Q30, is confirmed (DECISIONS Q223 and Q226, mos-1305,
mos-1309). F40 to F43 come with it, and mos-1308 sets the tool set. This ruling follows my impact list,
gosalyn-0148, with Mos's eight points, Mos's rulings on my flags in mos-1320, and Webby's round,
webby-0151 R1 to R3 and N1 to N5. It replaces D3, D5, M5, most of 5.2, the Q222 hold and 7.3's
"after the slice". Sections 0, 1.1, 1.2, 2.1, 3, 4, 5, 7, 8, 9.2 and 10 say so.
Story and supporting. Her chapters and story files stay read-only to agents. A human marks folders as
supporting through a new `supporting` list in each universe entry, beside `playground` (1.1). An agent
may create, edit and delete `.md` files under a supporting prefix, and nothing else in her repo. A folder
she hasn't marked is story. A folder in both `playground` and `supporting` is writable. A playground
folder alone isn't (Mos point 4). 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 prefix holds a book folder or another
supporting prefix, and one inside or holding another universe's book folder refuses the business file
with `library-supporting-other-universe`. M9 already keeps the list to hand edits, since it sits in
`projects`.
Chat. A reply is free text of any length, within the broker's message limits as they are today. The item
list goes, and with it the item kinds, `CREATIVE_KINDS`, the M5 caps, the three notices,
`cited-reply-required`, `reply-kind-not-allowed` and `correction-unbound`. A reply may carry citations
(5.2). Jason's "NEVER" now reads as story in her files, and M12 enforces it in code.
Citations are tunable (F43). `replyLimits` keeps the Q222 mechanism: the object, `library.limits.set`, M11
and the read at check time. The seven Q222 keys go, since their subject is gone. The keys now are:
| Key | Default | Valid value |
|---|---|---|
| `requireCitation` | `false` | `true` or `false` |
| `citationsPerReply` | 20 | an integer from 1 to 1,000 |
The fallback rule holds. A key is read only if `Object.hasOwn` finds it, `requireCitation` only when
`typeof` gives `boolean`, and `citationsPerReply` only when `Number.isInteger` is true and the value is in
range. Anything else resolves to the default, and `library.status` names the key. So a bad
`requireCitation` hand edit reads as off, which is Jason's default and not a weaker guard than he chose.
The verb refuses it with `invalid-request`. One case differs from the defaults. When the business file
can't be read at check time (the Q222 rule), `requireCitation` resolves to `true`, so a setting she turned
on doesn't fail open, and `citationsPerReply` takes its default. `library.status` shows
`reply-limits-unreadable`, and the runner's fixed texts still pass (webby-0151 R2). When
`requireCitation` is true, a reply with no citation refuses `citation-missing`. When a reply carries
citations, the four checks in 5.2 run whatever the setting.
Links. Wiki links already in her files stay. The hub never rewrites a file it wasn't asked to write, and
agents can't write a chapter at all, so they add no link inside one. Their cross-links go in supporting
docs, and Obsidian's graph shows those. The `[CH]` alternative, items 42 to 46 of my impact list, is
dropped. A Console brain map is a later option and has no row.
The write path. Two agent verbs, each with a typed tool:
- `library.doc.write {universe, path, base, content}`, tool `library_doc_write`. `base` is the blob id the
agent read, from `library_get`, or `null` to create.
- `library.doc.delete {universe, path, base}`, tool `library_doc_delete`.
Only a session of a `CREATIVE_DEFINITIONS` instance holding the new action `library.write` and a live
Gitea grant on the library project may call them (`library-write-refused`, `library-write-no-grant`).
The tester gets `tester-refused`. M12 checks the path before anything else. `content` is UTF-8 with no
NUL, at most 1 MiB, the reader's R1 limit. An agent's call passes the runner's tool socket and the bus
socket, and both cap a request at 64 KiB today (`runner.mjs:294`, `server.mjs:62` at aca16404c). So an
agent write over that refuses `request-too-large` before the broker sees it, and the 1 MiB limit binds
only her saves. `library.file.save` joins the `project.file.save` exception at `client.mjs:19` and
`server.mjs:9` and `:62`, with the same `PROJECT_SAVE_REQUEST_MAX`.
Each write is a plumbing commit on the bare mirror, with no working tree. Under the library lock the bus
fetches, reads the path at the remote tip, and compares it with `base`:
1. Equal, or both absent for a create: it writes the blob, builds the tree from the tip's tree with
`GIT_INDEX_FILE` pointing at a temporary index outside the mirror, and runs `commit-tree` with the tip
as the one parent. The author and committer are the seat's `identity`. The message is a fixed line,
`Mosaic: write <path>` or `Mosaic: delete <path>`, and a trailer `Mosaic-Agent: <business>/<instance>`.
The agent supplies no message text.
2. Not equal: it refuses `stale-base` and writes nothing.
3. It pushes `<commit>:refs/heads/<branch>` with no `+`, so the push is fast-forward only (D6). A
rejection for a moved tip fetches once more and goes back to step 1 one time. A blob still equal to
`base` re-applies on the new tip. Otherwise the write refuses `stale-base`.
4. Any other push failure refuses `library-push-failed`, and the commit is left unreferenced. Nothing is
kept to push later, so there is no `unpushed` state and C2-17 drops.
5. After the push, the bus moves `refs/remotes/library/<branch>` and the served ref to the new commit,
which is a fast-forward of the served commit, rebuilds the index, records `lastPush` in the sync state
and stores a notice.
The notice is a message from the writing instance to `human` in that instance's thread. The broker
builds its body, `{"notice": "library-write", "op", "path", "commit"}`, and no agent text reaches it.
The Console shows it as a fixed line with a Revert button (7.4).
`library.revert {commit}` is the human's. It refuses `library-revert-not-agent` unless `commit` carries
this business's `Mosaic-Agent` trailer and is reachable from the served commit. For the one path that
commit touched, it puts back the parent's blob, or deletes the path if the parent had none, as a new
commit by the same steps, with `base` the commit's own blob. A later change to that path makes it refuse
`stale-base`. A revert is her act, so its author and committer are her `editor`, and it pushes with slot
`library-human`, as her saves do. It still works after the agent's grant is revoked or the seat is fired.
A cite to a file whose last commit at the cited commit has a `Mosaic-Agent` trailer is labelled
agent-written (Mos point 3). The broker reads the trailer with `git log -1 --format=%(trailers:key=Mosaic-Agent,valueonly) <commit> -- <path>`,
and the label rides in the stored reply's check result, so no agent can set it.
M12 is the fence. A new pure function in `packages/library`, `supportingPath(mapping, universe, path,
tree)`, and the broker calls it on every agent write and delete. It refuses `library-write-story-path`
for a path that isn't a `.md` file under a supporting prefix of `universe`. That covers an unmapped path,
a `.canvas` or `.base` file, an attachment, any 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 whose any prefix in the tip's tree is a symlink
or a gitlink, a path that differs only in case from a tree entry it doesn't equal, and a path over 1,024
bytes. A delete also needs the path to exist at the tip as a `100644` blob.
Who writes, with what (F40). The shared bot account goes. Each writing seat writes with its own Gitea
account and token from the per-seat grant path (Q190 O1, #436b, row 236). The token stays in the bus's
credential store, out of every sandbox (#436b section 3c), and no agent session holds it. The askpass
helper from 7.2 reads the seat's store file for that one push, with the seat's Gitea user as the only
username it answers.
The chief of staff of her business is enrolled at install and grants the other seats through the
existing step: the chief raises a `credential.mint` decision with a gitea grant for `books`, and the
executor runs on its existing approve (436b section 4, step 0). Whether each such grant also needs
Jason's Approve is Q31, asked of him. This design adds no step for it, and a yes adds none here.
The executor today forks each project and refuses any other reach (436b steps 4 and 5). A library
project can't work that way, since the writes must land in her repo. C2-21 changes the two steps for a
project of `kind: "library"` only:
- The raise check. `#giteaRole` (`broker.mjs:2122-2135` at aca16404c) refuses `gitea-not-declared` for
any creative role, whatever its definition declares (mos-1163). F40 narrows that ruling. A creative
role passes only when every project the raise names is `kind: "library"`. A raise for a creative role
that names any other project still refuses `gitea-not-declared`, so a creative seat never gets a fork.
The other checks stay: the definition's gitea credential, `identity.gitea` and a non-empty `publish`,
which C2-08b supplies. Mos ruled the narrowing in mos-1320 item 2: mos-1163 still covers every gitea
project except `kind: "library"`, and covers host grants whole. The host grant writer, the executor's
step 1 recheck and `host.run` keep refusing a creative role with `host-creative-role`. The comment at
`broker.mjs:2120` cites mos-1320.
- Step 4 adds the seat's user as a collaborator on the project's own repository with the admin token,
`PUT /repos/{owner}/{repo}/collaborators/{user}`, in place of a fork. The permission is `write` for a
`CREATIVE_DEFINITIONS` instance and `read` for any other, the chief included. The definition decides
it, and the raise can't name one.
- Step 5 accepts that repository in the reach list when its `permissions` show exactly that level, with
`admin` false. Anything wider still refuses `gitea-reach-wider`.
- The rendered block names the repository and the level instead 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, which ends the write. The collaborator entry stays, as forks do.
A fire's delete sets `prohibit_login` as today.
Two facts aren't proven yet, and C2-21's first test proves them on a scratch Gitea 1.27.1 before any code,
as F1 to F11 were: that the admin token's scopes (436b 3b) can add a collaborator to a repository the
admin account doesn't own, and that `/user/repos` with the seat token shows that repository with the
granted `permissions`. If either fails, C2-21 stops and goes to Mos.
The mirror's fetch uses the chief's token, whose grant is `read`, and the chief's user in the askpass
helper. That replaces the bot slot `library-gitea` from C2-03a. C2-03b makes the change. The fetch falls
back to slot `library-gitea` only while the bus has never recorded a gitea grant event for the chief.
The check reads the append-only events table (`packages/bus/schema.sql:131-132` at aca16404c) for any
`GRANT_EVIDENCE` event (`broker.mjs:246`) with `method` `gitea` and `agent` the chief. It never reads
`gitea_grants`, whose row a revoke and a grant-lost delete (`broker.mjs:2785`, `:2609`), and which has
no state column (`schema.sql:469-480`). It names no project, since `grant.applied` and `grant.revoked`
carry none (`:2469`, `:2787`), so a chief grant on any project ends the fallback, which fails safe
(webby-0156 R4). An install that runs before C2-21 still syncs. Once such an event exists, a revoked
or `held` grant, or a fired chief, refuses the fetch with `library-fetch-no-grant`, and `library.status`
shows it. A write, a revert and a save never fall back to the slot. When `library.status` shows the fetch
on the chief's grant, Jason deletes slot `library-gitea` (10 step 4b, webby-0151 R3).
Her edits (F41). She edits in the Console. Each save is one commit by the same steps, through the human
verb `library.file.save {path, base, content}`. The author is a new `editor` object in the library entry,
`{name, email}`, which the setup sheet asks her for, and the push uses her own Gitea token from a new slot
`library-human` (mos-1320 item 3). A save or a revert with no `editor` refuses `library-mapping-invalid`.
One with slot `library-human` missing or failing the helper's file checks refuses `library-human-missing`
before any git call. Her saves reach any mapped `.md` file. M12's other path rules hold for them, and only
the supporting rule doesn't, since the files are hers. They still refuse
`stale-base` when the file moved, because an agent write or an Obsidian push can race her save. She sees
an agent's write at once in the Console, and Obsidian sees it on its next pull. Agents see her save at
once too, since the bus moves the served ref after its own push. Her Obsidian pushes reach them through
the 60-second sync. The editor is a Q195 add-on, so C2-11a and C2-11b are slice rows that don't gate
10-27.
Rows (F42). C2-02, C2-03b and C2-11b join the slice, and C2-11a with them, since C2-11b needs it. Sage
queues them. C2-07 folds into C2-06. C2-17 drops. Five new rows carry what landed or built rows can't:
C2-01d, the `supporting` list and scope, C2-05c, the `supporting` scope and `blob` in the lookups,
C2-08b, the role changes, C2-12b, the setup sheet rewrite, and C2-21, the grant change. C2-05b is row
224, built to its earlier brief, and it lands as built (sage-2833). Its tools already match M3 below.
Section 8 lists them. Mos asked for a PRD row too: REQ-LIB-6 rewritten to Q223 and Q226, before C2-01d
and C2-02 (mos-1320 item 8).
M3 (mos-1308, Mos point 8). A creative session's typed tools are exactly the five lookups,
`send_message`, `library_doc_write` and `library_doc_delete`. `companion` and `research` also get
`web_search` and `web_fetch` through the existing web grant. So the per-role snapshot holds eight names,
or ten for those two. mos-1308 said six or eight, before Q226 added the two write tools, and Mos
accepted eight and ten in mos-1320 item 4. None of `read_spec`, `list_agents`, `inbox`, `trail`,
`write_record`, `raise_decision` or a tracker read reaches a creative session.
`packages/harness/src/tools.mjs` at aca16404c adds all but `raise_decision` to every session, and
already skips `raise_decision` for a creative definition (`:241`, webby-0151 N3).
Residual. A supporting doc can hold any text, a drafted scene included, and code can't tell a scene from
a note. Every agent write is visible as a notice and a trailer, and she can revert it. Nothing an agent
writes lands in a story path (M12). At Gitea the seat token can write any path of her repo, and the bus's
check is the only path limit. The token never leaves the bus, so the limit holds while the bus code does.
C2-21's scratch Gitea run also records two facts that don't gate it: whether a protected file pattern
refuses a seat user's push to a story path while her push still passes (mos-1320 item 5), and whether
branch protection refuses a seat user's force-push while her normal push passes (webby-0151 N4). If the
first works, Mos files a task for it as a second fence after the slice. The second would back D6 at the
remote and wouldn't narrow the paths.
## Changes from r10
Webby returned r10 with B1-B3 and N1-N5 (webby-0036, `agents/webby/work/case2-r10/REVIEW.md` at
@@ -407,7 +692,8 @@ the line says so.
- B4. On a library business the conversation reader leaves creative sessions out of `catalogue()` and
refuses to open one by id (5.1 M7, C2-10a in the slice).
- B5. At most 10 items and 1,200 characters of item text per reply. An `idea` item is at most 200
characters, at most 5 per reply. Other kinds are at most 300 (5.1).
characters, at most 5 per reply. Other kinds are at most 300 (5.1). Q219 later set ideas to at most 3
of 120 characters (ruling after r11), and Q222 made every limit a setting with those defaults.
- B6. `correction.from` must equal one cite's `quote`, inside that cite's lines (5.2).
- B7. The broker keeps fetched page text per session, bounded and in memory, not in a table. The bundle
refuses the `harness-builtin` web source for creative definitions (5.2, 5.3).
@@ -441,10 +727,12 @@ the line says so.
Melanie's writing already lives in a git repository, her books repo on git.mosaicstack.dev, and her
Obsidian syncs it with obsidian-git. The hub reads that repo. The bus keeps a bare mirror of it, fetched
with a read-only token, and the business file maps her folders onto books, types, canon and playground.
Agents can't write to it at all. They read it through typed lookup tools. Every message an agent sends
her must be a short list of items, each item must cite her repo or a page the agent fetched, and the
broker checks each citation before it stores the message. Each creative agent holds its own thread.
with the chief of staff's read token, and the business file maps her folders onto books, types, canon,
playground and supporting folders. Agents never write her story. They read it through typed lookup
tools, and they may create, edit and delete `.md` files only in folders she marked as supporting. Each
such write is a commit she sees as a notice and can revert. An agent's reply is free text. Any citation
it carries must be real, and the broker checks each one before it stores the message. Each creative
agent holds its own thread.
Bean and Chet, her two agents from Hermes, keep their names and personalities as Mosaic agents, and the
specialist roles sit beside them. She talks to them in the Console and sees only stored messages that
passed the guard. Hermes keeps running until she has moved over.
@@ -454,14 +742,16 @@ Decisions (D-codes are this document's own):
- D1. Her repo stays as it is. The design adapts to her layout through a mapping in the business file,
which agents can't write.
- D2. Scope comes from the path. No file content can change a file's scope.
- D3. Agents get no write path into her repo. That covers canon, playground and everything else.
- D3. Agents never write story. An agent writes only `.md` files under a supporting prefix, and M12
enforces that in code (ruling after r11, Q223, Q226). Canon, playground that isn't also supporting, and
everything else stay read-only to agents.
- D4. One chat is a project. Each creative agent holds its own thread in it. The `threads` table and its
triggers don't change (section 6).
- D5. In the slice the bus reads a bare mirror and never writes. Bus writes come later, through the
record repo commit path (`packages/project/src/index.mjs:195-234`).
- D5. The bus writes to her repo only as plumbing commits on the bare mirror, pushed fast-forward only:
an agent's supporting doc write, the human's revert and her own Console save (ruling after r11).
- D6. The Gitea remote is where the bus and Obsidian meet. The bus only fast-forwards and never merges.
- D7. Anything fetched from the remote is untrusted input. The bus reads it as blobs, never checks it out
in the slice, and never runs anything from it.
and never runs anything from it.
- D8. Every channel to Melanie shows only messages the broker stored after the guard. A channel that
shows a model's raw output is refused on a library business.
@@ -486,8 +776,10 @@ The mapping is a new form of business file project entry. Business files live in
"vaultPrefix": "<repo folder name under ~/src>",
"universes": {
"<u>": {"name": "<display name>", "books": ["<book folder>", "<book folder>"],
"playground": ["<book folder>/<playground folder>/"]}
"playground": ["<book folder>/<playground folder>/"],
"supporting": ["<book folder>/<notes folder>/"]}
},
"editor": {"name": "<her name>", "email": "<her email>"},
"types": {"<characters folder>": "character", "<locations folder>": "place",
"<research folder>": "research", "<tracker folder>": "tracker",
"<chapter summaries folder>": "summary"},
@@ -508,6 +800,12 @@ The mapping is a new form of business file project entry. Business files live in
cite check (5.2) has something to compare (ruling after r11, three levels). Her repo has no playground
today as far as I know. The setup sheet suggests one folder per book, for example
`<book>/playground/`. Until she makes one, everything mapped is canon.
- `supporting` is a second list of path prefixes in each universe entry (ruling after r11, Q223). An
agent may write `.md` files under a supporting prefix and nowhere else (M12). The prefix rules are the
playground's, and the refusal for another universe is `library-supporting-other-universe`. A prefix
may sit in both lists, and that folder is writable. A playground prefix alone isn't. Her repo has no
supporting folder until she names one, and until then agents write nothing.
- `editor` is the name and email her Console saves commit under (F41). The setup sheet asks her for it.
- `types` maps a type folder name to a type. A file's front matter `type`, if present, is shown next to
it but doesn't change scope.
@@ -527,14 +825,16 @@ Each path in her repo, relative to the repo root, gets one scope:
| Scope | Rule |
|---|---|
| `playground` | A `.md` file that starts with a playground prefix of any universe entry, drafts included |
| `supporting` | A `.md` file that starts with a supporting prefix of any universe entry, drafts included. This comes first, so a folder in both lists is supporting. Agents may write it (M12) |
| `playground` | Otherwise, a `.md` file that starts with a playground prefix of any universe entry, drafts included |
| `canon` | Otherwise, a `.md` file inside a configured book folder |
| `unmapped` | Everything else: top-level notes, `.base`, `.canvas` (under a playground prefix too), attachments, anything outside a book folder and outside every playground prefix |
| `unmapped` | Everything else: top-level notes, `.base`, `.canvas` (under a playground or supporting prefix too), attachments, anything outside a book folder and outside every playground and supporting prefix |
Lookups default to canon and return playground only on request. Unmapped files are never returned or
citable. The universe of a canon file is the universe whose `books` list holds its book folder. The
universe of a playground file is the entry whose prefix holds it, and its book is the book folder that
holds the prefix, or `null` at universe level and in a draft (ruling after r11, three levels).
Lookups default to canon and return playground or supporting files only on request. Unmapped files are
never returned or citable. The universe of a canon file is the universe whose `books` list holds its book
folder. The universe of a playground or supporting file is the entry whose prefix holds it, and its book
is the book folder that holds the prefix, or `null` at universe level and in a draft (ruling after r11,
three levels).
### 1.3 Reading and reporting
@@ -571,7 +871,7 @@ setup sheet says so (C2-12).
Why reporting is enough for separation. The guarantee Jason asked for is that agents keep playground and
canon apart. The code that does that is the scope rule, the lookups' default and the cite check in 5.2,
which refuses a playground cite on a `fact`, `finding` or `correction`. None of those needs her files to
which refuses a playground or supporting cite marked as canon. None of those needs her files to
pass a rule. Refusing her latest commit would only stop the agents seeing her newest work.
### 1.4 Hybrid crossover (after the slice)
@@ -590,9 +890,8 @@ C2-15 builds this.
### 1.5 Promotion (after the slice)
Promotion moves files from a playground prefix into a book folder. Only Melanie does it, in Obsidian
today. A Console verb `library.promote {from, to}` comes with the write rows (C2-19). It is human only,
moves every file in one commit through a multi-path form of `commitFile` (today that commits one path,
`index.mjs:218-219`), and pushes.
today. A Console verb `library.promote {from, to}` comes later (C2-19). It is human only and moves every
file in one plumbing commit, by the steps of the ruling after r11 with several paths, and pushes.
## 2. What exists and how Case 2 uses it
@@ -617,12 +916,11 @@ moves every file in one commit through a multi-path form of `commitFile` (today
sets `protocol.allow=never` (`git.mjs:8`), and `inspectGit` refuses any `remote.*` key (`git.mjs:44`).
The mirror keeps both rules. The URL goes on the command line from the business file, and fetch uses
its own command builder (7.2).
- Bus writes, later (C2-02): `commitFile` (`index.mjs:195-234`) handles the lock, `inspectGit`,
`requireClean`, the layout check, publish, commit and rollback. `writeHumanFile` (`index.mjs:248-261`)
is the model for `writeLibraryFile`. C2-02 refactors `commitFile` to take the check function (it calls
`checkProject` at `:209`) and a list of paths, and adds r1's S0 rule for files in the working tree.
- Bus writes (C2-02) don't use `commitFile` (`index.mjs:195-234`), which commits in a working tree. The
mirror has none (D7). C2-02 writes plumbing commits on the bare mirror by the steps of the ruling after
r11, under the library lock and after M12. `commitFile` and `writeHumanFile` stay as they are.
### 2.2 ProseMirror editor (`packages/webui/editor`), after the slice
### 2.2 ProseMirror editor (`packages/webui/editor`)
- It edits Markdown (`model.mjs:17-18`). Blocks it can't draw, front matter included, are kept byte for
byte (`model.mjs:3-11`, `:220-231`).
@@ -656,8 +954,9 @@ moves every file in one commit through a multi-path form of `commitFile` (today
- `typedTools` (`tools.mjs:91-261`) builds the typed tools per session from held actions. `write_record`
exists only for `record.write` holders (`:211-220`), and `send_message` for `message.send` holders
(`:95-102`). `raise_decision` goes to every agent (`:221`), with free-text `question` and option `text`.
- Case 2 adds the lookup tools (section 3) when the business has a library project, and drops
`raise_decision` for creative definitions (5.1 M6).
- Case 2 adds the lookup tools (section 3) when the business has a library project, adds the two write
tools for a creative session holding `library.write`, and drops `raise_decision` and the other
non-creative tools for creative definitions (5.1 M3, M6).
### 2.5 Threads, messages, conversations and Discord
@@ -670,9 +969,10 @@ moves every file in one commit through a multi-path form of `commitFile` (today
(`packages/bus/schema.sql:296-299`) and deliver only to the thread's holder (`:300-306`). `threads`
can't be updated (`:310`). Thread names are unique per project (`:249`).
- The runner sends the model's final text as a `message.send` RESULT to the sender
(`packages/harness/src/runner.mjs:446-450`). It sends `(the session gave no answer)` for an empty turn
and `The turn failed: <reason>` for a failed one (`:449`). A refusal is logged and the message is still
marked read (`:451-456`), so the answer is lost.
(`packages/harness/src/runner.mjs:571-574` at aca16404c). It sends `(the session gave no answer)` for
an empty turn and `The turn failed: <reason>` for a failed one (`:572`). A refused send is logged and
`message.read` is skipped (`:577-583`), so the message stays unread and the answer never reaches her
(webby-0151 N2).
- Sessions are per thread: `threadStore(dataRoot, business, thread)` (`packages/seat/src/session.mjs:59-64`)
and the Claude session id file under it (`adapters/claude/adapter.sh:47-53`). One thread per agent
means one session per agent.
@@ -696,8 +996,8 @@ reads the mirror at its current served commit (7.2) and returns that commit.
| Tool | Arguments | Returns |
|---|---|---|
| `library_find` | `universe` (required), `query`, `type?`, `book?`, `scope?` (`canon` default, or `playground`) | up to 50 `{path, type, name, appears_in, scope, universe, book}` |
| `library_get` | `path` | `{path, commit, scope, frontmatter, lines: [[n, text]], links_out, links_in}` |
| `library_find` | `universe` (required), `query`, `type?`, `book?`, `scope?` (`canon` default, `playground` or `supporting`) | up to 50 `{path, type, name, appears_in, scope, universe, book}` |
| `library_get` | `path` | `{path, commit, blob, scope, frontmatter, lines: [[n, text]], links_out, links_in}` |
| `library_search` | `universe`, `text`, `book?`, `scope?` | up to 50 `{path, line, excerpt, scope}` |
| `library_manuscript` | `universe`, `book`, `scope?` (`canon` default), `chapter?`, `from?`, `to?` | `{path, commit, scope, lines}`, one chapter file, at most 400 lines per call |
| `library_universe` | `universe` | its books, type folders, and for a hybrid the sources, constraints and notes |
@@ -712,22 +1012,32 @@ reads the mirror at its current served commit (7.2) and returns that commit.
Her repo has 203 Markdown files today. I estimate a rebuild well under a second, and C2-05a's gate
measures it on a 3,000-file fixture.
Writes. `library_doc_write {universe, path, base, content}` and `library_doc_delete {universe, path,
base}` call the agent verbs of the ruling after r11. Only a creative session holding `library.write` gets
them (M3). `base` is the `blob` that `library_get` returned, or `null` to create. Each returns `{commit,
path}` or a refusal code, and a successful write also stores the notice.
## 4. Creative roles, Bean and Chet
Six new role definitions. Each is a version 2 role file plus a `.md` contract.
| Role | Purpose | `tools` | Reply item kinds |
|---|---|---|---|
| `companion` | Her general writing partner. Backs Bean and Chet | `read`, `web` | `idea`, `question`, `finding`, `fact` |
| `ideas` | Brainstorming, mostly in the playground | `read` | `idea`, `question` |
| `research` | Real-world facts from the web | `read`, `web` | `fact`, `question` |
| `proofreader` | Spelling, grammar, punctuation | `read` | `correction`, `question` |
| `fact-checker` | Text against canon and her research notes | `read` | `finding`, `question` |
| `continuity` | Consistency across books, timeline and her tracker | `read` | `finding`, `question` |
| Role | Purpose | `tools` |
|---|---|---|
| `companion` | Her general writing partner. Backs Bean and Chet | `read`, `web` |
| `ideas` | Brainstorming, mostly in the playground | `read` |
| `research` | Real-world facts from the web | `read`, `web` |
| `proofreader` | Spelling, grammar, punctuation | `read` |
| `fact-checker` | Text against canon and her research notes | `read` |
| `continuity` | Consistency across books, timeline and her tracker | `read` |
All six have `authority: {withinRole: ["message.send"], crossRole: []}`, `credentials: []`, and
`messaging: {sendTo: ["human"], receiveFrom: ["human"]}`, so they can't relay through each other.
`network` is `none`, except `api-only` for `companion` and `research`.
All six have `messaging: {sendTo: ["human"], receiveFrom: ["human"]}`, so they can't relay through each
other. `network` is `none`, except `api-only` for `companion` and `research`. C2-08 landed them with
`authority: {withinRole: ["message.send"], crossRole: []}` and `credentials: []`. C2-08b changes both,
which is a role change Mos authorized in mos-1309 item 5: `withinRole` becomes `["message.send",
"library.write"]`, and `credentials` becomes `[{"service": "gitea"}]`, the definition credential the
grant path needs (`business.mjs:132-167`). Each creative instance also gets the `identity` and `publish`
fields that path reads. `roles/chief-of-staff.json` gets the same credential and no `library.write`, so
the chief can hold the read grant the mirror fetches with.
Bean and Chet. Jason's ruling (Q180): "She will want to keep those agent names and personalities." They
are two business role instances, `bean` and `chet`, both with definition `companion`. The personas are
@@ -844,12 +1154,13 @@ wins on any conflict, and the broker checks every reply whatever the persona say
It is fenced the way skills are (`SKILL_END`, `bundle.mjs:107`, `:170`). The bundle refuses
`persona-invalid` for text that holds `</persona-file>` or has a line starting `# ` or `## `, so the text
can't close its fence or open a section of its own. C2-08's snapshot pins contract, persona, session, in
that order. The order is a hint to the model. The enforcement is the broker guard in 5.2, which doesn't
read the persona, so no persona text can lift a guard.
that order. The order is a hint to the model. The enforcement is in code, M12 on writes and the
citation guard in 5.2, and neither reads the persona, so no persona text can lift a guard.
What changes for her. In Hermes, Bean and Chet answer in free text and may write story text when she
asks. In Mosaic they answer in short cited items, and a request for prose gets a `question` or an `idea`
item, not a passage. That follows from Jason's "NEVER" and Q176. She should hear it before the move, not
What changes for her. In Hermes, Bean and Chet answer in free text and may write story text into her
files when she asks. In Mosaic they still answer in free text, of any length, and they never write into
her story files. Whatever they write lands in a supporting folder she marked, as a commit she sees and
can revert. That follows from Jason's "NEVER" as Q223 reads it. She should hear it before the move, not
discover it (10 step 7).
The five specialists stay available as further instances. Whether the slice starts with Bean and Chet
@@ -873,64 +1184,73 @@ New constants in `vocabulary.mjs`, each changed only by a reviewed commit:
as needed; unless user forbids per agent"). If Melanie wants it forbidden for an agent, the request
comes through Jason (mos-1038).
- `CREATIVE_DEFINITIONS`, the six above.
- `CREATIVE_KINDS`, mapping each definition to the item kinds in the table.
- `CREATIVE_TOOLS = ["read", "grep", "find", "ls", "web"]`. `validateRoleDocument` refuses a creative
definition whose `tools` include anything else, whose authority holds anything but `message.send`, that
has no `messaging` key, or whose `messaging` names anything but `human` (N6).
definition whose `tools` include anything else, whose authority holds anything but `message.send` and,
after C2-08b, `library.write`, that has no `messaging` key, or whose `messaging` names anything but
`human` (N6).
- C2-08 also landed `CREATIVE_KINDS`. C2-08b removes it, since replies have no item kinds now.
## 5. Guardrails enforced in code
### 5.1 Agents never write prose for the user
### 5.1 Agents never write story
| Mechanism | What it refuses | Test |
|---|---|---|
| M1 role validator (`role.mjs`, `CREATIVE_TOOLS`) | A creative role file with `write`, `edit` or `bash`, any authority but `message.send`, or a missing or wider `messaging` | C2-08: each rejected variant refuses with its message |
| M1 role validator (`role.mjs`, `CREATIVE_TOOLS`) | A creative role file with `write`, `edit` or `bash`, any authority but `message.send` and `library.write`, or a missing or wider `messaging` | C2-08: each rejected variant refuses with its message |
| M2 tool gate (`gate.mjs:137-138`, unchanged) | `Write`, `Edit` or `Bash` from a creative session | C2-08: `decide()` with each creative policy refuses all three, on pi and claude-code |
| M3 typed tool set (`tools.mjs`) | The tool list is exact: lookups and `send_message`. No write tool, no `raise_decision` | C2-05b: a snapshot of tool names per creative role |
| M4 library verbs are human only | `library.file`, `library.sync` and `library.status` from an agent session, with `human-required` as at `broker.mjs:1434-1435`. Later write verbs join the list | C2-04 and C2-03a: each verb from an agent session refuses |
| M5 cited-reply guard (5.2) | A reply that isn't a list of short cited items. At most 10 items and 1,200 characters of item text per reply. An `idea` item is at most 200 characters, at most 5 per reply. A `fact`, `finding`, `correction` or `question` item is at most 300. A `correction`'s `to` is within edit distance max(12, 25% of `from`) of its `from` | C2-06: each limit refused one past the boundary and accepted at it |
| M3 typed tool set (`packages/harness/src/tools.mjs`) | The tool list is exact: the five lookups, `send_message`, `library_doc_write` and `library_doc_delete`, plus `web_search` and `web_fetch` for `companion` and `research` (mos-1308, ruling after r11). No `raise_decision`, `read_spec`, `list_agents`, `inbox`, `trail`, `write_record` or tracker read | C2-05b: a snapshot of tool names per creative role (row 224, six or eight). C2-02 adds the write tools to it: eight names, or ten for `companion` and `research` |
| M4 human verbs and agent verbs | `library.file`, `library.file.save`, `library.sync`, `library.status`, `library.limits.set` and `library.revert` from an agent session refuse `human-required`, as at `broker.mjs:1434-1435`. `library.doc.write` and `library.doc.delete` refuse every caller but a creative session holding `library.write` and a live grant | C2-04, C2-03a, C2-06 and C2-02: each human verb from an agent session refuses, and each agent verb from the human, the tester, the chief of staff and a creative session without a grant refuses |
| M5 citation guard (5.2) | A cite that fails a check in 5.2, whenever a reply carries one. More than `citationsPerReply` citations. A reply with none when `requireCitation` is true. Both keys are `replyLimits` settings read at check time (Q222, Q226), and a missing or invalid one uses its default. An unreadable business file resolves `requireCitation` to true | C2-06: each check refused once and passed once. `citationsPerReply` refused one past the limit and accepted at it. `requireCitation` true refuses an uncited reply and false accepts it. A changed value takes effect on the next reply. Each invalid value uses the default, `"true"`, `1`, `null`, `[true]`, `{}`, `0`, `1001`, `2.5`, `true` for the count and `"5"` included. With the business file unreadable, a reply with 21 citations refuses, an uncited reply refuses `citation-missing`, and `messages` is unchanged in both |
| M6 no free-text decision channel | `decision.raise` from a creative definition refuses `decision-not-allowed` in the broker. The typed tool is also absent (M3) | C2-08: a creative session's raise refuses and no decision row appears. An assistant's raise still works |
| M7 no raw session view | On a business with a library project, the conversation reader leaves creative sessions out of `catalogue()`, and `open()` refuses them with `conversation-not-listed` | C2-10a: a creative session file under the root isn't listed and won't open by id. An assistant session still lists |
| M8 no raw Discord engine | On a business with a library project, `discord check` and `discord run` refuse a binding whose engine isn't `mosaic-bus` (`discord-engine-not-allowed`), and a `mosaic-bus` binding with `tools` | C2-10a: both refuse on a library business. A pi binding on a business without a library still checks clean |
| M9 no agent write to the mapping | An organization write whose `projects` differ from the file's refuses `organization-projects-protected`, in `organizationWriter.write` | C2-04: `write` called directly with a changed playground prefix, and again with a changed hybrid entry, refuses, and the file and `changes.jsonl` are unchanged |
| M10 personas and creative instances are the human's | Any write by an actor other than the business file's `human` that touches a creative instance: the chief of staff's `assistants`, `instances` and `models` keys through `organization.set`, and the tester's `instances.set` and `routes.set`. A persona file that fails its digest, mode or fence check. A harness argv that holds the prompt text | C2-08: each path refuses with its code (4) and the file is unchanged. The human's `instances.set` and `routes.set` on `bean` work, and so does her `limits.set` change to Bean's `launch.models` key. Tester cases through a bound tester session. A mutant without the `models` check fails. No `instances` read, `changes.jsonl` line or adapter argv holds a marker string from a fixture persona |
| M11 reply settings are the human's | `library.limits.set` from an agent session (`human-required`) or the tester (`tester-refused`). Any organization write whose `replyLimits` differs from the on-disk file's refuses `organization-reply-limits-human-only` in `organizationWriter.write`, on `document` and on `written`, unless the actor is the on-disk `human` and not an agent actor shape | C2-06: the human's `library.limits.set` works. 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 actor, that role's write refuses. `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` |
Residual, in numbers. One reply can carry up to 5 `idea` items of 200 characters, which is 1,000
characters of free suggestion text inside a total of 1,200 characters of item text. Each item must quote
a real line of her repo, but the quote can be any line. Over a chat that adds up. Code can't tell a
sentence of story from a sentence of advice, and this design doesn't propose a classifier. Jason wrote
"NEVER", and Q176 says the system enforces it, so the residual is his or Melanie's to accept (10 step
8). Lower numbers are a constant change: 3 ideas of 120 characters would make the residual 360. What
lands in her repo stays hers alone. M2 to M4 make an agent write there impossible.
| M12 agents write supporting docs only | `supportingPath` refuses `library-write-story-path` on `library.doc.write` and `library.doc.delete` for every path class the ruling after r11 lists, before any git call. A write commits only by the plumbing steps, pushes fast-forward only and carries the `Mosaic-Agent` trailer | C2-01d: one refusal per path class against a fixture tree with a symlink, a gitlink and a case variant. C2-02: each refusal through the broker leaves the remote ref, the served ref and the mirror's refs unchanged. One write and one delete land, 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`. A mutant without the supporting check fails |
Residual. The ruling after r11 states it. A supporting doc can hold any text, and the seat token's reach
at Gitea is wider than M12. Chat has no length or kind limit now (Q223), so the 360-character residual of
Q219 is gone with the caps. What lands in her story files stays hers alone. M2, M4 and M12 make an agent
write there impossible through the hub.
### 5.2 Agents always refer to the source material
The cited-reply guard runs in the broker. It covers every message from an agent session to `human` in a
The citation guard runs in the broker. It covers every message from an agent session to `human` in a
business with a library project, from the runner's RESULT and from `send_message` alike. It doesn't look
at the sender's definition, so a misconfigured role can't skip it. Citation checks need git, so they run
before the insert, outside the SQL transaction, as `#requestProject` does (`broker.mjs:1378-1430`).
Every channel to Melanie shows stored messages only (D8), so this one check covers the Console, Discord
and any later channel.
The reply body is JSON:
The reply body is free text, or a JSON object when the reply carries citations:
```json
{"items": [{"kind": "finding", "text": "Mara's eyes are grey in chapter 3 but green on her character page.",
"cites": [{"path": "<book>/<chapters>/03.md", "commit": "<40 hex>", "lines": [41, 41], "quote": "her grey eyes"},
{"path": "<book>/<characters>/Mara.md", "commit": "<40 hex>", "lines": [9, 9], "quote": "green eyes"}]}]}
{"text": "Mara's eyes are grey in chapter 3 but green on her character page.",
"citations": [{"path": "<book>/<chapters>/03.md", "commit": "<40 hex>", "lines": [41, 41], "quote": "her grey eyes", "as": "canon"},
{"path": "<book>/<characters>/Mara.md", "commit": "<40 hex>", "lines": [9, 9], "quote": "green eyes", "as": "canon"}]}
```
A body that parses as a JSON object must fit this schema: `text` a string, `citations` a list of library
cites `{path, commit, lines, quote, as?}` or web cites `{url, quote}`, and no other keys. Any other body
is a reply whose text is the whole body and which carries no citation. `as` is optional and its one value
is `"canon"`, which says the agent states the cited line as fact about her world.
| Check | Refusal code |
|---|---|
| The body parses as the schema, within the M5 limits | `cited-reply-required` |
| The item kind is allowed for the sender's definition (`CREATIVE_KINDS`). A sender outside `CREATIVE_DEFINITIONS` may send only `question` and `finding` | `reply-kind-not-allowed` |
| Every item has at least one cite | `citation-missing` |
| The path is canon or playground (not unmapped) and indexed, it exists at `commit`, `commit` is reachable from the served commit, the lines are in range, and `quote` occurs in those lines after whitespace normalization | `citation-invalid` |
| A `correction`'s `from` equals one cite's `quote`, after the same normalization, inside that cite's lines | `correction-unbound` |
| `fact`, `finding` and `correction` cite only canon paths | `playground-cited-as-canon` |
| All library cites in an item are in one universe, or a hybrid plus its sources | `citation-cross-universe` |
| A JSON object body fits the schema | `reply-invalid` |
| `requireCitation` is true and the reply carries no citation | `citation-missing` |
| The reply carries at most `citationsPerReply` citations | `citations-over-limit` |
| The path is canon, playground or supporting (not unmapped) and indexed, it exists at `commit`, `commit` is reachable from the served commit, the lines are in range, and `quote` occurs in those lines after whitespace normalization | `citation-invalid` |
| A cite with `"as": "canon"` names a canon path | `playground-cited-as-canon` |
| All library cites in a reply are in one universe, or a hybrid plus its sources | `citation-cross-universe` |
| A `url` cite is allowed only for `research` and `companion`, only for a URL this session fetched through the broker, and its quote must occur in the kept text of that fetch | `citation-unfetched` |
The guard doesn't read what the text says, only its citations (Q223). 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, so the Console can label it agent-written (7.4).
Fetched text (B7). The web trail keeps the URL (`broker.mjs:1521`) and a summary of status, bytes and
redirects (`packages/bus/src/web.mjs:46`), not the page. Case 2 adds a page store in the broker's memory,
keyed by session: the text of the last 20 broker fetches, each capped at 200 KB, dropped when the session
@@ -939,21 +1259,42 @@ runner retries, and the agent fetches again. Fetches through the harness's own w
broker, so `webPlan` (`bundle.mjs:28-35`) refuses the `harness-builtin` source for a creative definition
(`web-source-not-allowed`).
Notices (N3). The guard accepts exactly three bodies with no items: `{"items": [], "notice": "no-cited-answer"}`,
`{"items": [], "notice": "no-answer"}` and `{"items": [], "notice": "turn-failed"}`. Any other notice
value is refused. In a library business the runner sends `no-answer` and `turn-failed` instead of its two
texts at `runner.mjs:449`. The failure reason stays in the run log.
Runner texts. The runner's two fixed texts at `runner.mjs:572` (aca16404c) are plain text, which the
guard accepts while `requireCitation` is false. In a library business the runner sends `(the session
gave no answer)` and `(the turn failed)`, with the reason only in the run log, and the guard accepts
exactly those two bodies, and the two retry texts below, when `requireCitation` is true. An agent that
sends one of them gains nothing it couldn't say anyway.
Runner retry (C2-07). When `message.send` refuses with one of the codes above, the runner starts a new
turn in the same session with the code and reason, at most twice. Then it sends `no-cited-answer`. Today
the answer would be dropped (`runner.mjs:451-456`).
Runner retry (C2-06, which takes in C2-07). When `message.send` refuses with a code from the table, the
runner starts one new turn in the same session with the code and reason. If that reply also refuses, it
sends `(no answer passed the citation check)`. Today a refused send is logged and `message.read` is
skipped, so she gets no answer (`runner.mjs:577-583` at aca16404c). A send that refuses
`request-too-large` at the client (`packages/bus/src/client.mjs:19-20`), before the broker, 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. Only one retry runs per answer (webby-0151 R1).
The unslop note and the clip. The runner appends `unslop.note()` to every RESULT body, and `clip` cuts a
body over 32,000 characters (`runner.mjs:130-131`, `:572-573` at aca16404c). Either one turns a JSON body
into plain text and drops its citations, and either one breaks the fixed texts. In a library business
C2-06 changes both. A body that parses as a JSON object gets the note added to the end of its `text` and
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, as it does after a failed send. A plain body keeps today's clip and note.
Tests (C2-06): one passing and one refused case per row of the table, through both the runner's RESULT
path and `send_message`, and once with a non-creative sender in a library business (N8). The fixture is
a repo the test builds in her layout's shape, with spaces and capitals in folder names, a `.canvas` file
and an unmapped top-level note. Each refusal must leave the `messages` count unchanged. C2-07 adds a
runner test with a fake harness that answers free text first and then a valid reply, and asserts one
stored message.
path and `send_message`, and once with a non-creative sender in a library business (N8). Plain text
passes with `requireCitation` false and refuses with it true. The fixture is a repo the test builds in
her layout's shape, with spaces and capitals in folder names, a `.canvas` file, a supporting folder and
an unmapped top-level note. Each refusal must leave the `messages` count unchanged. A runner test with a
fake harness that answers with a bad cite and then a valid reply stores one message. One that refuses
twice stores 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. A cited reply over 64 KiB, then a valid one, stores the second. Two
replies over 64 KiB store `(the answer was too long to send)`, and with `requireCitation` true the guard
accepts it. With the business file unreadable, an uncited reply refuses `citation-missing` and
`messages` is unchanged.
### 5.3 Tool use is guarded
@@ -966,6 +1307,9 @@ stored message.
- `research` web use goes through the broker's web tools (`tools.mjs:197-206`), which check the grant on
every call (`broker.mjs:1516`).
- Discord: the relay has no tools of its own, and M8 refuses one that does.
- Writes: M12 and the grant check on `library.doc.write` and `library.doc.delete`. The broker re-checks
every call, so a session whose grant was revoked gets `library-write-no-grant` from a tool it still
holds.
## 6. One chat with N agents and @tag addressing
@@ -1009,17 +1353,19 @@ the first word is plain text. `room.open` against a same-name thread on another
### 7.1 Obsidian
She has it working already. `~/src` is her vault, the books repo is a folder in it, and obsidian-git
pushes it to git.mosaicstack.dev (mos-1000). The hub changes nothing on her machine and commits nothing
to her repo. The setup sheet (C2-12) tells her which folders the agents read, that a folder named in
`playground` is her playground, and how to add a graph colour group for that path in her own vault
settings if she wants one. The graph view and backlinks stay Obsidian's own. The sheet also says two
pushes it to git.mosaicstack.dev (mos-1000). The hub changes nothing on her machine. It commits to her
repo only an agent's supporting doc write, her revert and her own Console save, and obsidian-git pulls
each on its next pull. The setup sheet (C2-12, rewritten by C2-12b) tells her which folders the agents
read, that a folder named in `playground` is her playground, that a folder named in `supporting` is where
agents may write, and how to add a graph colour group for those paths in her own vault settings if she
wants one. The graph view and backlinks stay Obsidian's own. The sheet also says two
things she might not expect. Moving a note into a book folder makes it canon, so agents may cite it as
fact (N2). And a short link to a name that also exists in another of her repos can point somewhere else
for the agents than in Obsidian. A full-path link avoids that (N4, 1.3).
### 7.2 Sync (bus side)
One function, `syncCommand(kind, url)`, builds every fetch (and later push). Its argv is `SAFE`
One function, `syncCommand(kind, url)`, builds every fetch and push. Its argv is `SAFE`
(`git.mjs:7-9`), then `-c protocol.https.allow=always`, `-c http.followRedirects=false` and
`-c credential.helper=`, then `-C <mirror>`, then the command with the URL as an argument. Its env is
`gitEnv()` plus `LC_ALL=C` and `GIT_ASKPASS` naming a helper script outside the mirror, which reads the
@@ -1044,8 +1390,12 @@ written atomically, so it survives a bus restart. The human verb `library.status
`library.sync` runs on a 60-second timer in the bus, and before any lookup that finds the last fetch
more than 60 seconds old.
Later (C2-03b, with C2-02): push of bus commits, the push race retried once, `unpushed`, and
`library-remote-invalid` refusals of bus writes (B9).
C2-03b adds the push for C2-02's commits, the one race retry, `library-push-failed`, and
`library-remote-invalid` refusals of writes (B9). It also changes whose token the helper answers with: the
chief of staff's grant token and Gitea user for the fetch, in place of slot `library-gitea`, the writing
seat's own grant token for its write, and slot `library-human` for her save and revert (ruling after
r11). The fetch falls back to slot `library-gitea` only while the events table holds no gitea grant
event for the chief, and nothing else ever reads that slot. There is no `unpushed` state.
Tests (C2-03a): `syncCommand` argv equals `SAFE` plus the fixed sequence above, with no other
`protocol.*` key. Its env holds `LC_ALL=C` and `GIT_ASKPASS` pointing outside the mirror. The helper
@@ -1054,30 +1404,37 @@ refuses a wrong host, a wrong port and another user. A 302 to another host fails
a bare repo: fast-forward, R-problems recorded without holding the head, `library-diverged` kept until
`--accept`, a failed fetch kept serving. A mutant that sets `protocol.allow=always` fails the argv test.
### 7.3 Console WYSIWYG (after the slice)
### 7.3 Console WYSIWYG (slice rows that don't gate 10-27)
- Library pages (C2-11b) show a tree of universes, books, type folders, manuscript and playground, with
the playground in its own section and colour. They open files in the existing editor through
`library.file` and `library.file.save`, with `base` as today. Saving needs the write rows.
- Library pages (C2-11b) show a tree of universes, books, type folders, manuscript, playground and
supporting folders, the last two each in their own section and colour. They open files in the existing
editor through `library.file`. Each save is one commit through `library.file.save {path, base,
content}` (ruling after r11, F41). A file that moved refuses `stale-base`, and the page keeps her text
and offers a reload.
- Wikilinks render as links in the editor (C2-11a). Clicking one opens the target.
- Promote (C2-19) and crossover (C2-15) buttons.
- Promote (C2-19) and crossover (C2-15) buttons come later.
### 7.4 Console chat (C2-10b)
- The chat view renders a cited reply as items. Each item shows its kind, its text and its cites as
path, lines and quote, with a scope badge (canon, playground or web). It marks a cite "changed since"
when the file's last commit is newer than the cited one. A notice shows a fixed line ("No cited
answer", "No answer", "The turn failed").
- The chat view renders a reply as its text, then its citations as path, lines and quote, each with a
scope badge (canon, playground, supporting or web). A cite whose file's last commit carries a
`Mosaic-Agent` trailer gets an agent-written label (Mos point 3). It marks a cite "changed since" when
the file's last commit is newer than the cited one.
- A `library-write` notice shows a fixed line, "<name> wrote <path>" or "<name> deleted <path>", with the
commit and a Revert button that calls `library.revert`.
- A status line reads `library.status`: the served commit and its age, `library-diverged`, a fetch
error, and the count of R-problems with a link to the list.
- `renderItems(body)` is one pure function in `packages/library`, shared with the Discord relay.
- `renderReply(body)` is one pure function in `packages/library`, shared with the Discord relay. It
replaces C2-01's `renderItems`.
### 7.5 User and agent sync
- Agents read the served commit through the broker. They never have a private copy (2.1).
- Every reply cites a commit, and the Console marks stale cites.
- Her push is visible to agents within about 60 seconds.
- The bus token is read-only on the books repo in the slice. Nothing the hub does can change her repo.
- Agents write only supporting docs, each a commit with a notice she can revert. Her Console saves are
commits too. Obsidian sees both on its next pull. A race with her Obsidian push refuses `stale-base`
and never merges (D6).
### 7.6 Discord relay (C2-18, after the slice, optional)
@@ -1088,13 +1445,14 @@ stays and its engine changes, as below.
- A new engine module, `engine-bus.mjs`, next to `engine-pi.mjs`. Binding field `engine.provider:
"mosaic-bus"`, plus a map from each channel id to one thread id. `prompt(text)` calls `thread.send` on
the bound thread with her message as the body. It then waits for the next message in that thread from
the instance to `human`, up to the binding's turn timeout, and returns `renderItems(body)` as `text`.
the instance to `human`, up to the binding's turn timeout, and returns `renderReply(body)` as `text`.
The connector's loop, journal, delivery and authorization stay as they are (`connector.mjs:381-387`).
- The relay posts only bodies the broker stored, and those passed the guard (D8). It never sees a model's
output. A timeout posts the fixed line "No answer yet. It will appear here when it arrives", and a later
poll posts the stored reply.
- Rendered items can pass Discord's 2,000-character limit, because cites add to the 1,200 characters of
item text. The relay splits at item boundaries, each part with its own nonce.
- A rendered reply can pass Discord's 2,000-character limit, since replies have no length cap now. The
relay splits at paragraph boundaries, and a paragraph over the limit at line and then character
boundaries, each part with its own nonce.
- On each change of `library.status`, the relay posts one line to her channels: diverged, a fetch error,
a new R-problem count, or "in sync again".
- M8 refuses any other engine on a library business, and a `mosaic-bus` binding that sets `tools`.
@@ -1105,11 +1463,11 @@ stays and its engine changes, as below.
Tests (C2-18), against a fake bus and fake REST, with no Discord server:
- A posted Discord message equals `renderItems` of the stored body, byte for byte.
- A posted Discord message equals `renderReply` of the stored body, byte for byte.
- A refused agent reply (nothing stored) makes the relay post nothing but the fixed notice the runner
then stores.
- A pi-engine binding and a relay binding with `tools` both refuse on a library business.
- A long reply splits at item boundaries, and no part goes over 2,000 characters.
- A long reply splits at paragraph boundaries, and no part goes over 2,000 characters.
- Each `library.status` change posts exactly one line.
## 8. Row list
@@ -1119,7 +1477,8 @@ work as Sage-dispatched subagents (lead decision 129). Codex gpt-6.1-sol (Rocko)
gpt-6-luna takes low-level work. An independent reviewer (Filbert, Dewey or Webby) reviews every row.
Each gate is its package's changed test files run singly plus the full gate before landing. No test may
touch `~/.mosaic-dev`, `~/.config/mosaic-dev`, live units, slot files, her repo or any Discord server.
Chains follow Q177 merge-chaining. S marks a slice row.
Chains follow Q177 merge-chaining. S marks a slice row. C2-11a and C2-11b are slice rows that don't gate
10-27 (Q195).
| Row | S | Scope | After | Owner | Test gate |
|---|---|---|---|---|---|
@@ -1128,36 +1487,41 @@ Chains follow Q177 merge-chaining. S marks a slice row.
| C2-04 | S | Business file `kind: "library"` entry, skips at each consumer in 2.1, M9, mirror creation at the `projectRepo` path, human verbs `library.file` (read) and `library.status` | C2-01 | Sol (Herb, sage-1482) | business file tests. One test per consumer skip. A launcher test that the workspace has no clone. M9 test. Each verb from an agent session refuses (M4) |
| C2-03a | S | Fetch-only sync (7.2): `syncCommand`, askpass helper, served ref, `library-diverged`, `--accept`, sync state file, timer, `library.sync` | C2-04 | Opus | the C2-03a tests in 7.2 |
| C2-05a | S | Lookup index and queries: find, get, search, manuscript with scope, universe, isolation | C2-01 | Haiku high | isolation, scope labels on every tool, unmapped files never returned, paging under `REPLY_CAP`, rebuild time on a 3,000-file fixture |
| C2-05b | S | Broker lookup verbs and typed tools. Sync before a read older than 60 s | C2-05a, C2-03a | Sol (Rocko) | broker verb tests. Typed tool name snapshot per creative role (M3) |
| C2-06 | S | Cited-reply guard (5.2) in `packages/library`, the broker's pre-check on agent-to-human `message.send`, the page store, the three notices | C2-05a | Opus | one pass and one refusal per check in M5 and 5.2, through the RESULT path and `send_message`, and with a non-creative sender. `messages` count unchanged on each refusal |
| C2-07 | S | Runner retry on a guard refusal (at most 2), then `no-cited-answer`. `no-answer` and `turn-failed` in a library business | C2-06 | Haiku high | fake harness: free text then a valid reply stores one message. An empty and a failed turn store the fixed notices |
| C2-01d | S | `supporting` lists and their prefix rules, `library-supporting-other-universe`, the `supporting` scope ahead of playground and canon, the `editor` field, and `supportingPath` (M12) | C2-01c | Opus | one test per prefix rule and refusal, a folder in both lists, scope order, every M12 path class, `editor` refused when not two non-empty strings |
| C2-05b | S | Broker lookup verbs and typed tools. Sync before a read older than 60 s. Row 224, built to the earlier brief and landing as built (sage-2833) | C2-05a, C2-03a | Sol (Rocko) | broker verb tests. Typed tool name snapshot per creative role without the write tools (M3) |
| C2-05c | S | `scope` with `supporting` in the lookup verbs and tools, `blob` in `library_get` (sage-2833) | C2-05b, C2-01d | Sage picks | a `supporting` lookup in find, search and get, canon by default leaves supporting files out, `blob` equals the served blob id, the M3 snapshot unchanged |
| C2-06 | S | Citation guard (5.2) in `packages/library`, the broker's pre-check on agent-to-human `message.send`, the page store, `replyLimits` with `requireCitation` and `citationsPerReply` and M11 (Q222, Q226), the runner texts, and the runner retry that was C2-07 | C2-05a, C2-04 | Opus | one pass and one refusal per check in M5, M11 and 5.2, through the RESULT path and `send_message`, and with a non-creative sender. `messages` count unchanged on each refusal. The runner tests in 5.2 |
| C2-07 | | Folded into C2-06 (ruling after r11) | | | |
| C2-08 | S | Six role files and contracts, the new constants, M1, M6, the `webPlan` refusal, gate tests (M2), messaging test. `persona.name` and `persona.sha256` in `REGISTRY`, the persona file read and checks, and the fenced persona block between the contract and "## This session" in `prompt()`. M10: the `organization.set` refusals, the `models` check, the `routes.set` and `instances.set` creative branches (4). Both adapters pass the prompt as a file, not its text (4) | none | Opus | role tests, gate tests, a creative raise refused, `harness-builtin` refused for `research` and `companion`, `scripts/test-config.sh`. Persona vars refused at other layers, malformed and on a non-creative definition. The persona file refused when missing, a symlink, another uid's, group or other readable, over 8,000 characters, off its digest, holding `</persona-file>` or a `# `/`## ` line. A prompt snapshot pinning contract, persona, session. The M10 tests, tester cases through a bound tester session, the `models` mutant. Adapter tests: argv holds the prompt path and no marker from the prompt, and pi's adapter refuses a missing and an unreadable prompt file. M10's actor case: the human's `limits.set` change to Bean's `launch.models` key works next to the chief's refusal. Business file, `.history` and `changes.jsonl` modes after a write |
| C2-09a | S | `#threaded` and launcher read `THREAD_DEFINITIONS`. `thread.create` takes `instance`. The Console new-thread control picks it | C2-08 | Sol (Rocko) | the C2-09a tests in section 6 |
| C2-10a | S | M7 in the conversation reader. M8 in `discord check` and `discord run` | C2-08 | Haiku high | the M7 reader test, the M8 tests |
| C2-10b | S | Console chat: cited-item renderer through `renderItems`, the status line | C2-06, C2-09a, C2-03a | Haiku high | webui tests for items, scope badges, notices, "changed since" and the status line |
| C2-10b | S | Console chat: the reply renderer through `renderReply`, which replaces `renderItems`, the agent-written label, the notice line with Revert, the status line | C2-06, C2-09a, C2-03a, C2-02 | Haiku high | webui tests for text and citations, scope badges, the agent-written label, the notice line and its Revert call, "changed since" and the status line |
| C2-12 | S | The setup sheet for Melanie (7.1, including N2 and N4, and what changes for Bean and Chet in 4) and the survey run sheet for `<host>` | C2-01 | Luna | doc only, reviewed against the survey's real flags |
| C2-12b | S | Setup sheet rewrite for Q223 and Q226: free-text chat, supporting folders and how to mark one, agent writes as commits with notices and Revert, her Console saves, the `editor` name and email, the settings that replace the caps, and the residual | C2-12 | Luna | doc only, reviewed against the ruling after r11 |
| C2-20 | S | Persona import (9.1): a script that reads only the persona definitions of the two Hermes profiles, confirms profile to name, and writes each persona file (4) with its `persona.name` and `persona.sha256` for the business file. Prints names, byte counts and sha256 only. A `--dry-run` mode, run before Jason's go, prints only counts of files, lines and lines starting `# ` or `## ` | C2-08 | Opus (Gosalyn), run under Jason's go | fixture Hermes profile trees: the right fields are taken, memory and session files are never opened (a read trace on the fixture), a profile with no persona field refuses, a persona that would fail C2-08's fence check refuses, output holds no persona text, each written file is 0600 and its digest matches, `--dry-run` writes nothing and prints the right counts for a fixture with two heading lines and no marker string from it |
| C2-13a | S | `scripts/case2-unit.sh`, which renders the bus and Console system units from their existing templates and which root runs from a root-owned checkout in 10 step 2c, and `scripts/case2-precheck.sh` with the host checks. No library dependency, so row 195 chains on it (sage-1480) | none | Opus (Gosalyn) | the C2-13a brief's render and precheck cases: token rules, `@PATH@` from `--node`, the four hardening lines, the Console's capability lines and privileged port, one `WantedBy=multi-user.target` on each unit (Q189), copy-id pairing, the serve flag set, the key file and the unprivileged port start. Scratch unit directory and scratch data root |
| C2-13b | S | Install script for `<host>` as `<user>`: bootstrap, the stack copy, business file with the mapping, `bean` and `chet` with their persona vars and files, any specialists she picks, her human id, token slot path. The install writes no unit. Adds the library checks to the precheck. Jason or his delegate runs it (10) | C2-13a, C2-04, C2-03a, C2-20 | Opus (Gosalyn), Jason's go | precheck library checks: the business file, `.history`, `changes.jsonl` and persona files have the modes in 4 and each persona digest matches, no Discord binding exists, and after step 4 `--library-status` reads the bus's recorded sync state and lists R-problems (10 step 4a). Tested against a scratch data root and a local bare repo |
| C2-14 | S | End-to-end acceptance on a scratch data root and a bare repo in her layout: sync, ask a creative agent in its thread, a reply whose cites verify, a playground cite as canon refused, a force-push held as diverged, the Console shows the reply as items | slice rows | Opus | one script, which the reviewer also runs |
| C2-02 | | Bus writes: `writeLibraryFile`, `commitFile` refactor, r1's S0 rule on the working tree | C2-04 | Sol (Rocko) | writer tests, `packages/project/tests` unchanged and green |
| C2-03b | | Push path, race retry, `unpushed`, `library-remote-invalid` on writes | C2-02, C2-03a | Opus | push tests against a bare repo |
| C2-08b | S | Role changes, authorized in mos-1309 item 5: `library.write` in the creative authority and in `checkCreative`, the gitea credential on the six creative definitions and the chief of staff, instance `identity` and `publish`, `CREATIVE_KINDS` removed | C2-08 | Opus | role tests: a creative role with `library.write` validates, one with any other added action refuses, the chief holds the credential and not `library.write`. `scripts/test-config.sh` |
| C2-21 | S | Grant executor for `kind: "library"` (ruling after r11, F40): the creative raise check narrowed to library projects (mos-1163), collaborator in step 4, the level check in step 5, the rendered block, revoke. The install enrolls the chief of staff | C2-08b, C2-13b | Opus | first, the two Gitea facts proven on a scratch Gitea 1.27.1. Then executor tests: `write` for a creative definition, `read` for the chief and any other, a wider permission or `admin` refuses `gitea-reach-wider`, revoke deletes the token, a non-library project still forks, a creative raise naming a non-library project refuses `gitea-not-declared` |
| C2-03b | S | Push and credentials (7.2): the push form of `syncCommand`, the race retry once, `library-push-failed`, `library-remote-invalid` on writes, the helper answering with a seat's grant token, the chief's for the fetch, and slot `library-human` | C2-03a | Opus | push tests against a bare repo. The helper refuses another seat's user, and no token reaches argv, config or a log |
| C2-02 | S | Writes (ruling after r11): `library.doc.write`, `library.doc.delete`, their typed tools, the plumbing commit and trailer, the notice, `library.revert`, the trailer label in the check result, and `library.file.save` | C2-01d, C2-03b, C2-04, C2-05c, C2-08b | Opus | the M12 broker tests, write, delete and revert against a bare repo, the grant refusals, the notice body, her save's author, the M3 snapshot with the write tools |
| C2-14 | S | End-to-end acceptance on a scratch data root and a bare repo in her layout: sync, ask a creative agent in its thread, a reply whose cites verify, a playground cite as canon refused, a force-push held as diverged, an agent's supporting doc write and its notice, a story path write refused, a revert, her Console save as one commit, the Console shows the reply | slice rows | Opus | one script, which the reviewer also runs |
| C2-19 | | `library.promote`, multi-path commit | C2-02, C2-03b | Sol (Rocko) | promote in one commit, human only |
| C2-09b | | `room.open`, `room.send` with @tags, `room.read` | C2-09a | Sol (Rocko) | the C2-09b tests in section 6 |
| C2-18 | | Discord relay (7.6), optional, after its own Jason ruling | C2-09a, C2-06, C2-03a | Opus | the C2-18 tests in 7.6 |
| C2-10c | | Console room view: merged timeline, @name completion | C2-09b, C2-10b | Haiku high | webui tests, @tag parse parity with the broker |
| C2-11a | | Editor wikilink node. First measure whether `[[` round-trips today | none | Sol (Rocko) | `roundTrip` tests: plain, labelled, heading, in a list, next to front matter |
| C2-11b | | Console library pages: tree, open, save (with C2-02), promote and crossover buttons | C2-04, C2-11a | Haiku high | webui tests. Save refuses `stale-base` in the UI |
| C2-11a | S | Editor wikilink node. First measure whether `[[` round-trips today | none | Sol (Rocko) | `roundTrip` tests: plain, labelled, heading, in a list, next to front matter |
| C2-11b | S | Console library pages: tree with playground and supporting sections, open, one commit per save through `library.file.save`, `stale-base` keeps her text | C2-02, C2-11a | Haiku high | webui 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 |
| C2-15 | | Hybrid universes in the business file, `exclude` and `pin` | C2-05a | later | |
| C2-16 | | Agents see each other's replies in a room | C2-09b | later | |
| C2-17 | | Automatic replay of unpushed commits on divergence | C2-03b | later | |
| C2-17 | | Dropped: there is no unpushed state (ruling after r11) | | | |
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 and C2-20.
- Next: C2-03a, C2-07. Then C2-05b, C2-10b and C2-13b.
- Last: C2-14. The write rows and rooms follow when a seat is free.
- Landed: C2-01, C2-01c, C2-03a, C2-04, C2-08, C2-12, C2-13a, C2-20. C2-13b is in progress.
- Next, side by side: C2-01d, C2-03b, C2-05a, C2-08b, C2-09a, C2-10a, C2-11a, C2-12b.
- Then: C2-05b (row 224, built), C2-05c, C2-06, C2-21 and C2-02. Then C2-10b and C2-11b.
- Last: C2-14. Rooms, the relay and promotion follow when a seat is free.
Dependencies on other tracks: row 190 doesn't apply, since nothing else runs as `<user>`. SSO (Q175,
#412) gates the Console from her machine, and the Console is now her channel, so the slice needs SSO
@@ -1241,14 +1605,15 @@ The in-VM tightening is Vikunja #423. Only its step 1, the `localadmin` account,
### 9.2 The slice and the date
The smallest slice that lets her start is the S rows: C2-01, C2-03a, C2-04, C2-05a, C2-05b, C2-06, C2-07,
C2-08, C2-09a, C2-10a, C2-10b, C2-12, C2-13, C2-14 and C2-20. With it she keeps writing in Obsidian exactly
as now. In the Console she opens a thread with Bean, Chet or a specialist and reads each answer as items
whose citations the broker checked. A status line tells her when the hub falls behind her repo. Hermes
keeps running beside it.
The smallest slice that lets her start is the S rows: C2-01, C2-01c, C2-01d, C2-02, C2-03a, C2-03b, C2-04,
C2-05a, C2-05b, C2-05c, C2-06, C2-08, C2-08b, C2-09a, C2-10a, C2-10b, C2-12, C2-12b, C2-13a, C2-13b, C2-14,
C2-20 and C2-21. C2-11a and C2-11b are slice rows too, and they don't gate 10-27 (Q195). With it she keeps
writing in Obsidian exactly as now. In the Console she opens a thread with Bean, Chet or a specialist and
reads each answer as free text whose citations the broker checked. Each agent write shows as a notice she can
revert. A status line tells her when the hub falls behind her repo. Hermes keeps running beside it.
Not ready by 10-27: bus writes and the Console editor (C2-02, C2-03b, C2-11a, C2-11b, C2-19), rooms with
@tags (C2-09b, C2-10c), the Discord relay (C2-18), and C2-15 to C2-17. The slice makes 10-27 only if
Not ready by 10-27: promotion (C2-19), rooms with @tags (C2-09b, C2-10c), the Discord relay (C2-18), C2-15
and C2-16. The editor may land after 10-27 without holding it. The slice makes 10-27 only if
section 10 steps 1-7 are done by about 10-24. The Console from her machine needs SSO (#412) or an interim
sign-in ruling, and that is the step most likely to be late.
@@ -1334,7 +1699,9 @@ What closes the rest.
The Gitea host. Her books repo is already on git.mosaicstack.dev (mos-1000). On this host that name
resolves to 10.1.1.222, and STATE.md lists its certificate among the edge certificates, so I treat it as
reachable from the internet. Her writing is already there by her own choice. The hub adds no new remote.
What it adds is one bot account with read access to that one repo.
It adds no shared bot account. Each writing seat gets its own Gitea account through the grant path, a
collaborator on that one repo with `write`, and the chief of staff one with `read` (ruling after r11).
Her own token signs her Console saves.
1. The host is set (Q181): 10.1.1.54 as `mosaic` under `/srv/mosaic`, 16 GiB and 6 cores. Jason
confirmed Q4 (Q183), so #421 is closed and the install doesn't wait on it.
@@ -1382,17 +1749,25 @@ What it adds is one bot account with read access to that one repo.
the one file decision 119 and `packages/cli/README.md` document, so I kept it. He places it, or
says who may, per the Q173 secrets rule. Agents never print it. Until #423 step 5, `melanie`'s
sudo can read it, and so can her Hermes processes (9).
3. A new Gitea bot account for the hub, with read access to the books repo only. Not bean-brain, and not
her other three repos. Her repo's access list is hers, so adding the bot means asking her or acting as
admin, which is his call.
4. The bot's token in slot `library-gitea` under `<configDir>/secrets/<business>/`, which is
`/srv/mosaic/.config/mosaic-dev/secrets/<business>/` for `mosaic`, mode 0600. He puts it there, or
says who may, per the Q173 secrets rule. Agents never print it.
3. No bot account. The install enrolls the chief of staff, which raises the gitea grants for `books`
through the existing step, and the executor makes each seat a collaborator on her repo (C2-21). The
seats reach the books repo only, not bean-brain and not her other three repos. Whether each grant also
needs his Approve is Q31, asked of him, and this design adds no step for it. The collaborators change
her repo's access list, which is hers, so step 7 tells her.
4. Her own Gitea token for her Console saves, in slot `library-human` under
`<configDir>/secrets/<business>/`, which is `/srv/mosaic/.config/mosaic-dev/secrets/<business>/` for
`mosaic`, mode 0600. He puts it there, or says who may, per the Q173 secrets rule. Agents never print
it. If the install runs before C2-03b lands, the C2-03a fetch still reads slot `library-gitea`, which
then needs a read token as r11 said. After C2-03b the fetch reads that slot only while the bus has
no gitea grant event for the chief (7.2). Step 4b removes it.
4a. `scripts/case2-precheck.sh --business <business> --library-status` as `<user>`, once the bus is up
and step 4's token is in place. It reads the bus's recorded sync state only and syncs nothing. It
and the fetch token is in place. It reads the bus's recorded sync state only and syncs nothing. It
refuses when the mirror is missing, when `lastError` is set, or when `lastFetch` is null. It prints
each R-problem as a `problem` line and then a count, never text (sage-2189). The bus syncs every 60
seconds, so the first check after step 4 may have to wait for one sync.
4b. Once C2-21 has run and `library.status` shows the fetch on the chief's grant, Jason deletes slot
`library-gitea` and revokes its token in Gitea. The fetch never reads the slot again, since a gitea
grant event for the chief then exists and events are never deleted (webby-0151 R3, webby-0156 R4).
5. Her Console sign-in from her machine: her Authentik user and `sub` once SSO lands (#412 step 3 is the
model), or his ruling on an interim. The proxy forwards to the Console's port below 1024 and sets the
proof header. The proof file goes under `<configDir>/secrets/`, mode 0600, owned by `mosaic`, and
@@ -1401,12 +1776,14 @@ What it adds is one bot account with read access to that one repo.
6. The C2-20 persona import: who runs it on `<host>`, his check of the printed profile-to-name pairing,
and whether the per-channel prompts count as personality.
7. Running `library survey` on `<host>` after step 4, which prints names and counts, never text. Then
contacting Melanie: who tells her what changes for Bean and Chet (4), sends the setup sheet (C2-12),
and asks her to confirm the mapping, the universes, the playground folders and which specialists to
start with, and when. A specialist she wants later is added by the human through `instances.set`
contacting Melanie: who tells her what changes for Bean and Chet (4), sends the setup sheet (C2-12b),
tells her the agents become collaborators on her repo, and asks her to confirm the mapping, the
universes, the playground and supporting folders, her `editor` name and email, and which specialists
to start with, and when. A specialist she wants later is added by the human through `instances.set`
(4), with no hand edit and no new row.
8. The residual in 5.1: up to 5 idea items of 200 characters per reply, 1,000 characters of free text
within 1,200 total. He or Melanie accepts it or orders lower caps. Mos takes it to him.
8. The residual in 5.1. Closed. Q219 set lower caps, and Q223 then removed the caps with the item
format. What remains is the residual the ruling after r11 states, a supporting doc that holds any
text, and C2-12b tells her about it.
9. The instance start. Stopping Hermes comes later, and only when she chooses it.
- The bus and the Console are already running from step 2c, and both come back after a boot on
their own (Q189, which reverses mos-1097 item 1). Nobody starts the Console by hand here. With a
+331
View File
@@ -0,0 +1,331 @@
# Row 229 design: a per-instance pi agent dir for Chet's Codex login (#428, Gitea #1730)
Design round 2, Gosalyn, 2026-10-22, for Webby's webby-0148 (R1, R2, N1-N5). Brief:
`docs/plans/2026-10-20_case2-rows.md`, "Brief: row B, a per-instance pi agent dir for Chet's Codex login
(#428)". Base: `refactor` at aca16404c. Pi is the pinned
`@earendil-works/pi-coding-agent` in the repository's `node_modules`. Cites into pi are
`node_modules/@earendil-works/pi-coding-agent/dist/...` (`pi:`) and
`node_modules/@earendil-works/pi-ai/dist/...` (`pi-ai:`).
This is a design. Nothing in it is built or tested. The brief's gate holds: no build row starts until
the live check in section 9 passes, run with Jason's or Mos's go, after 10-27. Mos ruled that "starts
after 10-27" covers the build and the live check, not the design, and that the check gates every build
row (mos-1306). Mos asks Jason for the go when the build rows are ready.
## 0. Summary
- Each pi instance whose route uses provider `openai-codex` gets its own agent dir,
`<dataRoot>/pi-agents/<business>/<instance>/`. The host makes it with the same checks as a thread store:
a real directory, owned by the bus user, mode 0700, no symlink in the path.
- The launcher binds that dir read-write at the same path into the session and sets
`PI_CODING_AGENT_DIR` to it through the pi module's `env()`. No other instance's session sees it.
- Pi refreshes in place under its own file lock. The new tokens are on the host disk when the run ends,
so the next run starts with them.
- No tool in the session can read the dir. The gate holds every path tool to the workspace, and a role
with `bash` refuses at launch with `pi-agent-dir-shell`.
- Before each launch the host checks the dir's contents against a short allowlist: `auth.json`, its
`auth.json.lock` dir, and a `settings.json` holding only the three keys a fresh `/login` writes. Pi
reads `models.json`, `models-store.json` and `settings.json` from the agent dir, and any of them could
send the token elsewhere or run a command. Anything outside the allowlist refuses `pi-agent-dir-unsafe`.
- The pi module's preflight runs `pi auth check --provider openai-codex --no-refresh --json` against the
dir. No login refuses `pi-login-missing`, and a broken file refuses `pi-login-invalid`.
- The human logs in with a new `mosaic pi-login <business> <instance>`. It runs pi's own `/login` with
the dir set, `--offline`, `--no-session` and an empty temporary cwd, so Mosaic gets its own fresh
grant and pi writes nothing else there. It never copies Hermes's `auth.json`.
- Chet's route has no fallback by default (mos-1306). A fallback would quietly move him back onto Bean's
Z.ai key.
- Two build rows: 229a, the store, the checks, the bind, the env, the preflight and the tests. 229b, the
login command and the README.
## 1. Facts at aca16404c
Pi side:
- `getAgentDir()` returns `PI_CODING_AGENT_DIR`, else `~/.pi/agent` (`pi:config.js:421`). `auth.json`,
`settings.json`, `models.json`, `themes/`, `tools/`, `bin/`, `prompts/`, `sessions/` and `pi-debug.log`
all sit under it (`pi:config.js:430-462`), and so do `models-store.json`
(`pi:core/models-store.js:30`), a cached provider catalog, and `keybindings.json`
(`pi:core/keybindings.js:301`).
- `checkProviderAuth` builds a model runtime, and `ModelRuntime.create` loads `models.json` from the
agent dir (`pi:core/model-runtime.js:76`). So the auth check itself reads `models.json`.
- Interactive mode downloads `fd` and `rg` into `<agentDir>/bin/` when they are missing
(`pi:modes/interactive/interactive-mode.js:699-705`, `pi:utils/tools-manager.js:300-330`) unless
`PI_OFFLINE` is set, which `--offline` does (`pi:main.js:440-444`, `pi:utils/tools-manager.js:13`).
Without `--no-session` it makes `<agentDir>/sessions/` (`pi:core/session-manager.js:242-252`).
- A fresh interactive start writes `lastChangelogVersion` to `settings.json`
(`pi:modes/interactive/interactive-mode.js:918`). A first `/login` then selects the provider's
default model with `persist: true` (`:4766`), which writes `defaultProvider` and `defaultModel`
(`pi:core/settings-manager.js:470-476`). `enabledModels` changes only when scoped models are set
(`pi:core/agent-session.js:1272-1285`), and a fresh dir has none. No settings write runs outside
interactive mode.
- `AuthStorage` writes `auth.json` with mode 0600 only when it creates the file
(`pi:core/auth-storage.js:14-15`). It never fixes the mode of an existing file. It takes a
proper-lockfile lock before a refresh write (`:39`, `:85`), which is the directory `auth.json.lock`
beside the file, and writes with `writeFileSync` (`:66`, `:140`). The lock is a sibling path, so the
dir itself must be writable. A file-only bind isn't enough.
- pi-ai refreshes under a double-checked lock and treats a token with less than 5 minutes left as
expired (`pi-ai:...resolve.js:62-74`). Two sessions sharing one dir therefore serialize their refreshes.
- The `openai-codex` OAuth client has a browser flow with a callback on `localhost:1455` and a device-code
flow at `auth.openai.com/codex/device` (`pi-ai:auth/oauth/openai-codex.js:26-33`). A refresh posts
`grant_type=refresh_token` (`:125-140`), and the response carries a new refresh token. The old one is
then spent.
- Startup migrations write into the agent dir only when legacy files exist there: `oauth.json`, session
files in the dir root, `commands/`, `tools/` (`pi:migrations.js:17-63`, `:76-129`, `:161-177`).
`settings-manager.js:101` writes `settings.json` when a setting changes.
- `pi auth check --provider <p> [--json] [--credentials] [--no-refresh]` (`pi:cli/auth-command.js:5`,
`:23-25`). `checkProviderAuth` (`pi:cli/auth-check.js:5-34`) returns `ready`, `not_ready` or `invalid`.
It builds its model runtime with `allowModelNetwork: false` (`:43-49`). With `--no-refresh` it only
checks that a credential is present: a revoked refresh token still reads `ready`. It prints no
credential unless `--credentials` is given. `auth print-bearer-token` and `print-api-key` print
secrets, and Mosaic never runs them.
Mosaic side:
- `binds()` lists every mount and says why (`packages/seat/src/sandbox.mjs:102-131`). The thread store
binds read-write at `:119-125`. `sandboxArgv` sets `HOME` to the run's own `home/` (`:163-166`).
- `threadStore()` makes `<dataRoot>/threads/<business>/<thread>/` and refuses a symlink, a non-directory,
another owner or any group or other mode bit (`packages/seat/src/session.mjs:59-72`). `ENV_ALLOW`
doesn't list `PI_CODING_AGENT_DIR` (`:83-88`).
- The launcher's check order is at `packages/cli/src/launcher.mjs:1-40`. One instance per thread runs at
a time (`:289`, `registry.running(b.id, instance, thread)`), so a thread instance such as Chet can run in
two threads at once. Preflight is `harness.preflight({ env })` (`:333`). A failed primary preflight
takes the stated fallback (`:334-345`, row 65). The thread store is made at `:388-394`, mounts at `:456`,
the session file with `harnessEnv: bundle.env` at `:461-484`, the paths at `:486-495`.
- The pi module's `preflight()` checks only the binary (`packages/plugins/harness/pi/index.mjs:33-37`).
`env({ policy, tools, session })` builds the harness env (`:63-71`). The adapter runs pi with
`--no-extensions`, `--no-skills`, `--no-prompt-templates`, `--no-approve` and `--offline`
(`adapters/pi/adapter.sh:104-123`).
- The gate refuses a path tool whose path is outside the workspace (`packages/harness/src/gate.mjs:79-83`,
`:152`). Chet's role, `roles/companion.json`, has tools `["read","web"]`.
## 2. Where the dir lives and who can read it
`<dataRoot>/pi-agents/<business>/<instance>/`. A new seat function `agentStore(dataRoot, business,
instance)` in `session.mjs`, beside `threadStore()`, makes each level with mode 0700 and the same refusal:
a symlink, a non-directory, another owner or any 0077 mode bit refuses `pi-agent-dir-unsafe`. Business and
instance must match `ID_PATTERN` and hold no `..`.
The dir belongs to the instance, not the thread. Chet's threads share one login, and pi's lock keeps
their refreshes apart (section 1). Only the bus user can read it on the host. A seat that shares the bus
uid can read it too, which is the same exposure the thread stores and the bus's own credential files
have today (case 2 r11 B1). This row doesn't change that.
Which routes get one. The pi module exports `agentStore({ provider })`, true for `openai-codex`. The
launcher asks the primary route's module, and the fallback's if one is taken. A module without the hook
gets no dir. A provider key route such as Bean's Z.ai keeps working exactly as today.
## 3. How the launcher mounts it
After step 5 picks the route and before the bwrap check:
1. If the module says the route needs a store, the launcher calls `agentStore()`. A refusal stops the
launch with its code.
2. The contents check in section 5 runs.
3. The module's preflight runs with the dir (section 6).
4. `paths` gains `agentStore`, and `binds()` adds one entry:
`{ kind: "rw", src: dir, why: "the instance's pi agent dir, its own login, kept across runs (row 229)" }`.
It binds at the same path, so the path pi sees is the host path.
5. The session object passed to `bundle` gains `agentDir`, and the pi module's `env()` adds
`PI_CODING_AGENT_DIR: session.agentDir` when it is set. It reaches pi through `session.harnessEnv`,
which the runner spreads into the adapter's env. `ENV_ALLOW` doesn't change, so no host value of
`PI_CODING_AGENT_DIR` ever passes.
The preflight signature changes to `preflight({ env, agentDir })`. Every other module ignores the new key,
so their behaviour doesn't change. The adapter contract in `adapters/README.md` gains one line for it.
## 4. How a refresh survives the run
Pi writes the refreshed `auth.json` into the bound dir, which is the host dir. Nothing copies it in or out.
A run that dies mid-write leaves pi's lock behind. Pi's lock goes stale on its own timer. The contents
check in section 5 accepts a lock entry and never deletes one, so the host never breaks a lock a running
session holds.
Two clients on one grant would race: a refresh spends the old refresh token, and the other client's next
refresh fails. Mosaic avoids that by never sharing a grant. `mosaic pi-login` makes a new one (section 7).
Hermes keeps its own grant. Whether OpenAI revokes an older grant when a new one is made, or limits grants
per account, I don't know. Section 9's live check exists to find out.
## 5. What the session can read, and what the dir may hold
The session's tools:
- `read` and every other path tool go through the gate, which refuses a path outside the workspace. The
agent dir is outside it.
- `web` sends a URL and gets text back. It has no file argument.
- `bash` would reach the dir, since pi itself must open it. A role whose tool list has `bash`, or a route
with the harness's built-in shell, refuses `pi-agent-dir-shell` at launch. The same check runs in
`validate` for the role file, so the conflict shows before a launch.
- The pi process reads the dir. That is the point of the bind.
The dir's contents. Pi reads more than `auth.json` from the agent dir. A `models.json` there can set a
provider's base URL, which would send the OAuth token to another host, and `models-store.json` is a
cached catalog of the same kind. `settings.json` is worse than it looks. Every dangerous setting is a
key pi knows, so a check against pi's own key set would pass all of them (webby-0148 R1):
- `httpProxy` applies to every request pi makes, the Codex bearer and the refresh POST included
(`pi:main.js:455`, `:685`)
- `packages`, `extensions`, `skills`, `prompts` and `themes` name code or text to load
- `shellPath`, `shellCommandPrefix`, `npmCommand`, `externalEditorCommand` and `sessionDir` name a
command to run or a place to write
`--no-extensions` and the other flags turn discovery off, but this design doesn't rest on them alone.
Before each launch the host lists the dir and allows only:
- `auth.json`, a regular file, mode 0600, owned by the bus user. Pi sets 0600 only when it creates the
file, so this check is what holds the mode
- `auth.json.lock`, a directory, owned by the bus user. It is the only directory allowed, and the check
never deletes it (section 4)
- `settings.json`, a regular file of at most 4 KiB that parses to a plain object, empty or with only
these keys: `lastChangelogVersion`, a string of at most 32 characters matching
`^\d+\.\d+\.\d+[0-9A-Za-z.+-]*$`, `defaultProvider`, exactly `openai-codex`, and `defaultModel`, a
string of at most 64 characters matching `^[A-Za-z0-9._-]+$`. These are the keys a fresh `/login`
writes (section 1). Any other key refuses, `httpProxy` and the paths and commands above by name
Anything else refuses `pi-agent-dir-unsafe` and names the entry: `models.json`, `models-store.json`,
`keybindings.json`, `extensions/`, `tools/`, `bin/`, `themes/`, `prompts/`, `skills/`, `sessions/`,
`pi-debug.log`, `oauth.json`, a symlink, a device, or any other file or directory. Round 1 allowed
`models-store.json` and `keybindings.json`. Round 2 refuses both: the first can carry a base URL like
`models.json`, and nothing in a headless run or a login needs the second. Nothing in the session can
create a refused entry, because no session tool reaches the dir. The check guards against a human
copying a whole `~/.pi/agent` in by hand, and against a login that wrote more than it should.
The settings allowlist is the three keys above, written in Mosaic's code, not read from pi. A test pins
the sorted list of `SettingsManager` setter names in the pinned package by its sha256, so a pi bump that
adds, drops or renames any setting fails the suite, and the human reviewing the bump re-checks the three
keys before re-pinning.
## 6. How a launch with no login refuses
The pi module's `preflight({ env, agentDir })`, after the binary check, runs when `agentDir` is set:
```
node <pi bin> auth check --provider openai-codex --no-refresh --json
```
It runs only after the section 5 contents check passes, because the auth check loads `models.json` from
the dir (section 1). It uses `PI_CODING_AGENT_DIR=<agentDir>`, `HOME` and the cwd both set to one empty
temporary dir made with `mkdtemp` under the scratch tmp and removed afterwards, and `PATH` from the
system. It runs on the host with a 10-second timeout. Never `--credentials`. Results:
- exit 0, `ready`: pass
- exit 1, `not_ready`: `pi-login-missing`, with the message "run `mosaic pi-login <business> <instance>`"
- exit 2, `invalid`, a timeout or any other exit: `pi-login-invalid`
The check never refreshes, so a launch never spends a refresh token on the host. That leaves one gap I
can't close without a network call: a revoked or expired refresh token still reads `ready`. Pi then
fails the first turn when its refresh fails, and the turn's failure is visible in the run record. The
fix is the same login command. I chose `--no-refresh` over a refresh at preflight because a host-side
refresh would race a running session of the same instance.
Fallback. Row 65 lets a failed primary preflight take the stated fallback. Chet's route ships with none
(mos-1306). A fallback onto Bean's Z.ai key would make a lost login silent: Chet answers on another
provider and Melanie can't tell. Without one, the launch refuses with the preflight's code. A human can
still add a fallback through `routes.set`. No agent path can: Chet is a `companion` instance, a creative
definition (`packages/business/src/vocabulary.mjs:207`), and `creativeHumanOnly`
(`packages/business/src/launch-settings.mjs:13-17`, called for routes at `:293`) refuses
`organization-creative-human-only` unless the actor is the business file's human. The tester is not
that human, though `routes.set` is a tester verb (`packages/cli/src/broker.mjs:139`). This row only
makes none the default.
## 7. The login command (229b)
`mosaic pi-login <business> <instance>` in `packages/cli`, a human verb:
1. Refuses unless the instance's primary route is pi with provider `openai-codex`
(`pi-login-not-needed`), and unless the instance isn't running (`instance-running`).
2. Calls `agentStore()` and runs the contents check.
3. Moves an existing `auth.json` out of the dir, to
`<dataRoot>/pi-agents/<business>/.replaced/<instance>-<UTC>.json` at 0600, so every login is a fresh
grant and the contents allowlist doesn't grow. It never reads or copies `~/.pi/agent` or Hermes's
files.
4. Runs interactive pi on the attached terminal with `PI_CODING_AGENT_DIR=<dir>`, `HOME` and the cwd
both set to one empty temporary dir under the scratch tmp, and these flags: `--offline`, so pi
downloads no `fd` or `rg` into `<dir>/bin/` and sends no telemetry, `--no-session`, so it makes no
`<dir>/sessions/`, and the `--no-extensions`, `--no-skills` and `--no-prompt-templates` the adapter
uses. `--offline` doesn't block `/login`: it sets `PI_OFFLINE` and skips the version check and the
catalog refresh, and the OAuth flow makes its own requests. The command tells the human to type
`/login`, choose OpenAI Codex and pick the device code flow, since the callback on `localhost:1455`
won't reach a browser on another machine.
5. Afterwards runs the section 5 contents check, then the section 6 check, and prints only their
status. A login that left any entry outside the allowlist refuses `pi-agent-dir-unsafe` here, before
any launch.
The command prints no token. `.replaced/` gets the `agentStore()` checks. It can't collide with an
instance dir, since `ID_PATTERN` doesn't allow a leading dot.
`reset.sh` wipes the data root, and the login with it. The README says so.
## 8. Files the build touches
229a:
- `packages/seat/src/session.mjs`: `agentStore()`, the contents check
- `packages/seat/src/sandbox.mjs`: the `agentStore` bind in `binds()`
- `packages/cli/src/launcher.mjs`: the store, the check and the preflight argument after step 5, the
`pi-agent-dir-shell` refusal, `paths.agentStore`, `agentDir` on the session
- no change to `packages/harness/src/bundle.mjs`, which already passes `{ ...session, ...model }` to `env()`
(`:375`), so `agentDir` on the launcher's session reaches the module
- `packages/plugins/harness/pi/index.mjs`: `agentStore()`, the preflight, `PI_CODING_AGENT_DIR` in `env()`
- `adapters/README.md`: the `agentDir` preflight key
- the role validator for `pi-agent-dir-shell`
- tests in `packages/seat/tests`, `packages/cli/tests`, `packages/plugins/harness/pi/tests`
229b:
- `packages/cli/src/pi-login.mjs` and its dispatch
- `packages/cli/README.md`
- tests in `packages/cli/tests`
## 9. The live check (the gate)
Run after 10-27, with Jason's or Mos's go, on Chet's real ChatGPT login. A human does the login.
1. Hermes answers as Chet. Record the time.
2. `mosaic pi-login` for Chet. The status reads `ready`.
3. Launch Chet on the new route. Run one turn.
4. Force a Mosaic refresh. The human edits only `expires` in Mosaic's `auth.json` to a past time, then
runs one more turn. It succeeds, and `auth.json` changed. After steps 2, 3 and 4 the record lists the
dir's entry names, and each passes the section 5 check.
5. Hermes answers again. This is the check that Mosaic's refresh didn't revoke Hermes's grant.
6. Wait for Hermes's own next refresh, at least one access-token lifetime, then run a Mosaic turn. This is
the check the other way.
7. The record lists each step's time and result, and never a token.
Any failure stops the row. The answer then goes to Jason: Chet waits until Hermes stops, or he moves
alone. No build row starts before step 6 passes.
## 10. Tests the build rows carry
229a:
- `agentStore()` refuses a symlinked level, a wrong owner, a 0750 dir and a `..` id
- the contents check passes the allowed set and refuses each listed entry, `models-store.json`,
`keybindings.json`, `bin/` and `pi-debug.log` included, and a second directory beside
`auth.json.lock`
- `settings.json` passes when absent, `{}` and with the three allowed keys. It refuses each of
`httpProxy`, `packages`, `extensions`, `skills`, `prompts`, `themes`, `shellPath`,
`shellCommandPrefix`, `npmCommand`, `externalEditorCommand` and `sessionDir` by name, a
`defaultProvider` other than `openai-codex`, a non-string or overlong value, an array, a file over
4 KiB and a file that doesn't parse
- `auth.json` at 0644 refuses, so a file that pi didn't create still meets the mode
- the bump test: the sha256 of the sorted `SettingsManager` setter names in the pinned package matches
the pinned value, so any change to pi's settings key set fails it
- a `models.json` in the dir refuses `pi-agent-dir-unsafe` before the fake pi bin runs. The fake bin
records each call, and the record is empty
- the preflight's fake pi bin records its cwd and `HOME`, which are the same empty temporary dir
- the tester's `routes.set` adding a fallback to Chet's route refuses `organization-creative-human-only`
- a companion role with `bash` refuses `pi-agent-dir-shell`
- `binds()` adds the entry only when `agentStore` is set
- `env()` sets `PI_CODING_AGENT_DIR` only when `agentDir` is set
- a host `PI_CODING_AGENT_DIR` never reaches the session
- preflight maps exit 0, 1, 2 and a timeout to pass, `pi-login-missing` and `pi-login-invalid`. A fake
pi bin records its argv, which must hold `--no-refresh` and never `--credentials`
- a route with no store gets no bind and no preflight change. Bean's route is unchanged
- Chet's route with a failed preflight and no fallback refuses with the preflight's code
- a mutant without the contents check fails, and so does one without the shell refusal
- every test uses `mktemp -d` under the scratch tmp, never `/tmp`, and no real login
229b:
- `pi-login` refuses a non-codex route and a running instance
- it moves an existing `auth.json` out of the dir, never reads `~/.pi/agent`, and prints only the status
- its fake pi bin records argv, which holds `--offline` and `--no-session`, and a cwd that is an empty
temporary dir
- after a fake login that writes `auth.json`, `auth.json.lock/` and a `settings.json` with the three keys,
the dir holds only allowlisted entries and the command reports `ready`
- a fake login that also writes `bin/fd` or `sessions/` makes the command refuse `pi-agent-dir-unsafe`
## 11. Open items
- Whether OpenAI revokes or caps grants per account. Section 9 answers it.
- Whether pi's request-time refresh works under the adapter's flags. I read nothing that blocks it.
`--offline` affects only the model catalog (`pi:core/model-runtime.js:88`). Section 9 step 4 proves it.
- Whether a real `/login` on the pinned pi writes only the three settings keys. Section 1 reads it from
the code. Section 9's dir listings prove it, and a fourth key there stops the row for a design change.
+191
View File
@@ -0,0 +1,191 @@
# Row 283 (#1788) C2-01d design, draft 2 (Honker)
Brief: docs/plans/2026-10-20_case2-rows.md "C2-01d" at refactor 94c33a43c. Sources: the design at
agents/gosalyn/work/case2/DESIGN.md b68d79663, the ruling after r11 (:207), the M12 paragraph (:300),
1.1 (:755) and 1.2 (:817). Base 94c33a43c. Reviewer Webby, gate owner Sage. Nothing is built yet. The
build waits for 282 to land (sage-2906).
## 1. Mapping (packages/library/src/mapping.mjs)
`supporting`:
- Every universe kind takes an optional `supporting` list: plain `['name','books','playground','supporting']`,
hybrid adds it to its seven keys, and a draft gets `['kind','name','playground','supporting']`.
- Each value is checked as a playground prefix is: a string ending in `/` whose body passes `folder()`.
- The output carries `supporting` on every universe, `[]` when absent, the way a draft carries
`books: []`. classify then needs no `?? []`. The business writer writes the raw document
(organization.mjs:195), not this output, so no business file changes shape.
- The prefix rules, across every entry (ruling after r11):
- A supporting prefix holding another supporting prefix, or one listed twice, refuses
`library-mapping-invalid`.
- A prefix equal to one of its own entry's book folders, or holding one, refuses `library-mapping-invalid`.
- A prefix inside or holding another entry's book folder refuses `library-supporting-other-universe`.
- Level comes from where the prefix sits, as for the playground: inside one of its own entry's books
is book level, and outside every book is universe level. A draft's prefixes are all universe level.
- Between the two lists (my choice, Q2):
- A supporting prefix may equal a playground prefix of the same entry. That folder is writable.
- A playground prefix may hold a supporting prefix of the same entry. The inner folder is supporting
and the rest stays playground.
- A supporting prefix that holds a playground prefix refuses `library-mapping-invalid`. classify
checks supporting first, so that playground prefix could never match.
- A supporting prefix equal to, holding or inside a playground prefix of another entry refuses
`library-mapping-invalid`. Otherwise one path would have two universes.
- The book loop at mapping.mjs:128-140 becomes a helper that both lists call with their own code and
label. The playground's messages and codes stay byte-equal, so no existing test text moves.
`editor`:
- The entry takes an optional `editor`. When present, it is an object with exactly the keys `name` and
`email` (via `only()`). Each value is a string that is non-empty after trim and has no control
character (`[\u0000-\u001f\u007f-\u009f]`), `<` or `>` (webby-0169 R1: git drops them from an ident
and commits under a different name, so C2-02 would never see a failure). Anything else refuses
`library-mapping-invalid`.
- The output carries `editor: { name, email }` only when present, as `user` and `vaultPrefix` do. A save
with no `editor` refusing is C2-02's job.
## 2. Scope (scope.mjs)
- `classify` gets a supporting loop before the playground loop (design 1.2). It returns `place(...)`
with `{ scope: 'supporting', universe, book }`. The book is the book folder holding the prefix, or
null at universe level and in a draft. The type and folder come from the first folder under the
prefix, as for the playground.
- The `.md` check stays first, so a `.canvas` or `.base` under a supporting prefix stays unmapped.
- The comment above `classify` names the three prefix scopes and the order.
## 3. supportingPath (new supporting.mjs)
`supportingPath(mapping, universe, path, tree)` returns `path` or throws
`LibraryError('library-write-story-path', ...)`. It is pure and reads no git.
- `tree` is the array `listTree` returns (reader.mjs:26, `{ mode, type, oid, size, path }`), so the
C2-02 broker passes its listing as it is (Q3). A non-array `tree` throws a TypeError, because that is
a caller bug, not a path class.
- The checks run in this order, each with its own message naming the class:
1. The path must be a string of at most 1,024 UTF-8 bytes (`Buffer.byteLength`).
2. It must have no control character, using the same control class as `editor` (not `<` or `>`).
3. It must have no backslash.
4. It must not be absolute (a leading `/`).
5. It must have no empty, `.` or `..` segment. That also catches `//` and a trailing `/`.
6. `classify(mapping, path)` must give scope `supporting` and that same `universe`. This one check
covers an unknown universe, an unmapped path, a `.canvas`, `.base` or attachment, a canon or
story path, a playground path that isn't also supporting, and another universe's prefix. The scope
rule then has one source, and M1 below hits both.
7. Every proper prefix of the path (`a`, `a/b`, ...) and the path itself must not be in `tree` with
mode `120000` (symlink) or `160000` (gitlink).
8. No prefix of the path, and not the path itself, may equal a tree path or a tree directory
(derived from the entries' paths) after `normalize('NFC').toLowerCase()` on both sides, unless
it equals that tree path exactly. NFC as well as case, because APFS treats NFC and NFD forms of
a name as one file (webby-0169 N1).
Directories are checked too, because the folders under a supporting prefix are hers to name. A
path in a tree that holds both `a.md` and `A.md` refuses, which matches R7.
- The refusal messages quote the path with JSON, except for the control-character class, whose message
leaves the path out.
- The delete rule ("A delete also needs the path to exist at the tip as a 100644 blob", M12) isn't in
the brief's supportingPath list. It stays with C2-02's delete (Q4).
The C2-02 brief doesn't name it (only design :308), so BUILD.md carries it to C2-02 under Handoffs
(webby-0169 Q4).
- index.mjs exports `supportingPath`.
## 4. README (packages/library/README.md)
- Mapping: `supporting` and its rules, the cross-list rules, and `editor`.
- Refusals: a `library-supporting-other-universe` row, and the new cases in the
`library-mapping-invalid` row.
- Scope: supporting first, then playground, then canon.
- A new section, "Agent write paths", for supportingPath and its classes.
## 5. Callers of classify outside files owned (Q1 for Sage)
- survey.mjs:65 builds `{ canon: 0, playground: 0, unmapped: 0 }` and increments
`scopes[classify(...).scope]`. A supporting file adds `supporting: NaN`, and formatSurvey (:92) never
prints it. I propose a departure: add `supporting: 0` and print it, so the line becomes
`scopes canon 5, playground 1, supporting 0, unmapped 3; indexed 6`. That re-pins survey.test:73 and
adds a count test. Without it, the survey reports garbage once she marks a folder. The departure
covers two files, packages/library/src/survey.mjs and packages/library/tests/survey.test.mjs.
- No change in the rest, each for a reason:
- reader.mjs:183 skips only unmapped, so supporting files are indexed like playground files.
- R5 (:207) flags only canon to playground, as design 1.3 R5 says. A canon link to a supporting file
raises nothing.
- render.mjs:23 gives supporting a null badge. The supporting badge belongs to C2-10b (design :1415).
- mirror.mjs:246 refuses only unmapped. Reading supporting files by scope is C2-05c's.
- No bus or webui source filters on scope (git grep at 94c33a43c).
- M9 already protects the whole `projects` key from agent edits (library-projects-protected.test). No
test is added there.
## 6. Tests
- mapping.test:
- T1: `supporting` at book level, at universe level, in a hybrid and in a draft. The output carries
it, with `[]` when absent. The draft `deepEqual` at :48 gains `supporting: []`.
- T2: one test per prefix rule.
- These refuse `library-mapping-invalid`: no trailing `/`, a bad folder, one holding another, one
listed twice, one equal to its own book, one holding its own book, one holding a playground
prefix, and one overlapping another entry's playground.
- These refuse `library-supporting-other-universe`: one inside another universe's book, and one
holding another universe's book.
- An equal same-entry playground passes.
- T3: `editor` passes, and absent passes. It refuses `library-mapping-invalid` for a non-object, an
array, an extra key, a missing key, an empty or blank value, a non-string, and a control character,
`<` or `>` in either value.
- scope.test, T4:
- Supporting at book level, universe level and in a draft each give their universe and book.
- A folder in both lists is `supporting`, and a playground-only folder is `playground`.
- A supporting `.canvas` or `.base` is unmapped.
- supporting.test (new):
- T5 passes at book level, universe level, in a draft, in a both-lists folder, and at exactly 1,024 bytes.
- T6 has one refusal per class against a fixture tree with a symlink, a gitlink and a case variant.
The classes are:
- unmapped, `.canvas`, `.base`, an attachment, canon, playground-only, another universe's
supporting prefix, and an unknown universe;
- absolute, `..`, `.`, `//`, a trailing `/`, a backslash, a C0 control and DEL;
- a symlink folder, a gitlink folder, and a symlink at the path itself;
- a case-variant file, a case-variant folder, and an NFD-form variant of an NFC tree name;
- 1,025 bytes made with a multi-byte character, so bytes and characters differ.
- Each asserts the code `library-write-story-path`.
- business.test: T7 adds one assertion that `validateBusinessDocument` refuses
`library-supporting-other-universe`.
- survey.test (only if Q1 is approved): T8 counts one supporting file, and :73 is re-pinned.
## 7. Mutants (exact strings go in BUILD.md)
| Mutant | Change | Killed by |
|---|---|---|
| M1 | classify checks playground before supporting (the brief's mutant) | T4 both-lists, T5 both-lists |
| M2 | the other-universe fail for supporting is dropped | T2 |
| M3 | the symlink and gitlink check is dropped | T6 |
| M4 | the case check is dropped | T6 |
| M4b | check 8 drops `normalize('NFC')` | T6 NFD variant |
| M5 | the byte length uses `path.length` | T6 1,025 bytes |
| M6 | the `editor` control-character check is dropped | T3 |
| M6b | the `editor` `<` and `>` check is dropped | T3 |
| M7 | supportingPath skips the universe match | T6 other universe |
| M8 | the supporting-holds-playground check is dropped | T2 |
## 8. Gate
- Webby approves the manifest.
- The library and business suites and every scripts/test-*.sh are green on Sage's gate.
- Timeouts (mos-1275): each package `timeout -k 10 1800`, each script `timeout -k 10 300`, each single
file and mutant `timeout -k 10 600`. These come from my row 256 gate at 10:34Z.
- Before in-review, grep packages/*/tests for 64-hex pins on the changed files and for the changed
message text, then drift-check BASE..refactor.
## 9. Questions
- Q1 (Sage): approve the survey.mjs and survey.test.mjs departure in section 5?
- Q2 (Webby): the cross-list rules in section 1?
- Q3 (Webby): `tree` as the `listTree` array?
- Q4 (Webby): the delete's 100644 check stays with C2-02?
- Q5 (Webby): `editor` refuses only blank values and control characters, as the brief says. `<` and `>`
would break a git ident. I'd leave them to C2-02's commit step unless you want them refused here.
## 10. Draft 2 (webby-0169)
- R1 folded: `editor` refuses `<` and `>` too, with T3 cases and mutant M6b.
- Q2 accepted. BUILD.md records the tradeoff: refusing a supporting prefix that holds a playground
prefix goes past the brief, but it is the fail-closed reading and uses an existing code. The cost
is that she can't nest a playground under a supporting folder, though she can nest the other way.
- Q3 accepted. Q4: carried to C2-02 under Handoffs in BUILD.md, and Sage was asked to add it to C2-02's brief.
- N1 folded into check 8 (NFC and case), with an NFD case in T6 and mutant M4b.
- N2: the survey.test:73 pin gains only `supporting`, and the count test is the only addition
(mos-1330, its own commit).
- N3: before the build, re-grep tests outside packages/library for a `deepEqual` of the mapping output
on the build base.
+8
View File
@@ -0,0 +1,8 @@
# Huey
Builder seat, Codex gpt-6.1-sol, started by Mos for the T1 sessions design. Owns row 272 under Q220, reviewer Scrooge.
No launcher in this directory. The seat runs as a T3 thread (thread cabd45ca) and works in its own worktree. It has no
credentials of its own; it pushes only its own `huey/*` branches through the repository credential helper (mos-0981),
never `refactor`, `main` or another seat's branch. Registered by Sage on 2026-10-22 so the queue can name the seat as
row owner under Q220 (mos-1301).
+8
View File
@@ -0,0 +1,8 @@
# Louie
Builder and research seat, Codex gpt-6-luna, started by Mos for the editor evaluation. Owns row 174 under Q220.
No launcher in this directory. The seat runs as a T3 thread (thread bfd13d49) and works in its own worktree. It has no
credentials of its own; it pushes only its own `louie/*` branches through the repository credential helper (mos-0981),
never `refactor`, `main` or another seat's branch. Registered by Sage on 2026-10-22 so the queue can name the seat as
row owner under Q220 (mos-1301).
+8
View File
@@ -0,0 +1,8 @@
# Ludwig
Builder and reviewer seat, Claude Opus 5.5, started by Mos for the post-v1 email PRD. Owns row 258 (E01) under Q220.
No launcher in this directory. The seat runs as a T3 thread (thread 8615b2a6) and works in its own worktree. It has no
credentials of its own; it pushes only its own `ludwig/*` branches through the repository credential helper (mos-0981),
never `refactor`, `main` or another seat's branch. Registered by Sage on 2026-10-22 so the queue can name the seat as
row owner under Q220 (mos-1301).
+8
View File
@@ -0,0 +1,8 @@
# Scrooge
Builder and reviewer seat, Claude Opus 5.5, started by Mos for the Q198 review. Owns row 252 under Q220 and reviews row 272.
No launcher in this directory. The seat runs as a T3 thread (thread 629b6cd3) and works in its own worktree. It has no
credentials of its own; it pushes only its own `scrooge/*` branches through the repository credential helper (mos-0981),
never `refactor`, `main` or another seat's branch. Registered by Sage on 2026-10-22 so the queue can name the seat as
row owner under Q220 (mos-1301).
+24
View File
@@ -1525,3 +1525,27 @@ are never rewritten or removed; corrections are new entries.
2026-10-22 11:32 UTC | Sage (Opus 5.5, 1ef1e4f8) | Vikunja 502 landing; 281 and 264a slot reds placed | V502 LANDED 6af85df4b (ff from 980cec88c, Dewey carry dewey-0836). Row 281 c174f37ac and 264a r7 a23afbf95 each went red on first-run test defects. 281 b3 is test-only (T6b ruling accepted by Dewey 0837). 264a r8 pins the 3l seq-0 rows. Megavolt reruns 281 on 6af85df4b.
2026-10-22 13:10 UTC | Sage (Opus 5.5, 1ef1e4f8) | Rows 278 and 281 landings; slot order 281 then 264a r10 then 253 | 278 LANDED 385a7194c (Webby c30588), 281 LANDED e479521d3 (Dewey c30614). DEFERRED: grant gap closed, N1/N2 open. 264a rebasing to r10 on e479521d3; 253 waits on 264a.
2026-10-22 14:22 UTC | Sage (Opus 5.5, 1ef1e4f8) | Row 264a landing; doc sweep | 264a LANDED 84f53540d (Duckworth c30640, #1769 closed c30646, P19 463 c669) after four slot reds ruled in mos-1276 to mos-1279. Doc sweep: V502, 278/281 and 264a BUILD-LOG lines, DEFERRED 281 N1/N2 and 264a N1/N2. 253 rebases onto the landing next.
2026-10-21T16:04:23Z | Gyro (T3 Codex gpt-6.1-sol) | Q195/Q196 design only, #1752 / Vikunja #448, gyro/editor-design | Code-measured writing-editor draft with three options, local thesaurus, pane/pins, guarded API/MCP, DesignCraft handoff and proposed briefs; queue next refused linked worktree, canonical next gyro returned nothing; no implementation or live actions.
2026-10-22T16:02:06Z | Gyro (T3 Codex gpt-6.1-sol) | Q195/Q196 independent-review follow-up, mos-1300, Q220 | Recovered candidate 105811400 and current refactor aca16404c; canonical queue next gyro returned nothing; sent read-only reference drift map to Fenton, whose review is active. Design candidate left unchanged pending verdict. Docs-only fixes authorized on gyro/editor-design, no MVP gate slot.
2026-10-22T16:06:34Z | Gyro (T3 Codex gpt-6.1-sol) | Q195/Q196 fenton-0004 F1-F8 docs fixes, mos-1300, #1752 / Vikunja #448 | Verified findings against aca16404c, retained O3, corrected grants/dependencies/model routing/folds/citations and reduced checklist to six; docs-only candidate for Fenton re-check, no product checks or MVP gate slot.
2026-10-21T22:03:18Z | Della (GPT-6.1-Sol, ludwig-0006 / mos-1227) | PRD 1.11 candidate ea0b2a16d independent read-only review, Vikunja 459 | APPROVED for Sage landing after W12 and required landing gate. Every changed hunk checked against Q175/Q176/Q179-Q181/Q186-Q190/Q195-Q196/Q203/Q209-Q211; exact four-file scope and parent, Q211 quote, 71 ordered ids, version 1.11, max title 120 and both test expectations verified without Node. M1 provider choice stays with Mos/Jason; M2/M4 tracked follow-ups, no scope cuts inferred. Candidate unchanged, no product tests/service/secret/live action; verdict sent directly to Ludwig and Mos.
2026-10-21T22:03:48Z | Della (GPT-6.1-Sol, ludwig-0009 / mos-1229) | PRD 1.11 focused follow-up 73222011f | APPROVED exact head 73222011f2d2901e8e29e53e2c249eed6911f581. Read full PRD-only delta on reviewed ea0b2a16d: Q211 path attributed to Mos and VM provider open on 461, with conditional bundled Pocket ID v1 scope. Static version/71 ids/whitespace pass; JSON/tests unchanged. Node tests remain landing-slot work after W12; M2/M4 follow-ups on 462. Sent exact-pin verdict to Ludwig/Mos, candidate unchanged.
2026-10-21T22:16:56Z | Della (GPT-6.1-Sol, ludwig-0013 / mos-1229) | PRD 1.12 independent review 3576afffb, Vikunja 462 | CHANGES: viewer-own-thread access, thread-owner coverage for shares, stale sandbox/broker risk claim and fixed cited-reply notice exceptions. Checked pinned 193/194/Case2 designs, Q176/Q181/Q182/Q211, source sandbox and requirements diff. Static 82 ids/version/order/unique/title/test-count checks pass; no Node or live tests. Candidate untouched; direct numbered verdict to Ludwig/Mos.
2026-10-21T22:20:09Z | Della (GPT-6.1-Sol, ludwig-0016/0018 / mos-1231) | PRD 1.12 combined r2+r3 review, Vikunja 462 | APPROVED exact head 1509c7cc5747d4927323615afe29f87bfdbb1818. All della-0008 fixes plus owner-owned thread share exception and Mos Q1-Q5 checked. T1.7 463 and playground 464 stay v1 work; residual 465 stays with Jason. Static 82 ids/status/order/title/tests/whitespace pass. No Node or product/live tests. JSON/tests unchanged from r2; landing gate runs both requirements files after 1.11. Direct final-pin verdict to Ludwig/Mos; candidate untouched.
2026-10-21T22:35:30Z | Della (GPT-6.1-Sol, ludwig-0022) | PRD 1.13 independent review e38e755e3, Vikunja 463 | CHANGES: REQ-SESS-2 needs D4 A1 organization-owner own-thread exception to collaborator write-grant condition. Reviewed full diff against Huey 2c843d190 D1-D9/T1-T7/sections 7-8. Static parent/four-file scope/85 ids/version/title/test pins and unchanged ROLE-2 revoke protections pass. Design landing before/with PRD remains required; N2 466 is build dependency. No Node/product/live test; candidate untouched; direct verdict to Ludwig/Mos.
2026-10-21T22:36:30Z | Della (GPT-6.1-Sol, ludwig-0026) | PRD 1.13 focused r2 review, Vikunja 463 | APPROVED exact pin bd1f7d0b125a64bb1c367656b507acb17fab77fe. Owner own-thread and collaborator write-grant cases now match design D4 A1/A2; unproven-count title fixed. Exact parent/two-file delta/85 ids/status/title limits/unchanged test pins/whitespace checks pass. No Node/product/live test. Sage lands Huey 2c843d190 design before/with PRD and runs both full requirements tests on final candidate. Direct exact-pin approval to Ludwig/Mos.
2026-10-21T22:41:00Z | Della (GPT-6.1-Sol, ludwig-0030) | PRD 1.14 independent review, Vikunja 464 | APPROVED exact pin 9bd665976f866a9ee802ddd8470970b99e839eb9. Three playground levels, prefix-first scope, all mapping refusals/codes and null-book/draft lookup behavior checked against Gosalyn 51fdfb7e2 and Webby 7c00c8345. Static parent/three-file scope/85 ordered unique ids/status/title bounds/unchanged JSON titles and bus test/old LIB-2 quote/whitespace pass. No Node/product/live tests; Sage lands the cited ruling before/with PRD and runs both full requirements test files on the final candidate. Direct exact-pin verdict to Ludwig/Mos; candidate unchanged.
2026-10-22T16:09:56Z | Della (GPT-6.1-Sol, mos-1303) | Vikunja 460 Q217/Q222 email-plan settings revision | Canonical queue next della: nothing. Q1/Q2 closed; per-source default-none human-only scope and acceptance tests; Q3-Q7 grouped as settings/review, conditional external decisions only. Docs/static verification; no implementation, live access, secret read or mailbox/account creation. Commit/push only della/email-plan, direct Ludwig/Mos review handoff.
2026-10-22T16:12:26Z | Della (GPT-6.1-Sol, ludwig-0035) | Vikunja 460 email settings focused corrections | REQ-WAIT-2/3 checked at db1e7732a; Jason PRD approval gate restored. Parser/attachment settings/defaults and reviewed resource bounds explicit; Q8 retained as Jason choice. Static docs verification only; corrected pin pushed on della/email-plan for focused Ludwig/Mos review.
2026-10-22T14:26Z | Gizmoduck (Opus 5.5, 9312a896) | rows 264a (#1769) and 268 (#1773) after-landing docs; row 269 plan | 264a LANDED 84f53540d after r13 and r13p4 (mos-1279). Runs doc f7e4ec3df landed (mos-1280 item 1). Row 268 Runs doc 2428c4225 on gizmoduck/row268-runs-b for Sage. Row 216 may resume (268 landed c27207f6e). Row 269 (264b, #1774) plan sent as gizmoduck-0228, not claimed. r10-r13 worktrees removed, branches kept.
2026-10-22 13:06 UTC | Megavolt (Claude Opus 5.5, 81590691) | row 281 #1786 fire and delete revoke host and Gitea grants (owner) | design d1-d6 (Dewey approve dewey-0829), build b1-b5 on megavolt/row281 to row281-e; slot on 7979fb41e green (base 121+24, M1-M22 killed, dewey-0840 files, bus 819, business 231, cli 837, ten shells under 3x limits); docs e479521d3; in-review revs 3203/3204, Dewey APPROVE rev 3207; landed e479521d3 by Sage
2026-10-22T16:36:30Z | Della (sage-2795 / sage-2804, Q220) | Row 224 C2-05b #1725, della/row224 base 4f3fbc25a | Read both pinned briefs and C2-05a source; preparing broker/typed tools and authorized host runtime/bundle seams. No queue command, Node, HTTPS Git, claim, test or push during Gander gate. Dewey reviewer; Rocko/Huey runtime coordination sent. Tests and integration verification deferred to CLEAR.
2026-10-22 16:37:54 UTC | Rocko | row219 planning/code on rocko/row219 aca16404c; scope UI correction requested; no runs/claim.
2026-10-22 | Herb (gpt-6.1-sol, f6cf4287) | row214 C2-11a editor wikilinks authoring | On herb/row214 at aca16404c, authored baseline measurement, both-outcome design, inert inline node and tests under sage-2794/Q220. Claim, runs and pushes held during253; measurement/test results unavailable.
2026-10-22 | darkwing (T3 7525c8e4) | re-pin 86f95bb27->84f53540d window closeout (STATE.md b900d6dc, Gitea #1769/#1742/#1692/#1689/#1782/#1783 comments 30668-30673, window files on darkwing/window-repin-84f53540d 65edf2653); Vikunja 531 scratch move (~/darkwing-scratch -> /mnt/storage/scratch/darkwing-scratch); row 211 (#1709/#426) push guard, installer and slot-busy built on darkwing/row211 6db25b1f0 | window and 531 done; 211 waiting for a CLEAR and review
2026-10-22 | Della | Row258 E01 a8f0cf924 manifest/source review | CHANGES F1 lifetime/state fail-closed, F2 canonical secret placement; N1 pure lifecycle claim, M1-M11 named. Local Git reads/static manifest only; no Node/tests/HTTPS/live access.
2026-10-22 | Della | Row258 E01 3094e3bfa focused manifest review | APPROVED 11-path manifest/static F1/F2/N1 corrections; file and full-blob digest recipes verified. M1-M11 fixed; all execution/landing evidence still required in CLEAR. No shared-log edit, Node, tests, HTTPS Git or live action.
2026-10-22T17:55:40Z | Sage (Opus 5.5, 1ef1e4f8) | Row 253 landing; post-253 doc sweep | 253 LANDED 48e450ce8. Sweep on sage/sweep-253: Gosalyn Case 2 design and rows, 229 design, Gyro 0acbdb0d8, Della 179391055, 268 docs, seat dirs for Della, Scrooge, Huey, Louie and Ludwig, human-users-build:70 under Q173, DEFERRED 253 N1, 198 N2, row 203 text, 224 residual and 206 sharee list, and seat log lines.
2026-10-22 14:48 UTC | Researcher (Sonnet 5.5, 9bac0794) | Rows 220 and 216 resumed (sage-2708, sage-2716, sage-2719); local commits only | 220 e4a204836 on researcher/row220-r2 (on ea6dc9eff): C1 run tests, C2 default-reader M7 test, configDirectory fixture scope, BUILD.md round 2b. 216 4f3fbc25a on researcher/row216-r2 (on aca16404c): index cache, find/get/search/manuscript/universeInfo, PRD 1.14 levels. Nothing pushed, claimed or run; node --check not run. Waits for Sage's CLEAR and go; rebase onto 253 on a new branch before any gate.
File diff suppressed because it is too large. Load diff
+2 -1
View File
@@ -67,7 +67,8 @@ section 5.5 share filters. Schema 3k to 3l (Sage, 2026-10-20: machine grant row
the #436b build owns 3j to 3k, so this row builds on #436b's landing). The table is append-only, and row 181's restore guard and
`COUNTED` include it. Bus tests for shares and the migration, and mutants 2, 4, 7, 8 and 9 from section 8,
plus mutant 3 (rule 3 dropped), moved here from 193b1 because only shares can kill it (sage-2288).
Going live needs a `--migrate` window with Jason. Sage takes it to Mos when this row is near.
Going live needs a `--migrate` window on Mos's go under Q173 (mos-1310), with a backup first and a STATE report after.
It no longer waits for Jason. Sage takes it to Mos when this row lands.
Gate: Duckworth approves the manifest. The bus suite, the cli suite and every `scripts/test-*.sh` are green on
Sage's gate, and the mutants are killed.
+68 -68
View File
@@ -1,9 +1,9 @@
# Post-v1 email processing and monitoring plan
**Status:** phase C candidate, mos-1223, Vikunja 460. Ludwig reviews before Sage lands it.
**Status:** Q217/Q222 revision, mos-1303, Vikunja 460; Ludwig reviews before Sage lands it.
**Goal:** Stack-owned mail intake, monitoring, authorized reading/routing and local drafting for every harness.
**Boundary:** all rows post-v1, none required for 2026-10-27. No PRD 1.10 or live change here.
Q210 ready rows go to non-v1 seats after handoff. No ready label authorizes live connections,
Q210/Q220 ready rows proceed on non-v1 seats after handoff. No ready label authorizes live connections,
addresses/accounts, DNS/MX, exposure, spend or external mail. Original plans/reviews stay unchanged.
R21: **“The email specialist reads and drafts, never sends.”**
@@ -78,7 +78,11 @@ unnecessary. Any later finding is reported by path and commit only, never by val
per configured source. It owns connection pools, OAuth refresh, scheduling, cursors and intake. A harness can
stop, restart or change without losing monitoring. Multiple accounts are isolated by instance/business/source
ID. “One service” does not mean one global credential, mailbox or shared authorization. Default installation
is disabled and opens no connection.
is disabled and opens no connection. Per-source `mailboxScope` defaults to `[]` (none), including
agent ingress. A human sets exact mailbox IDs through the audited CLI and later Console; no agent session
may change this setting, even to narrow it. Recheck scope before scan/fetch and fence removed mailboxes
before returning in-flight content. Effective reads intersect source scope, provider rights and current
principal grants. Enrollment never grants agent body access. No personal mailbox is enrolled under Q222.
Use Node modules aligned with the Stack, adapting the Python design rather than embedding both applications
and their separate databases/authentication/UI. A Python sidecar would reuse more syntax but duplicate
@@ -112,7 +116,8 @@ are protocol references checked 2026-10-21, not evidence of installed support.
**D3 Addressing.** Version a route table mapping trusted delivery evidence to a stable Stack user or
role-instance ID, never a display name or a newly invented role. The org file holds this human-approved table
through its versioned audited write path. No agent reroutes messages or widens its own route. A route grants
no read right. The first source follows Q1’s provider. Offline adapters need not wait for that choice. Review
no read right. Q217 selects one agent mailbox on woltje.com at InMotion, created by Jason; its offered
protocol pilots first (IMAP expected, not yet verified). Exact address/slot enrollment awaits Jason. Review
maintained exact-pinned IMAP/MIME dependencies in their owning rows, initially Ludwig’s
`imapflow`/`mailparser` candidates. Do not write either protocol/parser in-house. Aliases on an approved
ingress mailbox are the recommended inexpensive start. Plus-addresses are acceptable only after fixtures and a
@@ -142,8 +147,11 @@ deliveries and 100 MiB of retained content initially. Persist quota accounting a
Reaching a quota holds new intake and raises an alert, while read-only health checks continue. First
activation establishes a from-now baseline without actions on old mail. Historical import is a separate
approved time/count window, previewed before activation. Cap violations, disk-full and unavailable audit
storage hold work without advancing past unstaged mail. These limits are proposed settings to validate, not
measured capacities.
storage hold work without advancing past unstaged mail. Mail-owned limits/caps/scopes are settings with
these defaults, not hardcoded policy; tuning cannot override the Q198 manager contract.
Human-only audited CLI, then Console, changes tune settings within resource bounds set by implementers and
reviewers in the versioned schema, backed by resource-exhaustion tests in E01/E04; widening bounds needs a
reviewed schema/test change. Agents cannot change protective settings; invalid settings fail closed. No gate bypass.
Provider identity is scoped to business/source/account: Graph immutable ID, IMAP folder/UIDVALIDITY/UID or
POP3 UIDL. RFC Message-ID, thread ID and content similarity are secondary correlation signals, never sole
@@ -155,13 +163,15 @@ expiry. Replay needs a new authorized intent, parent link and current grants. A
executes preview work.
**D5 Parsing and rules.** Preserve provenance for normalized sender, recipients, timestamp, subject, text,
thread references and attachment descriptors. Bound MIME depth to 10, parts to 100 and normalized text to 256
KiB. Decode safely, flag malformed or truncated content and quarantine failures. Prefer text, sanitize HTML
thread references and attachment descriptors. Human-tunable settings (D4) default to `parserInputBytes: 30 MiB`,
`mimeDepth: 10`, `mimeParts: 100`, `normalizedTextBytes: 256 KiB`. E04 tests changed values and bound refusals.
Decode safely, flag malformed/truncated content and quarantine failures. Prefer text, sanitize HTML
for display, disable remote images/URLs and never execute active content. Rules use finite allowlisted
fields/operators, stable priority and first-match by default. Reject executable rules and unbounded regex.
Record rule/version and routing reason. Unmatched mail is held for its authorized owner, not dropped or copied
fleet-wide. Routing a user notification does not launch an agent. Agent wake-up is a separate Q198-admitted
work request. Wake requires an approved route, current access grant, sender allowlist and trusted aligned
work request. `unattendedWake` defaults false; a human may enable it per route and set its empty-by-default sender
allowlist. When enabled, wake requires an approved route, current access grant and trusted aligned
DMARC authentication. Strip/ignore sender-supplied Authentication-Results. Ambiguous forwarded/list mail or
failed authentication goes to restricted review.
@@ -177,7 +187,8 @@ intake working and model-dependent work held. Summaries/drafts run as ordinary g
extra mail-service model client.
**D6 Attachments.** Initially enumerate metadata only. An authorized explicit fetch quarantines bytes under an
opaque ID, with a 10 MiB per-part and per-message total cap. Raw quarantine fetch needs no scanner. It never
opaque ID; human-tunable `attachmentPartBytes` and `attachmentMessageBytes` each default to 10 MiB (D4 bounds).
E04 tests changed caps and out-of-bounds refusal. Raw quarantine fetch needs no scanner. It never
implies permission to expose extracted content. Read-only files are not execution prevention.
Scanner/extraction is gated E16. Never trust filenames as paths, unpack archives, execute macros or follow
embedded links. Content extraction requires isolated no-network processing, resource bounds, scanner success
@@ -193,9 +204,10 @@ Protected key expiry/retirement metadata also stays outside the mail-backup rest
of ordinary logs. Default durable records hold opaque IDs, state, versions and counts, not bodies, subjects,
recipient lists or attachment names. Content access is explicit and audited.
Recommended content policy for an enabled pilot: encrypted bodies and local drafts for 30 days, explicitly
fetched attachments for 7 days, minimal metadata/tombstones for 365 days. Without Jason's approved
content/retention policy, persist metadata only and show that later content fetch requires the source online.
Content settings default to `persistContent: false`, bodies/drafts 30 days, explicitly fetched attachments
7 days and minimal metadata/tombstones 365 days. A human may set these through audited CLI, later Console,
within approved retention authority and holds. Until that authority exists, persist metadata only and show
that later content fetch requires the source online. A duration setting never authorizes deletion.
Expired content returns an explicit unavailable result. Define backup/key expiry and restore suppression
together: restoring an old backup must not resurrect expired content or redeliver old work. Use tested
backup-copy deletion or crypto-erasure with expiry of every relevant key copy/escrow, plus restore
@@ -225,7 +237,7 @@ every user's mail. Cross-business IDs return no data.
New mediated access uses proposed `mail.access`, distinct from provider credential creation through
`credential.mint`. The chief raises it to the authenticated human Approve path, with exact
principal/target/rights/expiry bound once. Add its access grant schema and action routing only through a
Jason-approved PRD version and reviewed vocabulary/role changes. Until then new actions remain hard waits.
Jason-approved post-v1 PRD version and reviewed vocabulary/role changes (REQ-WAIT-2/3). Until then hard waits apply.
Grant creation stays human-only. Read/draft actions become routine only inside approved grants. Manual mode needs
per-use approval. A hand-edited grant or approving email creates no authority.
@@ -309,28 +321,29 @@ have landed. Sage queues it now and assigns an available non-v1 seat when prereq
without a scheduled start or calendar wait. WAIT Qn identifies the positive authority needed before that row's
governed integration. Unanswered questions never block independent READY work. All rows remain disabled by
default, use synthetic mail/fake providers and run named tests plus affected existing suites. No ready row
enables a real source or weakens current grants/hard waits. Q3/Q9/Q10 propose keeping restrictive defaults,
not gates on code that preserves them. Q7 is a technical dependency review, not a new permission gate. Q11
enables a real source or weakens current grants/hard waits. Q3-Q7 are settings/review work, not unanswered
Jason choices. PRD/grant/deletion gates still apply;
Q9/Q10 retain external-action boundaries. Q7 dependency review adds no permission gate. Q11
affects neither build nor pilot because no old repository/service is altered. Queue enrollment/landings belong
to Sage after Ludwig approves this plan, with the v1 path and protected files preserved.
| Row / gate | Deliverable and dependencies | Acceptance tests (proposed filenames, not run here) |
| --- | --- | --- |
| E01 **post-v1 READY**; Jason: none for draft | PRD addendum draft, source/data/access/service and external content-key location/lifecycle schemas in `docs/prd/`, `packages/mail/schemas/`. Core mail-source registration/policy activation awaits approved addendum/Q4; content policy awaits E13/Q6. | `packages/mail/tests/schema.test.mjs`: unknown fields, wildcards, secret values, incomplete scopes and mismatched principal/source rejected; key location excludes dataRoot/backups, mode/symlink checks, disabled schema compatible with v1. Every requirement maps to a row. |
| E02 **post-v1 WAIT Q4** | Protected slot reader, source-service enrollment/credential lifecycle and separate `mail.access` grant action in `packages/{bus,business}/`. After E01 and approved PRD. No real credential enrollment in this build row. | `packages/bus/tests/mail-grants.test.mjs`, `packages/bus/tests/mail-principal.test.mjs`: non-chief/nonhuman approval, modified targets, expiry, duplicate consume, hand grants, failed issuance undo, manual mode, immediate revoke/in-flight fencing and secret sentinel leakage. Reject repo/dataRoot slots; existing grant tests pass. |
| E01 **post-v1 READY**; Jason: none for draft | PRD addendum draft, source/data/access/service and external content-key location/lifecycle schemas in `docs/prd/`, `packages/mail/schemas/`. Core mail-source registration/policy activation awaits approved addendum/Q4; retention authority awaits E13; settings use Q6 defaults. | `packages/mail/tests/schema.test.mjs`: unknown fields, wildcards, secret values, incomplete scopes and mismatched principal/source rejected; key location excludes dataRoot/backups, mode/symlink checks, default mailbox scope empty, disabled schema compatible with v1. Every requirement maps to a row. |
| E02 **post-v1 WAIT Q4** | Protected slot reader, source-service enrollment/credential lifecycle and separate `mail.access` grant action in `packages/{bus,business}/`. After E01 and approved PRD. No real credential enrollment in this build row. Add human-only per-source mailbox-scope CLI writer; Console parity follows in E13. | `packages/bus/tests/mail-grants.test.mjs`, `packages/bus/tests/mail-principal.test.mjs`: non-chief/nonhuman approval, modified targets, expiry, duplicate consume, hand grants, failed issuance undo, manual mode, immediate revoke/in-flight fencing and secret sentinel leakage. Default none makes zero reads; human mailbox-scope change audited; agent change (direct/tool/proxied) refused, revoked scope fences in-flight reads, widening creates no agent grant. Reject repo/dataRoot slots; existing grant tests pass. |
| E03 **post-v1 READY**; Jason: none | Durable scoped ledger, encrypted state, leases, cursor/continuation, outbox and quota accounting in `packages/mail/src/{store,intake,service}.mjs`. After E01. No dependency on live grants. | `packages/mail/tests/intake.test.mjs`, `packages/mail/tests/store.test.mjs`: crash every staging/delivery boundary, concurrent owners, missing/reused Message-ID, source-scoped identity collisions, 1,000/100-MiB quotas, disk/audit failure, restart and zero-connection disabled startup. Missing/expired key fails closed; restored backup with expired key returns unavailable, never plaintext. No skips or duplicate recipient effects. |
| E04 **post-v1 READY**; Jason: none, Q7 advisory | Bounded MIME parsing/metadata quarantine in `packages/mail/src/{parse,attachments}.mjs`. After E01; independently of grants. Review license/advisories and exact-pin maintained MIME dependency. | `packages/mail/tests/parse.test.mjs`, `packages/mail/tests/parser-corpus.test.mjs`: multipart, bad charset, missing IDs, S/MIME, 30-MiB input, depth/part/text limits, traversal, hidden HTML, comments, bidi/zero-width display tricks, archive and encrypted attachment quarantine. Fuzz corpus; no expansion/execution/network, no content logs. |
| E05 **post-v1 READY** offline; core registration **WAIT Q4** | Graph descriptor/module in `packages/plugins/mail-source/m365/` against test-local registry `packages/mail/tests/fixtures/mail-source-registry.mjs`. After E01/E03/E04. `packages/plugins/src/kinds.mjs` stays unchanged until E01 addendum approval/Q4; core entry lands together with or after that approved addendum, with its first production module. Q1 selects pilot, not offline work. | `packages/mail/tests/m365.test.mjs`, `packages/mail/tests/source-contract.test.mjs`: fake Graph delta/paging, immutable moves, token/cursor expiry, 401 single refresh, 429/5xx scheduling, restart, denied mailbox, additive-scope check and alias/plus/Bcc provenance. No source writes; test-local registry works with unchanged core registry before approval. Existing plugin suite passes. |
| E06 **post-v1 READY**; Jason: none, Q7 advisory | IMAP module `packages/plugins/mail-source/imap/`. After E05's test-local shared contract; no core registration before approved E01/Q4. Review/license/exact-pin maintained IMAP dependency. | `packages/mail/tests/imap.test.mjs`: local fake TLS server, certificate denial, selected-folder scope denial, BODY.PEEK/no Seen or STORE/EXPUNGE, IDLE drop/poll fallback, renumbering, UIDVALIDITY/move reconciliation and alias/plus/Bcc/forged-header provenance. Ambiguity holds effects. |
| E07 **post-v1 READY**; Jason: none for pure evaluation | Address/rule evaluator `packages/mail/src/{addresses,rules,route}.mjs`. After E01/E03/E04; no org-file activation or delivery privilege. | `packages/mail/tests/rules.test.mjs`, `packages/mail/tests/routing.test.mjs`: port all 36 prior rule cases; bounded operators, rule version/order, previews never execute, history import window, recipient ambiguity, multiple recipients, retired agents, repeated identical mail and no content logs. |
| E08 **post-v1 WAIT Q4/Q5** | Approved org route writer and narrow service delivery API/principal in `packages/{bus,business}/`, `packages/mail/src/delivery.mjs`. After E02/E03/E07. No agent route writer/reroute. | `packages/bus/tests/mail-delivery.test.mjs`, `packages/bus/tests/mail-routes.test.mjs`: exact human approval and versioned changes, fixed actor/business, recipient receipt idempotency, spoofed principal refused, route addition grants no read, revoke before dispatch fences content, no human-input impersonation. |
| E08 **post-v1 WAIT Q4 governance** | Approved org route writer and narrow service delivery API/principal in `packages/{bus,business}/`, `packages/mail/src/delivery.mjs`. After E02/E03/E07. No agent route writer/reroute. | `packages/bus/tests/mail-delivery.test.mjs`, `packages/bus/tests/mail-routes.test.mjs`: exact human approval and versioned changes, wake off/empty allowlist by default, human-only wake/route edits, fixed actor/business, recipient receipt idempotency, spoofed principal refused, route addition grants no read, revoke before dispatch fences content, no human-input impersonation. |
| E09 **post-v1 WAIT Q4** | Mediated typed/MCP tools, per-message thread view and local drafts in `packages/{bus,harness}/`, `packages/mail/src/access.mjs`. After E02/E08. | `packages/harness/tests/mail-tools.test.mjs`, `packages/mail/tests/access.test.mjs`: same schema/results/refusals on all supported harnesses, separate metadata/body/attachment/draft rights, page caps, thread filtering, cross-business ID denial, revoke/cache races, manual mode, local ack/draft and absence of send/reroute tools. |
| E10 **post-v1 WAIT Q5** | Restricted mail-triggered launch profile, sender-auth gate and origin propagation in `packages/{mail,harness,bus}/`. After E08/E09 and Q198 admission interface. | `packages/mail/tests/mail-turn.test.mjs`, `packages/mail/tests/injection.test.mjs`: fresh context/grant intersection, no git/credential/external tools at broker and harness, body/subject/display-name/filename injection, forged auth headers, aligned DMARC/allowlist, forwarded ambiguity, no resume of powerful thread, no authority laundering through fleet messages/decisions. |
| E10 **post-v1 READY** offline; governed activation after E08/E09 | Restricted mail-triggered launch profile, sender-auth gate and origin propagation in `packages/{mail,harness,bus}/`. After E08/E09 and Q198 admission interface. | `packages/mail/tests/mail-turn.test.mjs`, `packages/mail/tests/injection.test.mjs`: disabled wake never launches; enabled human-configured allowlist still requires DMARC/grants/admission; fresh context/grant intersection, no git/credential/external tools at broker and harness, body/subject/display-name/filename injection, forged auth headers, aligned DMARC/allowlist, forwarded ambiguity, no resume of powerful thread, no authority laundering through fleet messages/decisions. |
| E11 **post-v1 WAIT Q4** (mail decision schema approval) | `packages/mail/src/decisions.mjs` using the one approved Q207 decision API; no local model client. After E07/E09 and implemented Q198 decision module. | `packages/mail/tests/decisions.test.mjs`: 4-KiB closed schema, no body/free text, extra metadata denied until approved, out-of-enum/missing/uncalibrated confidence held, central fallback only, unavailable model leaves deterministic intake working; useful metadata-only outcomes or explicit unknown. |
| E12 **post-v1 READY**; Jason: none for synthetic recovery | Health/manager adapter `packages/mail/src/health.mjs`. After E03/E08 and implemented/verified SM D13-D15; fixtures can precede integration. | `packages/mail/tests/health.test.mjs`, `packages/mail/tests/outage.test.mjs`: mailbox/folder scope proof, full 60..600 cooldown and Retry-After, independent probes/one retry owner, bad-auth single retry then stop, no model probes or outage DLQ, synthetic canary, five-item/30-second batches, three-exit latch/release, human kill, alert-once/recovery and source gap reporting. |
| E13 **post-v1 WAIT Q4/Q6** | Authorized CLI/Console mail/draft/digest views and content/export/retention handlers in `packages/{mail,cli,webui}/`. After E09/E12. No live purge; keep `data.delete` hard wait. | `packages/mail/tests/retention.test.mjs`, `packages/cli/tests/mail.test.mjs`, `packages/webui/tests/mail.test.mjs`: per-user ACL on every item/export, separate decision inbox, exact deletion approval/holds/receipts, expiry of backup content and all relevant keys, restore suppression/tombstones, local draft ownership and metadata-only digest. |
| E13 **post-v1 WAIT Q4 governance/deletion authority** | Authorized CLI/Console mail/draft/digest views and content/export/retention handlers in `packages/{mail,cli,webui}/`. After E09/E12. No live purge; keep `data.delete` hard wait. | `packages/mail/tests/retention.test.mjs`, `packages/cli/tests/mail.test.mjs`, `packages/webui/tests/mail.test.mjs`: human-only Console mailbox/settings parity with CLI, default metadata-only and 30/7/365 settings, refused agent edits, per-user ACL on every item/export, separate decision inbox, exact deletion approval/holds/receipts, expiry of backup content and all relevant keys, restore suppression/tombstones, local draft ownership and metadata-only digest. |
| E14 **post-v1 READY**; Jason: none for offline proof | Operator runbook/acceptance packet in `docs/`; offline end-to-end harness test. After E01-E13. | `packages/mail/tests/predeploy-safety.test.mjs`, `packages/mail/tests/end-to-end.test.mjs`: port all 15 safety-gate cases; synthetic intake → route → authorized tools → local draft across supported adapters, zero source connections while disabled, from-now baseline, bounded preview, revoke/outage/restart and disable/rollback with evidence preserved. |
| E15 **post-v1 WAIT Q1/Q4/Q5/Q6**, plus Q2 if personal mail | Explicit separately authorized live pilot: enrolled agent ingress source, approved grant/route/content policy and reviewed re-pin. After E14. No v1 gate or external test mail. | `packages/mail/tests/pilot-reconcile.test.mjs` validates a redacted identity-level acceptance manifest; authorized operator supplies live receipts for delivered/held/missing identities, recipient proof, health, restart/revoke and bounded catch-up. Counts alone prove neither loss nor duplication. Complete on coverage/evidence, not a mandatory week or scheduled wait. |
| E15 **post-v1 WAIT** Jason mailbox provisioning, Q4 governance and live-pilot authorization | Explicit separately authorized live pilot: enrolled agent ingress source, approved grant/route/content policy and reviewed re-pin. After E14. No v1 gate or external test mail. | `packages/mail/tests/pilot-reconcile.test.mjs` validates a redacted identity-level acceptance manifest; authorized operator supplies live receipts for delivered/held/missing identities, recipient proof, health, restart/revoke and bounded catch-up. Counts alone prove neither loss nor duplication. Complete on coverage/evidence, not a mandatory week or scheduled wait. |
| E16 **post-v1 OPTIONAL WAIT Q8**, plus Q4 access | Isolated scanner/content extractor only if Jason chooses attachment content support, `packages/mail/src/extract.mjs`. After E04/E09, separate reviewed dependency/resource boundary. | `packages/mail/tests/extract.test.mjs`: no-network/resource isolation, scan failure/unavailable/encrypted/unsupported hold, execution/archive expansion prohibited, hostile extracted text marked untrusted, access/revoke checked before release. No scanner implies no extracted-content feature. |
Run proposed Node files with `node --test <filename>`; owning reviewers select affected existing suites and
@@ -338,62 +351,49 @@ require green evidence before commit/landing. Pure store/parser/rule rows can pr
approval work. Later adapters Gmail API/JMAP/POP3 require a named need and separate rows, not a committed
expansion of this add-on.
## Questions for Jason (merged list, recommendations are not approvals)
## Decisions and settings (stable Q1-Q11 identifiers)
1. **Q1 Pilot provider/domain/addresses:** which controlled domain, provider and agent ingress mailbox,
aliases or plus-tags? Recommend one approved non-USC agent mailbox, aliases after recipient proof, no
catch-all/MX change. Dedicated mailbox only for approved isolation. Its protocol pilots first; build both
adapters offline meanwhile.
Q217/Q222 settle Q1/Q2; mos-1303 separates settings from external decisions. Schema names are proposals.
Changes are validated/revisioned/audited with human identity; agent proxies cannot pose as human writers.
Configuration never replaces grants, PRD or external authority.
2. **Q2 Personal mail scope:** which existing user mailbox, if any, may be read? Recommend none initially.
Later enrollment names user/mailbox and exact consent or delegation, separately from agent ingress.
1. **Q1 CLOSED, Q217:** Jason creates/names one agent mailbox on woltje.com at InMotion and fills its protected
slot. No agent account creation/catch-all/MX change; aliases after delivery proof. Verify protocol before pilot.
2. **Q2 CLOSED, Q222:** no personal mailbox now. Per-source `mailboxScope: []` defaults none; exact IDs are
human CLI/Console settings (D1/E02/E13). Later personal enrollment needs user consent/delegation and grants.
3. **Q3 Source mutation:** mark read, flag, label, move or save provider drafts? Recommend no, read-only
sources and Stack-local drafts/acknowledgments.
### Group (a): Q3-Q7 settings and ordinary technical review
4. **Q4 Service/access grants:** approve protected source-service credential ownership, distinct
per-agent/user `mail.access` approval, mail-source kind and closed mail decision schema? Recommend yes through reviewed
post-v1 PRD/vocabulary changes, chief/human approval, exact targets/rights/expiry/revoke. New access action
stays a hard wait until the approved PRD explicitly routes it. Provider credential creation remains
distinct.
| Item | Setting and default | Who may change it; remaining boundary |
| --- | --- | --- |
| Q3 source mutation | `sourceMutations: []`; sources read-only, drafts/ack local | Human-only configuration within reviewed adapter capabilities. No mutation capability is implemented in this increment. Enabling provider writes needs a reviewed extension and exact provider authority; this setting cannot enable sending or override R21. |
| Q4 service/access | `sourceEnabled: false`, `mailboxScope: []`, access grants empty, `accessMode: manual` | Authenticated human changes source settings; chief raises exact per-principal grants and human approves through existing approval boundary. `mail.access`, service principal, mail-source kind and closed decision schema still require approved PRD/vocabulary extensions before integration. Routing `mail.access` out of hard waits requires a PRD version Jason approves under REQ-WAIT-2/3: an existing gate, not a new Q number. Q211 is not assumed to waive it. No self-grant or source credential shared with agents. |
| Q5 unattended wake | `unattendedWake: false`, `wakeSenderAllowlist: []`, route table empty | Human-only CLI/Console writer. When enabled, require allowlist AND trusted aligned DMARC, approved route/current grant, restricted profile and Q198 admission. Manual mode prevails; ambiguous/failing mail holds. No per-message Jason question inside this approved envelope. |
| Q6 retention | `persistContent: false`, `bodyDraftDays: 30`, `attachmentDays: 7`, `metadataDays: 365`, `automaticPrune: false` | Human-only writer, respecting holds and approved retention authority. Metadata-only default; encrypted content only within approved authority. Settings never grant `data.delete`; exact deletion approval or explicitly approved retention authority, backup/key expiry and restore suppression remain required. |
| Q7 dependencies | `sourceAdapter` unset until human enrollment; reviewed exact-pinned IMAP/MIME modules, initially imapflow/mailparser candidates | Humans select enrolled adapters via CLI/Console. Implementers/reviewers choose and pin maintained modules under repository license/security checks in E04/E06; not a Jason product question. Paid dependencies or external services go to group (b). |
5. **Q5 Unattended wake:** which senders/routes may wake granted agents without per-message approval?
Recommend allowlist plus trusted aligned DMARC, approved route, restricted fresh profile and Q198
admission. Manual mode overrides this. Ambiguous/failing mail quarantines. Neither identity nor email
approves external effects.
### Group (b): real Jason decisions, only when needed
6. **Q6 Content/retention/deletion:** what may be retained, for how long, with what holds? Recommend encrypted
30-day bodies/drafts, 7-day explicitly fetched attachments, 365-day minimal evidence/tombstones,
metadata-only until approved. Keep exact `data.delete` authorization, backup/key expiry and restore
suppression.
**No new external question is needed for default offline work.** Existing Q4 approval is still required;
Mos asks one at a time if a later increment requires:
7. **Q7 Dependencies:** maintained IMAP/MIME packages after license/security review and exact pins rather than
custom clients/parsers? Recommend yes, investigate Ludwig's imapflow/mailparser candidates in owning rows.
Routine technical review unless an existing repository policy adds an approval gate; no calendar wait for
package review.
- A new provider account/license, paid dependency/service, new spending, or outside-party permission beyond
Q217's Jason-created mailbox. Recommend use the existing InMotion service and free reviewed dependencies.
- New exposure/listener/webhook/DNS/MX/addresses/accounts: recommend none. Q217 creation is Jason’s existing task.
8. **Q8 Attachments:** allow bytes/extraction and require scanning? Recommend metadata first, explicit capped
raw fetch into quarantine, no scanner for opaque quarantine. Extracted content waits for isolated
no-network processing, resource limits and a scanner. Do not enable arbitrary execution or automatic
archive expansion.
Q4 Jason PRD approval/grants and Q6 deletion gates persist. No live access is authorized; offline work proceeds.
9. **Q9 Exposure/spend:** new listener/webhook, DNS/MX, paid infrastructure or model spend? Recommend none
initially. Outbound monitoring/internal alerts suffice. Every later exposure, address/account change or
spend needs its own authorization.
10. **Q10 Sending:** should this add-on send/forward/reply? Recommend no. R21 remains permanent for the
specialist. A future separate sender needs its own approved plan and exact-recipient human approval
through `message.external`.
11. **Q11 Prior repositories/service:** is the configured USC deployment still running, and should either
repo/service be archived or retired? Recommend no operational change here. Preserve both histories, use MP
as consolidated design reference. Any archival/retirement is a separate authorized task after actual-state
verification.
8. **Q8 Attachments:** metadata first; capped raw quarantine fetch. Extraction remains Jason’s choice (E16),
with isolation/scanning, no execution/archive expansion. Group (b) is not the full list of Jason gates.
9. **Q9 Exposure/spend:** none initially; later changes need their own authorization as group (b) describes.
10. **Q10 Sending:** remains out of scope. Recommend no sending/forwarding/replying; R21 remains permanent
for the specialist. Any future separate sender needs its own plan and exact-recipient human approval
through `message.external`; it is not a configurable bypass of this add-on's boundary.
11. **Q11 Prior repositories/service:** preserve histories/USC service; status unverified. Retirement is separate.
## Verification and handoff
Sources: Della 191dff32f, Ludwig 4a27b73ac, reviews 9182134e5/143415779, Mos's routing ruling.
Verify citations, coverage, row gates/tests and unchanged original blobs. No product suite,
live mail, secret-file read, install or service change here. After Ludwig approval, send Sage
this pin for refactor landing and READY-row queueing on non-v1 seats, copying Mos.
Resolve review changes without a calendar wait. Jason-gated work waits on its numbered decision.
Verify coverage, row gates/tests and unchanged originals. No product suite, live mail, secret read or service change.
After Ludwig approval, send Sage this revision for landing/row-gate updates on non-v1 seats, copying Mos.
Q220 authorizes progress without calendar waits, not live access/gate bypass. Q217/Q222 applied; originals unchanged.
@@ -0,0 +1,835 @@
# Q195 writing editor design
Gitea #1752, Vikunja #448, DECISIONS Q168, Q177, Q194, Q195 and Q196. Design for review, not build
authority. Originally measured at `6c75887b0`, rechecked against `origin/refactor` at
`aca16404c` for Fenton's fenton-0004 review. Repository line references use `aca16404c` unless
a different revision is named. WordCraft and DesignCraft retain their separately pinned revisions.
Work is confined to `gyro/editor-design`.
## Outcome and boundaries
D1. Give Melanie a familiar Word-like writing interface inside the Console. She writes, saves,
selects a word for synonyms, and moves between documents without losing her draft or chat. Her
corpus includes hundreds of .doc files, several .qxd books and recent Markdown. Obsidian is not the
interface model. Ship a useful writing slice before legacy import. This add-on does not gate the
October 27 Markdown-library MVP, which Q196 leaves unchanged. Editor build gates are
post-October 27 work and never take the gate slot ahead of MVP rows. Design and review
continue now under Q220.
D2. Q196 withdraws the earlier assumption that the current Markdown editor must be the solution.
Compare all three options below. WordCraft's canvas/WebGPU browser UI
(`apps/wordcraft-web/Cargo.toml:3,26`, `index.html:17`) has a different rendering and build system.
Read-only reference checkout: `/mnt/storage/src/artcraft/wordcraft`, commit
`e05b7fe86f3b85454000dffc321de638d7009817`. No code is copied by this design. Any later copy must
preserve its applicable MIT or Apache-2.0 license and notices.
D3. No direct agent writes. Proposals appear outside the document until Melanie accepts one, then
become an undoable draft edit. Saving remains a separate human action. Q176 and Case 2 C2-06 already
prohibit agent prose and constrain corrections. Q195 does not explicitly remove that rule. The
design below specifies the interface, but agent access to unsaved text and new replacement-prose
permissions remain gated on Jason's choices, not silently enabled.
All examples and tests use invented fixtures. No books content, host access, live stack changes,
role changes, queue changes or access to `~/.mosaic-dev` is part of this assignment.
## What exists
| Code | Measured finding and implication |
|---|---|
| F1 | Row 154 landed as `9353675d1` (#1648). `packages/webui/editor/model.mjs:16-18` imports ProseMirror and markdown-it. `view.mjs:162` mounts the DOM editor with history, formatting, lists and a link bar. `model.mjs:1-14` and the serializer retain original blocks unless edited. Front matter and unsupported structures remain source blocks. This is the implementation to extend. |
| F2 | The actual vendor path is `packages/webui/third_party/editor/`, relative to the WebUI package, not repository-root `third_party/editor/`. `PROVENANCE.json:1-13` records 170 files, 19 packages and no local changes. `NOTICE:1-10` names tagged source, hashes and license paths. `editor/build.mjs:16-24,34-56` aliases source and produces an unminified bundle with inline legal notices and hashes. `editor/BUILD.json:7-14` records 758,420 JS bytes and 4,462 CSS bytes. These are checked against bytes, not inferred from the README. |
| F3 | `packages/webui/src/public/bus.js:892-911,949-951` keeps a shared Markdown draft and falls back to Source when exact round-trip fails. Redrawing destroys the editor. `editor/view.mjs:183-205` creates a new history on `setText`. The README's “Visual editor” section (`packages/webui/README.md:572`) records this limit. A writer needs a stable editor mount while chat polls. |
| F4 | Row 148's save is `POST /api/project-file`, with exactly `{project,path,base,content}` (`bus.js:999-1010`, `serve.mjs:522-555`, keys :527 and handler :531). Authentication, exact Origin, Fetch Metadata, strict keys and a 1 MiB request bound apply. `packages/project/src/index.mjs:244-260` restricts it to a human and refuses a changed file base. `paths.mjs:12-15` allows only `spec/north-star.md` and direct `docs/*.md`. This route cannot save book paths today. Do not widen its regexp to admit the vault. |
| F5 | The Console already has chat projects, named threads, message paging, send and explicit resume (`packages/webui/src/chat.mjs:19-27`, `public/chat.js:9-14,66-70`). Chat has its own route and render lifecycle (`public/bus.js:1124-1128`), so Docs and Chat currently replace each other. `chat.js:68` renders bodies as escaped text. There is no document pin in these structures. |
| F6 | Sessions are durable thread identity plus replaceable runs. Row 121 landed as `2c16d6b93` (#1615). `packages/bus/src/broker.mjs:845-853` exposes thread state and idle release. Thread creation at `:5716-5728` checks FED R3 grants; open/send at `:5766-5780` also use `#grantedThread` (`:790-793`) after thread ownership checks. Open requests launch explicitly. Current chat uses the thread's default session; pins remain thread IDs, never individual runs. Existing tester isolation is in rows 177/182 and `chat.mjs:1-4`. |
| F7 | C2-11a, row 214, owns `editor/model.mjs`, bundle output and editor tests (`docs/plans/2026-10-20_case2-rows.md:235`). It measures and preserves wikilink forms. All proposed changes under `editor/` wait behind it. C2-10b owns `public/bus.js`, its stylesheet, `serve.mjs` and WebUI tests (`:784`). It supplies cited items, scope badges and sync status. Our shared-file rows wait behind it and reuse its renderer. Neither brief proves that its feature has landed. |
| F8 | Sage's input via mos-1179: row 220, C2-10a (#1721), makes the conversation reader hide creative sessions and refuse direct opens with `conversation-not-listed` (`case2-rows.md:522`). Row 198 (#1696) retires the 7331 board writer, currently referenced at `serve.mjs:14`. This design uses neither raw session files nor the board route. It does not depend on row 5 (waiting on Jason) or parked row 29. |
Original draft verification at `6c75887b0`: `node --test packages/webui/tests/editor-provenance.test.mjs
packages/webui/tests/editor.test.mjs` passed 10 tests with 1 explicit skip. The skip is the rebuild
because pinned esbuild 0.28.2 is absent. The source tree, output hashes, licenses, route serving and
exact round-trip tests passed. This is fixture evidence, not a live check.
## Options and recommendation under Q196
O1. Extend row 154 into a Word-like DOM interface. Reuse its safe source-built editor, add a
File/Home/Review ribbon, named heading/paragraph styles, a document navigation pane, familiar
shortcuts, status bar and synonyms. It preserves browser text behavior and the current CSP. The
measured JS/CSS sizes are F2, not an estimate. Its current schema has no footnote, comment or
tracked-change nodes (`editor/model.mjs:27-114`), no page layout engine and no DOCX writer.
Word-like controls do not create those features. This option needs a production export path before
it meets Q196's DesignCraft requirement.
O2. Put WordCraft's engine and canvas interface in the document pane. It offers Word-like commands
and rich file support rather than recreating all of them in ProseMirror. Louie's scratch evaluation
is the source for build/runtime and round-trip measurements below. Its own browser file
pickers/downloads and preferences are not a Mosaic save or permission boundary. An adapter must
disable direct uncontrolled I/O, connect owned git documents, expose selection/version/undo
operations, and put all agent access through Mosaic's broker. Its native MCP server is not
automatically a browser bridge. Q167 D3 bars the required CSP loosening today. An iframe or separate
origin is not a way around that ruling. O2 requires Jason's explicit scoped exception and an
independent security review before build rows.
O3. Keep a Word-like DOM writing interface, use WordCraft's headless formats/engine behind a narrow
server operation for production export. Recommend this bounded mix for the first release: O1
supplies editing and local synonyms, a pinned headless exporter supplies DOCX for DesignCraft. Do
not run two live document models or send every keystroke to a server. This avoids introducing a
canvas editor and a CSP exception while adding a concrete book handoff. The cost is a reviewed
format adapter and an explicit feature boundary. It does not promise full Word parity. If footnotes,
comments, tracked changes or precise pagination must be editable in the first release, choose O2
after its bridge and CSP gates instead of silently extending Markdown beyond what it can represent.
The first release includes prose, headings, emphasis, lists, links, native spelling, find, word
counts, undo, explicit save and synonyms in familiar controls. True page layout, headers/footers,
rich tables, footnotes, comments and tracked changes are later rich-document work, not buttons that
discard unsupported information. Existing documents with unsupported structures remain
source-preserved or read-only with a clear reason. Jason confirms that feature cut, not an
assumption that the existing vault defines her needs.
## WordCraft measurements used for O2 and O3
Louie's evaluation is pinned at `origin/louie/wordcraft-eval`, commit
`880ef19e017dad0194ecc9921ae55a0cf2e8fef2`,
`docs/plans/2026-10-21_q195-wordcraft-eval.md`. Louie built a scratch clone of WordCraft
`e05b7fe86f3b85454000dffc321de638d7009817` (0.9.0), with no source modifications.
These are Louie's measured results, not estimates or a claim that this combined design ran
the application in the Console.
| Evidence | Measured result | Design implication |
|---|---|---|
| Release build twice | 392.786 s, then 172.324 s, under `nice -n 19`, four Cargo jobs | First-build cost is material. Exact timings are that scratch host, not a product startup benchmark. |
| WASM | 43,994,316 bytes, SHA-256 `90dec375924b47b40054eb4ecafb4235b6ed0bd63880b91ec138fdbb6f939e75` | The current shipped build is much larger than the packaging README's approximate figure. Use the measured artifact. |
| JS / HTML / icon | 164,941 / 1,604 / 18,378 bytes | Total 44,179,239 bytes, identical sorted hashes for all four artifacts across both builds. No compression transfer size was measured. |
| Review scope for web target | 15 local crates, 251,457 physical Rust source lines and 199 third-party packages | O2 imports a maintained application, not a small editor widget. All dependency licenses and pinned build inputs need review. |
| Browser API | Session and selection stay in WASM. No exported page API or postMessage bridge. Browser has no TCP control channel | The native MCP server cannot control the browser document. The O2 adapter is new work. |
| `.doc` → `.docx` fixture | Heading 1, italics and footnote reference/body preserved. Comment absent, inspect reports zero comments | Useful import candidate, with a real loss boundary. Legacy import must retain the original and disclose comment loss. |
| `.doc` → `.md` fixture | Heading and italics preserved, footnote reference/body and comment absent | Markdown is not a lossless intermediate for rich legacy books. |
| `.md` → `.docx` → `.md` fixture | Heading and italics preserved. Footnote marker/definition stay ordinary text and brackets are escaped | O3's adapter must detect unsupported constructs, not rely on conversion success as proof of semantics. |
| Thesaurus | 29 headwords, 122 synonym values | WordNet is needed under every option. Hunspell spelling dictionaries are a separate mechanism. |
| MCP | 16 native tools, stdio/headless or loopback desktop connection | Reuse operation ideas through Mosaic's scoped tools. Do not expose native execute/save or a loopback control port to agents. |
For the unmodified O2 browser app, Louie identifies `script-src 'wasm-unsafe-eval'` for
WebAssembly, `frame-src 'self' blob:` for the PDF preview, and `frame-ancestors 'self'`
on an embedded WordCraft response rather than the shell's existing `'none'`. A restricted
integration could disable PDF preview, but must demonstrate the resulting narrower policy.
The shell policy stays unchanged when only an editor response changes. Q167 D3 still
requires Jason's ruling on that editor policy. No worker/GPU-specific CSP directive is
needed for this build. The report's Console CSP reference was `serve.mjs:495` at
`6c75887b0`; the identical policy is now `packages/webui/src/serve.mjs:605` at `aca16404c`.
Pane fitting is a source-based inference: eframe 0.36.2 uses ResizeObserver on the canvas.
Louie did not prove a functioning Console embedding, keyboard escape, browser startup
latency or browser selection bridge. Those remain O2 acceptance work, not measured successes.
The native fixture conversions do prove format behavior on the synthetic fixture. They do
not prove preservation of Melanie's corpus or the complete Word feature set.
For O3, the headless format path is supported by the successful native fixture commands.
A restricted exporter service and its exact native dependency subtree, memory usage and
service latency have not been measured. W6 must measure them. O3 avoids the measured
browser WASM/app UI and a browser CSP exception, but is not claimed to be a zero-cost or
already-built backend. This is why the recommendation is limited to the tested prose,
heading/emphasis/list subset plus explicit loss checks, with richer editing as a separate
choice in Jason's checklist.
## Stored format and DesignCraft handoff
D20. For the recommended first slice, existing Markdown stays canonical in git, including front
matter. No forced migration. Produce DOCX as a derived, human-requested book handoff from a specific
commit. The first slice stores Markdown only in git and downloads the derived DOCX with an export
manifest. Optional storage of that output under a dedicated `exports/` directory in the writing
project requires its own accepted write-path policy brief. The manifest records source paths, source
commit/digests, converter revision, style map, output digest and conversion warnings. The Markdown
source is the sole editable copy. The DOCX is regenerated, never independently edited then merged
back. Existing Case 2 mapping does not automatically index exports or gain write permission for
them.
Markdown-only git is easiest to inspect but cannot retain all Word features. DOCX-only git retains a
richer document model but is binary to git and outside Case 2's Markdown reader, writer and
citations. Two editable masters drift. If git later holds both, DOCX is a derived output, not
another editable format to synchronize. A later rich-document mode may make DOCX canonical per
document, with a derived Markdown reading projection and stable identity mapping, but that needs
separate writer, index, citation and loss-preservation design. It does not change this week's Case 2
MVP.
D21. DesignCraft checkout `3ce3a24f08d03935bc8cf7cb23755aca8065558c` lists DOCX and MD at
`docs/file-format-parity.md:69,72`. Code qualifies that claim: `crates/textimport/src/lib.rs:94-126`
recognizes `.md` but falls through to plain text. Markdown headings, emphasis and YAML are not
automatically interpreted. `crates/textimport/src/docx.rs:493-548` reads named styles and footnotes.
Its regression `tracked_changes_import_as_accepted_including_moves` at `:706` shows that
insertions/move destinations become accepted content, with deletions removed, not an editable review
history. `crates/engine/src/cmd/place_text.rs:46-93` maps style names and resolves conflicts.
`crates/engine/src/cmd/file.rs:138` documents `file.place` for DOCX/MD and
`crates/mcp/src/tools.rs:98-110` exposes typed commands through the MCP server.
D22. `POST /api/editor/export` takes exactly `{project,path,base,format:"docx"}` under the existing
human write fences, reads the authorized saved commit and returns a bounded download containing DOCX
plus manifest. Dirty drafts must be saved first. No arbitrary server path or browser-selected
converter command is accepted. Export strips front matter into metadata, resolves source links as
text or explicit cross-reference labels and maps Normal, Title, Heading 1-3, Quote and list styles
by name. It refuses unsupported semantic content or offers an explicit loss report before export.
Jason's later layout agent fetches the reviewed Markdown commit into its own approved workspace,
produces the matching DOCX and manifest (or reads the separately authorized export commit), verifies
hashes, and uses DesignCraft MCP `execute` with `file.place`, sets an explicit style map and
`styleConflicts`, then reviews autoflow and warnings before saving a layout. That later agent
automation is not authorized by this design assignment. No editor agent gets DesignCraft credentials
or automatically sends Melanie's text to another service.
Acceptance uses invented fixtures: body, styled headings, emphasis, list, table, link and
footnote/track-change cases appropriate to the chosen format. Compare source text and style
identities after WordCraft export and DesignCraft placement, including Unicode and paragraph
boundaries. Unsupported footnotes/revisions in Markdown must refuse or warn, not vanish. For rich
DOCX input, explicitly choose a final accepted reading before production layout, retaining the
original review-bearing file. A direct `.md` place is a plain-text fallback only, not the production
path for styled Markdown.
D23. Legacy import is a separate assignment. It must supply stable document IDs, original hashes, a
read-only retained original, converted editable file, named styles, metadata, attachment mapping and
a loss/warning report with a human acceptance decision. It must also name the supported
format/features, source-to-output paragraph identifiers and asset paths so navigation, pins and
citations can be repaired explicitly. Never replace .doc or .qxd originals. WordCraft's `.doc`
parser is a candidate to test, not proof that every 20-year-old document survives. Neither project
reads .qxd today (DesignCraft's gap at `docs/file-format-parity.md:32`). QXD extraction needs its
own evaluated tool and license. The editor accepts only reviewed converted artifacts, not a bulk
import button.
## Writing and moving between documents
D4. Present a File/Open view with recent documents and a collapsible book/chapter navigation pane,
plus a heading outline. The Case 2 mapping supplies scope underneath it. Show book, folder and
document in the breadcrumb, without requiring Melanie to operate a vault UI. The host recon at
`/mnt/storage/src/jason-master-restart/projects/mosaic-mvp/MELANIE-HOST-RECON.md:23` describes
Markdown book folders, chapters, characters, summaries, locations, research and tracker notes, with
front matter keys `type`, `name`, `appears_in` and `relationships`. Only that structure is used
here. `.base` and `.canvas` stay untouched and are not offered as editable documents.
D5. Reuse C2-04's human-only `library.file` read (`packages/bus/src/broker.mjs:99,4610-4616`).
The future `library.file.save` verb does not exist at `aca16404c`. C2-11b has no accepted
brief yet (`docs/plans/2026-10-20_case2-rows.md:11-14`), and the Case 2 design
(`origin/gosalyn/case2-design:agents/gosalyn/work/case2/DESIGN.md`, section 7.3) proposes
its navigation and save UI. C2-11b's brief, the future human save verb, C2-02's writer and
C2-03b's push behavior must be implemented and independently reviewed before W2 can ship
book editing. They are explicit blockers, not an existing save boundary or work hidden in
a UI row. Until then only `docs/` fixtures can exercise the new writing surface.
D6. Keep one working editor with a collapsible file tree, heading outline, recent documents and a
path/name filter. Opening a fixture such as `Lantern Book/Chapters/03 - Rain.md` uses the mapping's
document identity, not an absolute path. Back/forward and wikilink navigation use the same open
operation. Do not invent a second tree or a vault-wide search service.
D7. Each document keeps its text, base commit, selection, scroll and undo history in memory for this
signed-in tab. Chat polling and divider movement never remount it. Switching back restores it. Keep
at most ten clean inactive editor states. Before releasing a dirty state, require Save, Copy or
explicit Discard. Never evict a dirty document silently. No unsaved text goes in URLs, localStorage
or logs. A dirty-document badge and an unsaved count remain visible. Closing/reloading a dirty tab
warns. Explicit sign-out clears text and access context, with an unsaved warning before it proceeds.
Expired credentials revoke agent shares and block writes/server reads, while preserving the tab's
local draft for Copy or reauthentication. The UI distinguishes expiry from an explicit sign-out.
D8. Use Word-like File/Home/Review controls and a familiar Normal/Heading 1-3 style picker. Provide
a clean page-colored writing surface, readable line width (about 70 characters), text-size controls,
spacious line height, native spellcheck, keyboard formatting, find within the open file, word count
and a focus view. Keep the toolbar compact and keyboard accessible. Hide front matter behind a
collapsed “Document details” source panel, preserving its exact bytes. Unsupported blocks remain
marked source blocks. Do not reinterpret YAML or normalize wikilinks. All-day use means stable
selection/history and clear save status, not Word-style pagination.
D9. Save is explicit (button and Ctrl/Cmd+S), with “Unsaved”, “Saving”, a returned commit, or a
refusal. No autosave commits initially. Newer content refuses `stale-base` and preserves the draft.
Show current saved text beside the draft for manual reconciliation, never an automatic overwrite.
Unknown save/push results are read back before retry. A local library commit awaiting push is
labelled as such, using C2-03b's result, never called synced. No create, rename, delete, promote or
crossover work enters this add-on's first slice.
## Thesaurus
D10. Use Princeton WordNet 3.0, indexed locally by synset (meaning) and part of speech. This is
substantially broader than WordCraft's small static `T` table
(`crates/engine/src/cmd/review.rs:914`). WordNet is a lexical database, so it will miss some
inflected or literary terms and does not rank words for a sentence. Group results by meaning, show
the gloss, omit the selected word and duplicates, and say “No local synonyms” on a miss. Do not
promise every alternative is interchangeable.
Princeton's [statistics](https://wordnet.princeton.edu/documentation/wnstats7wn) report 117,659
synsets and 206,941 word-sense pairs. The 155,287 strings total counts words again across parts of
speech. The distinct total is 147,278. Its
[license](https://wordnet.princeton.edu/license-and-commercial-use) permits copying, modification
and distribution for any purpose, retaining its copyright, terms and disclaimer on copies and
modifications. This fits this repository's private `UNLICENSED` package and source-review rules. Do
not label WordNet MIT or Apache. Preserve the actual WordNet license.
Measured download on this design session: Princeton's archive host timed out. The NLTK project's
[WordNet data
mirror](https://github.com/nltk/nltk_data/blob/96f9b3252457a2b97e52aec64c3dfceeb5c312d5/packages/corpora/wordnet.zip)
at commit `96f9b3252457a2b97e52aec64c3dfceeb5c312d5` supplied the archive in memory. The archived
LICENSE is WordNet 3.0's Princeton license. No archive entered this repository.
| Measurement | Exact value |
|---|---|
| Archive | 10,775,600 bytes |
| SHA-256 | `cbda5ea6eef7f36a97a43d4a75f85e07fccbb4f23657d27b4ccbc93e2646ab59` |
| All uncompressed entries | 36,353,991 bytes |
| `data.*`, `index.*` and exception files | 35,435,978 bytes |
| Measured synsets | noun 82,115, verb 13,767, adjective 18,156, adverb 3,621 |
D11. At build time, import the original text, license and a provenance manifest under
`packages/webui/third_party/thesaurus/`. A small reviewed converter bakes a static index into a
self-hosted Web Worker asset, shipped with the Console build. Start that worker when the writing
surface loads. No runtime dependency install, upstream lookup, CDN, on-demand dictionary fetch or
model call occurs when she selects a word. Data is reviewable source in the release, like row 154's
editor source. Browser assets still come from the installed Console itself. CSP stays byte-for-byte
unchanged. Worker startup and index decoding stay off the typing thread.
The build row records exact generated bytes and worker memory/load measurements rather than guessing
compression size here. Gate limits: at most 50 MiB shipped dictionary assets, at most 128 MiB added
worker heap on the fixture browser, warm lookup under 100 ms for 95 of 100 fixture lookups. A failed
limit requires a reviewed representation change, not dropping words silently. Worker startup never
blocks typing. Only the selected word goes to the worker, and it stays in the browser.
D12. Right-click an explicitly selected word to open an accessible Synonyms menu. Also support
Shift+F10/Menu key and a toolbar action. Freeze its selection bookmark and document revision when
opening. Preserve selection while the menu has focus. For a whole-word selection, normalize lookup
case and use WordNet exception lists for inflections, while displaying the original word. Do not
silently inflect a replacement. Clicking a result replaces exactly that selection in one undoable
transaction and retains surrounding marks. If the document or selection changed while loading, close
the menu and ask her to select again. Empty/multiword selection, source blocks and read-only mode
offer no replacement. Escape closes and returns focus. Keep ordinary browser copy/paste reachable.
D13. “Ask the agent for options in this sentence” is an optional second menu action, never a
fallback request sent automatically. It previews the selected word and one sentence, then sends only
that context to the chosen thread after her action. Default off pending the policy/access choices
below. With Q176 unchanged, the answer must be allowed cited advice or bounded corrections under
C2-06, never an unrestricted rewritten sentence.
Reviewer checks: fetch the exact mirror commit, hash the archive, compare the archived LICENSE with
Princeton's published text, recompute every imported file hash and the four synset counts, and run
the converter twice from separate directories for identical output. Include license text with both
original and derived data. Check generated records against source offsets, malformed input refusals,
missing license/hash failures, no executable data, no network calls, CSP equality and absence of
document/word logging. The mirror's pin and measured hash establish repeatability, not a claim that
its bytes were compared with the unavailable Princeton archive.
## Agent API and MCP access
D14. Reuse the existing broker and harness typed-tool channel. Mosaic already implements
`initialize`, `tools/list` and `tools/call` in `packages/harness/src/mcp-server.mjs:23-43`, with
session-scoped tool dispatch (`tools.mjs:1-17`). WordCraft's `get_text`, `inspect_document` and
`select_text` (`crates/mcp/src/tools.rs:109-122`) illustrate small, typed operations. Its `execute`,
`type_text` and `save_document` are too broad here. Its `server.rs:130-163` exposes document
resources, but Mosaic needs no second MCP server.
D15. The browser publishes an immutable, short-lived context snapshot only when Melanie chooses
“Share selection” or “Share document” in the visible thread. A pin does not grant access. Snapshot
identity includes owner, business, library/project, mapped path, tab ID, thread ID, editor revision,
base commit, scope, text digest and expiry. An opaque ID is a reference, not authority. The broker
binds it to the authenticated thread holder and user. No agent chooses another user's tab, changes
the active file, reads a session file or uses `library.file` to evade M4. Sign-out, permission loss
or explicit revoke removes access. A browser heartbeat lease expires after 30 seconds without
renewal, including a lost connection. Absolute expiry is 15 minutes. Navigation suspends access
until explicit sharing for the new file. Multiple tabs have separate IDs and never guess which is
“open”.
Proposed new interfaces, all gated design contracts:
| HTTP / broker operation | Typed MCP tool | Result and authority |
|---|---|---|
| `POST /api/editor/contexts` → `editor.context.publish` | none (human only) | Publish selection or document snapshot. Return context ID, revision, digest and expiry. Max 256 KiB snapshot UTF-8, within a 1 MiB HTTP envelope. Reject overflow without truncating silently. |
| `DELETE /api/editor/contexts/:id` → `editor.context.revoke` | none (human only) | Revoke one owned share. Browser disables its card immediately. |
| `GET /api/editor/contexts/:id` → `editor.context.read` | `editor_read_document({context,revision,cursor?})` | Only explicitly shared document text. Page whole blocks below `REPLY_CAP`, with revision/digest on every page. Selection-only sharing refuses whole-document reads. |
| Same read boundary → `editor.selection.read` | `editor_read_selection({context,revision})` | Exact selected text, optional shared sentence and offsets, no implicit surrounding document. |
| `POST /api/editor/suggestions` → `editor.suggestion.propose` | `editor_propose_edit({context,revision,expected,replacement,reason,requestId})` | Store a proposal only. Bind to shared selection offsets and exact expected text, not a browser-supplied filesystem path. No file save. |
| `GET /api/editor/contexts/:id/suggestions` → `editor.suggestion.list` | none required | Human reads proposals for this context. |
| `POST /api/editor/suggestions/:id/decision` → `editor.suggestion.decide` | none (human only) | Accept or reject the card. Acceptance authorizes one local transaction, never a git write. |
D16. All arguments use strict schemas with unknown keys refused. Reads stay within the existing
session capability and approved universe/scope. HTTP mutations use the signed-in human, exact Origin
and same-origin Fetch Metadata fences, never a browser-selected actor. No MCP accept, save or
general execute tool ships. Publishing unsaved text is a new broker boundary and needs a reviewed
capability/policy decision. The first agent slice can remain read-only on explicitly shared saved
material through existing allowed lookups.
D17. Use normalized LF Markdown UTF-16 offsets at valid code-point boundaries, not ProseMirror
positions or rendered DOM offsets. The browser builds a source mapping for the selected editable
text and verifies the exact substring. Refuse selections crossing raw blocks, front matter, nodes or
unmappable syntax. The context revision increases on every document edit. A changed digest/revision,
revoked share or changed text makes a proposal stale. Do not auto-rebase it. Acceptance is one
ProseMirror transaction, undoable, and a later Save uses the original file base and the existing
human writer. Applying an accepted card twice is refused. A crash leaves a proposal unapplied, never
retries it.
D18. Bound proposals to 20 per context, each replacement/expected at most 4 KiB and reason at most
500 characters, with 15-minute expiry and idempotent `requestId` per thread/context. Deduplicate
transport retries without resending to the agent. Log IDs, hashes and outcomes, not bodies. Proposal
content remains protected context data and is deleted on expiry/revoke. Audit records retain
metadata only. Display all generated text as text, with authoring thread, old/new text, reason and
Accept/Reject. Nothing is interpreted as HTML or commands.
D19. Q176 remains enforced on this new channel too. With no new ruling, only the already permitted
C2-06 cited corrections/advice may appear, with its exact citation and edit-distance limits, and no
agent replacement tool is exposed. Unsaved text has no git citation. Sharing it does not turn a
digest into a valid C2-06 source. Supporting cited unsaved drafts needs an explicit decision and
review of M3-M5 and the guard, not a private suggestion channel around the existing guard. Reject
arbitrary prose even when a human would later approve it until that decision is made. Direct edits
remain outside this recommendation.
## Split pane, chat and pins
D24. This section folds Fenton's pane, chat and pin design from `origin/fenton/editor-design` at
`7caf8008b5887c15f74303a50db2e54a9778375f`, `docs/plans/2026-10-21_q195-pane-and-pin.md`. Fenton's
separate work supplies the layout, durable thread pin and send-time context model. The following
reconciles it with Q196, Case 2 paths and this brief's explicit sharing rules. It is design input,
not Fenton's review approval of this combined document.
D25. On writing routes put the editor container, a divider and sibling `aside#chat-pane` under
`main`. Navigation replaces the document container only. Embedded chat takes an explicit thread ID
and container, never owns `location.hash`, and reuses the same renderer as full Chat. Keep the
existing 10-second polling, paused for hidden tabs and sign-out. No new WebSocket service. Chat
polling updates only its own DOM. O2, if selected later, would use the same outer pane with an
engine adapter rather than DOM selection access.
D26. Start at 62% document, 38% chat. The divider uses pointer capture and the WAI-ARIA separator
pattern, names both panes, reports current/min/max width, and supports arrows (2%), Shift+arrows
(10%), Home/End and Enter to collapse/restore. Double-click resets. For the recommended ribbon UI,
clamp document/chat widths to 44rem/18rem and clamp again on resize. Below 76rem show Document/Chat
tabs, preserve both drafts and show reply counts without switching focus. Store width/collapse only
in `mosaic-console-split`, with invalid values reset. No document text in browser storage. These are
design sizes, not benchmark results. The compact O1 toolbar alternative uses 28rem/18rem and tabs
below 60rem. For the ribbon UI, tabs deliberately start at 76rem, including the entire
62–76rem band where the two pane minimums alone would fit. That leaves room for navigation
and avoids another intermediate layout. This deliberately replaces Fenton's E6 2.5rem chat
rail in that band. Existing CSP stays unchanged for the recommended O3 headless-export mix.
D27. Unpinned chat “Follows you” as documents change. A pin binds a bus thread UUID that passes current ownership and FED R3 grant checks to a
document, never a harness run or session-file path. On A pinned to T1 and B pinned to T2, opening B
shows T2, keeping each thread's draft/read cursor. A document without a pin restores the follow-you
thread. Unpinning leaves the displayed thread following the user. Switching thread on a pinned
document offers “Pin this thread here instead?” and never changes the pin silently. A thread may
serve several documents, one pin per document/owner. Document navigation never launches a run. Reads
do not call `thread.open` or send messages.
D28. Recommend durable pins on the bus, following Fenton's S6. Add
`thread_pins(business,owner,project,path,thread,at)`, unique per business/owner/project/path, with
`POST /api/chat/pins` → `thread.pin`, `DELETE /api/chat/pins` → `thread.unpin`, and `GET
/api/chat/pins` → `thread.pins`, under the existing chat identity/write fences. Testers retain their
own isolated pins, never the human's. The pin belongs to its authenticated business/user.
`thread.pin` and `thread.pins` reuse `#grantedThread` after the existing thread ownership
check, as open/send/messages do (`packages/bus/src/broker.mjs:790-793,5766-5780`). Ownership
alone is insufficient. Pin operations never grant thread or document access. For library
documents validate the mapped path/scope through the library reader,
not row 148's narrow `documentPath`. Do not widen that existing function. Keep the thread's chat
project separate from the writing repository identity and validate their declared association. Poll
pin unread metadata separately from message bodies so background threads can get a badge without
rendering or waking them. Row 121's current API has no such metadata contract, so W3a must implement
and test it rather than assume it exists.
No automatic rename following. A missing path shows a stale pin and explicit Re-pin/Unpin controls.
A missing/forbidden thread or revoked/expired thread grant shows the stale-pin state,
offers Unpin only and exposes no former content or unread metadata. The owner may remove
that stale pin without regaining access to the thread. Two tabs update pins
with a revision precondition, so an old tab cannot silently replace a new pin. The follow-you thread
ID may live under an owner/business-scoped browser key and is cleared on sign-out. A pin confers no
document permission or context sharing.
D29. First agent context is saved material at send time. A visible chip, explicitly enabled by
Melanie and removable for each send, carries `{project,path,commit,lines:[from,to]}` and no text. It
appears only after her share action. Check commit is reachable in the allowed served history, path
is mapped, caller/thread can read that scope, and both lines exist in that exact commit. When the
draft is dirty or mapping is uncertain, omit selection lines and label “Saved version only. Save to
cite this selection.” Do not pretend draft offsets are saved line numbers. The agent uses existing
approved library lookup tools and C2-06 citations, not session files or an expanded generic `read`
root. `packages/project/src/paths.mjs:34-40` now refuses a library repository unless
`allowLibrary` is explicit. Use the library reader and do not widen `documentPath`.
Store structured context in `message_context` keyed to the message, kept apart from body and
inserted atomically. `thread.send` accepts this optional strict field, and message reads return it
separately. The broker validates the source rather than trusting the browser's chip. This first
saved-context route does not enable D15's unsaved snapshots. Both tables and verb changes need
Jason's explicit schema/API approval. They grant no prose authority.
D30. Fenton found C2-10b's file list names `bus.js`, while actual thread messages render in
`public/chat.js:68`. Sage must amend that row's ownership/acceptance to include `chat.js` or a
shared renderer before W3 starts. Otherwise cited JSON would reach the pane as plain text. C2-09a's
brief also cites older broker positions. Re-measure against `broker.mjs:5716` when building. No
edits to those briefs or queue are made here. M7 and M8 remain intact, and neither row 5 nor row 29
is a dependency.
D31. Fenton's Q196 update adds a host contract that works with each option: `mount`, `destroy`,
`dirty`, `focus`, `selection` and `resized`. For the recommended DOM surface these adapt the
editor's real state, not a second document model. F6/Shift+F6 cycle focus among editor, divider and
chat. Scope editing shortcuts to the focused pane, so chat paste and undo never mutate the document.
Before releasing any editor state, check `dirty`; a missing/failed response counts as dirty.
Preserve the state or offer the explicit Save/Copy/Discard controls in D7. A hash navigation alone
is not a browser `beforeunload` event and must run this check itself.
For O2, Fenton recommends an iframe so WordCraft's page-wide canvas and document/window listeners do
not capture chat input. A new browser bridge must check `event.origin`, `event.source`, message
schema, per-instance generation and request ID. It needs document load/export, dirty, selection,
focus-out, resize and a safe transaction operation for the thesaurus, not just a notification hook.
A failed dirty response after one second blocks unmounting. During divider drag capture the pointer,
prevent iframe interception, resize at most once per animation frame and send the final `resized`
notification. Test F6 escape, chat paste isolation, narrow ribbon behavior and stale messages after
document changes. A same-origin iframe is UI isolation, not a security boundary. Native file I/O and
network features still need the adapter restrictions in O2.
An iframe-route-only `wasm-unsafe-eval` exception would be narrower than changing the Console shell
policy, but it still changes the editor's CSP and requires Q167 D3's ruling. The recommended
headless-export O3 needs neither an iframe nor a browser WASM exception. No alternative-origin
hosting or broad `unsafe-eval` is authorized as a workaround. Fenton's conditional P5 brief at the
pinned source describes the iframe bridge. If O2 is chosen, revise W2/W6 into source-import/build
and browser-bridge rows before queue entry, with their exact owned source paths and the measured CSP
delta.
O2's DOCX selection cannot use Markdown line citations. A versioned document locator and C2
guard/renderer support would need a separate reviewed format-policy brief. A DOCX path is not made
legal by leaving `lines` out. Until that change is approved, rich-document agent context is
disabled. This deliberately replaces Fenton's J7 proposal to send DOCX context without
lines. Do not use paragraph indexes as if they were existing git lines.
## Build order and review gates
Order: W1 → W2 → W3 → W6 → W4 → W5. W3a can prepare after its Case 2 dependencies and must finish
before W3. W2 is a useful writing increment. W6 completes the recommended release's styled
production handoff. W4/W5 remain separately gated.
These are proposed briefs, not queue entries. Jason confirms the checklist, Mos routes the decision,
then Sage accepts and schedules each brief and creates its Gitea build issue. Every code row gets an
independent Opus 5.5 reviewer who did not author that row. Q177 allows building on a reviewed parent
candidate, with landings in order. It does not allow overlapping file owners or bypassed reviews.
Sage grants suite slots and the final gate. Rows owning `serve.mjs` also serialize behind row 198's
current ownership. That is a file handoff, not a dependency on the retired board.
Common gate G1: run each changed/touched test file independently, the affected package suites and
every `scripts/test-*.sh` on Sage's slot. Search tests for changed path/hash pins. For editor
changes use `MOSAIC_REQUIRE_EDITOR_BUILD=1` with pinned esbuild 0.28.2 and reproducible output
hashes. Browser checks use scratch roots and invented vaults only. No row activates a stack, changes
roles or pushes to protected branches by implication.
## W1 local thesaurus data and converter
### Problem
WordCraft's small built-in list cannot provide general writing coverage (D10). No Console thesaurus
dataset exists in the measured editor inputs.
### Owner and reviewer
- Owner: a T3 Codex gpt-6-luna seat, assigned by Sage (lower-level data import and converter).
- Reviewer: Fenton, Opus 5.5, independent of this implementation.
- After: Jason's first-slice approval. WordNet is the design's routine data-source choice.
May start independently of C2 shared-file work.
### Files owned
`packages/webui/third_party/thesaurus/{LICENSE,NOTICE,PROVENANCE.json,wordnet/}`,
`packages/webui/editor-data/{import-wordnet.mjs,build-thesaurus.mjs,BUILD.json}`,
`packages/webui/src/public/thesaurus-data/`, `packages/webui/tests/thesaurus-data.test.mjs`,
`packages/webui/README.md` (thesaurus section).
### What ships
Pinned WordNet originals and deterministic derived local data (D10-D11). Tests reproduce counts and
hashes, license preservation, invalid-input refusal and stable sorted output. No change inside
`editor/` or existing editor tests. Record actual output size.
### Out of scope
Menu, worker wiring, agent requests, editor source and Case 2 library writes.
### Gate
Fenton verifies source/license and two independent builds. Data tests pass and output stays within
D11's size budget, then G1. Sage accepts the manifest.
## W2 writing surface and local synonyms, first usable slice
### Problem
F3's redraw loses undo history and no selected-word menu exists. Books also need Case 2's library
read/write path (F4, D5).
### Owner and reviewer
- Owner: Gyro, Codex gpt-6.1-sol (editor lifecycle and selection handling).
- Reviewer: Fenton, Opus 5.5, independent of the code.
- After: W1, C2-11a row 214, C2-10b, and C2-11b's future accepted brief and the unimplemented human `library.file.save` verb,
with C2-02/C2-03b for usable book saves. No acceptance claim until all are reviewed.
### Files owned
`packages/webui/editor/{view.mjs,entry.mjs,editor.css,thesaurus.mjs}`,
`packages/webui/editor/BUILD.json`,
`packages/webui/src/public/{editor.js,editor.css,bus.js,bus.css,thesaurus-worker.js}`,
`packages/webui/tests/{editor.test.mjs,editor-browser.test.mjs,docs-browser.test.mjs,writing-browser.test.mjs,thesaurus-browser.test.mjs,editor-provenance.test.mjs}`,
`packages/webui/README.md` (writing section). `model.mjs` remains C2-11a's result.
### What ships
D4-D12, D31 and O1's familiar controls: stable per-document state, explicit save, writing controls,
navigation through the existing library tree, preserved front matter and local accessible synonyms.
Wire only reviewed self-hosted assets. Tests cover selection/focus, Unicode, stale lookup,
replacement undo, read-only, exact saved-selection source mapping, unsupported blocks, no external
requests and unchanged CSP. A fixture session edits two nested chapters with front matter/wikilinks,
saves, navigates back and checks bytes, selection and undo. Test `stale-base`, unknown result,
unpushed feedback, credential expiry preserving local text and explicit sign-out clearing it after
the unsaved warning. Measure D11's load/heap/lookup budgets and a 100,000-word fixture without lost
input during chat-style refresh events. Passing responsiveness means no typing task above 100 ms
after load in 95 of 100 measured edits, with the browser/host recorded.
### Out of scope
Split pane, pins, agent permissions, library writer/sync implementation and Word layout.
### Gate
Fenton repeats the fixture writing flow and reviews accessibility and byte preservation. `node
--test packages/webui/tests/` with the required build passes, then G1. Sage accepts.
## W3a durable pins and saved-message context
### Problem
Pins and structured message context do not exist (F5-F6). Browser-only pins would differ across
machines. D28-D29 require new durable records and verified source references.
### Owner and reviewer
- Owner: Gyro, Codex gpt-6.1-sol (schema, broker and channel changes).
- Reviewer: Fenton, Opus 5.5, independent of the implementation.
- After: C2-09a, C2-10b ownership correction, C2-05b, and Jason's schema/API decision.
### Files owned
`packages/bus/{schema.sql,src/store.mjs,src/broker.mjs,src/editor-pins.mjs}`,
`packages/bus/tests/{schema.test.mjs,editor-pins.test.mjs}`,
`packages/plugins/channel/webui/index.mjs`, `packages/plugins/tests/` (pin/context tests),
`packages/webui/src/{chat.mjs,chat-http.mjs,serve.mjs,cli.mjs}`,
`packages/webui/tests/{chat.test.mjs,editor-context.test.mjs}`, `packages/webui/README.md`,
`packages/bus/README.md`. The next migration identifier is allocated by Sage's schema window before
build.
### What ships
D28-D29, an idempotent versioned migration in the repository's existing migration mechanism,
owned-pin revision checks, background unread metadata and atomic saved-message context. Tests refuse
another user's/thread's/business's context, wrong scope, unreachable commit, missing file and
out-of-bounds lines. Failed context insertion leaves no message. Pin tests cover tester separation,
stale paths, grant revocation/expiry and concurrent repin refusal. `thread.pin` refuses an
owned thread whose grant no longer covers it. `thread.pins` redacts thread content and
unread metadata after revocation while permitting stale-pin removal. No document text
is stored in the new context table.
Migration/restore-count and bus tool allowlist tests remain green.
### Out of scope
UI, raw session access, unsaved context, new role authority and replacement prose.
### Gate
Fenton reviews schema/ownership and failure cases. Bus, plugins and WebUI suites pass plus G1. Sage
accepts within the allocated schema window. No live migration is part of this row.
## W3 split pane and pinned conversations
### Problem
F5's page-level routing prevents a persistent conversation beside navigation. No pin exists. D24-D30
supply the behavior to implement.
### Owner and reviewer
- Owner: a T3 Haiku 5.5 high seat, assigned by Sage (mid-level pane and state wiring),
separate from C2-10b's reviewer.
- Reviewer: Fenton, Opus 5.5, independent of the implementation.
- After: W2, W3a, C2-10b, C2-09a and row 220 C2-10a. Shared files wait behind those owners.
### Files owned
`packages/webui/src/public/{index.html,bus.js,bus.css,chat.js,chat.css,writing-pane.js}`,
`packages/webui/src/{serve.mjs,chat.mjs,chat-http.mjs}`,
`packages/webui/tests/{chat.test.mjs,chat-browser.test.mjs,writing-pane.test.mjs,writing-browser.test.mjs}`,
`packages/webui/README.md` (pane section). Persistent pin storage is W3a, not a hidden schema change
here.
### What ships
D24-D31: a resizable accessible split pane that reuses the existing grant-accessible chat thread and C2-10b
cited-item rendering, retains editor selection/history during polling and follows the pin rules in
D27-D29. Tests cover pin/unpin, two files, two tabs, removed/inaccessible thread, revoked/expired grant, keyboard divider,
narrow viewport, reconnect/sign-out and navigation during delayed replies. No implicit `thread.open`
on read or navigation. Existing chat tests remain green. The saved context chip uses W2's exact
source mapping, and dirty drafts omit selection lines.
### Out of scope
7331 board route, row 5, row 29, raw sessions, fresh agent permissions and auto-launched runs.
### Gate
Fenton verifies the pane/pin scenarios against the folded design. WebUI suite and G1 pass. Sage
accepts the manifest after W3a's migration and API review.
## W6 DOCX production export and DesignCraft fixture handoff
### Problem
DesignCraft's `.md` import is plain text (D21). A Word-like writing surface still needs a verified
styled book-production handoff, without creating two editable masters.
### Owner and reviewer
- Owner: Gyro, Codex gpt-6.1-sol (isolated headless format adapter and semantic checks).
- Reviewer: Fenton, Opus 5.5, independent of the implementation.
- After: W3, Louie's evaluated engine revision, and Jason's O3/storage choice.
This serializes shared `serve.mjs` and `writing-pane.js` ownership.
### Files owned
`packages/writing-export/` (new, source, tests, pinned source manifest and build records),
`packages/webui/src/{editor-export.mjs,serve.mjs,cli.mjs}`,
`packages/webui/src/public/writing-pane.js`, `packages/webui/tests/writing-export.test.mjs`,
`packages/webui/README.md`. No change to WordCraft/DesignCraft checkouts. Any copied source retains
full applicable license notices. Export-commit authorization must be covered by a separate accepted
writing-project path/policy brief before a repository write is enabled.
### What ships
D20-D23's bounded Markdown-to-DOCX export from a pinned source commit, style map, warning report and
manifest, plus a reproducible source build of the needed WordCraft headless component. Review the
pinned Cargo.lock and dependency licenses, preserve required notices and bundle any required fonts
locally. Hard starting caps: 1 MiB Markdown input, 16 MiB DOCX output, 30-second conversion deadline
and one conversion per user at a time. Record actual fixture results and revise these limits only in
the accepted brief when measurements justify it. Refuse filesystem/network escape, external
linked-resource fetches, scripts/macros, malformed archives, decompression bombs and unsupported
semantic loss. Run the conversion without network or credential access. Fixture tests place the
result through DesignCraft's headless MCP path and compare body, styles, Unicode, paragraphs and
warnings. Conversion is an explicit action, not every Save. The initial UI can download the export
until the reviewed git-output path is authorized.
### Out of scope
Legacy .doc/.qxd import, new rich-document editing, live DesignCraft control and layout work on
Melanie's actual content. No guessed “full fidelity” claim.
### Gate
Fenton independently runs the export/place fixture using the pinned two source revisions and
inspects the loss report. New export tests and affected WebUI suite pass plus G1. Sage accepts only
if the target chapter's tested supported features place without loss.
## W4 scoped context API and typed reads
### Problem
The broker knows saved library material, not a tab's open document or selection. Exposing unsaved
drafts without explicit consent would cross D15-D16's new boundary.
### Owner and reviewer
- Owner: Gyro, Codex gpt-6.1-sol (broker identity and capability integration).
- Reviewer: Fenton, Opus 5.5, independent of the implementation.
- After: W6, C2-05b, C2-06, row 220, and Jason's explicit context-access decision.
### Files owned
`packages/webui/src/{serve.mjs,cli.mjs,editor-http.mjs}`,
`packages/webui/src/public/writing-pane.js`, `packages/bus/src/{broker.mjs,editor-context.mjs}`,
`packages/harness/src/tools.mjs`, `packages/{webui,bus,harness}/tests/` (editor-context test files
only), `packages/webui/README.md`, `packages/harness/README.md`. Any capability/role schema change
needs a separately accepted policy brief, not this allowlist.
### What ships
D14-D16's publish/revoke/read operations with strict schemas, expiry, bounded paging and explicit
sharing UI. Pi and Claude use the same broker checks. Tests refuse cross-user, thread, business,
universe, tab, guessed context, stale revision, revoked shares, expired heartbeat and absolute
expiry, whole-document read on selection-only access and attempts to use raw session endpoints.
Confirm raw-session `conversation-not-listed` and human-only `library.file` still refuse.
### Out of scope
Replacement proposals, save authority, new roles and bypass of M3-M5.
### Gate
Fenton approves boundary tests and confirms no tools are exposed without the approved capability.
WebUI, bus and harness suites pass with G1. Sage accepts.
## W5 guarded proposals and optional sentence options
### Problem
Q195's requested editing API needs a human-controlled proposal flow, but Q176 currently prohibits
unrestricted prose. An unsaved draft is not a valid existing git citation (D19).
### Owner and reviewer
- Owner: Gyro, Codex gpt-6.1-sol (revision checks and undoable proposal application).
- Reviewer: Fenton, Opus 5.5, independent of the implementation.
- After: W4 and an explicit Jason ruling on proposal kinds and unsaved-draft citations.
With no ruling this row does not enter the queue.
### Files owned
`packages/bus/src/{broker.mjs,editor-context.mjs}`, `packages/harness/src/tools.mjs`,
`packages/webui/src/editor-http.mjs`, `packages/webui/src/public/writing-pane.js`,
`packages/webui/editor/{view.mjs,thesaurus.mjs}`, `packages/webui/editor/BUILD.json`,
`packages/webui/src/public/{editor.js,editor.css}`, `packages/{webui,bus,harness}/tests/`
(editor-suggestion test files only), `packages/webui/README.md`. Any
`packages/library/src/guard.mjs` or role-policy change is excluded until its own reviewed policy
brief names the approved change and tests.
### What ships
D13 and D17-D19, restricted to the confirmed policy. Tests cover old/new text rendering,
accept/reject, undo, stale and expired context, changed base, no autosave, retry deduplication,
invalid UTF-16 boundary, hostile markup and guard failures on both RESULT and tool paths. Negative
tests prove an agent cannot accept, apply or save, and no proposal widens C2 scope.
### Out of scope
Direct agent edits, prose permission by inference, arbitrary execute and automatic sending.
### Gate
Fenton verifies that the recorded ruling matches the allowed proposal shapes and source citations.
WebUI, bus, harness and library guard regression suites pass with required editor rebuild and G1.
Sage accepts. No deploy or activation authority follows.
## Design verification receipt
Checked 2026-10-21T16:22:36Z (from `date -u`). Both companion branches were fetched and their
report commits pinned above. Seven briefs have every BRIEF-TEMPLATE field, unique anchors
and an independent implementation reviewer. The initial checklist had 12 lines before fenton-0004. Only this design
and append-only BUILD-LOG/SESSIONS records change. No product or queue files change.
Fresh editor/provenance checks: 10 passed, zero failed, one rebuild skip because pinned
esbuild 0.28.2 is absent. This is not a claim that O3's exporter, new UI, live Console or
DesignCraft handoff has shipped or passed acceptance. Those tests belong to the build rows.
The push queue guard ran as `MOSAIC_GIT_IDENTITY=gyro` for `gyro/editor-design`, purpose
`push`, timeout/interval zero. It could not resolve the per-seat credential and recorded
`CANNOT_ASSERT credential-unresolvable` with its documented push `degraded-pass` (exit 0),
audit at `/mnt/storage/scratch/gyro-q195-ci-queue-audit.jsonl`. No identity fallback or guard
bypass was used. CI status was not asserted. Fenton reviews the combined draft through Mos.
## Review-fix verification receipt
Checked 2026-10-22T16:06:34Z (from `date -u`) against `aca16404c`. Addressed fenton-0004 F1-F8:
grant-aware pins and revoked-access redaction, refreshed source references, model-only
seat allocation, the unbriefed save dependency, deliberate tabs below 76rem, the explicit
J7 replacement, seat-name wording and six owner-choice questions. Additional reference
refresh: `public/bus.js` moved by 24 lines, and the Visual editor README heading is at 572.
The code behavior cited there is unchanged. The O3 recommendation and companion report
measurements are unchanged. Fenton re-checks this revision before approval.
Validation is static: current-source anchors, seven complete brief templates, six checklist
lines, documentation-only diff and append-only logs. No build, product suite, gate-slot
reservation or live action is part of this review fix. The earlier 10-pass/one-skip result
above is the original draft's test receipt, not a new test run on `aca16404c`.
## Jason's checklist
- Q1. Choose O3 with Markdown source, generated DOCX, and footnotes/comments/tracked changes/page layout deferred? Recommend yes.
- Q2. Ship writing controls, safe chapter saves and local WordNet synonyms first, after the Case 2 dependencies? Recommend yes.
- Q3. Store pins and saved-source message context on the bus through the reviewed schema/API change? Recommend yes.
- Q4. Share saved context only when Melanie enables the visible chip for that send? Recommend yes.
- Q5. Keep Q176, with no agent prose and no unsaved-draft reads? Recommend yes. Any later relaxation requires Accept/Reject and a separate Save.
- Q6 (O2 only, if Q1 is declined). Require a Q167 D3 CSP exception and rich-document citation design before O2? Recommend yes.
+31
View File
@@ -487,6 +487,37 @@ at every gate. Started 2026-09-12 during the control board MVP.
touches the webui build: install the pinned esbuild in the gate checkout
so the check runs, or record why it can't. (2026-10-22)
- **The case supervisor reports a fast child as an inner-session failure.**
(Row 253 N1, mos-1297 item 5.) A child that exits before the supervisor
verifies its session is reaped first, so the supervisor exits 85
(inner-session) instead of the child's own code. Mos measured rc85 in 39
of 40 fast-child bash runs. It fails closed, so it is not a 10-27 defect;
row 253 fixed only its own fixture (PYTHON_STUB execs python3). The
product fix is Vikunja 532, after 10-27. (2026-10-22)
- **boardSend /api/seen returns 400 for a scan failure after writeMark.**
(Webby N2 on row 198, ruled sage-2867.) The mark is written, then the
rescan fails, and the client gets 400 as if its request were bad. Webby
suggests 500. It does not hold the 198 gate; binds the next row that
touches serve.mjs. (2026-10-22)
- **Stale "row 203" text in the access command.** (Quackerjack, row 203
BUILD.md follow-up.) packages/cli/src/access.mjs lines 26-27, its help
string at line 36 and line 44, and packages/cli/README.md line 1107 say
peer homes stay unread "until row 203" adds the federation key. Row 203
changes only scripts/mosaic-config.mjs and test-config.sh, so once it
lands those lines point at a row that did not do the work. Binds the next
row that edits access.mjs. (2026-10-22)
- **A remote outage can make the first library lookup outlast the tool
timeout.** (Row 224 residual, sage-2819.) In each 60 s window the first
lookup still runs an inline fetch, and that fetch can take longer than the
60 s tool timeout. Bound the inline fetch in a later row. (2026-10-22)
- **A sharee's session.list stays refused.** (Row 206, sage-2818, Q7.)
Whichever of row 193's session wiring or the share wiring lands second
wires it. (2026-10-22)
## Queue
Moved to `docs/plans/QUEUE.md` on 2026-09-13. This file holds only gaps.