# ADR-0162 — Workbench Design System (v1) **Status:** Proposed **Date:** 2026-05-26 **Author:** Shay **Parent:** [ADR-0160 — CORE Workbench v1](./ADR-0160-core-workbench-v1.md) **Companions:** [ADR-0161 — HITL async queue](./ADR-0161-hitl-async-queue.md), [ADR-0153 — TurnEvent.trace_hash back-stamp](./ADR-0153-turn-event-trace-hash-backstamp.md), [ADR-0159 — Contemplation-quality eval](./ADR-0159-contemplation-quality-eval.md) --- ## Context ADR-0160 named the Workbench's product doctrine — read-only first, calm by default, infinite depth, replay before persuasion, no accept/reject in v1. W-026 landed the read-only API with typed schemas and closed-set error codes. What the cascade still lacks is a **ratified design substrate**. Without one, every downstream branch (W-027 frontend shell, W-028 chat + trace drawer, W-029 proposal queue, W-030 eval center, W-031 replay theater) will hand-roll its own tokens, badge mappings, motion rules, JSON-viewer behavior, and empty/error/loading states. Five branches' worth of silent drift is exactly the failure mode ADR-0160 forbids ("no dashboard soup"). This ADR pins the design substrate **before** any frontend code exists. Tokens, typography, motion, semantic state mapping, the `StableJsonViewer` invariants, empty/error/loading contracts, keyboard behavior, and the explicit no-go list are doctrine, not styling. They are the trust surface the visual layer sits on. The principle behind every call below is the same as ADR-0161's: select the **narrowest commitment** that still names testable invariants. This ADR ships zero implementation; it commits the contract that W-027 will honor and that every later branch will inherit. --- ## Product north star (one paragraph) The Workbench should feel like Linear's calm + Raycast's speed + GitHub PR review's auditability + Xcode Instruments' precision — operating a deterministic cognition engine with full traceability. Not a chat clone. Not a SaaS dashboard. Not animated AI theater. The beauty comes from structural truth, hierarchy, responsiveness, and precision; from operational sovereignty, not decoration. Every meaningful surface must be able to answer six questions: *What happened? Why was it allowed? What evidence supports it? Can it be replayed? Did it mutate anything? Who/what has authority to ratify the next step?* A panel that cannot answer one of those questions does not ship in v1. --- ## Decision summary | Concern | Decision | |---|---| | Token namespace | Semantic, not literal. `--color-surface-base`, not `--color-zinc-900`. Tokens are CSS custom properties + a typed TS export. | | Theme | Dark default. Light theme deferred to v2. No system-theme auto-switch in v1. | | Typography | Inter (UI), JetBrains Mono (hash/JSON/trace). System fallback chain pinned. | | Color semantics | Bound to ratified `EpistemicState` (15) and `NormativeClearance` (4) enums in `core/epistemic_state.py`. No badge color without an enum binding. | | Motion | Allowed: drawer/palette/expand/diff/route transitions. Forbidden: "thinking" animations, particle effects, glowing networks, avatar pulses, shimmer-everywhere. | | `StableJsonViewer` | Deterministic, lossless, copyable, diffable. Trust surface, not a code block. Six tested invariants. | | Empty / error / loading | Every route ships all three. Empty includes a next action. Error includes reproducer + mutation status. Loading uses specific labels. | | Keyboard | Keyboard-first. `⌘K` palette, `Esc` closes overlays, `Enter` opens selection, focus-visible everywhere, no color-only encoding. | | Layout shell | Five-region grid: TopBar / LeftNav / MainSurface / RightInspector / StatusFooter. Routes may collapse the rail but not the bar/nav/footer. | --- ## Decision detail ### 1. Token namespace and theme Tokens live in `workbench-ui/src/design/tokens/` as CSS custom properties (`tokens.css`) plus a typed TypeScript mirror (`tokens.ts`). The TS export is generated from the CSS to keep one source of truth. **Naming rule.** Token names are **semantic**, not literal. A consumer reads what the value *means*, not what shade it is: ```text --color-surface-base --color-surface-raised --color-surface-overlay --color-border-subtle --color-border-strong --color-text-primary --color-text-secondary --color-text-mono --radius-sm --radius-md --radius-lg --space-1 .. --space-12 --shadow-panel --shadow-floating --motion-fast --motion-base --motion-slow --motion-ease-out --motion-ease-spring ``` Forbidden: `--color-blue-500`, `--color-zinc-900`. Literal-named tokens leak palette decisions into consumers and prevent palette evolution. **Theme.** Dark by default. Light theme is deferred to a follow-up ADR. No system-theme auto-switching in v1 (avoids first-render flash and keeps screenshots stable for the audit trail). ### 2. Typography UI: **Inter** with the system stack as fallback (`-apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif`). Mono: **JetBrains Mono** with fallback (`"SF Mono", "Menlo", "Consolas", monospace`). Used for every surface that carries audit-significant text: `trace_hash`, `proposal_id`, JSON, file paths, CLI commands, digests. Hierarchy is pinned by token, not by class proliferation: ```text --font-size-display --font-size-h1 --font-size-h2 --font-size-body --font-size-meta --font-size-mono --line-height-tight --line-height-base --font-weight-regular --font-weight-medium --font-weight-semibold ``` No more than two weight steps per surface. Density without clutter. ### 3. Color semantics — bound to ratified enums This is the load-bearing call. Every status badge color **must** be bound to an enum value that the engine already emits. No "aspirational" badge colors. No two badges may share a color. This keeps the badge layer honest and audit-grade. #### 3a. EpistemicState (15 values from `core/epistemic_state.py`) | State | Badge label | Token | Felt meaning | |---|---|---|---| | `decoded` | Decoded | `--color-state-decoded` (cool blue-green) | Engine reconstructed the answer in canonical form | | `decoded_unarticulated` | Decoded (silent) | `--color-state-decoded` (muted) | Reconstructed but no surface emitted | | `verified` | Verified | `--color-state-verified` (cool green) | Cross-checked against reviewed evidence | | `evidenced` | Evidenced | `--color-state-evidenced` (teal) | Direct evidence supports the surface | | `evidenced_incomplete` | Evidenced (partial) | `--color-state-evidenced` (muted) | Evidence partial; surface qualified | | `inferred` | Inferred | `--color-state-inferred` (indigo) | Composed from existing chains, not directly evidenced | | `unverified_possible` | Unverified | `--color-state-unverified` (blue-gray) | Plausible but unverified | | `unverified_novel` | Unverified (novel) | `--color-state-unverified` (warm) | New territory, no prior evidence | | `perceived` | Perceived | `--color-state-perceived` (cyan) | Input registered, not yet grounded | | `contradicted` | Contradicted | `--color-state-contradicted` (red) | Evidence falsifies the surface | | `ambiguous` | Ambiguous | `--color-state-ambiguous` (violet) | Multiple coherent readings | | `undetermined` | Undetermined | `--color-state-undetermined` (muted gray) | Insufficient evidence to choose | | `scope_boundary` | Out of scope | `--color-state-scope` (slate) | Outside the engine's accepted domain | | `computationally_bounded` | Bounded | `--color-state-bounded` (orange) | Computation hit a deliberate ceiling | | `epistemic_state_needed` | Needs review | `--color-state-needed` (amber) | Engine declined to self-classify | Clicking any badge opens a popover explaining the state, citing the ADR that pinned it, and listing the evidence that produced the classification. #### 3b. NormativeClearance (4 values from `core/epistemic_state.py`) | State | Badge label | Token | |---|---|---| | `cleared` | Cleared | `--color-clearance-cleared` (cool green) | | `violated` | Violated | `--color-clearance-violated` (red) | | `unassessable` | Unassessable | `--color-clearance-unassessable` (muted gray) | | `suppressed` | Suppressed | `--color-clearance-suppressed` (muted red) | #### 3c. ReviewState (4 values from `teaching/proposals.py`, per ADR-0057 / ADR-0161) | State | Badge label | Token | |---|---|---| | `pending` | Pending | `--color-review-pending` (amber) | | `accepted` | Accepted | `--color-review-accepted` (cool green) | | `rejected` | Rejected | `--color-review-rejected` (muted red) | | `withdrawn` | Withdrawn | `--color-review-withdrawn` (muted gray) | #### 3d. Grounding source (6 values from cold-start-grounding lane) | State | Badge label | Token | |---|---|---| | `teaching` | Teaching | `--color-grounding-teaching` (cool green) | | `pack` | Pack | `--color-grounding-pack` (teal) | | `vault` | Vault | `--color-grounding-vault` (indigo) | | `partial` | Partial | `--color-grounding-partial` (muted teal) | | `oov` | Out of vocab | `--color-grounding-oov` (orange) | | `none` | Not grounded | `--color-grounding-none` (slate) | **Color-only encoding is forbidden.** Every badge carries its label; the color is reinforcement. Accessibility and screenshot-audit fidelity both depend on this. ### 4. Motion The motion rule is one line: **motion reveals structure, not cognition.** Allowed motion (each with a single token, no per-surface bespoke easing): | Motion | Duration | Easing | |---|---|---| | Drawer open/close | `--motion-base` (180ms) | `--motion-ease-out` | | Command palette enter/exit | `--motion-fast` (120ms) | `--motion-ease-out` | | Trace row expand | `--motion-base` (180ms) | `--motion-ease-out` | | Diff highlight pulse | `--motion-fast` (120ms) | `linear` (one cycle, no loop) | | Route transition | `--motion-base` (180ms) | `--motion-ease-out` | | Skeleton loading shimmer | `--motion-slow` (1200ms) | `linear`, ≤2 cycles before falling back to static "Loading…" label | Forbidden: - "thinking…" pulses, dots, or orbs - particle effects - glowing neural-network animations - avatar movement - glassmorphism / frosted-glass surfaces - shimmer applied to anything that is not a loading skeleton - animated graph or chain construction "theater" - background gradients that move - any animation that loops indefinitely Reduced-motion (`prefers-reduced-motion: reduce`) must collapse every allowed motion to instant. No exceptions. ### 5. `StableJsonViewer` — trust surface invariants This is the component that, if done casually, silently erodes the audit invariant. It is **doctrine, not styling**. Six invariants, each testable: 1. **Deterministic key ordering.** Keys render in the order the source serialized them. When the source is a dict, sort lexicographically. When the source is an already-serialized JSON string, preserve appearance order. Never silently re-sort. 2. **Lossless string preservation.** No smart quotes. No whitespace stripping. No unicode normalization. No HTML entity coercion. What you see is what is on disk. 3. **No semantic auto-format.** Numbers display as the source typed them (`1e-6` ≠ `0.000001`). Booleans, null, and integer/float distinctions are preserved. 4. **Copy paths.** Right-click (and a keyboard shortcut) yields the JSON Pointer path to the selection (e.g., `/scenes/3/detail/proposed_chain/object`). This is the audit-trail handle. 5. **Diff mode.** Given two values, render side-by-side with structural alignment. Highlight only the leaf fields that changed. Color-only encoding forbidden — changed leaves carry a glyph (`≠ added`, `≠ removed`, `≠ changed`). 6. **Large-document safety.** Virtualize rendering above 1,000 leaf nodes. Above 16 MiB raw bytes, refuse to inline-render and surface a "Open in external viewer" affordance with a copy-path button — same ceiling as the W-026 read API. The viewer must also display the SHA-256 digest of the rendered source as a `--font-mono` badge. Clicking the digest copies it. ### 6. Empty / error / loading state contract Every route ships **all three** states from day one. This is acceptance criterion #9 from the vision doc, lifted into doctrine here. #### Empty Every empty state must contain: - A one-line statement of what is absent. - A **next action** — either a CLI command to copy, a route to navigate to, or a runtime config to inspect. Example: ```text No pending proposals. Run: core demo learning-arc ``` Never empty-empty. Never just a "—". #### Error Every error state must surface: - **What failed** (one sentence) - **Whether state was mutated** (`No corpus mutation occurred.` is a load-bearing line per CLAUDE.md doctrine) - **A reproducer** — the CLI command or curl that triggers it - **Whether retry is safe** Example: ```text Replay failed before comparison. No corpus mutation occurred. Reproduce: uv run core demo learning-arc --json Retry: safe ``` #### Loading Loading labels are specific, never "Thinking…": ```text Loading trace… Computing replay… Reading proposal log… Running eval lane… Comparing artifacts… ``` The skeleton shimmer caps at two cycles (per §4) and then collapses to the static label. Indefinite shimmering is forbidden. ### 7. Keyboard contract The Workbench is keyboard-first. Baseline: - `⌘K` / `Ctrl+K` opens the command palette from any route - `Esc` closes any overlay (drawer, palette, popover) - `Enter` activates the focused item (open, expand) - `↑` / `↓` traverse lists; `←` / `→` traverse adjacent panels - `?` opens the keyboard-shortcut cheat sheet - Every interactive element has `focus-visible` styles drawn from `--color-focus-ring` - `Tab` order matches visual order; explicit `tabIndex` only where the visual order conflicts with the semantic order - Every drawer/dialog announces a `role` and an `aria-label` - No interactive surface is reachable only by mouse The command palette must support, at minimum, fuzzy search across: ```text Run eval lane Open proposal Replay trace Compare run Inspect pack Search trace hash Copy ratification command Open latest contemplation report ``` ### 8. Layout shell The shell is a five-region grid, named consistently across routes: ```text +---------------------------------------------------------------+ | TopBar (command palette / context / status) | +---------+-------------------------------------------+---------+ | | | | | LeftNav | MainSurface | Right | | | | Inspect | | | | | +---------+-------------------------------------------+---------+ | StatusFooter (runtime / replay / mutation mode) | +---------------------------------------------------------------+ ``` Routes may **collapse** the RightInspector but not hide the TopBar, LeftNav, or StatusFooter. Removing the persistent surfaces breaks the "always know what runtime you're talking to" affordance that ADR-0160 required. LeftNav contents are pinned and ordered: ```text Chat Trace Replay Proposals Evals Runs Packs Vault / Recall Audit Log Settings / Runtime ``` No giant sidebar. No collapsible sub-trees in v1. StatusFooter surfaces four signals at all times: - `status` from `GET /health` (`Healthy` / `Unhealthy` / `Checking`) — a liveness dot polled independently of `/runtime/status` so a live server still reads healthy when runtime metadata is degraded; any non-`ok` status fails safe to `Unhealthy` - `mutation_mode` from `GET /runtime/status` (`read_only` or `runtime_turn`) — color-encoded **and** labeled - `git_revision` (short SHA, mono) - `checkpoint_revision` (short SHA, mono) — turns amber when `revision_warning: true` per ADR-0157 ### 9. Component map — v1 must-ship vs follow-up The vision doc lists ~30 components. The honest v1 scope is narrower. This ADR commits to **must-ship** for W-027..W-031; everything else is named here so the design system anticipates it, but ships in a follow-up. **Must ship in v1 (W-027..W-031):** ```text WorkbenchShell TopBar LeftNav StatusFooter CommandPalette ChatTurnList ChatTurnCard ResponseEvidenceStrip TraceDrawer EpistemicStateBadge GroundingSourceBadge NormativeClearanceBadge ReviewStateBadge TraceHashBadge CopyableHash ReplayTheater ReplayComparisonPanel ReplayDiffViewer ProposalQueue ProposalCard ProposalDetailPanel RatificationCommandPanel EvalCenter EvalLaneList EvalFailureViewer StableJsonViewer StableJsonDiffViewer ArtifactLink ``` **Follow-up after v1 milestone:** ```text TraceStepList (inline trace step navigator) ProposalChainViewer (chain visualization) SourceProvenancePanel (deep provenance graph) PackInspector CorpusInspector VaultRecallInspector MetricGateTable RegressionDiffPanel ``` Pinning the v1 component set keeps the implementation branches honest. ### 10. The no-go list Explicit, because doctrine drift is what produces "AI dashboard" UIs: - No chat-clone styling. No avatar bubbles. No "AI is thinking…" affordances. - No animated cognition theater (particles, glowing networks, pulsing orbs). - No glassmorphism / frosted glass. - No purple gradients, no neon accents, no cyberpunk styling. - No live "hallucinated" chain or graph construction animations. - No node-graph builder in v1. - No workflow automation canvas in v1. - No "accept proposal" button anywhere in v1 (per ADR-0160 + ADR-0161 §2). - No dashboard-soup pages. - No vanity green-dashboard eval views. - No "active session count" / "total turns" splash metrics on a landing page. - No system-tray notifications, no toasts that auto-dismiss audit events. - No icon-only buttons without accessible labels. - No color-only state encoding. --- ## Implementation plan — Branch 1 Pre-W-027 deliverable. Single PR titled `feat(workbench-ui): design system v1 (ADR-0162)`: 1. `workbench-ui/` directory created at repo root with `package.json`, `vite.config.ts`, `tsconfig.json`. Pin React 18, Vite 5, TS 5.x, Tailwind 3.x, shadcn primitives. No app routes yet. 2. `workbench-ui/src/design/tokens/tokens.css` — every token from §1–§4. 3. `workbench-ui/src/design/tokens/tokens.ts` — typed TS mirror, generated from `tokens.css` at build time (a `scripts/generate-tokens.ts` reader). 4. `workbench-ui/src/design/components/StableJsonViewer/` — full implementation honoring all six invariants in §5, with tests. 5. `workbench-ui/src/design/components/EpistemicStateBadge/` and the four other badge primitives — each generated from the enum tables in §3 so the badge set is provably exhaustive. 6. `workbench-ui/src/design/components/EmptyState/`, `ErrorState/`, `LoadingState/` — primitives the route screens compose. 7. `workbench-ui/preview/` — a Vite route exposing every primitive on one page so the design baseline is reviewable in a browser. Not shipped to end users; it is the operator's "Storybook lite". 8. Frontend tests: token presence, badge-set exhaustiveness against the Python enums (parsed from `core/epistemic_state.py` and `teaching/proposals.py` at build time), `StableJsonViewer` invariants 1–6, reduced-motion collapse, keyboard contract for the palette and drawers. 9. Docs update: `docs/workbench/design-system.md` linking the ADR, `docs/workbench/README.md` runbook addition (`cd workbench-ui && pnpm preview`). **Crucially: no app shell, no routes, no API client yet.** Branch 1 is *only* the substrate. W-027 (frontend shell) is the first branch that consumes it. --- ## Acceptance criteria This ADR is ratifiable when: 1. Every token in §1–§4 is a CSS custom property with a matching typed TS export, and a test fails if either drifts. 2. Every enum value in §3a–§3d has exactly one badge component, and a test parses the Python enums and asserts 1:1 coverage. Adding an enum value to the engine without adding a badge fails the test. 3. `StableJsonViewer`'s six invariants each have a test: - byte-identical round-trip on a synthetic source, - copy-path returns a valid JSON Pointer, - diff highlights only changed leaves and adds the glyph, - large-document virtualization triggers above 1,000 leaves, - oversize-document refusal triggers above 16 MiB, - SHA-256 digest matches the source bytes. 4. Every primitive component renders an `EmptyState` / `ErrorState` / `LoadingState` snapshot test from a fixture and asserts the "next action" / "reproducer" / "specific label" rules in §6. 5. `prefers-reduced-motion: reduce` collapses all motion to instant in a Playwright test. 6. The command palette is reachable via keyboard from every primitive's preview page (Playwright). 7. The preview page (`pnpm preview`) renders every primitive without network access (offline-safe, deterministic). --- ## Out of scope This ADR is the design substrate. It does not commit to: - the app shell or routes (W-027) - chat or trace UI (W-028) - proposal queue UI (W-029) - eval center UI (W-030) - replay theater UI (W-031) - a backend API beyond what W-026 already exposes - light theme / system-theme switching - internationalization - multi-tenant or remote operation - mobile layouts (Workbench is a desktop tool; the mobile path for ratification stays the GitHub mobile app + workflow_dispatch per ADR-0161 §2) - a frontend authentication layer (deferred to a separate ADR; v1 is local-only, unauthenticated, per ADR-0160) - Storybook / Chromatic — the `/preview` route is the substitute - Figma component mirroring — design tokens are the source of truth, not a Figma file --- ## Consequences ### Positive - Five downstream branches inherit one substrate. No silent drift between W-027 and W-031. - The badge layer is provably exhaustive against the engine's ratified enums — adding a new `EpistemicState` value cannot ship without a badge. - `StableJsonViewer` becomes a tested trust surface, not a code-styling choice. Replay-equivalence claims gain a UI-side audit handle. - The no-go list closes the "AI dashboard drift" failure mode by name, not by hope. - Empty / error / loading contracts ship from day one. The "we'll add empty states later" failure mode is forbidden. ### Negative - Branch 1 is overhead before any user-visible UI exists. ~1 day's work that shows nothing in the chat surface. Worth it. - Tying badges to enums means engine refactors that rename or remove enum values cascade into the UI. This is the intended behavior, not a cost — the UI cannot diverge from the engine's self-classification. - The 16 MiB `StableJsonViewer` ceiling matches the W-026 API ceiling. If either ceiling moves, the other must move with it. ### Risks - Token bikeshedding. Mitigation: tokens are semantic-only. The conversation is about *what the surface means*, not what shade it is. - Inter / JetBrains Mono licensing. Both are OFL-licensed; bundling is permitted. Self-hosted, not fetched at runtime. - Component scope creep. The §9 component map names what ships in v1 vs follow-up; deviation requires a separate ADR. --- ## Cross-references - [ADR-0160 — CORE Workbench v1](./ADR-0160-core-workbench-v1.md) — product doctrine; this ADR is its design substrate. - [ADR-0161 — HITL async queue](./ADR-0161-hitl-async-queue.md) — the proposal-queue UI's semantic contract (admits ≠ ratifies). - [ADR-0153 — TurnEvent.trace_hash back-stamp](./ADR-0153-turn-event-trace-hash-backstamp.md) — `TraceHashBadge` consumes this. - [ADR-0157 — Revision-mismatch warning](./ADR-0157-revision-mismatch-warning.md) — `StatusFooter` consumes this. - [ADR-0159 — Contemplation-quality eval](./ADR-0159-contemplation-quality-eval.md) — `EvalCenter` reads against this lane shape. - [`core/epistemic_state.py`](../../core/epistemic_state.py) — the enum tables in §3a–§3b are bound to this file. - [`teaching/proposals.py`](../../teaching/proposals.py) — `ReviewState` for §3c. - [`workbench/schemas.py`](../../workbench/schemas.py) — typed dataclasses the UI mirrors as TS discriminated unions. - [CLAUDE.md](../../CLAUDE.md) — deterministic replay, exact recall, proposal-only learning; this ADR encodes those into the UI substrate. ### Memory cross-references - [[thesis-decoding-not-generating]] — the UI must reveal the engine *decoding* (badges, replay, trace hashes), not staging a *generating* performance (animation theater). Every motion / animation choice is measured against this. - [[feedback-address-critiques-dont-waive]] — the vision-doc gaps (StableJsonViewer underdefined, empty-states absent, badge taxonomy outrunning the engine) are addressed here, not deferred. - [[feedback-adr-cross-reference-discipline]] — every badge color is bound to a ratified enum. No badges without a Python source of truth. - [[user-circumstances]] — the operator works from a tent / library, on intermittent connectivity. Local-only, offline-safe, no fonts or assets fetched at runtime. The `/preview` page must render with the network disabled.