diff --git a/agents/gosalyn/work/case2/DESIGN.md b/agents/gosalyn/work/case2/DESIGN.md index fba567f71..05a79cbca 100644 --- a/agents/gosalyn/work/case2/DESIGN.md +++ b/agents/gosalyn/work/case2/DESIGN.md @@ -1,266 +1,416 @@ -# Case 2 design: the creative writing hub +# Case 2 design: the creative writing hub (r2) Gosalyn, 2026-10-20 (from `date -u`). Track T2 in jason-master-restart `projects/mosaic-mvp/PLAN.md`, Vikunja #416, DECISIONS Q176-Q178. The requirement is Jason's text in TARGET.md (addendum 2026-10-20). This is a design only. No product code changes here. -Base: `refactor` at 5d019d876. Every `file:line` below was read at that commit. Paths are relative to -the repository root. +Base: `refactor` at 9ae8c73ac. r1 cited 5d019d876. `git diff --stat 5d019d876 9ae8c73ac -- packages +adapters roles scripts` is empty (Mos in mos-0980, Webby in webby-0001), so every `file:line` holds at +9ae8c73ac. Paths are relative to the repository root. + +## Changes from r1 + +Jason's rulings (DECISIONS Q180, relayed in mos-1014) come first, because they set the slice: + +- She talks to her agents inside Mosaic Stack. The 10-27 slice has no Discord dependency. Its chat + channel is the Console (C2-10b), which renders cited items and shows sync status, so B1 and B2 are + solved there. The M5 guard and cite check cover it through D8. The Discord relay (C2-18) moves after + the slice as an option, and M8 stays in the slice so no Discord path can skip the guard (5.1, 7.6). +- Bean and Chet become Mosaic agents with the same names and personalities. They are two instances of + a new creative definition, `companion`, and each carries its persona in a new agent-layer var (4). + New row C2-20 reads only the persona definitions of the two Hermes profiles on 10.1.1.54 (default and + chet) and confirms which profile is which name. Her Hermes memory store (188 facts) and chat history + stay where they are unless she chooses otherwise through Jason. +- Hermes keeps running until Bean and Chet are live in Mosaic. Nothing in this design stops, changes or + shares anything with Hermes, its bots or their tokens (9). +- The host is a variable, ``, with the instance under its own user `` without sudo. Mos's + reading, which he is confirming with Jason, is her VM 10.1.1.54 with RAM added (9, 10). + +Mos's host check (mos-1000, jason-master-restart `projects/mosaic-mvp/MELANIE-HOST-RECON.md`) changed +the data model: + +- Her library already exists. Her books repo on git.mosaicstack.dev has 203 Markdown files and 3,486 + commits. obsidian-git syncs it, and each book folder has its own subfolders. r1 invented a new + `universes/` tree and a `library.json`. r2 drops both and reads her repo as it is. A mapping in the + business file names her book folders, her type folders and her playground folders (1.1). Her repo + gets no new file. +- Canon and playground map onto her layout by path prefix. A file under a configured book folder is + canon. A file under a configured playground prefix is playground. Anything else is unmapped, and + agents can't see or cite it (1.2). +- Her rules aren't mine to impose on 3,486 commits of her work. The validator now reports problems and + never holds back her latest commit. The scope guard in code is the path rule plus the cite check, and + neither depends on her files passing a rule (1.3). +- In the slice the bus never writes to her repo. Agents can't write anyway (D3), and the Console editor + comes after the slice. So the slice reads a bare mirror through a read-only token, and pushing moves + to later rows (C2-02, C2-03b). +- Her vault root is `~/src`, one level above the books repo, so Obsidian's absolute links may start with + the repo's folder name. The link resolver strips a configured `vaultPrefix` and also resolves + Obsidian's shortest-path form. A link into another of her repos never resolves. That keeps bean-brain + out by construction, and the bus token reaches only the books repo (mos-1000 item 3). +- The instance has its own user and no shared state with anything else on its host. Row 190 doesn't + block it. C2-13 delivers an install script and a precheck that Jason or his delegate runs on `` + (9, 10). +- Mos, mos-0992 item 3. Any Discord output must go only through the broker's stored, guarded messages. + Today's connector posts its own pi engine's text straight to Discord + (`packages/discord/src/connector.mjs:381`, `:386-387`), so it would skip every guard. M8, in the slice, + refuses that engine on a library business. The later relay (C2-18) posts only stored messages, and its + tests prove it (5.1, 7.6). It would need a new Discord application, never her two Hermes bots. +- Mos, mos-0992 item 4 and mos-1000 item 5. The B5 caps have numbers, and the residual is stated in + numbers in 5.1. +- Mos, mos-0980 item 4. Section 10 names the Gitea host and lists Jason's steps. C2-13, and anything that + creates an account, contacts Melanie or touches her repo's access list, waits for Jason. + +Webby's review (webby-0001, CHANGES, notes at agents/webby/work/case2-r1/REVIEW.md, f9d218ab1). I +re-read every line Webby cites, and each one holds. Where her existing repo changed what an item means, +the line says so. + +- B1. The slice now holds C2-09a: `#threaded`, the launcher and `thread.create` read + `THREAD_DEFINITIONS`, and the chat channel picks the agent. `room.*` is C2-09b, outside the slice + (6, 8, 9). +- B2. The slice holds the Console renderer and status line (C2-10b, 7.4). It renders cited items and + reads the new human verb `library.status` (7.2). +- B3. `decision.raise` is refused for creative definitions, and their typed tool set drops + `raise_decision`. An agent asks with a `question` item, which the guard checks (5.1 M6). +- 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). +- 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). +- B8. Split by path. The slice reads blobs from a bare mirror and never checks out a working tree, so + symlinks, gitlinks, `.gitattributes` filters and `.gitmodules` can't act. The reader reads only + `100644` blobs up to 1 MiB and reports the rest (rule R1). r1's S0 rule moves to the bus write path + (C2-02), where it guards the working tree. Whether her repo holds any such entry is a C2-13 survey + fact. If it does, C2-02 needs a ruling before it can write. +- B9. Moves to C2-03b with the push path. The slice never writes, so there is no write to refuse. +- B10. One function, `syncCommand`, builds the production argv and env. Tests assert both exactly. The + file-protocol switch is a module parameter that no business file field or bus verb can reach (7.2). +- B11. C2-04 lists every consumer of `root`, and each one skips a library project with its own test + (2.1). +- B12. Path fields are now her keys `appears_in` and `relationships`, plus the business file mapping. + They are indexed like links, and scope comes from the target's path. `library_manuscript` takes + `scope` and labels its result. A hybrid is a business file entry (1.4). Agents can't write the + business file (M9), so `sanctioned` and the hand-edit question go away. +- N1. Blob reads are cited at `read.mjs:19-26`. +- N2. `END_PROTECTED` becomes `THREAD_DEFINITIONS`, so no organization write can end Melanie's agents (4). +- N3. The runner's two failure texts become the fixed notices `no-answer` and `turn-failed`. The guard + accepts exactly three notice bodies (5.2). +- N4. Promotion moves to the write rows (C2-19), as a multi-path commit. +- N5. Her file names can have capitals and spaces, and they aren't mine to rename. Two paths that differ + only in case are reported (R7) and both are left out of lookups. +- N6. A creative role must have a `messaging` key (4). +- N7. Row order is redone for the read-only slice (8). +- N8. The C2-06 gate runs the guard through both send paths and with a non-creative sender (8). +- N9. `room.open` names its refusal `room-thread-name-taken` (6). ## 0. Summary -Melanie's writing lives in one git repository, the library. It is a project record repo with a new -layout kind (section 1). The bus commits to it for her, from the Console editor, and pushes every -commit to a Gitea remote. Her Obsidian vault is a clone of that remote, so the graph and backlinks -come from Obsidian itself. Agents can't write to the library at all. They read it through typed -lookup tools, and every reply they send her must be a list of short items. Each item cites the -library or a fetched web page, and the broker checks each citation before the message is stored. -Several creative agents share a project. The Console shows their threads as one chat, and a leading -`@name` sends a message to that agent only. +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. +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. -Decisions taken in this design (D-codes are this document's own): +Decisions (D-codes are this document's own): -- D1. The library is a record repo kind (`library`), not a code checkout. That reuses the lock, - clean-tree check, atomic publish and rollback in `packages/project/src/index.mjs:195-234`. -- D2. Scope comes from the path. `universes/` is canon and `playground/` is playground. No file - content can change a file's scope. -- D3. Agents get no write path into the library. That covers canon, playground and notes. This is - stricter than the requirement asks, and it is the cheapest guardrail to prove. -- D4. One chat is a project. Each creative agent holds its own thread in that project. This needs - no change to the `threads` table or its triggers (section 6). -- D5. The remote URL lives in the business file and is passed on the git command line. The repo's - `.git/config` keeps its no-remote rule (`packages/project/src/git.mjs:44`). -- D6. The Gitea remote is where the bus and Obsidian meet. The bus only fast-forwards and never - merges. Obsidian's git plugin does its own merges on Melanie's machine. +- 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. +- 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`). +- 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. +- 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. ## 1. Data model -### 1.1 Layout +### 1.1 Her repo and the mapping -``` -library.json {layoutVersion: 1, kind: "library", business, project, name, createdAt} -.obsidian/app.json committed vault settings (section 7) -.obsidian/graph.json -universes//universe.json {id, name, kind: "canon" | "hybrid", sources?, constraints?, sanctioned?} -universes///.md universe-level entity, shared across books -universes//books//book.json {id, title, order} -universes//books//manuscript/-.md Melanie's prose -universes//books///.md book-level entity -playground/universes//... universe playground, including draft universes -playground/universes//books//... book playground +From mos-1000: each book folder has subfolders for characters, chapters, chapter summaries, locations, +research and a tracker. Front matter keys seen are `type`, `name`, `appears_in` and `relationships`. The +repo also has `.base` and `.canvas` files. I haven't seen the exact folder names, and I shouldn't read +her text to find them. The survey command from C2-01 prints names and counts only (10 step 7). + +The mapping is a new form of business file project entry. Business files live in +`/businesses/.json` (`packages/business/src/business.mjs:1-3`, `:30-32`): + +```json +"projects": { + "books": { + "kind": "library", + "remote": "https://git.mosaicstack.dev//.git", + "branch": "main", + "vaultPrefix": "", + "universes": { + "": {"name": "", "books": ["", ""]} + }, + "types": {"": "character", "": "place", + "": "research", "": "tracker", + "": "summary"}, + "manuscript": [""], + "playground": ["/"] + } +} ``` -- `` is one of a closed list: `characters`, `places`, `items`, `groups`, `events`, `lore`. The - list is a constant in the new package. Adding a type is a reviewed commit. -- ``, `` and `` match the project id pattern `^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$` - (`packages/project/src/paths.mjs:7`), lowercased by convention. -- This is Jason's `universe.book.{character,place,etc}`, read as a path. One assumption: a character - who spans books lives at universe level, and a book-level file may add `extends:` naming it. If - Melanie wants every entity strictly under a book, drop the universe-level `/` directories. - Nothing else changes. +- Every value in `<>` comes from the survey and Melanie's say, through Jason (10 step 7). +- `universes` groups her book folders. If she has one world, it is one universe holding every book + folder. Jason's `universe.book.{character,place,...}` then reads as universe, book folder, type folder. +- `playground` is a list of path prefixes. Her repo has no playground today as far as I know. The setup + sheet suggests one folder per book, for example `/playground/`, or one at the top. Until she makes + one, everything mapped is canon. +- `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. -### 1.2 Entity files +Agents can't change this mapping. No organization verb writes `projects` today: `organization.mjs` has no +write of it, and `instance-settings.mjs:141` only reads `proposed.projects`. An organization write sends +a whole proposed document, though, so C2-04 adds a refusal, `organization-projects-protected`, for any +organization write whose `projects` differ from the file's, with a test (M9). -An entity is Markdown with front matter and wikilinks. Obsidian reads both natively. +### 1.2 Scope -```markdown ---- -type: character -id: mara-vell -name: Mara Vell -aliases: [Mara, The Archivist] -extends: universes/hollow-sea/characters/mara-vell # book level only, optional ---- -Mara grew up in [[universes/hollow-sea/places/saltmarket]] ... -``` +Each path in her repo, relative to the repo root, gets one scope: -- Front matter is a restricted subset: `key: scalar` and `key: [a, b]`. The new parser refuses - anything else (nested maps, anchors, multi-line strings) with the line number. The repo has no YAML - parser today. The editor only keeps front matter byte for byte (`packages/webui/editor/model.mjs:220-231`). -- `type` must equal the directory, and `id` must equal the file name. -- Links are `[[path-without-.md]]` or `[[path|label]]`, written as absolute vault paths. The committed - `.obsidian/app.json` sets Obsidian's new-link format to absolute, so links Obsidian writes pass - validation. A bare `[[name]]` resolves only if exactly one file has that base name in the same - universe. Otherwise it is a validation problem. +| Scope | Rule | +|---|---| +| `playground` | Starts with a configured playground prefix | +| `canon` | Otherwise, inside a configured book folder, and a `.md` file | +| `unmapped` | Everything else: top-level notes, `.base`, `.canvas`, attachments, anything outside a book folder | -### 1.3 Structural rules (the validator, `checkLibrary`) +Lookups default to canon and return playground only on request. Unmapped files are never returned or +citable. The universe of a file is the universe whose `books` list holds its book folder. -The validator runs on a commit, not on the working tree. It reads blobs with `git ls-tree` and -`cat-file` the way `packages/project/src/read.mjs:67-86` does. +### 1.3 Reading and reporting -| Code | Rule | Refusal code | +The reader works on a commit, not a working tree. It lists the tree with `git ls-tree -r -z` and reads +blobs with `cat-file`, as `blob()` does in `packages/project/src/read.mjs:19-26`. That function accepts +`100644` or `100755` (`:21`). The library reader is stricter. + +None of these rules holds back her latest commit. Each produces a problem in `library.status`, with path +and line where there is one, and the index leaves the affected file or link out. + +| Code | Rule | Effect | |---|---|---| -| S1 | Top level holds only `library.json`, `.obsidian/`, `universes/`, `playground/`, `.git` | `library-unknown-path` | -| S2 | A canon file (`universes/...`) links only to canon files | `library-canon-links-playground` | -| S3 | A canon file in universe U links only inside U. A hybrid also links into its declared sources | `library-cross-universe` | -| S4 | Front matter parses, `type` matches the directory, `id` matches the file name | `library-entity-invalid` | -| S5 | Every link resolves to exactly one file | `library-link-unresolved` | -| S6 | A hybrid names 2 or more `sources`, each an existing canon universe, and no source is itself a hybrid | `library-hybrid-invalid` | -| S7 | Hybrid constraints hold (1.5) | `library-constraint` | -| S8 | `playground/` may link anywhere. It is never read as canon (sections 3 and 5) | none | +| R1 | Only `100644` blobs up to 1 MiB are read. Symlinks, gitlinks, executables and larger blobs aren't | not indexed | +| R2 | A path is unmapped (1.2) | not indexed | +| R3 | Front matter the reader can't parse fully. It reads scalars, quoted strings, flow lists and block lists of scalars or wikilinks. Anything else, nested maps for example, stays raw text | indexed by path, file name and body | +| R4 | A link or path field that resolves to no file, or to more than one | link not followed | +| R5 | A canon file refers into the playground | reported only. Lookups and the guard still go by the target's path | +| R6 | A canon file refers into another universe | reported only. Lookups never cross universes (3) | +| R7 | Two paths differ only in case | both not indexed | -S2 is what keeps playground and canon apart. Canon can never depend on a playground file. Lookups -default to canon. A citation's scope comes from its path. A finding that cites a playground path is -refused (section 5). +Links are `[[target]]`, `[[target|label]]` and `[[target#heading]]`. The resolver tries, in order, a +path from the repo root, the same path with `vaultPrefix/` stripped, a path relative to the linking file, +and Obsidian's shortest-path form, a base name that matches exactly one indexed file. A link whose target +is in another of her repos under `~/src` resolves to nothing (R4). `appears_in` and `relationships` +values are read as links when they are wikilinks, and as names otherwise. -### 1.4 Promotion +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 +pass a rule. Refusing her latest commit would only stop the agents seeing her newest work. -Promotion moves a playground file or a draft universe into canon. Only Melanie does it, from a -Console button. The verb `library.promote {from, to}` is human only (section 2.4). It moves the -files in one commit and runs the validator on the result. If the promoted file still links into the -playground, S2 refuses the promotion and names the link. +### 1.4 Hybrid crossover (after the slice) -### 1.5 Hybrid crossover +A hybrid is a business file universe entry with `kind: "hybrid"`, `sources` (two or more non-hybrid +universes), `books` (its own folders), `constraints` and `notes`. Agents can't change `projects` (M9), so +a hybrid exists only by a human edit of the business file. In a hybrid's scope, lookups return its own +files plus the sources' canon, each labelled with its source universe. Constraint kinds checked in code: -- Only the human verb `library.crossover.create {id, name, sources, constraints}` creates a hybrid. - The Console dialog lists both sources and the constraints, and Melanie types the new universe's - name to confirm. That is the deliberate action. Agents have no tool that writes anything (D3), so - no agent can create one. -- The verb writes `universes//universe.json` with `kind: "hybrid"`, `sources`, `constraints` and - `sanctioned: {by: , at}` in one commit. -- Reading both sources: in a hybrid's scope, lookups return the hybrid's own files plus the sources' - canon files. Each result is labelled with its source universe. A hybrid can override a source - entity with `extends:`. -- The code checks two constraint kinds: - - `{"exclude": ""}`. Lookups in the hybrid never return it, and a - hybrid file that links it fails S7. - - `{"pin": {"entity": path, "field": name, "value": v}}`. A hybrid override may not change that - front matter field. -- Constraints written in plain words go in `universe.json` `notes`. Code can't check those. The - consistency role reads them and cites them in its findings. +- `{"exclude": ""}`. Lookups in the hybrid never return it. +- `{"pin": {"entity": path, "field": name, "value": v}}`. Reported (R-class) when a hybrid file changes it. + +Plain-word constraints go in `notes`. The continuity role reads them and cites them in its findings. +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. ## 2. What exists and how Case 2 uses it -### 2.1 Project record repos (`packages/project`) +### 2.1 The business file and project record repos -- Location and safety: `projectRepo` (`paths.mjs:34-48`) puts the repo in - `/projects//` and refuses overlap with a code root and symlinked - ancestors. Case 2 reuses it as is. -- Commit path: `commitFile` (`index.mjs:195-234`) handles the lock, `inspectGit`, `requireClean`, - layout check, publish, commit and rollback. Case 2 refactors it to take the layout check as a - parameter, because it calls `checkProject` directly at `index.mjs:209`. Then the library writer - calls it with `checkLibrary`. The project tests must stay green unchanged. -- Human writer: `writeHumanFile` (`index.mjs:248-261`) is the model for `writeLibraryFile`. The - differences are the path rule, creating new files (it refuses a missing file at `:256`), and - the commit that follows. -- Layout: `checkLayout` (`index.mjs:31-84`) flags any top-level path outside `DIRECTORIES` - (`:12`, `:36`). A library can't pass it, so `kind` in `library.json` selects the library checker. - `initProject` (`:97-156`) is the model for `initLibrary`. -- Identity: commits use `identity(name)` (`git.mjs:55-56`) with the business human's id, as the - human writer does at `index.mjs:254`. -- Remotes: `inspectGit` refuses any `remote.*` key (`git.mjs:44`), and `SAFE` sets - `protocol.allow=never` (`git.mjs:8`). Under D5 both stay. Sync passes the URL as an argument and - adds `-c protocol.https.allow=always` only to its own fetch and push (section 7.2). -- Business file: project entries accept only `root` and `repo` (`packages/business/src/business.mjs:186`), - and `root` is required (`:187`). C2-04 adds `kind: "library"` and `remote`, and lets a library - project leave out `root`. `projectRepo`'s overlap loop (`paths.mjs:41-44`) then skips projects - without a root. The launcher clones every project root into each agent workspace - (`packages/cli/src/launcher.mjs:353`), so a project with no root gives agents no clone. +- The business file accepts only `root` and `repo` per project (`packages/business/src/business.mjs:186`) + and requires `root` (`:187`). C2-04 adds the `kind: "library"` entry from 1.1, which has no `root`. + Several places assume every project has a root. Each must skip a library project, with its own test + (B11): + - `ensureCheckouts` clones every entry's root (`packages/seat/src/checkout.mjs:310`, `:319`). The + launcher passes it every project (`packages/cli/src/launcher.mjs:353`). + - `projectFilePath(entry.root)` (`packages/business/src/project.mjs:9-10`) is called at + `packages/business/src/cli.mjs:72`, `:78`, `:118`, `packages/business/src/resolve.mjs:44` and + `packages/business/src/instance-settings.mjs:142`. + - `projectRepo`'s overlap loop calls `absolute(project.root)` (`packages/project/src/paths.mjs:41-44`). -### 2.2 ProseMirror editor (`packages/webui/editor`) + With those skips, agents get no clone of her repo. C2-04's launcher test proves it. +- Mirror location: `projectRepo` (`paths.mjs:34-48`) puts a project repo in + `/projects//` and refuses symlinked ancestors. The bare mirror uses the + same path rule. +- Git: `git()` passes only `gitEnv()`, the ceiling directory and the identity (`git.mjs:10-12`). `SAFE` + 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. -- 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`). -- The Console opens the visual editor only if `roundTrip(text) === text`, and opens Source otherwise +### 2.2 ProseMirror editor (`packages/webui/editor`), after the slice + +- 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`). +- The Console opens the visual editor only if `roundTrip(text) === text`, otherwise Source (`packages/webui/src/public/bus.js:919-920`). The commonmark rules (`model.mjs:119-120`) have no - wikilink rule. Whether the serializer escapes `[[` was not measured. Row C2-11's first test - measures it. Either way, a wikilink node with an exact serializer is needed for wikilinks to show - as links. -- Load and save: `GET/POST /api/project-file` (`bus.js:907`, `:969-974`) go to `serve.mjs:545-547` - and `591-593`, then bus verbs `project.file` and `project.file.save` (`packages/webui/src/cli.mjs:75-76`), - then `broker.mjs:1434-1458`. The path allowlist is in two places: `documentPath` (`paths.mjs:14-15`) - and `DOC_PATH` (`bus.js:885`). The listing only lists `spec/north-star.md` and `docs/` (`read.mjs:54`). -- Case 2 adds library verbs next to these instead of widening `documentPath`. That keeps the record - repo rules for every existing project unchanged. + wikilink rule. I didn't measure whether the serializer escapes `[[`. C2-11a's first test does. +- Load and save: `GET/POST /api/project-file` (`bus.js:907`, `:974`), then + `packages/webui/src/serve.mjs:545-547` and `:591-593`, then `project.file` and `project.file.save` + (`packages/webui/src/cli.mjs:75-76`), then `packages/bus/src/broker.mjs:1434-1458`. The allowlist is in + `documentPath` (`paths.mjs:14-15`) and `DOC_PATH` (`bus.js:885`). Case 2 adds library verbs next to + these and leaves `documentPath` as it is. ### 2.3 Roles (`roles/*.json`, `packages/business/src/role.mjs`) -- A role file is validated by `validateRoleDocument` (`role.mjs:138-169`). Its keys are closed - (`:19-20`), `tools` must be non-empty and drawn from `TOOLS` (`:23-28`, - `packages/business/src/vocabulary.mjs:92-97`), and `messaging` limits who it talks to (`:55-66`). -- Tools reach the harness through `resolve.mjs:60-64`, then `bundle.mjs:247-256` (`policy.json`), - then `gate.mjs:130-138`, which refuses any built-in not in the policy. -- `roles/researcher.json` is a version 1 role with `bash`. It isn't reused. The Case 2 research role - is a new file. +- `validateRoleDocument` (`role.mjs:138`) checks a role file. Its keys are closed (`:19-20`), `tools` + must be non-empty and drawn from `TOOLS` (`:23-28`, `packages/business/src/vocabulary.mjs:92-97`), and + `messaging` limits who it talks to (`:55-66`). A role with no `messaging` key may message anyone + (`:50-51`). +- Tools reach the harness through `packages/business/src/resolve.mjs:60-64`, then + `packages/harness/src/bundle.mjs:247-256` (`policy.json`), then `gate.mjs:130-138`, which refuses any + built-in not in the policy. +- The harness's own web tools are on only when the organization's search source is `harness-builtin` + and the role grants `web` (`bundle.mjs:28-35`, `gate.mjs:28-33`). Those fetches never reach the broker + (5.2). ### 2.4 Tool gate and typed tools (`packages/harness/src`) - `decide()` (`gate.mjs:130-167`) allows a typed tool by name (`:134`), allows a built-in only if the - policy holds it (`:137-138`), and keeps every file path inside the workspace (`:146-156`). The - Claude Code hook turns a refusal into exit 2 (`claude-gate.mjs:13`). -- Typed tools are generated per session from held actions (`tools.mjs:91-261`). `write_record` - exists only for `record.write` holders (`:211-220`). `send_message` exists for `message.send` - holders (`:95-102`). -- Case 2 adds the lookup tools (section 3) to `typedTools` when the business has a library project. - Creative roles never hold `record.write`, so they never get `write_record`. + policy holds it (`:137-138`), and keeps file paths inside the workspace (`:146-156`). The Claude Code + hook turns a refusal into exit 2 (`claude-gate.mjs:13`). +- `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). -### 2.5 Threads and messages (`packages/bus`) +### 2.5 Threads, messages, conversations and Discord -- `thread.create` picks the single instance whose definition is `assistant` and refuses more than - one (`broker.mjs:2331-2334`). It stores that instance on the thread (`:2336-2347`). The same rule - appears in `#threaded` (`:290-291`), in `vocabulary.mjs:131`, and as a second copy in - `launcher.mjs:135` and `:236-238`. -- `thread.send` stores one message to `t.instance` and routes it (`broker.mjs:2374-2399`, `#route` - `:2541-2544`). Triggers require every threaded message to be to or from the thread's instance - (`schema.sql:296-299`) and deliver only to the thread's holder (`:300-306`). `threads` can't be - updated (`schema.sql:310`). -- The runner sends the model's final text as `message.send` RESULT to the sender - (`packages/harness/src/runner.mjs:446-450`). A refusal is logged and the message is still marked - read (`:451-456`), so the answer is lost. +- `thread.create` picks the single instance whose definition is `assistant` and refuses more than one + (`broker.mjs:2331-2334`). It stores that instance on the thread (`:2336-2347`). The same rule is in + `#threaded` (`:290-291`), `vocabulary.mjs:131`, and a second copy in `launcher.mjs:135` and `:236-238`. + This is code, not schema. +- `thread.send` stores one message to `t.instance` and routes it (`broker.mjs:2374-2399`). Triggers + require every threaded message to be to or from the thread's instance + (`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: ` for a failed one (`:449`). A refusal is logged and the message is still + marked read (`:451-456`), so the answer is lost. - 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 - therefore means one session per agent, with nothing new to build. + means one session per agent. +- The Console's conversation view proxies `/api/conversations` and `/api/conversation` to the control + board (`packages/webui/src/serve.mjs:559-569`), which answers from `reader.catalogue()` and + `reader.open()` (`packages/control-board/src/serve.mjs:210-219`). The reader lists session files + (`packages/conversation/src/reader.mjs:225-240`) and emits the model's raw text blocks + (`packages/conversation/src/events.mjs:58`, `:82`). A refused answer stays in the session file, so this + view would show it (5.1 M7). +- The Discord connector (`packages/discord`) binds one seat's conversation to listed channels. It builds + its own engine in `packages/discord/src/cli.mjs:210-212` from `engine-pi.mjs`. Each turn calls + `engine.prompt` (`connector.mjs:381`) and posts the result through `deliver` (`:386-387`, + `rest.createMessage` at `:106`). The broker never sees that text. The README names the engine as the + module to swap (`packages/discord/README.md:13-14`). Authorization is the binding's `users` list + (`connector.mjs:84`, `:443`). ## 3. Agent lookup tools -These are broker verbs with typed tools. They exist for every agent session in a business that has -a library project. Each reads the library at its current HEAD commit and returns that commit. +Broker verbs with typed tools. Every agent session in a business with a library project gets them. Each +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, id, name, aliases, scope, universe, book}` | +| `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_search` | `universe`, `text`, `book?`, `scope?` | up to 50 `{path, line, excerpt, scope}` | -| `library_manuscript` | `universe`, `book`, `chapter?`, `from?`, `to?` | numbered lines of one chapter, at most 400 per call | -| `library_universe` | `universe` | `universe.json`, its books, and for a hybrid the sources, constraints and notes | +| `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 | -- Universe isolation is in code. `universe` is required, and results never leave it. The only - exception is a hybrid's declared sources, minus exclusions. -- Scope is labelled on every result, from the path (D2). `scope: "playground"` is opt-in. +- Universe isolation is in code. `universe` is required, and results never leave it, except for a + hybrid's declared sources minus exclusions. +- Every result carries `scope` from the path (D2). Playground is opt-in. Unmapped files never appear. - Replies stay under `REPLY_CAP` (`tools.mjs:271-280`) by paging. They are never cut. -- The index (entities, aliases, links and backlinks) is built per commit and cached in memory by - commit id. A library of a few thousand files rebuilds in well under a second. This is an estimate, - and C2-05a's gate measures it. +- The index (types, names, links and backlinks) is built per commit and cached in memory by commit id. + 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. -## 4. Creative roles +## 4. Creative roles, Bean and Chet -Five new role definitions. Each is a version 2 role file plus a `.md` contract. +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 entities and research | `read` | `finding`, `question` | -| `continuity` | Consistency across books, timeline and hybrid constraints | `read` | `finding`, `question` | +| `fact-checker` | Text against canon and her research notes | `read` | `finding`, `question` | +| `continuity` | Consistency across books, timeline and her tracker | `read` | `finding`, `question` | -All five have `authority: {withinRole: ["message.send"], crossRole: []}`, `credentials: []`, and +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 `research`. +`network` is `none`, except `api-only` for `companion` and `research`. -`read` is there because `role.mjs:24` requires a non-empty tool list. The workspace has no library -clone (2.1), so `read` reaches nothing useful. All library reads go through the cited lookup tools. +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`. Instance entries +already take agent-layer `vars` (`packages/business/src/business.mjs:89`), and every var key is declared +once in `REGISTRY` (`packages/business/src/vars.mjs:102`). C2-08 declares two keys, agent layer only: + +- `persona.name`, a string of at most 40 characters, the name she sees ("Bean", "Chet"). +- `persona.text`, a string of at most 8,000 characters, the personality prompt from Hermes. + +The bundle's `prompt()` (`packages/harness/src/bundle.mjs:175-177`) puts the role contract first. The +persona text goes after the contract, under a heading that says the contract wins on any conflict. That +order is a hint to the model. The enforcement is the broker guard in 5.2, which doesn't read the persona +at all, so no persona text can lift a guard. A persona var is refused on a non-creative definition. + +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 +discover it (10 step 7). + +The five specialists stay available as further instances. Whether the slice starts with Bean and Chet +only or with all seven is her choice through Jason (10 step 7). Each running agent session measured 300 +to 390 MB on dragon-lin (mos-1000), and a thread's session stops after 900 idle seconds +(`THREAD_IDLE_SECONDS`, `business.mjs:20`), so memory depends on how many she talks to at once. + +`read` is there because `role.mjs:24` requires a non-empty tool list. The workspace has no clone of her +repo (2.1), so `read` reaches nothing of hers. All library reads go through the lookup tools. New constants in `vocabulary.mjs`, each changed only by a reviewed commit: -- `THREAD_DEFINITIONS = ["assistant", "ideas", "research", "proofreader", "fact-checker", "continuity"]`. - This replaces the single `THREAD_DEFINITION` wherever it means "may hold a thread". The definition - the chief of staff may set stays `assistant` (`tools.mjs:186`). -- `CREATIVE_DEFINITIONS`, the five above. +- `THREAD_DEFINITIONS = ["assistant", "companion", "ideas", "research", "proofreader", "fact-checker", "continuity"]`. + This replaces the single `THREAD_DEFINITION` wherever it means "may hold a thread". The definition the + chief of staff may set stays `assistant` (`tools.mjs:186`). +- `END_PROTECTED` (`vocabulary.mjs:135`) becomes `THREAD_DEFINITIONS`. These agents hold Melanie's + threads, so only the human may end them (N2). +- `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, or whose authority holds anything but - `message.send`. + 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). ## 5. Guardrails enforced in code @@ -268,193 +418,355 @@ New constants in `vocabulary.mjs`, each changed only by a reviewed commit: | Mechanism | What it refuses | Test | |---|---|---| -| M1 role validator (`role.mjs` with `CREATIVE_TOOLS`) | A creative role file with `write`, `edit` or `bash`, or any authority but `message.send` | C2-08: load each rejected variant, assert the refusal text | -| 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, for pi and claude-code | -| M3 typed tool set (`tools.mjs`) | No write tool exists. The tool list is exact | C2-05b: snapshot of tool names for each creative role | -| M4 library verbs are human only | `library.file.save`, `library.promote`, `library.crossover.create` and `library.sync` from an agent session (`human-required`, as at `broker.mjs:1434-1435`) | C2-04: each verb from an agent session refuses and writes nothing (HEAD unchanged) | -| M5 cited-reply guard (5.2) | A reply that isn't a list of short cited items: free text, an item over 400 characters, or a `correction` whose `to` differs from its `from` by more than max(12, 25% of `from`) characters of edit distance | C2-06: each case refused, boundary values accepted | +| 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 | +| 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 | +| 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` | C2-04: a write that changes a playground prefix refuses and the file is unchanged | -Limit: M5 caps an item at 400 characters and holds proofreading to small edits. A one-sentence -suggestion can still fit inside an `idea` item. Code can't tell a sentence of story from a sentence -of advice. Closing that gap would need a classifier, which this design doesn't propose. Everything -that lands in the library is hers: M2-M4 make an agent write there impossible, not just disallowed. +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. ### 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 business with a library project, whether it comes from the runner's final answer or from -`send_message`. Role choice doesn't matter, so a misconfigured role can't skip it. Citation checks -need git, so they run before the insert, outside the SQL transaction, the same way -`#requestProject` does it (`broker.mjs:1381-1430`). +The cited-reply 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: ```json -{"items": [{"kind": "finding", "text": "Mara's eyes are grey in ch. 3 but green on her entity page.", - "cites": [{"path": "universes/hollow-sea/books/tides/manuscript/03-salt.md", "commit": "<40 hex>", "lines": [41, 41], "quote": "her grey eyes"}, - {"path": "universes/hollow-sea/characters/mara-vell.md", "commit": "<40 hex>", "lines": [9, 9], "quote": "green eyes"}]}]} +{"items": [{"kind": "finding", "text": "Mara's eyes are grey in chapter 3 but green on her character page.", + "cites": [{"path": "//03.md", "commit": "<40 hex>", "lines": [41, 41], "quote": "her grey eyes"}, + {"path": "//Mara.md", "commit": "<40 hex>", "lines": [9, 9], "quote": "green eyes"}]}]} ``` | Check | Refusal code | |---|---| -| Body parses as the schema, with 1 to 20 items | `cited-reply-required` | -| Item kind is allowed for the sender's definition (`CREATIVE_KINDS`) | `reply-kind-not-allowed` | +| 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` | -| Path exists at `commit`, `commit` is reachable from library HEAD, lines are in range, and `quote` occurs in those lines after whitespace normalization | `citation-invalid` | +| 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 in a hybrid plus its sources | `citation-cross-universe` | -| A `url` cite only from `research`, only for a URL this run fetched (the broker's web trail, `tools.mjs:197-198`), and the quote occurs in the fetched text | `citation-unfetched` | +| All library cites in an item 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` | -Runner change (C2-07): when `message.send` refuses one of these codes, the runner starts a new turn -in the same session with the refusal code and reason. It does this at most twice. Then it sends the -fixed body `{"items": [], "notice": "no-cited-answer"}`, which the guard accepts as the one exception. -Today the answer would just be dropped (`runner.mjs:451-456`). +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 +ends. It doesn't survive a bus restart. A cite made before a restart then fails `citation-unfetched`, the +runner retries, and the agent fetches again. Fetches through the harness's own web tools never reach the +broker, so `webPlan` (`bundle.mjs:28-35`) refuses the `harness-builtin` source for a creative definition +(`web-source-not-allowed`). -Tests (C2-06): one passing and one refused case per row of the table. The fixture is a library repo -built by the test. Each refusal must leave the `messages` count unchanged. C2-07 adds a runner test -with a fake harness that answers free text, then a valid reply. It asserts one stored message. +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 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`). + +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. ### 5.3 Tool use is guarded -- Built-ins: M1 and M2 above. The gate is unchanged. The test proves creative policies hit it. -- Typed tools: M3. The broker re-checks every call (`tools.mjs:6-7`). The lookup verbs refuse a - session from a business without a library (`library-not-configured`) and a missing `universe`. -- Messaging: `messaging.sendTo: ["human"]` is enforced by `#checkMessaging` on `message.send` (`broker.mjs:1867`). C2-08 +- Built-ins: M1 and M2. The gate is unchanged. The test proves creative policies hit it. +- Harness web tools: `webPlan` refuses `harness-builtin` for creative definitions. C2-08 tests it. +- Typed tools: M3 and M6. The broker re-checks every call (`tools.mjs:6-7`). The lookup verbs refuse a + session in a business without a library (`library-not-configured`) and a missing `universe`. +- Messaging: `#checkMessaging` on `message.send` (`broker.mjs:1867`) enforces `sendTo: ["human"]`. C2-08 tests that a creative instance sending to another instance is refused. -- `research` web use goes through the broker's web tools (`tools.mjs:199-206`), which already check - the grant on every call. +- `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. ## 6. One chat with N agents and @tag addressing -D4: the chat is a project. Each creative instance holds one thread in it. Today's tables carry this -without a migration, because each thread still has exactly one instance, so `schema.sql:296-306` -hold as written. +D4: the chat is a project. Each creative instance holds one thread in it. Each thread still has exactly +one instance, so `schema.sql:296-306` hold as written and no migration is needed. Webby checked this +against the schema in webby-0001. -- `thread.create` takes `instance`. It must be a thread definition instance (`THREAD_DEFINITIONS`). - If `instance` is left out and exactly one exists, today's behaviour holds (`broker.mjs:2331-2334`). -- New verb `room.open {project}`. Human only. It creates the missing thread for each thread instance - in the project, named after the instance. -- New verb `room.send {project, body}`. Human only. Leading `@name` tokens, before the first other - word, select the recipients. Names resolve through `#named` (`broker.mjs:642-650`) or instance - keys. With no tag, every member gets the message. An unknown tag refuses `unknown-addressee` and - inserts nothing. The broker inserts one message per target thread in one transaction, the same - way `thread.send` does (`:2383-2397`). Each message keeps the full body, tags included. -- `room.read {project, after?}` returns the merged timeline of the project's threads, ordered by - message sequence. -- Launch: the launcher's copy of the thread rule (`launcher.mjs:135`, `:236-238`) reads +C2-09a, in the slice: + +- `#threaded` (`broker.mjs:290-291`) and the launcher (`launcher.mjs:135`, `:236-238`) read `THREAD_DEFINITIONS`. A run starts per thread when that thread has waiting messages, as the assistant's does now. -- Limit: an agent sees only its own thread, so it doesn't see what the other agents answered. - Melanie can quote one agent's item to another. Showing agents each other's replies is row C2-16, - not part of the 10-27 slice. +- `thread.create` takes `instance`, which must be a `THREAD_DEFINITIONS` instance. With no `instance` and + exactly one such instance, today's behaviour holds (`broker.mjs:2331-2334`). With more than one, it + still refuses `thread-instance-ambiguous`. +- The Console's new-thread control lists the thread instances by `persona.name` or instance name and + passes the one she picks. A thread with Bean and a thread with Chet sit side by side, much as her + Hermes channels do today (mos-1000). -Tests (C2-09): `@continuity x` makes exactly one message, in continuity's thread. No tag makes N. -`@nobody x` refuses with the message count unchanged. A tag after the first word is plain text. -Two thread instances with no `instance` argument still refuse `thread-instance-ambiguous`. +C2-09b, after the slice: -## 7. Obsidian, Console WYSIWYG and sync +- `room.open {project}`. Human only. It creates a thread for each thread instance in the project, named + after the instance. A same-name thread that belongs to the same instance is reused. One that belongs to + another instance makes `room.open` refuse `room-thread-name-taken` and create nothing, because names + are unique per project (`schema.sql:249`). +- `room.send {project, body}`. Human only. Leading `@name` tokens, before the first other word, pick the + recipients. Names resolve through `#named` (`broker.mjs:642-650`) or instance keys. With no tag, every + member gets the message. An unknown tag refuses `unknown-addressee` and inserts nothing. The broker + inserts one message per target thread in one transaction, as `thread.send` does (`:2383-2397`). +- `room.read {project, after?}` returns the merged timeline of the project's threads. +- A Discord channel bound to a room uses `room.send`, so @tags work in Discord too. +- An agent sees only its own thread. Showing agents each other's replies is C2-16. + +Tests. C2-09a: a creative instance's thread starts a run, `thread.create` with `instance` binds it, and +two thread instances with no `instance` still refuse. C2-09b: `@continuity x` makes exactly one message, +in continuity's thread. No tag makes N. `@nobody x` refuses with the message count unchanged. A tag after +the first word is plain text. `room.open` against a same-name thread on another instance refuses. + +## 7. Obsidian, Console, Discord and sync ### 7.1 Obsidian -- The vault is a clone of the Gitea remote on Melanie's machine. The community plugin Obsidian Git - does pull, commit and push on a timer. -- `initLibrary` commits `.obsidian/app.json` with `newLinkFormat: "absolute"` and wikilinks on. - It also commits `.obsidian/graph.json` with colour groups `path:universes` and `path:playground`, - so playground nodes always look different in the graph. Other `.obsidian/` files are her machine's - business, and a committed `.gitignore` entry keeps workspace files out. -- The graph view and backlinks are Obsidian's own. No code builds them. The Console shows backlinks - per entity from the lookup index (`links_in`). +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. -### 7.2 Remote sync (bus side) +### 7.2 Sync (bus side) -Under the library lock, `library.sync` and every library write do these steps: +One function, `syncCommand(kind, url)`, builds every fetch (and later push). Its argv is `SAFE` +(`git.mjs:7-9`), then exactly `-c protocol.https.allow=always`, then `-C `, then the command with +the URL as an argument. Its env is `gitEnv()` plus `GIT_ASKPASS` naming a helper script outside the +mirror, which reads the slot file and prints the token. The token is never in the URL, the mirror's +config, argv, logs or replies (CONSTITUTION gate 13). The URL must be `https://`. Tests reach a local bare +repo only through a module parameter that replaces `syncCommand`. No business file field or bus verb can +reach that parameter. -1. `git fetch main:refs/remotes/library/main`. The token comes from the secrets slot - through a `GIT_ASKPASS` helper. It is never in the URL, `.git/config`, logs or replies (CONSTITUTION - gate 13). `protocol.https.allow=always` is added for this command only. -2. If remote main is ahead: run `checkLibrary` on it. If it passes, fast-forward. If it fails, stay - on the last valid commit and record the problems. The Console shows them with file and line, - and lookups keep serving the last valid commit. This is fail-closed: an Obsidian edit that - breaks S2 doesn't reach the agents. -3. Write and commit (the `commitFile` path, with the `stale-base` guard from `index.mjs:257-258`). -4. Push. If the remote moved in between: undo the local commit, go back to step 1, then retry once. - If the file changed meanwhile, the guard refuses `stale-base`. -5. If the push fails for any other reason, the commit stays local, marked `unpushed`, and the - Console shows it. The next sync pushes it. If the remote moved too, sync refuses - `library-diverged` and lists the files. No automatic merge happens in the MVP. +The slice (C2-03a) is fetch only. Under the library lock, `library.sync` runs: -`library.sync` also runs on a 60-second timer in the bus and before every lookup that finds the -last sync older than 60 seconds. +1. Fetch the configured branch into `refs/remotes/library/`. +2. If it fast-forwards from the served commit, move the served ref `refs/heads/served` to it and rebuild + the index and the R-rule problems. +3. If it doesn't fast-forward, keep serving the old commit and record `library-diverged`. A force-push to + her repo would be unusual, so the bus waits for a human `library.sync --accept ` instead of + guessing. +4. If the fetch fails, keep serving and record the error and its time. -### 7.3 Console WYSIWYG +The sync state (served commit, problems, last fetch, last error) is a JSON file inside the mirror, +written atomically, so it survives a bus restart. The human verb `library.status` reads it. +`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. -- Library pages show a tree of universes, books, entity types, manuscript and playground. Playground - has its own section and colour. They open files in the existing editor through `library.file` and - `library.file.save`, with `base` as today. -- New entity: a type and id form creates the file from a front-matter template. -- Promote and Create crossover buttons (1.4, 1.5). +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). + +Tests (C2-03a): `syncCommand` argv equals `SAFE` plus exactly one `protocol.https.allow=always` and no +other `protocol.*` key. Its env holds `GIT_ASKPASS` pointing outside the mirror, and the helper prints +the slot file's content. A search of argv, the mirror's config and the run log finds no token. Against +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) + +- 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. - Wikilinks render as links in the editor (C2-11a). Clicking one opens the target. +- Promote (C2-19) and crossover (C2-15) buttons. -### 7.4 User and agent sync +### 7.4 Console chat (C2-10b) -- Agents read through the broker at the library's HEAD after sync. They never have a stale private - copy (2.1). -- Every reply cites a commit. The Console marks a citation "changed since" when the cited file's last - commit is newer than the cited one. -- The window is the sync interval. An Obsidian push becomes visible to agents within about 60 - seconds, and a Console save at once. +- 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"). +- 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. + +### 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. + +### 7.6 Discord relay (C2-18, after the slice, optional) + +Jason's Q180 ruling takes Discord out of the slice. Hermes keeps her Discord bots running until Bean +and Chet are live in Mosaic. If she later wants Bean and Chet reachable from Discord, the connector +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 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. +- 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`. +- It is a new Discord application and bot. It never uses Hermes's two bots or their token slots. +- Human authority. `thread.send` is human only, and the broker accepts the human through the human CLI's + path-bound proof today. A Discord message from a listed user becoming human input is a new way through a + security boundary. It needs its own Jason ruling before C2-18 is built. + +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 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. +- Each `library.status` change posts exactly one line. ## 8. Row list -Owners follow Q177 and Q178. Opus 5.5 takes hard design and builds. Haiku 5.5 at high effort takes -mid work as Sage-dispatched subagents (lead decision 129). Codex gpt-6.1-sol (Rocko) takes parallel -builds. gpt-6-luna takes low-level work. Every row gets an independent review by Filbert or Dewey, -and its gate is its package's test files run singly plus the full gate before landing. Chains follow -Q177 merge-chaining: a row builds on its parent's candidate. +Owners follow Q177 and Q178. Opus 5.5 takes hard design and builds. Haiku 5.5 at high effort takes mid +work as Sage-dispatched subagents (lead decision 129). Codex gpt-6.1-sol (Rocko) takes parallel builds. +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. -| Row | Scope | Builds on | Owner | Test gate | -|---|---|---|---|---| -| C2-01 | `packages/library`: layout constants, front matter subset parser, wikilink parser, `checkLibrary(repo, commit)` with S1-S8 | none | Opus (Gosalyn) | `packages/library/tests/validate.test.mjs`: one pass and one refusal per S-rule, parser refusal with line number | -| C2-02 | `initLibrary`, `writeLibraryFile` (create, update with `base`), `promoteLibrary`, `createCrossover`. Refactor `commitFile` to take a check function | C2-01 | Sol (Rocko) | library writer tests, plus `packages/project/tests` unchanged and green | -| C2-03 | Remote sync (7.2): askpass from the secrets slot, fetch, validate, fast-forward, push, race retry, `unpushed`, `library-diverged` | C2-02 | Opus | tests against a local bare repo as the remote (file protocol allowed by a test-only argument). A grep test that the token never appears in argv, config or logs | -| C2-04 | Business file `kind: "library"`, `remote`, optional `root`. Human-only bus verbs `library.list`, `library.file`, `library.file.save`, `library.promote`, `library.crossover.create`, `library.sync`. `projectRepo` overlap loop skips no-root projects | C2-02 | Sol (Rocko) | `packages/business` tests for the file rules. Broker tests: each verb from an agent session refuses with HEAD unchanged (M4) | -| C2-05a | Lookup index and queries in `packages/library`: find, get, search, manuscript, universe, hybrid sources and exclusions, universe isolation | C2-01 | Haiku high | index tests: isolation, hybrid exclusion, scope labels, paging under `REPLY_CAP`, rebuild time on a 3000-file fixture | -| C2-05b | Broker lookup verbs and typed tools in `tools.mjs`. Sync-before-read if older than 60 s | C2-05a, C2-04 | Sol (Rocko) | broker verb tests. Typed tool name snapshot per creative role (M3) | -| C2-06 | Cited-reply guard (5.2) in `packages/library` (pure, given a blob reader) and the broker's async pre-check on agent-to-human `message.send` | C2-05a | Opus | one pass and one refusal per check in 5.1 M5 and 5.2. `messages` count unchanged on refusal | -| C2-07 | Runner: re-turn on a guard refusal (max 2), then the fixed notice | C2-06 | Haiku high | runner test with a fake harness: free text, then valid, gives one stored message | -| C2-08 | Five role files and contracts. `THREAD_DEFINITIONS`, `CREATIVE_DEFINITIONS`, `CREATIVE_KINDS`, `CREATIVE_TOOLS`. Role validator check (M1). Gate tests (M2). Messaging test | none | Haiku high | role tests, gate tests, `scripts/test-config.sh` | -| C2-09 | Broker `#threaded` and launcher read `THREAD_DEFINITIONS`. `thread.create` takes `instance`. `room.open`, `room.send` with @tags, `room.read` | C2-08 | Sol (Rocko) | broker tests in section 6. Launcher test: a creative instance needs a thread | -| C2-10 | Console room view: merged timeline, composer with @name completion, cited items rendered with scope badges and "changed since" | C2-09, C2-06 | Haiku high (Dewey reviews) | webui tests for rendering and @tag parse parity with the broker | -| C2-11a | Editor wikilink node: markdown-it rule and an exact serializer. First measure whether `[[` round-trips today | none | Sol (Rocko) | `roundTrip` tests: plain, labelled, inside a list, next to front matter, escaped brackets | -| C2-11b | Console library pages: tree, open and save, new entity, promote, crossover dialog with typed confirmation, sync status and validation problems | C2-04, C2-11a | Haiku high (Dewey reviews) | webui tests. Save refuses `stale-base` in the UI | -| C2-12 | Committed `.obsidian/app.json`, `graph.json` and `.gitignore` in `initLibrary`. A five-step setup sheet for Melanie (clone, Obsidian Git, settings) | C2-02 | Luna | init test asserts the files and settings | -| C2-13 | Melanie's instance: business file (library project, five creative instances, human id), Gitea repo, token in the slot. Needs T1 row 1. Instance start is a Mos or Jason go | T1.1, C2-04 | Opus (Gosalyn), go by Mos | precheck script: config validates, `library.sync` round-trips one commit to Gitea | -| C2-14 | End-to-end acceptance on a scratch data root: create a universe and an entity, edit in the Console, see the commit in a clone, ask `@continuity`, get a reply whose citations verify, playground citation refused as a finding | all above | Opus | one script, run by the reviewer too | -| C2-15 | Hybrid constraint kinds beyond `exclude` and `pin` | C2-05a | later | | -| C2-16 | Agents see other agents' replies in a room | C2-09 | later | | -| C2-17 | Automatic replay of unpushed commits on divergence | C2-03 | later | | +| Row | S | Scope | After | Owner | Test gate | +|---|---|---|---|---|---| +| C2-01 | S | `packages/library`: mapping schema, scope rule, front matter reader, link resolver (`vaultPrefix`, relative, shortest path), R1-R7 reporting, `renderItems`, and `library survey` | none | Opus (Gosalyn) | one case per R-rule and per resolver step, a link into another repo unresolved, `appears_in` wikilinks indexed. `renderItems` snapshot per kind and notice. The survey prints names, counts, key sets and link-form counts, and a marker string in a fixture body never appears in its output | +| 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 (Rocko) | 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-08 | S | Six role files and contracts, the new constants, M1, M6, the `webPlan` refusal, gate tests (M2), messaging test. `persona.name` and `persona.text` in `REGISTRY`, and the persona block after the contract in `prompt()` | none | Haiku high | 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, over their caps and on a non-creative definition. A prompt snapshot with the contract before the persona | +| 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-12 | S | The setup sheet for Melanie (7.1, and what changes for Bean and Chet in 4) and the survey run sheet for `` | C2-01 | Luna | doc only, reviewed against the survey's real flags | +| 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 `persona.name` and `persona.text` into a 0600 file for the business file. Prints names, byte counts and sha256 only | 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, output holds no persona text | +| C2-13 | S | Install script and precheck for `` as ``: bootstrap, release activate, business file with the mapping, `bean` and `chet` with their persona vars, any specialists she picks, her human id, token slot path. Jason or his delegate runs it (10) | C2-04, C2-03a, C2-20 | Opus (Gosalyn), Jason's go | precheck: config validates, `` has no sudo, `library.sync` fetches her repo read-only, `library.status` lists R-problems, no Discord binding exists. 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-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-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 | | Parallel waves: - Start now: C2-01, C2-08, C2-11a. -- On C2-01's candidate: C2-02, C2-05a, C2-06. C2-09 runs on C2-08's. -- Next: C2-03, C2-04, C2-07, C2-12, then C2-05b and C2-10. -- Last: C2-11b, then C2-13 and C2-14. +- 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-13. +- Last: C2-14. The write rows and rooms follow when a seat is free. -Dependencies on other tracks: T1 row 1 (several instances per host) blocks C2-13. T1 row 2 (SSO, -Q175) gates Melanie reaching the Console from her own machine. T1 row 7 owns compaction count and -clear context (C2.12, C2.13 in GAP-MAP). Case 2 adds nothing there. Until then, a new thread is a -fresh context (`session.mjs:59-64`). +Dependencies on other tracks: row 190 doesn't apply, since nothing else runs as ``. SSO (Q175, +#412) gates the Console from her machine, and the Console is now her channel, so the slice needs SSO +landed or Jason's ruling on an interim sign-in (10 step 5). T1 row 7 owns +compaction count and clear context (GAP-MAP C2.12, C2.13). Until then, a new thread is a fresh context +(`session.mjs:59-64`). -## 9. What can't be ready by 2026-10-27 +## 9. Where it runs, Bean and Chet's move, and what can't be ready by 2026-10-27 -Melanie can't use Case 2 on her own instance by 10-27 unless T1 row 1 lands by about 10-24, and she -can't reach the Console from her own machine unless SSO (Q175) lands too. Everything in C2-01 to -C2-12 can be built and reviewed in the week if the waves above start on 10-21. C2-15 to C2-17 won't -be ready. +Where it runs. The host is a variable, ``, and the instance runs as its own user ``, which +has no sudo. Mos's reading, which he is confirming with Jason, is her VM 10.1.1.54 with RAM added. The +design doesn't depend on the host. The bus, data root, mirror and runners live under ``. It needs +outbound HTTPS to git.mosaicstack.dev and the model providers. It needs nothing from her user, because +her writing reaches it through the Gitea remote she already uses. -The smallest slice that lets her start: C2-01, C2-02, C2-03, C2-04, C2-05a, C2-05b, C2-06, C2-07, -C2-08, C2-12 and C2-13. With those she writes in Obsidian on her own machine, with the graph view and -playground colours, synced through Gitea. She chats with each creative agent in its own thread, and -every answer is cited and checked. The room with @tags (C2-09, C2-10) and the Console library editor -(C2-11a, C2-11b) follow as soon as they land, because Obsidian covers writing until then. +Memory, from mos-1000. Her two Hermes services use about 1.8 GB together, and 1.2 GiB was free on a 3.8 +GiB VM. Each Mosaic agent session measured 300 to 390 MB on dragon-lin. With Hermes still running, +Bean and Chet in use at once, and the bus and Console, I estimate at least 2 GB more is needed, and 4 GB +more if she also talks to specialists at the same time. That is an estimate from those numbers, not a +measurement on her VM. C2-13's precheck prints free memory before the start. + +Isolation on that host. `` without sudo keeps the hub from touching her files. The reverse doesn't +hold: `melanie` has passwordless sudo (mos-1000), so anything that runs as her, and anyone who can ssh in +as her, can read the hub's data root and its token slot. That is the B7 residual for this host. It is +Jason's to accept (10 step 1). + +### 9.1 Moving Bean and Chet (C2-20) + +- What the row reads: the persona definition of each of the two Hermes profiles, default and chet, and + nothing else. Not the memory store (188 facts), not `state.db` or any session or message, not the four + per-channel prompts, not the bot token slots. It lists the per-channel prompts by name and size only, so + Jason can say whether they belong to the personality. +- Profile to name. I expect default to be Bean and chet to be Chet. The row confirms it from each + profile's own configured name, and prints the pairing for Jason before anything is written. If a profile + names neither, it stops and asks. +- How it reads. A script, run on `` by whoever Jason names, because the persona files sit in her + home directory and `` can't read them. It writes `persona.name` and `persona.text` to a 0600 file + that the business file entry for `bean` and `chet` takes, and prints names, byte counts and sha256 only. + No agent sees the persona text, and it never enters the repository. +- Hermes keeps running. The row changes nothing in the Hermes checkout, its profiles, its services or its + Discord bots. Bean and Chet in Mosaic are new agents with the same persona, not a move of the Hermes + ones. She uses both until she is happy, and stopping Hermes is her choice through Jason, after Bean and + Chet are live (10 step 9). +- Memory and history stay in Hermes unless she chooses otherwise through Jason. Bean and Chet in Mosaic + start without her 188 facts. Their knowledge of her work comes from the lookup tools. + +### 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. + +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 +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. + +## 10. Jason-gated steps for C2-13 + +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. + +1. The host: `` and ``. If it is her VM 10.1.1.54, the RAM to add (9) and acceptance of the + B7 residual from `melanie`'s sudo (9). +2. Running the install on `` as ``, by him or his delegate: `scripts/bootstrap.sh`, release + package and activate, the pinned harness, and which model accounts its agents use. Model use is spend, + so the accounts are his call. +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 `~/.config/mosaic-dev/secrets//` of ``, + mode 0600. He puts it there, or says who may, per the Q173 secrets rule. Agents never print it. +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. +6. The C2-20 persona import: who runs it on ``, his check of the printed profile-to-name pairing, + and whether the per-channel prompts count as personality. +7. Running `library survey` on `` 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. +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. +9. The instance start. Stopping Hermes comes later, and only when she chooses it.