StatusFooter now surfaces four signals, not three — add the GET /health liveness dot to the design-system footer contract to match shipped UI.
25 KiB
ADR-0162 — Workbench Design System (v1)
Status: Proposed Date: 2026-05-26 Author: Shay Parent: ADR-0160 — CORE Workbench v1 Companions: ADR-0161 — HITL async queue, ADR-0153 — TurnEvent.trace_hash back-stamp, ADR-0159 — Contemplation-quality eval
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:
--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:
--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:
- 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.
- Lossless string preservation. No smart quotes. No whitespace stripping. No unicode normalization. No HTML entity coercion. What you see is what is on disk.
- No semantic auto-format. Numbers display as the source typed
them (
1e-6≠0.000001). Booleans, null, and integer/float distinctions are preserved. - 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. - 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). - 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:
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:
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…":
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+Kopens the command palette from any routeEsccloses any overlay (drawer, palette, popover)Enteractivates the focused item (open, expand)↑/↓traverse lists;←/→traverse adjacent panels?opens the keyboard-shortcut cheat sheet- Every interactive element has
focus-visiblestyles drawn from--color-focus-ring Taborder matches visual order; explicittabIndexonly where the visual order conflicts with the semantic order- Every drawer/dialog announces a
roleand anaria-label - No interactive surface is reachable only by mouse
The command palette must support, at minimum, fuzzy search across:
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:
+---------------------------------------------------------------+
| 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:
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:
statusfromGET /health(Healthy/Unhealthy/Checking) — a liveness dot polled independently of/runtime/statusso a live server still reads healthy when runtime metadata is degraded; any non-okstatus fails safe toUnhealthymutation_modefromGET /runtime/status(read_onlyorruntime_turn) — color-encoded and labeledgit_revision(short SHA, mono)checkpoint_revision(short SHA, mono) — turns amber whenrevision_warning: trueper 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):
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:
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):
workbench-ui/directory created at repo root withpackage.json,vite.config.ts,tsconfig.json. Pin React 18, Vite 5, TS 5.x, Tailwind 3.x, shadcn primitives. No app routes yet.workbench-ui/src/design/tokens/tokens.css— every token from §1–§4.workbench-ui/src/design/tokens/tokens.ts— typed TS mirror, generated fromtokens.cssat build time (ascripts/generate-tokens.tsreader).workbench-ui/src/design/components/StableJsonViewer/— full implementation honoring all six invariants in §5, with tests.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.workbench-ui/src/design/components/EmptyState/,ErrorState/,LoadingState/— primitives the route screens compose.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".- Frontend tests: token presence, badge-set exhaustiveness against the
Python enums (parsed from
core/epistemic_state.pyandteaching/proposals.pyat build time),StableJsonViewerinvariants 1–6, reduced-motion collapse, keyboard contract for the palette and drawers. - Docs update:
docs/workbench/design-system.mdlinking the ADR,docs/workbench/README.mdrunbook 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:
- Every token in §1–§4 is a CSS custom property with a matching typed TS export, and a test fails if either drifts.
- 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.
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.
- Every primitive component renders an
EmptyState/ErrorState/LoadingStatesnapshot test from a fixture and asserts the "next action" / "reproducer" / "specific label" rules in §6. prefers-reduced-motion: reducecollapses all motion to instant in a Playwright test.- The command palette is reachable via keyboard from every primitive's preview page (Playwright).
- 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
/previewroute 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
EpistemicStatevalue cannot ship without a badge. StableJsonViewerbecomes 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
StableJsonViewerceiling 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 — product doctrine; this ADR is its design substrate.
- ADR-0161 — HITL async queue — the proposal-queue UI's semantic contract (admits ≠ ratifies).
- ADR-0153 — TurnEvent.trace_hash back-stamp —
TraceHashBadgeconsumes this. - ADR-0157 — Revision-mismatch warning —
StatusFooterconsumes this. - ADR-0159 — Contemplation-quality eval —
EvalCenterreads against this lane shape. core/epistemic_state.py— the enum tables in §3a–§3b are bound to this file.teaching/proposals.py—ReviewStatefor §3c.workbench/schemas.py— typed dataclasses the UI mirrors as TS discriminated unions.- 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
/previewpage must render with the network disabled.