mosaic org config validate|resolve runs runConfigCommand (packages/organization/src/config-command.mjs), the callable service the old mosaic business validate|resolve executable now calls too. The service returns the exit code, never exits the process and does nothing on import. The spelling sets only the stderr prefix and the usage text; status and stdout are byte-identical, and the v1 output keeps the business key. mosaic org instances|instance|veto takes the same arguments as mosaic business instances|instance|veto and runs the same proof-bearing instances command. It is dispatched before orgCommand, so it never builds the dev-local-owner context. mosaic org validate FILE is unchanged. Help, the UI labels for agent actions and instance reasons, and the current docs now use the org spelling and the packages/organization paths. Two webui test regexes follow the label text. packages/cli/tests/ rename-b-commands.test.mjs compares the two spellings golden for golden on status, stdout, stderr and authorization outcomes, and covers a draft file named like an installed ID.
325 lines
17 KiB
Markdown
325 lines
17 KiB
Markdown
# Implementing the Mosaic Stack design
|
||
|
||
This brief is for the agents who build the design. Read it with
|
||
`README.md` (the spec) and the preview `mosaic-stack-design.html`.
|
||
|
||
## Status
|
||
|
||
- **2026-10-10:** Jason accepted the direction of draft 1. The mark
|
||
stays the refined Relay as a placeholder until he revises it. Do not
|
||
spend work on logo variants.
|
||
- **Provisional defaults**, carried in the files and reversible:
|
||
- Harbor palette, following the system light or dark setting;
|
||
- the web shows decisions and copies `mosaic decide`; it never
|
||
resolves them (REQ-DEC-3);
|
||
- `mosaic` with no arguments opens the full-screen app. Today it
|
||
exits 4 (usage).
|
||
- **Not authorized by this brief:** queue rows, dispatch, push, or
|
||
any change to a security boundary. Sage owns the queue. Each
|
||
implementation row needs its own brief and review.
|
||
|
||
## Sources of truth
|
||
|
||
When two sources disagree, the higher one wins:
|
||
|
||
1. The PRD (`docs/prd/mosaic-stack.md`) and the repository invariants
|
||
(`AGENTS.md`).
|
||
2. Shipped behaviour and strings in `packages/`. The design quotes
|
||
them, and the code is the reference.
|
||
3. This brief.
|
||
4. `tokens.json`, and the generated `tokens.css`.
|
||
5. The preview. It uses sample data and its own internal names. Copy
|
||
layout and states from it, not data, ids or code.
|
||
|
||
## Files
|
||
|
||
| File | Use |
|
||
|---|---|
|
||
| `tokens.json` | Every design value: 10 palettes × light, dim and dark (14 colours each), type scale, radii, spacing, layout widths, terminal colours, status vocabulary |
|
||
| `tokens.css` | Generated from `tokens.json`. Do not edit by hand |
|
||
| `icons.svg` | Sprite: `mark` (placeholder) and 17 line icons `i-board` … `i-flag` |
|
||
| `brand/relay-refined.svg` | The placeholder mark as a standalone file |
|
||
| `tools/build-tokens.mjs` | Regenerates `tokens.css`; `--check` exits 1 when it is stale |
|
||
| `tools/check-design.mjs` | Browser checks for the preview (see Acceptance) |
|
||
|
||
## Tokens
|
||
|
||
- **Names match the shipped console.** `tokens.css` uses the property
|
||
names `packages/webui` already uses: `--canvas` … `--danger`,
|
||
`--onAction` (camelCase, as `brand.js` sets it), `--font`, `--mono`,
|
||
`--r`, `--r-lg`, `--pad`, `--gap`. It adds two names:
|
||
- `--r-table` (8 px);
|
||
- `--strong` (a `color-mix` of `--text`).
|
||
- **Selectors.** No attribute means Harbor, light or dark from
|
||
`prefers-color-scheme`. `data-mode="light|dim|dark"` sets the mode.
|
||
`data-palette="<id>"` picks any other palette. The webui already sets
|
||
both attributes on `<html>`.
|
||
- **Values are the shipped values.** For all 10 × 3 × 14 colours,
|
||
`tokens.json` equals what `packages/webui/src/public/brand.js`
|
||
`tokens()` computes. Checked 2026-10-10: 0 mismatches.
|
||
- **Adopting it in the webui:**
|
||
- serve `tokens.css` (add it to the `files` map in
|
||
`packages/webui/src/serve.mjs`);
|
||
- stop the inline `setProperty` loop in `app.js` `theme()`, which
|
||
would override the stylesheet;
|
||
- keep setting `data-palette` and `data-mode`;
|
||
- add a System choice that removes `data-mode`;
|
||
- keep the localStorage key `mosaic-console-appearance`.
|
||
- **Changes the design makes to existing values:**
|
||
- `--r` goes from 8 to 6 px;
|
||
- `--r-lg` goes from 14 to 10 px;
|
||
- `--gap` stays 12 px and `--pad` stays 16 px;
|
||
- `--shadow` stays owned by `app.css`. Use it only for overlays: the
|
||
inspector over the table, the command palette and the toast.
|
||
- **Fonts.** The console's CSP is `font-src 'self'`, so every font is
|
||
self-hosted. Manrope 400 to 700 is already served. Add JetBrains Mono
|
||
400, 500 and 700. Manrope 800 is optional: titles may use 700.
|
||
- **Type scale:**
|
||
- page title 28/800;
|
||
- section 20/800;
|
||
- question 16/700;
|
||
- text 14/400 at 1.5;
|
||
- table cells 13;
|
||
- labels 11/700 in capitals with 0.06em tracking;
|
||
- mono 12.5.
|
||
|
||
Use `font-variant-numeric: tabular-nums` in columns.
|
||
- **Regenerate** after editing `tokens.json`:
|
||
`node docs/design/tools/build-tokens.mjs`. Copy the result to the
|
||
webui; the design directory stays the source.
|
||
|
||
## Icons and the mark
|
||
|
||
- **Format.** 24-unit grid, `stroke="currentColor"`, width 1.75, round
|
||
caps and joins, no fills. The webui inlines icons through
|
||
`Brand.icon(id)` with the same stroke rules. Replace those paths with
|
||
the sprite's paths so there is one icon set, or serve `icons.svg` and
|
||
use `<svg><use href="/icons.svg#i-inbox"/></svg>`. CSP
|
||
`default-src 'self'` allows a same-origin sprite.
|
||
- **Labels.** An icon never carries state alone. Every icon in
|
||
navigation sits next to its text label.
|
||
- **The mark.** The `mark` symbol uses `fill="currentColor"` and
|
||
`viewBox="0 0 16 17"`. Draw it at 16, 24 or 32 px. It is a
|
||
placeholder: keep it in one component so a later swap touches one
|
||
file.
|
||
|
||
## Components
|
||
|
||
Every component needs the listed states, keyboard behaviour and
|
||
focus ring (`--focus`, 2 px, offset 2 px).
|
||
|
||
| Component | States and rules |
|
||
|---|---|
|
||
| Command bar | Business name, freshness line ("Vikunja last read 14:21:07 (12 s ago)"), Ctrl+K button. Warning colour plus words when stale |
|
||
| Section list | Icon + label + count; `aria-current="page"` on the active view. Below 760 px it becomes a horizontal strip that scrolls inside itself |
|
||
| Dense table | 13 px cells, padding 7/10, header labels in caps. Row hover, selected row, ↑/↓ moves, Enter opens the inspector. Wide tables scroll inside their own container |
|
||
| Inspector | Right drawer, 300 to 370 px. Over the table below 1100 px; a full-screen sheet below 760 px. Esc closes and returns focus to the row |
|
||
| Status mark | Glyph + word, never colour alone: ● working, ◐ waiting, ▲ error, ○ offline, – idle, ? unknown |
|
||
| Class chip | routine, within-role, cross-role, gated; BLOCKING is a separate chip |
|
||
| Copy command | Mono command + Copy button. Uses the shipped strings (below). Never runs anything |
|
||
| Toast | One line, polite live region, auto-dismiss; errors stay until dismissed |
|
||
| Command palette | Ctrl+K opens; type to filter views, decisions and tasks; Enter goes; Esc closes and restores focus |
|
||
| View states | Five per view: normal; loading (skeleton rows, `aria-busy`); empty (says what would appear and where it comes from); error (banner, last good data stays on screen with its read time); refusal (fail closed: the refusal code in mono and the file or rule that is missing) |
|
||
| Not found | Names the bad route, links to the Board, mentions Ctrl+K |
|
||
|
||
## Views and the gap map
|
||
|
||
Existing routes in `packages/webui` are hash routes: `#/` (the board),
|
||
`#/inbox[/<id>]`, `#/tasks[/<ref>]`, `#/agents`,
|
||
`#/trail/task/<ref>`, `#/trail/decision/<id>` (with `?kind=`). New
|
||
views follow the same form: `#/talk`, `#/queue`, `#/runs`,
|
||
`#/ledger`, `#/business`, `#/library`, `#/settings`, `#/setup`.
|
||
|
||
Kinds of work:
|
||
- **restyle:** built; apply tokens, layout and states;
|
||
- **extend:** built; add design pieces whose data the current read
|
||
already returns;
|
||
- **new view:** the data exists in a module; add a read-only endpoint
|
||
to the webui server and a view;
|
||
- **backend:** the data is not produced or exposed yet;
|
||
- **blocked:** waits on another row or on Jason.
|
||
|
||
| View | Data today | Kind | Notes |
|
||
|---|---|---|---|
|
||
| Board `#/` | `GET /api/board`, `POST /api/seen` (control board) | restyle | Seat table with Input needed and Seen/Unsee already works |
|
||
| Inbox `#/inbox` | `GET /api/bus/inbox`: open decisions routed to `human` | restyle | Detail, authorization sentence, copy commands and history already work |
|
||
| Inbox tabs: Routed to roles, Recent | none: `inbox` returns open decisions for the caller's role only | backend | Needs a reader op for decisions routed to other roles and for closed ones |
|
||
| Decision Seen | broker `decision.seen` needs a human session (`broker.mjs:614-633`) | blocked | The web holds a reader session. Writing Seen from the browser gives it human authority, a security-boundary change for Jason. The web may show existing `seen` events (bus.js already reads them). The TUI may write Seen once a CLI verb exists |
|
||
| Tasks `#/tasks` | `GET /api/bus/tasks`: title, bucket, done, markers, freshness | restyle | |
|
||
| Task priority and requirement columns | not in `task_current.fields`; requirement is in the `task.created` event body | backend | |
|
||
| Field writers in the task inspector | prose in `docs/plans/2026-10-04_slice-1.md:62-72` | backend | Encode the table as data first |
|
||
| Agents `#/agents` | `GET /api/bus/agents`: latest claim per role | restyle | |
|
||
| Agent state and Input needed per role | the control board has state per seat; the bus has none per role | backend | Needs a seat-to-role mapping, or state on the bus |
|
||
| Launch budget (Opus n of 4, Sonnet n of 4) | ceiling only (`vocabulary.mjs:85-86`); `session.launched` has no model family | blocked | Row 41 (S6) |
|
||
| Stop launches (`mosaic stop`) | broker `launch.revoke` / `launch.restore`, no CLI verb | blocked | Row 41 (S6). Web shows the command only |
|
||
| Credentials (service, account, role, expiry) | `Credentials.status()` in `packages/bus/src/credentials.mjs:118`; not on the broker | backend | Metadata only. Never a value, token or path |
|
||
| Trail `#/trail/...` | `GET /api/bus/trail?subject=`; filter chips shipped (`bus.js:313`) | restyle | |
|
||
| Talk `#/talk` | the conversation History view exists; the live PM session is row 41 | blocked | Row 41 (S6). Reuse the controller/observer rules of `packages/conversation` |
|
||
| Queue `#/queue` | `packages/queue/src/store.mjs` `list`/`show`; no endpoint | new view | Read-only. Rows move only through `queue move` |
|
||
| Ledger `#/ledger` | `packages/ledger` `summarize`, `queueChecks` | new view | Read-only |
|
||
| Business `#/business` | `packages/organization` `loadBusiness`, `loadRole`, `resolveInstance`, `vocabulary.mjs` | new view | Read-only. A missing or invalid file shows the refusal state. Credential refs as metadata only |
|
||
| Runs and releases `#/runs` | readers live inside `scripts/mosaic-task.mjs` and `scripts/release.sh`, not exported | backend | Extract a read-only module first |
|
||
| Library `#/library` | no meta-harness in committed code | blocked | Row 41 (S6) creates `packages/harness` |
|
||
| Settings `#/settings` | appearance exists in the webui header; notifier config `readNotifyConfig` | extend | Appearance, notifications (binding names only), about (loopback, release). Read-only apart from appearance |
|
||
| Set up `#/setup` | validation lives in `scripts/mosaic-config.mjs` (a script) | backend | First-run checklist; the installer is out of v1 scope |
|
||
| Command palette, five states, not found | client-side | extend | |
|
||
|
||
The Board is the console home. The control board's own page on port
|
||
7331 stays until the console's Board covers it.
|
||
|
||
## The full-screen terminal app
|
||
|
||
- **Where.** `packages/cli`. Node built-ins only; the CLI has no
|
||
dependencies and must keep none.
|
||
- **Talk.** Embed the conversation `Terminal` class
|
||
(`packages/conversation/src/terminal.mjs:54`,
|
||
`new Terminal({client, write, rows, onQuit})`). Its `main()` owns the
|
||
process, so do not call it. The app owns the screen and draws the
|
||
conversation inside the Talk pane.
|
||
- **Size.** Read `process.stdout.columns` and `rows`; redraw on
|
||
`resize`. Designed at 120×36; every screen must fit 80×24. Below
|
||
100 columns:
|
||
- the nav shows keys only;
|
||
- Inbox drops RAISED BY and REACHES YOU;
|
||
- detail moves below the list.
|
||
- **Colour.**
|
||
- Use the terminal's own 16 colours (`tokens.json` `terminal.ansi`),
|
||
never 256-colour or RGB.
|
||
- Honour `NO_COLOR` (any non-empty value): no colour codes; reverse
|
||
video stays for the selected row, and every state still reads
|
||
from its word and mark.
|
||
- Not a TTY: refuse the full-screen app and point at the verbs.
|
||
- **Refusal.** In an agent run, exit 3 with the transport's message
|
||
(`packages/cli/src/transport.mjs:30`). That is the same check the
|
||
verbs use; do not write a second one.
|
||
- **Deciding.** In Inbox, a digit picks an option. Then print the
|
||
CLI's lines and resolve only on `y`. Reuse the `decide` code path
|
||
rather than a copy of it. "Outcome unknown" offers no retry key.
|
||
- **Keys:**
|
||
- `c` talk, `i` inbox, `t` tasks, `a` agents, `r` trail;
|
||
- ↑↓ or j/k move, Enter opens, `v` opens the trail of the selection;
|
||
- `:` command line, `?` help, `q` quit;
|
||
- Tab moves between the nav and the composer;
|
||
- the composer keeps the conversation keys.
|
||
- **No-argument behaviour** is a CLI change from exit 4 to opening the
|
||
app. It is provisional; land it behind its own review.
|
||
|
||
## Strings to keep exactly
|
||
|
||
These are shipped. Reuse them; do not reword.
|
||
|
||
- **Decide:**
|
||
- `your choice: <key> (<text>); this authorizes the action`, or
|
||
`…; this declines the action`;
|
||
- `resolve <id8> with <key>? [y/N] `;
|
||
- `resolved <full id>: <key>`;
|
||
- `outcome unknown: the broker may have recorded it. Check mosaic inbox or mosaic trail <id> before trying again`.
|
||
- **Refusal:** `refused: this runs only from a human shell, outside any agent run (found <VARS> in the environment)`.
|
||
- **Web copy:**
|
||
- `Copied: <command>`;
|
||
- `The clipboard is unavailable. The command is selected; press Ctrl+C to copy it.`;
|
||
- `Decisions are answered in a terminal with mosaic decide; Console only shows them.`
|
||
- **CLI list formats** (`packages/cli/src/format.mjs`):
|
||
- inbox `<id8> <action> (<class>[, blocking]) raised <at> by <role>`;
|
||
- tasks `<ref> <state> <title>`;
|
||
- agents `<role> <holder_run> <harness|-> since <at>`;
|
||
- trail `<at> <table>#<seq> <summary>`.
|
||
- **Trail sentences, empty states and not-found lines** are in
|
||
`packages/webui/src/public/bus.js` (for example
|
||
`No task <ref> in this business.`). The "feat" labels there mark
|
||
the gaps this design fills: poller status, bucket names, role
|
||
instances and launch limits.
|
||
- **Bus wording stays in mono:** event kinds (`task.changed.external`),
|
||
refusal codes (`decision-closed`, `launch-revoked`), task refs and
|
||
ids.
|
||
|
||
## Boundaries
|
||
|
||
- **No Resolve button on the web.** Copy the `mosaic decide` command
|
||
only (REQ-DEC-3).
|
||
- **Seen is not resolution.** Board Seen/Unsee exists. Decision Seen
|
||
from the web is blocked (see the gap map).
|
||
- **Read-only web**, apart from Board Seen/Unsee, the board's
|
||
existing reply path, and appearance settings stored in the browser.
|
||
Queue rows move only through `queue move`; the business file is
|
||
written by the human only.
|
||
- **Secrets.** Show service, account, role and expiry. Never show
|
||
values, tokens or file paths. Never log them.
|
||
- **Server.**
|
||
- Loopback only, no sign-in;
|
||
- keep the existing CSP, Host and Origin checks;
|
||
- new endpoints are `GET` only and return JSON;
|
||
- no new dependencies in `packages/webui` or `packages/cli`.
|
||
|
||
## Acceptance
|
||
|
||
For every implementation row:
|
||
- the package's own suite passes: `node --test tests/` in
|
||
`packages/webui` or `packages/cli`, plus the repository suites the row
|
||
touches;
|
||
- `node docs/design/tools/build-tokens.mjs --check` passes when tokens
|
||
changed;
|
||
- every changed view shows its five states. Seeded tests cover the
|
||
empty, error-with-stale-data and refusal states;
|
||
- browser tests (`packages/webui/tests/browser.mjs` style):
|
||
- no script errors;
|
||
- no sideways page scroll at 400 px;
|
||
- keyboard path: ↑/↓, Enter, Esc returning focus to the row;
|
||
- focus is visible;
|
||
- terminal app tests:
|
||
- every screen fits 120 columns and 80×24;
|
||
- `NO_COLOR` output has no escape codes;
|
||
- an agent-run environment exits 3;
|
||
- strings in "Strings to keep exactly" are unchanged (a grep test is
|
||
enough).
|
||
|
||
`node docs/design/tools/check-design.mjs` checks the preview itself:
|
||
- no script errors across 4 tabs, 13 views × 5 states and 13 terminal
|
||
screens;
|
||
- no sideways scroll at 400 px;
|
||
- terminal widths;
|
||
- palette colours against `tokens.json`.
|
||
|
||
It needs a local Chromium (`$CHROMIUM` or `/usr/bin/chromium`). Run it
|
||
after editing the preview.
|
||
|
||
## Proposed rows (for Sage)
|
||
|
||
Draft order. These go after rows 41 and 42. Sage decides whether to
|
||
queue them, and in what order.
|
||
|
||
1. **Console tokens and shell restyle** (frontend; Dewey's area).
|
||
- Scope:
|
||
- adopt `tokens.css`;
|
||
- command bar, section list, dense table, inspector, toast,
|
||
command palette, not found;
|
||
- five states on Board, Inbox, Tasks, Agents and Trail.
|
||
- No new data.
|
||
2. **Read-only views with existing data** (frontend plus webui
|
||
server). Queue, Ledger, Business, Settings: `GET` endpoints over the
|
||
existing modules.
|
||
3. **Full-screen `mosaic` app** (CLI): Inbox, Tasks, Agents, Trail,
|
||
Help, decide, NO_COLOR and 80×24. Talk joins after row 41.
|
||
- Needs Jason's confirmation of no-argument `mosaic`.
|
||
4. **Backend reads for the design** (bus):
|
||
- decisions routed to roles and recent decisions;
|
||
- task priority and requirement;
|
||
- agent state per role;
|
||
- credential status on the broker;
|
||
- the field-writer table as data;
|
||
- a runs and release reader module.
|
||
|
||
Each is a separate small row.
|
||
5. **After row 41:** Talk view, launch budget, `mosaic stop` in both
|
||
surfaces, Library.
|
||
|
||
## Open decisions for Jason
|
||
|
||
1. Default palette and mode (provisional: Harbor, following the system
|
||
setting).
|
||
2. The final mark (placeholder: refined Relay).
|
||
3. No-argument `mosaic` opens the full-screen app (provisional).
|
||
4. Decision Seen from the web. It needs a human-authority write path
|
||
from the browser. The recommendation is to keep it in the terminal
|
||
for v1.
|