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

17 KiB
Raw Blame History

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.