Files
stack/docs/design/IMPLEMENTING.md
T
jason.woltjeandClaude Opus 5.5 811e7ba562 docs(design): handoff package for implementing the design
Jason accepted the draft 1 direction on 2026-10-10; the refined Relay
mark stays as a placeholder. Adds IMPLEMENTING.md (gap map against
row 40 and the bus/CLI modules, strings, boundaries, acceptance,
proposed rows for Sage), tokens.json and generated tokens.css using the
webui's existing names, icons.svg, and tools/build-tokens.mjs and
tools/check-design.mjs. Coverage statuses corrected against the code
(digest implemented; mosaic stop is row 41; priority and agent state
not in the bus reads). No packages/ change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 11:22:06 -05:00

325 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/business` `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.