gosalyn: Case 2 design r1 for #416 (T2), seat README and session line

Library record repo with canon/playground scope by path, human-only library
verbs, cited-reply guard, five creative roles, rooms with @tags without a
schema change, Obsidian through a Gitea remote, and rows C2-01..C2-17.
This commit is contained in:
2026-10-20 13:51:02 -05:00
parent 9ae8c73acb
commit 88222f3f3f
3 changed files with 472 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# Gosalyn
Worker seat for Mosaic Stack, MVP track T2 (Case 2, the creative writing hub, Vikunja #416).
- T3 thread: 7604438a-0ded-41c8-bd3f-26f57fdb5cd2
- Model: Opus 5.5 (Claude Code, T3)
- Launched by Mos (Jason's main agent, T3 38bc4c81-6270-4021-b00c-6a15c43b9b0e), who briefs the seat
- Repository lead and queue: Sage (T3 1ef1e4f8-3ead-4208-beca-38f9f1add079)
- Branch: gosalyn/case2-design, pushed only by this seat, landed on refactor by Sage
Work lives in `work/`. The Case 2 design is `work/case2/DESIGN.md`.
+460
View File
@@ -0,0 +1,460 @@
# Case 2 design: the creative writing hub
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.
## 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.
Decisions taken in this design (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.
## 1. Data model
### 1.1 Layout
```
library.json {layoutVersion: 1, kind: "library", business, project, name, createdAt}
.obsidian/app.json committed vault settings (section 7)
.obsidian/graph.json
universes/<u>/universe.json {id, name, kind: "canon" | "hybrid", sources?, constraints?, sanctioned?}
universes/<u>/<type>/<id>.md universe-level entity, shared across books
universes/<u>/books/<b>/book.json {id, title, order}
universes/<u>/books/<b>/manuscript/<nn>-<slug>.md Melanie's prose
universes/<u>/books/<b>/<type>/<id>.md book-level entity
playground/universes/<u>/... universe playground, including draft universes
playground/universes/<u>/books/<b>/... book playground
```
- `<type>` 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.
- `<u>`, `<b>` and `<id>` 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 `<type>/` directories.
Nothing else changes.
### 1.2 Entity files
An entity is Markdown with front matter and wikilinks. Obsidian reads both natively.
```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]] ...
```
- 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.
### 1.3 Structural rules (the validator, `checkLibrary`)
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.
| Code | Rule | Refusal code |
|---|---|---|
| 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 |
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).
### 1.4 Promotion
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.5 Hybrid crossover
- 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/<h>/universe.json` with `kind: "hybrid"`, `sources`, `constraints` and
`sanctioned: {by: <business human>, 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": "<source path or source/type>"}`. 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.
## 2. What exists and how Case 2 uses it
### 2.1 Project record repos (`packages/project`)
- Location and safety: `projectRepo` (`paths.mjs:34-48`) puts the repo in
`<dataRoot>/projects/<business>/<project>` 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.
### 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`).
- The Console opens the visual editor only if `roundTrip(text) === text`, and opens Source otherwise
(`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.
### 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.
### 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`.
### 2.5 Threads and messages (`packages/bus`)
- `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.
- 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.
## 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.
| 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_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 |
- 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.
- 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.
## 4. Creative roles
Five new role definitions. Each is a version 2 role file plus a `.md` contract.
| Role | Purpose | `tools` | Reply item kinds |
|---|---|---|---|
| `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` |
All five 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`.
`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.
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.
- `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`.
## 5. Guardrails enforced in code
### 5.1 Agents never write prose for the user
| 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 |
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.
### 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 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"}]}]}
```
| 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` |
| 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` |
| `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` |
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`).
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.
### 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
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.
## 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.
- `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
`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.
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`.
## 7. Obsidian, Console WYSIWYG 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`).
### 7.2 Remote sync (bus side)
Under the library lock, `library.sync` and every library write do these steps:
1. `git fetch <remote-url> 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.
`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.
### 7.3 Console WYSIWYG
- 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).
- Wikilinks render as links in the editor (C2-11a). Clicking one opens the target.
### 7.4 User and agent sync
- 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.
## 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.
| 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 | |
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.
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`).
## 9. 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.
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.
+1
View File
@@ -1357,3 +1357,4 @@ are never rewritten or removed; corrections are new entries.
2026-10-20T15:26:33Z | Sage (Opus 5.5, 1ef1e4f8) | row 163 (#1660) r2 for Filbert's r1 changes (F1-F3, N1) | in-review r2, Filbert reviewer: candidate 2c090eefd on code 2a5f3a3a0, base 4f02486d5, manifest c6f4f4c12596; tests R14 R15 + R4/R9/R11 cases, read-only Registry #write() returns; gate passed, K34-K41 killed; Mos checked manifest (mos-0967); revs 2230 (Filbert) and 2231-2233 committed 92cc6011c c539995cf; request comment 29446; P19 337 comment 367; packet 7af166574; lands after B7
2026-10-20T15:40:36Z | Filbert (Opus 5.5, 048f3c63) | row 163 r2 review (D2 rehearse, candidate 2c090eefd, manifest c6f4f4c12596) | approve, comment 29447 rev 2234: 249/249 singles pass; my M1-M6 and N1 mutants all killed (R14, R9, R11, R4, R15, session test)
2026-10-20T15:42:00Z | Sage (Opus 5.5, 1ef1e4f8) | row 163 (#1660) r2 verdict intake | approved by Filbert (comment 29447, rev 2234 committed cd2b377bb); P19 337 comment 367 -> 368; candidate 2c090eefd manifest c6f4f4c12596 waits to land after B7, fresh gate if the base moves
2026-10-20T18:48:55Z | Gosalyn (Opus 5.5, 7604438a) | T2 Case 2 design (#416), agents/gosalyn/work/case2/DESIGN.md on gosalyn/case2-design, static read at 5d019d876 | design r1 committed, review requested from Dewey, rows C2-01..C2-17 proposed, briefs follow approval