Docs

Repository reference

How the RepoOps repository itself is built. The design tokens and the checks that keep their copies honest, the design decisions that look odd until you read the reasoning, the component registry, and the table that says which page, route and endpoint exists today. This is contributor material. If you came to learn the product, start at the docs hub.

Design tokens, type and motion

Every color, radius, shadow, spacing step, font stack and motion duration is a named custom property in one file: public/theme/repoops-tokens.css. Use the token. Do not hand-type a hex. When you need a color the system does not have, the question is not which hue, it is which role: name it --accent-warn, not --orange.

public/theme/repoops-theme.css imports the token file, so every served brain HTML twin and every hand-rolled public/*.html tab picks it up through the one stylesheet link they already carry. Three other surfaces need the same values and cannot import across a build boundary, so npm run copy-theme-tokens generates them:

  • website/app/repoops-tokens.css, imported at the top of website/app/globals.css. The generated copy carries a header above a marker line; the parity check strips the header and compares the rest.
  • website/remotion/theme-values.json, which website/remotion/tokens.ts imports for the video walkthroughs.
  • website/public/theme/repoops-logo.svg, a copy of the brand lockup.

npm run check-theme-parity fails CI when the CSS copy or the Remotion values drift from the source, so a token you change in one place cannot quietly stay old in another.

The families, and what each is for:

  • Surfaces. --bg, --surface, --surface-2, --surface-hover.
  • Text. --text primary, --text-2 body, --muted for eyebrows, captions and table headers. --muted is not decorative: the footer and the status rows render real body copy in it, so both values clear 4.5:1.
  • Lines. --border for card edges, --border-strong, and --border-control for form fields, which need 3:1 where a card border does not.
  • Accents. --amber is the brand and the selected state. --emerald is success, --rose is an error, and --blue is reserved for info badges and dots. Blue is not the link color.
  • Readable amber. --accent-ink is amber dark enough to read as text, and it is the link color on every surface. --accent-glow is the one alive wash, behind a live figure, an alert chip or the play spotlight.
  • Rhythm and shape. --space-1 to --space-6, and the radius ramp --radius-sm, --radius-md, --radius-lg, --radius-card, with --shadow-sm, --shadow-md and --shadow-lg for resting, raised and floating.
  • Aliases. --fg, --accent, --card and --code-bg exist so a brain twin from another generator inherits the palette without its generator changing. --accent resolves to amber; it is not a second action color.

Light and dark ship together. Every color token has a light value in :root and a dark value under prefers-color-scheme: dark. Add both in the same edit. There is no toggle yet, and no phase where dark is coming later.

The type ramp. --font-sans leads with system-ui and --font-mono with ui-monospace. No webfont file is loaded anywhere in the system. Body copy is 15px at a line height of 1.6 on the local shell, the brain twins and the website alike. Rendered prose runs h1 at 1.75rem under a 2px amber rule, h2 at 1.2rem under a hairline, h3 at 1.02rem, and h4 at .92rem in the mono stack. A page title set by the shared page head is 37px, dropping to 32px below 1100px wide, and 1.6rem on a tab that carries the dense modifier.

Motion is a short list. One easing curve, --ease at cubic-bezier(0.22, 1, 0.36, 1), and three durations that all use it: --motion-fast at 120ms for hover and focus rings, --motion-base at 180ms for tab transitions and panels opening, and --motion-slow at 220ms for a page load fade. One looping animation is allowed, and it earns it by carrying status: the freshness dot that pulses while a data tab refreshes in the background. It, the cold-start skeleton shimmer, and every transition stop under prefers-reduced-motion.

Accessibility floor. Body text clears WCAG AA in both themes. Focus is a 2px ring, never hidden. Every interactive element is reachable by Tab in source order. Color is never the only carrier of meaning, so a status color is paired with a text label.

⚠
Do not shadow a shared token locally
npm run check-no-local-root-tokens lints every public/*.html for an inline :root block that redefines a shared token name. That drift class is what once put the dashboard's dark mode out of step with the rest of the product. A domain-specific token that collides with nothing shared is fine.

Design notes and the twin rules

A handful of decisions here look wrong if you do not know why they were made, so the reasoning is written down before someone cleans them up. It lives in .claude/brain/design-lessons-html-over-markdown.html: the case for HTML as the cross-repo contract, then seven lessons from rendering many repositories' brains in one shell, then what stays markdown, then the contract itself.

The seven, in order:

  • Visual coherence across repositories is not negotiable.
  • The aggregator owns the chrome; each repository owns the content.
  • The manifest, not the filesystem, is canonical.
  • Generated and bespoke twins share chrome and differ in ownership.
  • Some twins belong to the aggregator, because no single repository can be canonical for them.
  • The mirror model keeps the open-the-file-directly property working.
  • Match the visualization to the question, at aggregator scale.

The marker convention comes out of the fourth lesson. <meta name="brain-twin-source"> marks a page generated from a .md file, and scripts/brain.mjs owns it. <meta name="brain-twin-bespoke" content="true"> marks a page written by hand, and the generator never overwrites it. The design-lessons page is bespoke: it is an essay, it has no .md source, and normalizing it to markdown would throw away the reasoning it exists to carry.

The architecture map sits beside it and works differently. It is generated, by scripts/gen-architecture-twin.mjs from .claude/brain/architecture-map.json, and it declares that in a brain-twin-generated meta tag. Its route, module and flow tables are written into the file at generation time rather than fetched when the page loads, because every committed twin is served with script-src 'none'. An earlier version fetched them, and five sections rendered empty for about two months before anyone noticed. Edit the JSON and run npm run sync-brain.

Those two are the only HTML files tracked under .claude/brain/. The rest of the brain is markdown, rendered per request, and .gitignore lists exactly these two as exceptions so a stray generated twin cannot be committed.

✓
Read the note before the cleanup
Open this page before any change whose reason is that something should be normalized, tidied, or moved to markdown. If the thing you are about to change has a note, the note is the answer. If it does not, write one with your change so the next contributor inherits the reasoning instead of re-deriving it.

The component registry

The desktop dashboard is plain HTML with no framework, so a component here is whatever you would otherwise re-implement. .claude/brain/components.md is the list. Each row names the file that owns the piece and the tab or page it draws on, so before you change one you can see who breaks. A new row is a signal to look twice: most of the time the right move is widening an existing component, not adding a sibling.

Components live in three places:

  • website/components/ for the hosted dashboard and the marketing site, in React.
  • public/lib/*.js for renderers several desktop tabs share.
  • Inline in public/*.html for something local to a single tab.

The shell contract. The desktop shell (public/dashboard.html) and the hosted shell (website/components/team-sidebar.tsx) had grown into two different species: different active signatures, different badge families, different widths, different search copy. No framework crosses that boundary, so the shared CSS is the contract instead. Seven primitives are declared once, in the token file both sides already load:

  • .repoops-btn with .repoops-btn-primary, the one amber action button.
  • .repoops-card, surface plus border plus card radius plus resting shadow.
  • .repoops-badge with four intents: ok, warn, err, info. One badge species, named by severity rather than by color.
  • .repoops-navitem with .repoops-navitem-active, the one active signature: an amber inset rail and a heavier weight. The old amber background glow is deleted, not deprecated.
  • .repoops-group-eyebrow, the quiet mono uppercase section header.
  • .repoops-table.
  • .repoops-empty with its title and sub classes.

A global :focus-visible ring goes with them, no class needed. Both shells read one rail width from --sidebar-w and show the same search copy. npm run check-shell-contract runs in preflight and in CI and greps both files for the load-bearing class names and strings. It catches a deleted shared class or reintroduced old copy. It is a grep, not a render diff, so it will not catch a pixel regression.

Page anatomy. .repoops-page is the outer container and .repoops-page-head wraps the eyebrow, title and lead so those rules cannot leak into unrelated headings. .repoops-page-dense is the one registered modifier, for a tab whose fold is dominated by a table or a log. Do not invent a second modifier: extend this one, or bring a new one to review first.

Shared renderers name their callers. A module under public/lib/ that more than one tab loads records who loads it, and carries a test file beside it. The fix-decision panel, the production lane badge, the five-check verdict chips, the operator-confirmation header and the held-proposal summary all work this way, so a rule about honesty lives in one place and two tabs cannot answer the same question differently.

Before adding a component: search this registry for the behavior, search public/*.html for the surface, search lib/canonical-tabs.mjs for a duplicate tab, then extend what you found. Add a new component only when extension would be wrong, meaning two different mental models or two different data shapes. A duplicate is a defect. Three similar lines beat a premature abstraction, and a duplicate component is worse than either. A row with nothing using it is an orphan: wire it up or delete it.

Pages, routes and status

.claude/brain/website.md is one table that answers whether something exists yet. Desktop dashboard tabs, hosted repoops.ai surfaces, marketing routes and API endpoints all carry a status: shipped and in use, under active work, planned, or experimental. The status flips in the same commit that ships the code, which is what makes the table worth trusting over a README.

lib/canonical-tabs.mjs is the registry behind it and the ground truth for which surfaces exist. A tab is either source: "aggregator", backed by a file in public/, or source: "repo", which renders a brain markdown file from the tracked repository and falls back to a placeholder when that repository has no such file.

Three invariants, all enforced. scripts/brain-drift/surface-drift.sh, part of the brain-drift CI job, fails the pull request on any of them:

  • No dead tab. Every aggregator tab resolves to its file under public/. Repository-sourced tabs are exempt, because degrading to a placeholder is their designed behavior.
  • No orphan surface. Every public/*.html except the dashboard shell is a registered tab. A page that exists but is wired into nothing gets caught before it ships.
  • No undocumented tab. Every aggregator tab has its own row in the Pages table.

The third check matches the row shape, not the filename. It used to be a substring test over the whole file, which cannot fail for a tab whose filename sits inside another tab's filename. Eleven did. Ten happened to have rows anyway; one did not, and the gate printed a pass for as long as that tab had shipped.

Adding a surface means three edits in one pull request: register it in lib/canonical-tabs.mjs, create the public/ file, and add the Pages row. Retiring one means removing all three. Read the table before proposing a new page: if something close exists, extend it, because a duplicate tab is a defect.

⚠
When CI reports an orphan surface
You shipped a public/*.html and skipped the registry. Wire it into the tab registry and the Pages table, or delete the file. The fix is mechanical. The point is that it was caught at review time rather than by a user finding a page nothing links to.

The other repository reference documents

Everything above describes how the RepoOps repository is built, not how the product is used. It is here for contributors and for agents working in the codebase. For product documentation, start at the docs hub.

Three more internal documents used to have public pages of their own. Those pages were retired on 15 September 2026, and the documents stayed where they always were, in the repository. The strategic vision, which holds the wedge, the moat and the positioning that every roadmap row traces back to, is docs/strategy/strategic-vision.md. The personas, three named users with their goals and the surfaces they work in, are .claude/brain/personas.md. The documentation track, which records which doc pages and walkthroughs exist and what each feature still owes, is .claude/brain/documentation.md.

Each is markdown, hand-edited, with no build step. They were published once because the repository's own brain was rendered wholesale to the public site. Contributor process notes are not product documentation, so they read better next to the code than on a page a customer can land on.

Last updated