Skills
Reusable AI instructions, tuned for this project
These 20 skill files live in .claude/skills/ (most in this repo; only ga-reportlives in my personal skills folder) and encode this project's conventions: component patterns, token rules, navigation wiring, and more. Invoke any skill by name in Claude Code and it follows the exact steps without re-explanation each session. Expand any skill to read the full file, and copy it to adapt it for your own project. Two of them run on their own schedule: see Loops.
new-componentScaffolds a new design system component (a typed React component, a token-only CSS stylesheet, and a Storybook stories file), then registers it in the build-enforced component registry and design.md. Enforces the ds- BEM naming prefix, semantic token usage, and the correct stories format without needing reminders.
new-component.mdmd---
name: new-component
description: Scaffold a new design system component with all required files and registration steps. Use when asked to add, create, or scaffold a new design system component.
---
# new-component
Scaffold a new design system component: the three component files plus the build-enforced registration steps.
## When invoked
Use this skill any time you are asked to add or create a new component to the design system — phrases like "add a [Name] component", "create a [Name] component", "scaffold [Name]".
## Instructions
1. **Ask** for the component name (PascalCase) and a one-sentence description of its purpose, if not already provided.
2. **Read these reference files before writing anything:**
- `src/components/Button/Button.tsx` — structural reference for a button-or-anchor component (own-props split, forwardRef, rest spread, BEM class usage, conditional rendering)
- `src/components/Input/Input.tsx` — structural reference for a form control (native `onChange` + `onValueChange` convenience callback, label/helper/error wiring)
- `src/components/Badge/Badge.css` — CSS token reference (no raw hex/pixels, semantic token usage)
- `src/components/Badge/Badge.stories.tsx` — stories file reference (`satisfies Meta`, `StoryObj`, autodocs)
- `src/tokens/tokens-light.css` — full list of available semantic tokens
3. **Create the directory** `src/components/ComponentName/` and write these three files. Three is the norm, not a limit: a component may split extra modules out beside them (`ShaderField` keeps its hook and its shader source in sibling `.ts` files). If you do, read the packaging note in step 4 — a sibling `.ts` module does **not** get a deep-import subpath for free.
### File 1: `ComponentName.tsx`
- Named export (not default)
- BEM class naming with `ds-componentname` root prefix (e.g. `ds-button`, `ds-badge`)
- Modifier classes follow `ds-componentname--variant` pattern
- Imports CSS: `import "./ComponentName.css"`
- If the component renders as `<a>` when an `href` prop is passed, follow the Button pattern of conditional element rendering
**The component API contract — every one of these, no exceptions.** This package is published to npm, so the props interface is a public contract. Getting it wrong is a breaking change later. `src/components/Button/Button.tsx` (button-or-anchor) and `src/components/Input/Input.tsx` (form control) are the reference implementations.
1. **`'use client'` on the first line** — if and only if the component uses hooks, event handlers, or browser APIs. **Purely presentational components must NOT have it** (see `src/components/Table/Table.tsx`), or consumers lose the ability to render them from a React Server Component.
2. **Split the props type in two.** Own props as a `type`, then an exported `interface` that merges in the native element's props:
```ts
type ComponentNameOwnProps = { /* ...props this component owns... */ };
export interface ComponentNameProps
extends ComponentNameOwnProps,
Omit<React.ComponentPropsWithoutRef<'div'>, keyof ComponentNameOwnProps> {}
```
Add `| 'type'` (or any other attribute the component hardcodes) to the `Omit` list.
3. **`React.forwardRef`** onto the primary DOM node, with `ComponentName.displayName = 'ComponentName'` after it. If the component already keeps an internal ref (focus trap, click-outside, picker trigger), merge them:
```ts
const setRef = (node: HTMLDivElement | null) => {
internalRef.current = node;
if (typeof ref === 'function') ref(node); else if (ref) ref.current = node;
};
```
4. **Spread `{...rest}` onto that same node**, placed *first* so the component's own attributes win. This is what makes `data-testid`, `aria-*`, `autoComplete`, `maxLength` and form-library registration work.
5. **Event handlers keep native React signatures.** `onChange` must be `React.ChangeEventHandler`, never `(value: string) => void` — that shape breaks react-hook-form, Formik and TanStack Form. Put the convenience callback under a name matching the value's shape, and fire both:
| Value shape | Convenience prop |
|---|---|
| string / number | `onValueChange` |
| boolean | `onCheckedChange` |
| array | `onValuesChange` |
```ts
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange?.(e);
onValueChange?.(e.target.value);
};
```
6. **Never invent a prop that shadows a native one with different semantics.** Prefer `variant` (not `priority`/`kind`) and `disabled` (a real boolean, never a value inside a `state` enum). **Figma variant properties are not code props** — `hover` and `active` belong to CSS pseudo-classes, so a `state` prop that includes them has two sources of truth. Where a collision is unavoidable and intentional (`size` vs the native character-width attribute, `title` vs the native tooltip), say so in the prop's JSDoc.
7. **Discard props that would be invalid on the rendered node** rather than spreading them — e.g. `name` on a `<div role="radio">`. Destructure with a `_` prefix (`name: _name`) and document why in the JSDoc; the ESLint config allows `^_`.
8. **Deprecate, never remove.** Keep the old prop working and mark it `@deprecated` with the replacement named — and put a description sentence **before** the tag, or the prop renders as a blank cell (see 9). `className` stays wherever it already is (usually the wrapper) — moving it is a silent visual break.
9. **Give every own prop a JSDoc description — this is build-enforced.** That JSDoc is the single source for Storybook's props table and for the `.d.ts` consumers get; `scripts/validate-prop-docs.mjs` fails the build on a prop without one. Two traps it catches:
```ts
/** Visual treatment */
variant?: 'primary' | 'secondary';
/**
* Legacy alias for `variant`. ← without this line the row renders blank:
* the parser moves the tag and everything
* @deprecated Use `variant` instead. after it into a separate `tags` field
*/
priority?: 'primary' | 'secondary';
```
And **never write a prop `description` into the story's `argTypes`.** Those entries override docgen, so a description there shadows the JSDoc and drifts from it. Stories set `control` and `options` only; the source owns the words.
**If the component is a labelled form control, compose it inside `Field`** (`src/components/Field/Field.tsx`) rather than re-implementing the scaffolding. Field owns the label and its `htmlFor`, the required marker, the helper/error text, the generated ids, and the `aria-describedby` / `aria-invalid` wiring — six components each rolled their own before it existed, and two of them silently diverged (Dropdown announced neither its helper text nor its error state). Pass `className` (your own root classes), `label`, `helperText`, `error`, `required`, `disabled`, `size` and `id`; render the control as children. Field owns no layout, so your root class keeps its own flex/gap.
Two details worth knowing before you reach for it:
- `htmlFor` only associates with **labelable** elements (input, select, textarea, button…). If your control is a composite built from a `div` — a `role="combobox"` trigger, say — point `aria-labelledby` at `` `${id}-label` `` instead, which is the id Field puts on its label.
- Content that belongs *opposite* the helper text (a character counter, a unit) goes in Field's `aside` prop, not a hand-rolled footer.
### File 2: `ComponentName.css`
- CSS custom properties exclusively — **no hardcoded hex colours**, no raw `rgb()`/`rgba()`
- Icons are sized by setting `--icon-size: var(--icon-size-sm|md|lg|xl)` on the icon element — never `font-size` or raw pixel dimensions on an icon (the scale's values live in the token files)
- Transitions/animations compose `--motion-duration-*` with `--motion-ease-*` from `tokens-motion.css` — never literal timings like `0.2s ease` (new code must use the motion tokens from the start)
- All other spacing, padding, gap, border-radius, font sizes must use semantic tokens from `tokens-light.css` / `tokens-typography.css`
- **If a value genuinely cannot use a token** (colour-space physics, a glyph inside a control's geometry, decorative timing tuned by eye), sanction it *at the site*: a `/* ds-allow(<category>): <reason> */` line inside a comment at the value (`ds-allow-file(...)` in the header for file-wide cases), plus a sentence in the component's design.md spec. The category set is the closed list in `scripts/validate-css-directives.mjs` (grammar is build-enforced). **Never add an exception to the token-audit skill** — it reads the directives; it maintains no list
- Section comments grouping related rules (e.g. `/* Base */`, `/* Variants */`, `/* States */`)
- **No dark-theme overrides and no `prefers-color-scheme` queries** — dark mode comes entirely from the semantic tokens (every token has a light and dark value; no component CSS in the library contains a `data-theme` selector)
### File 3: `ComponentName.stories.tsx`
- Import: `import type { Meta, StoryObj } from '@storybook/react-vite'`
- Meta uses `satisfies Meta<typeof ComponentName>`
- `title: 'Components/ComponentName'`
- `tags: ['autodocs']`
- `parameters: { layout: 'centered' }` for compact components; `'padded'` for full-width ones (cards, layouts, nav)
- One named `StoryObj` export per meaningful variant or state combination
- Story names are descriptive (e.g. `Default`, `WithIcon`, `Disabled`, `Small`)
4. **Register the component** in `src/components/registry.json` — add an **object** to the `components` array (alphabetical by `name`):
```json
{ "name": "MyComponent", "label": "My component", "slug": "my-component",
"description": "One line, ending in a full stop, under 160 characters.",
"category": "forms", "client": true }
```
The `description` is shipped copy (it feeds the sidebar, page metadata, and README): a verbless one-line fragment ending in a full stop, written per the register table in `content-design.md`. `category` must be one of the registry's `categories` — add a new one deliberately rather than inventing one per component. `client` must match whether the file declares `'use client'`; the validator compares them and fails on a mismatch, so the registry can never document a component as server-renderable when it isn't. **The sidebar nav entry, sitemap, breadcrumbs, mega-nav and the page's title and description all derive from this entry** — never hand-add a nav link. This file is the single source of truth for the component count; `scripts/validate-component-registry.mjs` runs before every build and **fails if the folder is unregistered**. Run `npm run validate-registry` to confirm — this also regenerates the README's component count/list **and the package barrels** (`src/index.ts`, or `src/charts.ts` if the component imports recharts — the generator routes by detecting the import; never hand-edit either barrel). **Commit the updated `README.md` and regenerated barrel alongside the registration.** The component is then automatically part of the published `@robr0/design-system` API — both the barrel and the `./components/*` deep-import subpath. **That wildcard maps `.tsx` only.** A sibling `.ts` module in the same folder (a hook, a data table, a shader source) reaches consumers through the barrel if the `.tsx` re-exports it, but has no deep-import path of its own until you add one by hand to `SUBPATHS` in `scripts/package-manifest.mjs` — `Avatar/demoAvatars` and `ShaderField/useShaderField` are the two that exist, and both were missed on the first pass. Decide deliberately: a module that is genuinely internal wants no subpath, and one you document as an escape hatch needs one. `scripts/validate-package-exports.mjs` checks the answer either way — an unexported `.ts` sibling must be re-exported by the component or named in that script's `INTERNAL_MODULES` list with a reason. If the component needs a new runtime dependency, stop and ask: the package's only runtime deps are the react peer and the optional recharts peer, and adding one is a packaging decision.
One exception: a **docs-only helper** (a component that exists purely for the website/Storybook docs — the registry's `docOnlyHelpers` array is the authoritative list; note the public `Swatch` component is *not* one of them) goes in the registry's `docOnlyHelpers` array instead of `components` — it gets no showcase page, no barrel export, and doesn't count. Putting it in `components` fails the build for a missing website page.
5. **Document it in `design.md`** — add a short component spec section (class name, tokens used, key behaviours), following the format of the existing component sections.
6. **Hand off the website work.** A component is not done until it has a showcase page: a `page.tsx`, a `page.module.css`, a `layout.tsx` containing exactly `export const metadata = componentPageMetadata("<slug>");`, and a `TocCard` in the grid in `website/src/app/components/page.tsx`. The `TocCard` is the only surface still hand-maintained — each card holds a bespoke live preview. Everything else (sidebar, sitemap, breadcrumbs, title, description) derives from the registry entry. All of it is build-enforced by `scripts/validate-website-surfaces.mjs` and `scripts/validate-page-titles.mjs`. Ask Rob: "Should I add the website documentation page now? (invokes the `component-doc-page` skill)" — and whoever does that work must complete every registration above.
new-pageCreates a new website page by mirroring a live exemplar page's layout shell, then wires it into every place the site tracks pages: the section sidebar and breadcrumbs, with the sitemap deriving automatically. Prevents the common mistake of adding a route without registering it.
new-page.mdmd---
name: new-page
description: Add a new page to the website with the standard layout shell and correct navigation wiring. Use when asked to add or create a new page on the site.
---
# new-page
Add a new page to the website with the standard layout shell and correct navigation wiring.
## When invoked
Use this skill when asked to add or create a new page on the website — phrases like "add a page for [X]", "create a [section] page", "add [X] to the site".
For a **component documentation page**, use the `component-doc-page` skill instead — it covers the variant showcase and component-specific registrations.
## Instructions
1. **Gather requirements** if not already provided:
- Page URL path (e.g. `/foundations/motion`)
- Which section it belongs to — the sidebar arrays in `website/src/config/navigation.ts` are the authoritative list of sections (foundations, the docs cluster, work; writing is fed dynamically from Substack; standalone pages like `/playground` and `/contact` live in no sidebar array and declare their metadata as a literal). **Components are the exception**: `componentsSidebarLinks` is derived from `src/components/registry.json`, so a component page is registered by adding a registry entry, not by editing the array — use the `component-doc-page` skill for those.
- Page title, a short `subDisplay` tagline, and a 1–2 sentence description (for metadata and the intro block)
- Figma URL and Storybook path (optional) — for `PageLinks`
2. **Read the exemplars before writing anything.** The live pages are the source of truth for structure — mirror them rather than writing a shell from memory:
- `website/src/app/skills/page.tsx` — a standard content page (layout shell, sidebar wiring, header/intro blocks, entry animations)
- `website/src/app/components/button/page.tsx` + `page.module.css` + `layout.tsx` — the richest example, with `PageLinks` and per-page CSS
- `website/src/config/navigation.ts` — nav config (single source of truth for sidebars, mega menu, and breadcrumbs; the sitemap derives its routes from the sidebar configs)
3. **Create the directory** `website/src/app/<path>/` with three files, mirroring the exemplar:
### File 1: `page.tsx`
- Copy the exemplar's shell exactly — same components, same nesting, same class names. Don't improvise structure.
- Invariants the exemplar can't teach:
- `subDisplay` is a *tagline* inside the intro block (e.g. the Skills page's "Reusable AI instructions, tuned for this project") — not the section name; the breadcrumb already shows where you are
- All copy on the page (tagline, intro, body, metadata description) follows `content-design.md` — voice, register, and the words-to-avoid tables
- Sidebar links come from `getSidebarLinks(<section>SidebarLinks, "<your path>")`
- Include `PageLinks` only if Figma/Storybook URLs exist
- **Do not render a background.** `BlurBackground` is mounted once in the root layout and covers every route; adding it per page would build a second canvas and a second GL context on top of the first. `scripts/validate-single-background-mount.mjs` fails the build if you do
### File 2: `page.module.css`
- Copy the exemplar's layout classes; add page-specific classes as needed
- Semantic design tokens only — no hardcoded colours or magic values
- No `ch`-based `max-width` on prose — doc paragraphs run the full content column; the layout column is the only width constraint (build-enforced by `scripts/validate-page-titles.mjs`)
- Mobile type and section rhythm collapse at the **token layer** (display sizes and section-gap tokens step down at 768px system-wide) — do not add per-page `@media` overrides for tokenized values; when a page genuinely needs a breakpoint, use the canonical set in `design.md`'s responsive spec
### File 3: `layout.tsx`
- Sidebar-registered pages export `metadata` via the shared helper: `export const metadata = pageMetadata("<your path>", "<one-line description>")` (import from `@/config/navigation`)
- Standalone pages (`/playground`, `/contact` — pages in no sidebar array) export a literal `Metadata` object instead, with an explicit `alternates.canonical` — see `website/src/app/playground/layout.tsx`
- **Deliberately hidden pages are the exception to both rules**: a test bench or scratch page that must stay dark sets `robots: { index: false, follow: false }`, skips the canonical *and* the sitemap entirely, and records why in a comment beside that metadata — in its `layout.tsx`, or in the `page.tsx` itself when the page needs no layout of its own (`/covers` is the precedent for the simpler shape). A full-viewport or immersive page that should also render none of the shared chrome (the layout-mounted footer and chat panel) additionally adds its route to `CHROMELESS_ROUTES` in `website/src/config/chromeless.ts` — the set in that file is the authoritative list of what has taken the exception. A hidden page also needs an entry in `EXCLUDED_ROUTES` in `scripts/generate-site-corpus.mjs` with a written reason, or `validate-chat-coverage.mjs` fails the build for an uncovered route. Suppressing chrome does **not** make the background full-bleed: by default every page gets the 450px band that fades into the page floor. An immersive page must also render `<FullBleedBackground />` (exported from `website/src/components/BlurBackground/BlurBackground.tsx`), a hidden marker that CSS in `globals.css` reads to drop the fade and fill the viewport. Skip it and the page is chrome-free but band-limited, which looks wrong with nothing to explain why
- Only **component** pages are build-enforced (`scripts/validate-page-titles.mjs` requires `componentPageMetadata("<slug>")` there); for everything else the helper is convention, not a gate — follow it anyway
- Default export wraps `{children}` in a fragment
4. **Register the page everywhere the site tracks pages:**
- **Sidebar** (only if the page belongs to a section): add `{ href, label }` to the section's array in `website/src/config/navigation.ts`, matching that array's existing order convention (foundations and docs are curated in reading order; work is curated newest-first; components are derived from the registry — see the exception in step 1)
- **Work pages are a second exception**: a `/work/<slug>` page must also be registered in `website/src/data/case-studies.json` (top of the list if it is the newest — `/work` and the home page both derive from that order), with every field `scripts/validate-case-studies.mjs` requires (its `REQUIRED` list is authoritative — `href`, `title`, `dek`, `companyName`, `companyLogo`, `coverSrc` today) and the logo/cover assets in `website/public`. The validator fails the build for an unregistered case-study folder, so this is a gate, not a convention
- **Standalone pages** (no sidebar section): no array to edit — but the sitemap then knows nothing about the route, so add it as a top-level literal in `website/src/app/sitemap.ts` (the `/playground` entry is the pattern), and extend `dsActiveMatchers`/`dsMegaItems` only if it belongs under the Design system umbrella. If it is a top-level site page a visitor should reach from anywhere, also add it to the `siteLinks` array in `website/src/components/SiteFooter/SiteFooter.tsx` — the one footer column that is hand-maintained (of the other four, three derive from the nav config and Elsewhere from `PROJECT_LINKS` in `website/src/config/social.ts`). Deliberately hidden pages (see File 3) register nowhere — no sitemap, no nav, no matchers, no footer
- **Sitemap** for sidebar-registered pages: automatic — it derives from the sidebar configs, so the entry above covers it
- **Breadcrumbs**: sub-pages of an existing section resolve automatically from the sidebar entry. Only if the page starts a *new* section: add a `breadcrumbSections` entry, and if it lives under the Design system umbrella, extend `dsActiveMatchers` (and `dsMegaItems` if it should appear in the mega menu)
5. **Verify**: load the page in the browser and confirm the sidebar highlights it, the breadcrumb trail is correct, and both themes render properly.
visual-reviewOpens the site in a browser preview, drives each page through both light and dark mode at desktop and mobile widths, and screenshots them. Checks for invisible text, broken layouts, overflow, and stuck hover states, then reports findings or confirms all clear.
visual-review.mdmd---
name: visual-review
description: Start the website dev server and screenshot pages in both light and dark mode, at desktop and mobile widths, to catch visual issues. Use when asked to visually review changes, check light and dark mode, or screenshot pages.
---
# visual-review
Start the website dev server and screenshot pages in both light and dark mode, at desktop and mobile widths, to catch visual issues.
## When invoked
Use this skill when asked to visually review changes — phrases like "check how this looks", "review light and dark", "does this look right", "screenshot the page", "visual check".
## Instructions
Use the browser/preview tools available in the current environment for every step below — this skill describes *what* to do; map it to whatever tools the harness currently provides. Never launch the dev server through a raw shell command.
1. **Determine which URLs to review.** If not specified, default to the page(s) most recently modified in the current conversation. Ask if unclear.
2. **Open the website's Next.js dev server** (the `website` configuration in `.claude/launch.json`; port 3000 by default, but the config sets `autoPort`, so read the URL the preview actually reports rather than assuming 3000) in the browser preview and wait for it to be ready.
3. **For each URL, check both themes at both viewports.** The site's theme is driven by the `data-theme` attribute on `<html>` — not by `prefers-color-scheme`, so forcing the browser's colour scheme does nothing. To switch: click the theme toggle in the top nav (`MegaNav`, top-right), or set the attribute programmatically. Verify the attribute actually changed before screenshotting, then take a screenshot in each theme. Repeat at a mobile viewport (~375px wide — mobile is a first-class surface: type and spacing collapse at the token layer, and navigation moves into a drawer): screenshot both themes there too, and on at least one page open the drawer nav, expand a section, and screenshot it open.
4. **Examine each screenshot for:**
- Text that is invisible or the same colour as its background
- Components that appear broken, overflow their container, or clip
- Spacing that looks inconsistent or misaligned compared to other pages
- Hover/focus states that appear stuck in an active state
- Images or assets that failed to load (broken image icons)
- Any layout that differs unexpectedly between light and dark
- At mobile width: horizontal overflow (a page that scrolls sideways), content clipped by the viewport, and drawer navigation that fails to open, scroll, or close
5. **Report findings** concisely:
- Format: `[URL] [dark|light] [desktop|mobile] — description of issue`
- If no issues found, say: `[URL] — looks correct in both themes at both widths`
6. **Stop the preview server** when all pages are reviewed, unless the session is still using it.
## Key context
- Theme state lives on `document.documentElement` as `data-theme="light"` or `data-theme="dark"`; it persists via the localStorage key `theme` and is applied before first paint by an inline script in the root layout
- The theme toggle is rendered by `MegaNav` (top-right of every page)
- The sitemap footer (`SiteFooter`) and the chat button/panel are site chrome mounted once from the root layout, not per page — expect both in every screenshot's lower region. On the mobile pass, check the footer's collapse: a brand block beside four link columns at desktop, then 3 columns and 2 as the width drops, with the brand block leaving the row and sitting above the links at the same 960px breakpoint the page's nav rail disappears at. The footer is identical on every page, so a difference between two pages is a finding; the brand block's width matching the rail is deliberate, not a coincidence to report
- The routes in `CHROMELESS_ROUTES` (`website/src/config/chromeless.ts`) deliberately render neither the footer nor the chat — their absence there is not a finding
- The `animate-in` class on page elements triggers CSS entry animations — these are normal on first load
- The ambient background (`BlurBackground`) is layout-mounted chrome too, and it is the largest thing in every screenshot. Three of its behaviours produce **false findings** if you do not expect them:
- **It has two renderers.** A WebGL2 field normally, the CSS blobs underneath as the fallback. Which one you capture depends on the machine's GPU, so the same page can legitimately screenshot two different ways on two runs. A background that differs between runs is not a finding; a *broken-looking* one is.
- **There is a moment with no background at all.** While the renderer resolves, the blobs are hidden and the canvas has not faded in. A screenshot caught in those first frames shows bare page floor. Let the page settle before capturing.
- **Most pages get a 450px band, not a full screen.** Only pages rendering `FullBleedBackground` fill the viewport; grep `data-bg-full-bleed` for the current set rather than trusting a list here. Judge each page against its own variant.
- To rule the background in or out of a finding, `?tune=1` on any page in dev opens its control panel, which reports the live renderer and lets you A/B the shader against the CSS blobs
token-auditScans CSS files for hardcoded hex colours, raw rgb() values, pixel values, and transition timings that should reference design tokens. Reports file, line number, offending value, and recommended token replacement. Accepts a single component, all-components, or website as scope.
token-audit.mdmd---
name: token-audit
description: Scan CSS files for hardcoded values that should use design tokens, and report violations. Use when asked to check for hardcoded values, raw colours or pixel values, or audit token usage and design system compliance.
---
# token-audit
Scan CSS files for hardcoded values that should use design tokens, and report violations.
## When invoked
Use this skill when asked to check for hardcoded values, audit token usage, find raw colours or pixel values, or check design system compliance — phrases like "check for hardcoded values", "token audit", "are there any raw colours", "audit [component] CSS".
## Instructions
1. **Determine scope.** Accept one of:
- A specific component name (e.g. `Avatar`) → scans `src/components/Avatar/Avatar.css`
- `all-components` → scans all `src/components/**/*.css`
- `website` → scans all `website/src/**/*.css` (which includes the `.module.css` files)
- A specific file path
**The site background is outside every CSS scope, and deliberately so.** Its eight colours are token *names* in `website/src/data/shader-background.json`, resolved at runtime and handed to the GPU — no CSS file names them, so a `website` scan will pass without ever looking at what actually picks the site-wide background palette. Do not hand-audit it: `scripts/validate-shader-background.mjs` already fails the build if any blob references a token that is not in the registry, which is a stronger guarantee than this skill can offer. Say so in the report rather than leaving a reader to assume the surface went unexamined.
2. **Read the token files in `src/tokens/` first** to know what tokens are available and what raw values they map to — primitives (raw hex/px), the light *and* dark semantic files, typography (font size, weight, line-height), and motion (`tokens-motion.css` — durations and easings). The fastest authoritative index is the **generated** `src/tokens/registry.json` — every semantic token with its category and per-theme values, machine-readable; read it instead of parsing the CSS by hand (never edit it — it regenerates from the CSS).
3. **Scan each CSS file** in scope for violations:
**Flag as violations:**
- Hardcoded hex colours: `#rrggbb`, `#rgb`, `#rrggbbaa`
- Raw `rgb()` or `rgba()` calls that could map to a semantic colour token
- Pixel values for `padding`, `margin`, `gap`, `border-radius`, `font-size`, `line-height` that correspond to a known token (cross-reference the primitives file)
- Hardcoded font weights (e.g. `font-weight: 600`) where a typography token exists
- Icon sizing done wrong: `font-size` set directly on a Material Symbols icon, or raw pixel icon dimensions matching an `--icon-size-*` step — the fix is `--icon-size: var(--icon-size-sm|md|lg|xl)` (the icon font reads that one property for size, width, and height; the scale's values live in the token files)
- Hardcoded `transition`/`animation` durations and easings (`0.2s`, `ease`, literal cubic-beziers) where a `--motion-duration-*`/`--motion-ease-*` token matches — component and website CSS is fully migrated, so any literal timing is a violation unless a directive sanctions it
**Do NOT flag:**
- Files within `src/tokens/` themselves (these define the tokens)
- `0px`, `0`, `100%`, `50%` — these are structural, not token-replaceable
- **Any value sanctioned by a `ds-allow` directive** — the one and only signal that an off-token value is deliberate. `/* ds-allow(<category>): <reason> */` inside a comment covers the declaration/rule/section it sits at; `/* ds-allow-file(<category>): <reason> */` in a file header covers the whole file for that category. Enumerate the current sanctions with `grep -rn "ds-allow" src website/src --include='*.css'` (the same scope `validate-css-directives.mjs` scans); the category set and grammar are build-enforced by `scripts/validate-css-directives.mjs`. This skill deliberately names no components: an off-token value with **no** directive is a violation, and the fix is either a token or a new directive at the site (plus a design.md note) — never an exception added to this skill
- `1px` border widths — acceptable
- Values inside `calc()` that are genuine arithmetic, not replaceable with a single token
- CSS variable declarations themselves (lines starting with `--`)
4. **For each violation**, output:
- File path (relative to repo root)
- Line number
- The offending value
- Recommended token replacement (if a clear match exists in the token files)
Format: `path/to/file.css:42 — #118AB2 → var(--color-action-primary-bg)`
5. **Summarise** at the end:
- `X violation(s) found`
- `Y acceptable raw value(s) noted (documented exceptions)`
- If zero violations: "No token violations found. CSS is token-compliant."
content-auditScans shipped prose against content-design.md: banned words, em dashes, promotional register, first person where the system should be the subject, and rhythm problems no word list catches. Reports each finding with its location, the offending text, and a suggested rewrite. Accepts a page, a data file, or a whole surface as scope.
content-audit.mdmd---
name: content-audit
description: Audit prose against content-design.md for AI-writing tells, voice violations, and register mismatches. Use when asked to audit copy, check content quality, review prose for AI slop, or check writing against the content guide.
---
# content-audit
Audit prose against `content-design.md`, and report violations with suggested rewrites.
## When invoked
Use this skill when asked to audit copy, check prose quality, find AI-writing tells, or review text against the content guide — phrases like "content audit", "audit the copy on the homepage", "does this read like AI", "check this against content-design.md".
## Instructions
1. **Determine scope.** Accept one of:
- A specific file path (a page, a data file, a markdown doc)
- `site-updates` → `website/src/data/site-updates.json` (titles + story bodies)
- `registry` → the `description` fields in `src/components/registry.json`
- `case-studies` → the `title` and `dek` fields in `website/src/data/case-studies.json` (shipped copy on /work and the home page)
- `readme` → `README.md` prose plus `src/stories/Configure.mdx`
- `skills` → the `displayDescription` frontmatter strings across `.claude/skills/` (they render on /skills, so they are published copy; skill instruction *bodies* are out of scope)
- `website` → user-visible strings in `website/src/app/**` page files; a page slug (e.g. `about`) scopes to that page folder
- `chat` → the site chat's shipped prose: the greeting and starter-suggestion copy in `website/src/components/SiteChat/`, and the persona and easter-egg strings in `website/src/app/api/chat/` (visitor-visible through the chat panel, but not "page files", so the `website` scope misses them; the answers' register row lives in the guide's Register by Surface table)
- `footer` → the sitemap footer's shipped copy: the column titles and copyright in `website/src/components/SiteFooter/SiteFooter.tsx`, and the link labels in `website/src/config/social.ts` (visible on every non-chromeless page, but outside `app/**`, so the `website` scope misses them — the same gap the `chat` scope closes for the panel)
2. **Read `content-design.md` first — it is the only rule source.** The Words to Avoid and Patterns to Avoid tables, the Voice rules, the Register by Surface table, and the Microcopy section are the checklist. This skill deliberately maintains no word list and no pattern list of its own: when the guide changes, the audit changes with it. If a rule seems missing, the fix is an entry in `content-design.md` (per its Iteration Guide), never a rule added here.
3. **Scan the scoped prose** and classify every finding at one of three severities:
**Banned** (the guide allows no use in shipped copy):
- Hard-ban words and phrases from the Words to Avoid table
- Em dashes anywhere in shipped copy. `scripts/validate-shipped-prose.mjs` already fails the build on these, so a clean tree means the surfaces it reads are clear and you are checking the ones it cannot judge: the chat's persona and greeting strings, and any prose outside its scope (its doc block is authoritative). Report a hit there as Banned exactly as before
- Title Case in shipped headings, buttons, or labels
- First person in surfaces whose register says "None" (check the Register by Surface table for the scoped surface)
- Emoji, exclamation marks in UI copy, unsourced statistics
**Rationed** (legitimate in a narrow sense; flag for a density check):
- Words from the Rationed table — flag every use, note which look literal, and count per page
- American spellings in prose (colour/color and friends) — never flag code identifiers, token names, CSS properties, or file paths
**Judgment** (needs a reader, not a regex — quote the passage and say why):
- Rhythm uniformity: three or more similar-length sentences in a row
- Rule-of-three adjective stacks, copula avoidance, participial tails, negative parallelism, hedge stacking, elegant variation, bolded-label bullets, summary closers, throat-clearing openers
- Register mismatches: promotional tone in a neutral surface, a tagline restating its section name, an empty state describing absence instead of the next action
4. **Never flag:**
- `content-design.md` itself, and quoted examples anywhere (a rule must be able to name what it bans)
- Skill instruction bodies, `design.md`, `CLAUDE.md` — agent-facing references, out of scope by design (see the guide's Overview)
- Code, identifiers, token names, class names, and anything inside backticks or code fences
- Text authored by third parties (external-skill copies keep their upstream voice)
5. **For each finding**, output:
- File path (repo-relative) and line number, or the entry label for JSON surfaces
- Severity, the offending text, and the guide rule it breaks
- A suggested rewrite that keeps the sentence's meaning and any links intact
Format: `website/src/app/example/page.tsx:42 — banned — "a seamless theming journey" → "theming by overriding one primitive"`
6. **Summarise** at the end:
- Counts per severity, then the strongest single finding
- If nothing is found: "No content violations found. Prose follows content-design.md."
- Run the guide's three Self-Review Tests over the longest passage in scope and report the result, pass or fail
pre-deployRuns the same checks as CI before a push to Vercel: lint, the library type-check, the publishable npm package build, every Storybook story as a render *and* accessibility test (Vitest + headless Chromium + axe), the Storybook build, and the website lint + build (Next.js). Knows the npm-workspace layout and watches for SSR-unsafe code, portal regressions, and static generation failures.
pre-deploy.mdmd---
name: pre-deploy
description: Run the full local verify (lint, library build, story tests, Storybook build, website lint, website build) and confirm the site is safe to push to Vercel. Use when asked whether changes are ready to push, deploy, or ship, or for a pre-deploy check.
---
# pre-deploy
Run the full local verify and confirm the site is safe to push to Vercel.
## When invoked
Use this skill when asked to check if changes are ready to push, deploy, or ship — phrases like "is this ready to push?", "run the build", "pre-deploy check", "check before I push".
## Instructions
1. **Run the full verify** from the repo root:
```
npm run verify
```
This is the single source of truth for local checks and mirrors the CI jobs in `.github/workflows/ci.yml`. It runs, in order: ESLint, the library type-check, the publishable package build (`build:lib` — vite lib mode + d.ts into `dist/`), the story tests (every Storybook story rendered in headless Chromium, **with axe asserting WCAG 2.1 AA on each one** — an accessibility violation fails the suite exactly like a render error), the Storybook build, the website lint, and the website build. The registry validators run via the builds' `prebuild` hooks, so a registry-drift failure surfaces before the compiles even start. The `validate-registry` entry in the root `package.json` is the authoritative list of what runs; read the failing script's own doc block for what it guards, because the failures do not share a family resemblance — one means an unregistered component, another that a prose edit removed a fact the chat eval depends on (`validate-chat-coverage.mjs`), another that the ambient background references a colour token that no longer exists (`validate-shader-background.mjs`). This list used to be enumerated here and went stale twice; a pointer cannot. The prebuild also regenerates the derived surfaces owned by the `validate-registry` chain (the generator scripts at the front of its entry in the root `package.json` are the authoritative list) — if any come out modified, commit them with the work that changed their source.
Note: the repo is an npm workspace — one `npm install` at the root covers the website too, and the website resolves `@robr0/design-system` through a symlink to the repo root. The website build is a plain `next build`; there is no separate install step inside `website/`.
2. **Check the output of each step for:**
**Lint failures:**
- Any ESLint `error` lines (warnings don't fail the run, but mention them)
**TypeScript errors:**
- Any `error TS` lines
- Type mismatches, missing props, invalid imports
**Next.js-specific issues:**
- `"use client"` missing on components that use browser APIs (`window`, `document`, `localStorage`, `useEffect`, `useState`, etc.)
- SSR-unsafe code running outside client guards — particularly watch `website/src/app/layout.tsx` (the inline `themeScript`)
- Portal/modal components that reference `document` at module or render scope — these have caused past static-build failures (AlertDialog and Toast both needed fixes; watch for regressions if portal-based components change)
- Pages that fail static generation (look for `Error occurred prerendering page`)
**General failures:**
- Any non-zero exit code
- `Build failed` or `Compiled with errors`
3. **Report result:**
If everything passes:
> Verify passed (lint + story tests + library, package, Storybook, and website builds). Safe to push.
**If the change touched component CSS, `src/tokens/`, or `.storybook/`, also dispatch the Chromatic workflow** (`gh workflow run chromatic.yml`) and check the diff before or right after pushing — visual regressions are the one thing `verify` cannot see, and Chromatic is deliberately not part of it because every run bills cloud snapshots against a monthly budget. Text-only, script-only, or website-prose changes don't need a run.
If any step fails, show:
- Which step failed (lint, component library, story tests, Storybook, website lint, or website build)
- If **story tests** failed, say whether it was a render error or an **a11y violation** — they surface identically but are fixed differently. An axe failure names the rule (e.g. `button-name`, `nested-interactive`) and the offending markup; contrast is deliberately excluded from the gate, so a contrast complaint means someone re-enabled `color-contrast` in `.storybook/preview.ts`
- The exact error message(s)
- File path and line number if available
- A brief diagnosis of likely cause
4. **Do not push** — this skill only checks and reports. Pushing is Rob's decision.
shipMakes finished work live on robertritacca.com. Surveys the tree so unrelated files never get swept into a commit, runs the full local verify (lint, story tests, and the library, package, Storybook, and website builds, mirroring CI) before anything is committed, merges branch work into main when needed, pushes, confirms the CI run goes green, and reports exactly what deployed.
ship.mdmd---
name: ship
description: Make finished work live on robertritacca.com. Commit, run the full verify, merge branch work into main when needed, push, and watch CI go green. Use when asked to ship it, make it live, push to main, or deploy this. If asked to "merge and push" (a retired skill name), confirm the intended end state — ship, checkpoint, or land — before acting.
---
# ship
Make finished work live on robertritacca.com — builds green first, no unrelated files swept in, a clear report after. The end state is always the same: the work is on `main`, pushed, and deployed.
## When invoked
Use this skill when asked to make completed work live — phrases like "ship it", "make it live", "push to main", "deploy this". This skill always ends with a deploy; to save progress without deploying, that's `checkpoint` (or `park` to also return to main).
**If asked to "merge and push"**: that's the retired ambiguous skill name, and it now maps onto more than one verb. Confirm the intended end state before doing anything: `ship` (merge into `main`, push, deploy), `checkpoint` (push the branch, keep working), or `land` (combine several pieces of pending work into a local `main`, pushing nothing).
## Instructions
0. **Check the branch**: `git branch --show-current`.
- **On `main`**: follow steps 1–7 directly. **A push to main deploys robertritacca.com.**
- **On any other branch**: the work rides the branch into `main`. Follow steps 1–4 on the branch (commit there), then merge in step 5. Never cherry-pick or copy files across branches to avoid a merge.
1. **Survey the tree before touching anything**: run `git status --short` and classify every entry:
- **In scope** — files created or modified as part of the work just completed in this session
- **Out of scope** — anything untracked or modified that predates the session, or that wasn't part of the requested work
**Never run `git add -A`, `git add .`, or `git add` on a directory** — always add explicit file paths. Out-of-scope files are excluded by default and named in the final report; if it's genuinely unclear whether something belongs, ask before including it.
2. **Run the full verify before committing**:
```bash
npm run verify # lint + library type-check + package build + story tests + Storybook build + website lint + build
```
This one script is the single source of truth for local checks and mirrors the CI jobs in `.github/workflows/ci.yml` — if CI gains a check (tests, a11y), it gets added to `verify`, never listed here separately. The registry validators run automatically via the builds' `prebuild` hooks.
**Run it plainly and let its own exit status be the verdict. Never pipe verify through `tail`, `head`, or `grep`** — a pipeline reports the last command's exit code, not verify's, and that exact mistake masked two red builds on 2026-07-26. **If any step fails, stop** — fix the failure if it was caused by this session's work, otherwise report it. Never push red.
Note: the build regenerates the derived surfaces owned by the `validate-registry` chain — the generator scripts at the front of the `validate-registry` entry in the root `package.json` are the authoritative list of what gets rewritten. If a generated file changed after the builds, it changed because this session's work made it stale — treat it as in scope and commit it alongside the edits that caused it (CI's drift guard is a `git diff --exit-code` step after the generators run in `.github/workflows/ci.yml`, so an uncommitted regeneration fails the build).
3. **Delta-scoped prose check** — the step that keeps drift audits boring. For the identifiers this session's diff touched (component names, prop names, script names, moved/deleted paths), grep the prose surfaces — `.claude/skills/`, `README.md`, and every root spec (the doc list in `scripts/validate-doc-refs.mjs` is the authoritative set; it includes tracked specs that are not published to /blueprints, which the `FILES` array in `scripts/sync-blueprints.mjs` deliberately omits) — and judge whether any claim just became false (`README.md` ships in the npm tarball, so a false claim there reaches every consumer). Any prose the session wrote or rewrote also follows `content-design.md` (run its Self-Review Tests on anything longer than a sentence). Fix what did in the same push; `validate-doc-refs` catches dead references mechanically, but only a reader catches a sentence that is now wrong.
4. **Group changes into logical commits** — one commit per concern, not one giant commit. Match the repo's conventional style (`feat(scope):`, `fix(scope):`, `chore(scope):`), with a 1–3 sentence body explaining the why. Check `git log --oneline -5` if unsure of the voice.
5. **Merge (branch case only)**: with the branch committed and verify green:
```bash
git checkout main
git pull --ff-only
git merge <branch>
```
A fast-forward or a merge commit are both fine. **If the merge conflicts, stop and report** — never resolve conflicts silently as part of a ship. Never force-push to make a merge "work".
6. **Push**: `git push` on `main`. Remember: **a push to main deploys robertritacca.com via Vercel** — pushing is publishing.
If the pushed work changed component CSS, anything under `src/tokens/`, or `.storybook/`, offer to dispatch Chromatic (`gh workflow run chromatic.yml`) — `verify` proves nothing about pixels, and this is the decision point pre-deploy's Chromatic rule exists for. It bills cloud snapshots, so it's an offer, not an automatic step.
Chromatic only snapshots Storybook, so it says nothing about the website. If the pushed work touched the ambient background — the config (`website/src/data/shader-background.json`), the site's composition of it (`website/src/components/BlurBackground/`), or the renderer itself (`src/components/ShaderField/`) — offer a `visual-review` pass instead: any of the three changes the background on all of the site's pages at once, and no automated gate covers them. `verify` proves the config validated and the shader compiled, not that the result looks right. The renderer is the easiest of the three to miss, because it lives in the library rather than the website and Chromatic's Storybook snapshots do not cover a full-viewport site background.
Same pattern for the chat: if the pushed work changed what the site chat answers from or how it answers — the corpus sources (page prose feeds `site-corpus.generated.ts` by construction), the persona or guardrails in `website/src/app/api/chat/`, or the route itself — offer to run the answer-quality eval (`npm run eval:chat`, ritual in `evals/chat/README.md`). `verify` proves the corpus regenerated, not that the answers stayed good, and the eval costs real API spend — an offer, not an automatic step.
7. **Confirm CI went green**: after the push, watch the GitHub Actions run to completion:
```bash
gh run watch $(gh run list --workflow=ci.yml --branch main --limit 1 --json databaseId --jq '.[0].databaseId') --exit-status
```
The `--workflow=ci.yml` filter is load-bearing: a push to main also triggers CodeQL within the same second, and an unfiltered `--limit 1` can hand you that run instead. `--exit-status` makes a red run exit non-zero rather than reporting and returning 0. CI runs in parallel with the Vercel deploy — it gates nothing, but a red run on main means something the local verify missed (or an environment difference) and must be investigated, not left as a red X.
**Branch case, after CI is green**: delete the merged branch — `git branch -d <branch>`, and `git push origin --delete <branch>` if it was pushed. Its commits are on `main`; the repo stays main-only by default. Name the deletion in the report.
8. **Report** in the final message:
- Each pushed commit (hash + subject), confirmation `npm run verify` passed locally, and the CI run result
- Every file deliberately left out and why
- Anything the deploy will visibly change on the live site
- Branch case: the merge and the branch deletion
## Guardrails
- Never force-push, never rewrite pushed history
- Never commit `ga-analysis/output/`, `ga-analysis/service-account.json`, `.env*`, or anything credential-shaped — even if explicitly staged by mistake
- If there is nothing in scope to commit and nothing unmerged on the branch, say so and stop — don't invent a commit
- Shipping is Rob's call: only invoke this flow when asked to ship, and never chain into it automatically from other work
checkpointSaves work in progress to a remote branch as a safety net, then stays on that branch so work continues. It never touches main and never deploys: if invoked on main it moves the work to a new branch first, because pushing main publishes the site. A quick lint runs before the push and failures are flagged without blocking the backup.
checkpoint.mdmd---
name: checkpoint
description: Save work in progress to a remote branch and keep working. Never touches main and never deploys; if invoked on main it moves the work to a new branch first. Use when asked to checkpoint, save progress, back this up, or push to the branch.
---
# checkpoint
Save the session's work-in-progress to a remote branch, then keep working on it. Nothing merges, nothing deploys, `main` is never touched. The end state: the work is safely on GitHub and the session stays on the branch.
## When invoked
Use this skill when asked to save unfinished work — phrases like "checkpoint", "save my progress", "back this up", "push to the branch". When the work is finished and should go live, that's `ship`; to save and *stop* working on it, that's `park`.
**If asked to "merge and push"**: that's the retired ambiguous skill name, and it now maps onto more than one verb. Confirm the intended end state before doing anything: `ship` (merge into `main`, push, deploy), `checkpoint` (push the branch, keep working), or `land` (combine several pieces of pending work into a local `main`, pushing nothing).
## Instructions
1. **Check the branch**: `git branch --show-current`.
- **On a work branch**: commit and push there (steps 2–5).
- **On `main`**: move the work to a branch first — there is no "push to main but don't deploy", because a push to `main` publishes robertritacca.com. Create a branch named for the work, not the date: `git checkout -b wip/<short-topic>` (e.g. `wip/nav-search`, `wip/chart-tokens`). Uncommitted changes ride along automatically. Never commit directly to `main` from this skill.
2. **Survey the tree**: run `git status --short` and classify every entry as in scope (this session's work) or out of scope (predates the session or wasn't part of the requested work). **Never run `git add -A`, `git add .`, or `git add` on a directory** — always add explicit file paths. Out-of-scope files stay out and are named in the report.
3. **Quick check, not the full verify**: run `npm run lint`; if the session touched `website/`, also run `npm --prefix website run lint` (root ESLint ignores the website workspace, so the root lint alone carries no signal about website work). This is a backup, not a release — the full `verify` (several minutes) is `ship`'s job. **A lint failure does not block the push**: the whole point is that the work is saved even mid-mess. But flag any failure loudly in the report so it isn't a surprise at ship time.
4. **Commit in the repo's conventional style** (`feat(scope):`, `fix(scope):`, `chore(scope):`, with a short why in the body) — checkpoint commits eventually reach `main` through a merge, so they are real history, not throwaways. One commit is fine if the work is one concern; split if it's clearly several.
5. **Push the branch**: `git push -u origin <branch>` (same-named remote branch). Pushing a branch publishes nothing — no deploy, no site change.
6. **Stay on the branch** and report:
- The branch name and each commit (hash + subject)
- Lint status, including any failure being flagged rather than fixed
- Every file deliberately left out and why
- The closing line: work is backed up, session continues on the branch; say `ship` when it should go live, `park` to set it aside
## Guardrails
- Never touch `main`: no commits to it, no merges into it, no pushes of it
- Never force-push, never rewrite pushed history
- Never commit `ga-analysis/output/`, `ga-analysis/service-account.json`, `.env*`, or anything credential-shaped — even if explicitly staged by mistake
- If there is nothing in scope to commit, say so and stop — don't invent a commit
- Never chain into `ship` automatically — going live is always a separate, explicit ask
parkShelves an experiment: commits the session's work, pushes it to a remote branch, then returns to a clean main. Nothing deploys and nothing merges; the report names the branch so the work is easy to resume later.
park.mdmd---
name: park
description: Commit and push the session's work to a branch, then return to a clean main. Nothing merges and nothing deploys. Use when asked to park this, shelve this, or set an experiment aside for later.
---
# park
Save the session's work to a remote branch, then step off it and return to a clean `main`. Same safety net as `checkpoint`, different end state: the session ends back on `main`, with the experiment shelved under a named branch.
## When invoked
Use this skill when asked to set the current work aside — phrases like "park this", "shelve this", "set this aside". To save and *keep* working on the branch, that's `checkpoint`; to make the work live, that's `ship`.
**If asked to "merge and push"**: that's the retired ambiguous skill name, and it now maps onto more than one verb. Confirm the intended end state before doing anything: `ship` (merge into `main`, push, deploy), `checkpoint` (push the branch, keep working), or `land` (combine several pieces of pending work into a local `main`, pushing nothing).
## Instructions
1. **Get the work onto a branch**: `git branch --show-current`.
- **On `main`**: create a branch named for the work, not the date — `git checkout -b wip/<short-topic>` (uncommitted changes ride along). Never commit to `main` from this skill.
- **On a work branch already**: stay on it.
2. **Survey the tree**: run `git status --short` and classify every entry as in scope (this session's work) or out of scope. **Never run `git add -A`, `git add .`, or `git add` on a directory** — always add explicit file paths. Note that out-of-scope untracked files are untouched by branch switches — they will still be sitting in the tree after the return to `main`; name them in the report.
3. **Quick check, not the full verify**: run `npm run lint`; if the session touched `website/`, also run `npm --prefix website run lint` (root ESLint ignores the website workspace). A failure does not block the park — a shelved experiment is allowed to be mid-mess — but it goes in the report so resuming starts with eyes open.
4. **Commit in the repo's conventional style** (`feat(scope):`, `fix(scope):`, with a short why) — a parked branch may later merge to `main` via `ship`, so its commits are real history.
5. **Push the branch**: `git push -u origin <branch>`. Pushing a branch publishes nothing.
6. **Return to main**: `git checkout main`, then confirm with `git status --short` that the tree is clean (out-of-scope untracked files excepted).
7. **Report**:
- The branch name — this is the resume handle; say it plainly ("to pick this back up, ask to resume `wip/<topic>`")
- Each commit (hash + subject), lint status, files left out and why
- Confirmation the session is back on a clean `main` and nothing deployed
## Guardrails
- Never touch `main`: no commits to it, no merges into it, no pushes of it
- Never force-push, never rewrite pushed history
- Never commit `ga-analysis/output/`, `ga-analysis/service-account.json`, `.env*`, or anything credential-shaped — even if explicitly staged by mistake
- If there is nothing in scope to commit, say so and stop — don't invent a branch
- Never delete the parked branch — it is the only copy of the shelved work
landResolves every piece of pending work in the repo in one pass. Sweeps worktrees, branches, uncommitted changes and stashes, reads each one to judge whether it is finished, superseded or abandoned, and proposes a disposition for all of it at once: land, keep, or delete. Approved work merges into a local main one branch at a time and the combined result runs the full verify; anything deleted is archived to a recoverable tag first, and nothing is ever pushed.
land.mdmd---
name: land
description: Triage every piece of pending work in the repo and resolve all of it in one pass. Sweeps worktrees, local and remote branches, the working tree, and stashes; assesses each one as worth landing, worth keeping, or junk; then merges what lands into a local main, archives and deletes the junk, and verifies the combined result. Never pushes and never deploys. Use when asked to land the work, combine parallel sessions, clean up branches and worktrees, or work out what is worth shipping.
---
# land
Resolve **all** the pending work in the repo in one pass. Some of it is finished and should ship, some is half-built and should wait, some is stale and should go. `land` finds every piece, forms a view on each, gets one decision from Rob, and leaves the repo in the state that decision implies.
The end state: a local, unpushed `main` carrying the work that was worth keeping and passing `npm run verify`, with everything discarded archived to a recoverable tag and every branch and worktree that no longer earns its place gone.
Fourth verb in the shipping vocabulary. `ship` makes one line of work live, `checkpoint` saves it, `park` shelves it; `land` is the one that runs when several of them have piled up and it is no longer obvious what is worth shipping.
## When invoked
Use this skill when pending work has accumulated and needs sorting out — phrases like "land the work", "combine my branches", "clean up the branches", "what's worth shipping". It is the right skill whether the work arrived from parallel sessions, from experiments that were parked and forgotten, or from a working tree that drifted.
**If asked to "merge and push"**: that's the retired ambiguous skill name, and it now maps onto more than one verb. Confirm the intended end state before doing anything: `ship` (merge into `main`, push, deploy), `checkpoint` (push the branch, keep working), or `land` (combine several pieces of pending work into a local `main`, pushing nothing).
**It never pushes and never deploys.** Landing is local, deliberately: a batch of accumulated work reaching robertritacca.com is a decision, not a side effect of tidying up. When `main` is landed and green, `ship` takes it live.
## Instructions
### 1. Establish the base
```bash
git branch --show-current
git rev-parse main
git fetch --prune
```
**Record the `main` SHA before anything moves.** It is the unwind handle for step 8, the diff anchor for step 7, and belongs in the final report; nothing else can restore a half-landed `main`.
Then check divergence with `git rev-list --left-right --count main...origin/main`. If `main` is behind, `git pull --ff-only`. If it has genuinely diverged, stop and report: something pushed elsewhere, and reconciling that is its own decision.
The primary checkout should be on `main`. Uncommitted changes there are **not** a blocker — they are a candidate like any other, handled in step 2. Do not bounce out to `checkpoint`.
### 2. Sweep every source of pending work
Five places work hides. Sweep all of them; a session that ended may have left any combination.
```bash
git status --short # the working tree
git worktree list --porcelain # worktrees (exclude the primary checkout)
git worktree prune --dry-run -v # registrations whose directory is gone
git branch --no-merged main # local branches carrying work
git branch -r --no-merged origin/main # remote branches carrying work
git branch --merged main # merged leftovers: cleanup only, nothing to land
git stash list # forgotten stashes
git tag -l 'archive/*' # what past runs already archived
```
- **The working tree is a candidate.** Uncommitted changes usually contain more than one concern. Classify them into piles by what they belong to, not by file type, and treat each pile as its own candidate. This repo's generated files (see step 6) often span piles, because regenerating reflects everything in the tree at once.
- **Merged branches** carry nothing. They skip triage and go straight to disposal.
- **Stale worktree registrations** are normal, not a problem to investigate: agent worktrees auto-remove when they end unchanged.
- **Existing `archive/*` tags** are reported, not acted on. They are the recovery trail from earlier runs. Only propose pruning one when its work has demonstrably shipped by another route.
### 3. Read each candidate, then judge it
```bash
git log --oneline main..<branch> # what it carries
git log -1 --format='%ci (%cr)' <branch> # how old the tip is
git diff --stat main...<branch> # how wide it reaches
git -C <worktree-path> status --short # what is NOT in those commits
```
**Read the diff, never the branch name.** Agent branches are auto-named (`claude/musing-sanderson-5a94ee`) and say nothing about their contents. Even hand-named branches describe the intent at creation, not what survived.
Then test whether `main` has moved out from under it — the difference between *old* and *stale*:
```bash
git log --oneline main --not <branch> -- $(git diff --name-only main...<branch>)
```
Commits here are changes `main` made to the very files the candidate touches. A short list means the work still applies. A long list, or a rewrite of the same component, means the candidate is probably superseded and its conflicts are not worth resolving.
Form an actual view on each candidate, and say it plainly in step 5:
- **Finished** — coherent, complete, matches current conventions. A landing candidate.
- **Unfinished** — mid-build, or its own notes record it as still being tuned. Keep, do not land.
- **Superseded** — `main` solved this another way, or rewrote underneath it. Delete.
- **Abandoned** — old, narrow, and nothing since referenced it. Delete.
**The dirty-worktree rule.** Uncommitted changes in a worktree are not on its branch, so merging the branch silently drops them and removing the worktree destroys them. Treat any dirty worktree as a session possibly still in flight: name every uncommitted file, and never land or remove it on an assumption. Committing another session's half-finished work is not this skill's call.
Do not run `npm install` in a worktree, and do not lint one with no `node_modules`. A fresh worktree has none, installing root plus the website workspace in each is slow, and step 8's combined `npm run verify` carries the real signal.
### 4. Predict the collisions
For every landing candidate, before anything moves and without touching the working tree:
```bash
git merge-tree --write-tree --name-only main <branch>
```
Exit 0 is clean; exit 1 conflicts. On a conflict the first output line is the merged tree's object id, not a path — conflicted paths are the lines after it, up to the blank line. Read the exit status directly and **never pipe this through `head`**, for the same reason `verify` is never piped: the pipeline reports the wrong command's status.
Two honesties for the report: predictions are against **today's** `main`, so once one branch lands the rest are provisional and get re-predicted before each merge; and two branches that each merge cleanly can still conflict with each other.
### 5. Propose a disposition for everything, then confirm once
Present **one table covering every candidate**, with a proposed disposition and the reason for it:
| Candidate | Kind | Age | Carries | Conflicts | Proposed | Why |
|---|---|---|---|---|---|---|
Dispositions are exactly three:
- **Land** — merge into `main` in this run
- **Keep** — leave exactly as it is, changing nothing
- **Delete** — archive to a tag, then remove the branch and worktree
Follow the table with a short plain-English reading of anything non-obvious, especially every **Delete** proposal. A delete needs a stated reason, not just an age.
**Confirming at scale.** The candidate count is unbounded, and `AskUserQuestion` caps at four options, so never ask per candidate. Ask **one** question about the triage as a whole, with options along the lines of: accept as proposed; accept the landings but keep everything marked delete; land nothing and only dispose; stop and change nothing. Rob names exceptions in free text ("delete these 4", "keep cosmic-wind"). Apply any exceptions, re-present the amended table in two or three lines, and proceed without asking a second full question.
The default is that **nothing lands and nothing is deleted until it is named**. Silence is not approval.
If predicted conflicts make the order matter, recommend one: fewest conflicts first, so the hardest merge happens against the most complete `main` and gets resolved once.
**Mixed candidates.** When a branch is partly worth landing, offer to land a subset by `git cherry-pick <sha>` rather than forcing all-or-nothing, and archive the full branch before deleting the remainder. This is the one place cherry-picking is sanctioned; `ship` bans it because there it means dodging a merge, which is a different act from deliberately selecting commits.
### 6. Land what lands
One branch at a time, each as its own merge:
```bash
git merge <branch>
```
**Never octopus-merge** (`git merge a b c`): it refuses outright on any conflict and leaves history that cannot be bisected. Never rebase another session's branch. Never force anything.
Uncommitted piles that were approved for landing get committed on `main` in the repo's conventional style (`feat(scope):`, `fix(scope):`, `chore(scope):`), one commit per concern, with a 1–3 sentence body explaining the why. **Never `git add -A`, `git add .`, or `git add` on a directory** — always explicit paths. When piles must be committed separately but share generated output, stash the other pile by pathspec, regenerate, commit, restore, regenerate, commit — so each commit carries a generated state that matches its own sources.
**Conflict policy** — three cases resolve mechanically, one never does:
- **Generated surfaces** (`website/src/data/site-corpus.generated.ts`, `website/src/data/skills-content.generated.ts`, `src/index.ts`, `src/charts.ts`, `src/tokens/registry.json`, the marked regions of `README.md`, the blueprint copies under `website/public`): never hand-merge. Take either side to get a resolvable tree, run `npm run validate-registry` to rebuild them from the merged sources, and commit the result. The generator scripts at the front of the `validate-registry` entry in the root `package.json` are the authoritative list of what gets rewritten. A hand-merged generated file is wrong even when it looks right, and CI's drift guard catches it later at a worse moment.
- **Registry entries that are lists** (`src/components/registry.json`, `.claude/skills/registry.json`, `website/src/data/site-updates.json`, `website/src/data/case-studies.json`): two branches each adding an entry is a textual conflict, not a semantic one. Keep both, then restore the ordering the registry requires (components alphabetical by `name`; skills and case studies in their curated order). Dropping one side is a silent feature loss no validator can see.
- **Registries that are a single tuned state**, not a list — `website/src/data/shader-background.json` is the current example: keeping both sides is meaningless and actively breaks things. Its `blobs` array is a fixed-size set the shader's `BLOB_COUNT` must match, and its `params` are one coherent look, so a merged pair fails `validate-shader-background.mjs` or produces a design nobody chose. Treat two sessions retuning the background as a semantic conflict and use the stop-and-ask rule below.
- **Version bumps** (`PACKAGE_VERSION` in `scripts/package-manifest.mjs`, the root `package.json` version, `package-lock.json`): keep the single higher version, make all three agree, refresh with `npm install --package-lock-only`. `scripts/validate-package-exports.mjs` fails the build when they disagree.
- **Source, CSS, and prose conflicts**: stop and ask. A semantic conflict between two sessions' intentions is not resolvable from inside a batch cleanup.
### 7. Check the prose the landed work invalidated
Landed work can make a sentence elsewhere in the repo false, and no validator can see it. This runs before the verify so its fixes are covered by the same green run.
`land` has an exact anchor for the scope, which `ship`'s equivalent check does not — it works from "this session's diff", while here everything landed in this run is one range:
```bash
git diff --name-only <pre-land-sha>..main
```
From that diff, take the identifiers it touched — component and prop names, script names, token names, moved or deleted paths, and any rule the work made stricter — and grep the prose surfaces for them: `.claude/skills/`, `README.md`, and every root spec (the doc list in `scripts/validate-doc-refs.mjs` is the authoritative set). Then judge whether any claim just became false.
Two classes matter most, because both actively mislead:
- **An instruction that now contradicts the build.** The worked example: a page moved from a curated subset to full coverage with a validator enforcing it, while `CLAUDE.md` still told the next agent that skipping an entry was fine. Following the doc would have produced a build failure the doc called acceptable. Whenever landed work makes a rule *stricter*, the instructions that describe the old latitude are the first place to look.
- **A false claim in `README.md`**, which ships inside the npm tarball and so reaches every consumer.
Fix what drifted, in this run, as its own `docs(...)` commit. Any prose the landing itself wrote follows `content-design.md`. `scripts/validate-doc-refs.mjs` catches dead references mechanically, but only a reader catches a sentence that is now wrong.
### 8. Verify the combined result
```bash
npm run verify
```
Run it plainly and let its exit status be the verdict. **Never pipe it through `tail`, `head`, or `grep`** — a pipeline reports the last command's exit code, not verify's. This is the step the skill is built around: each candidate may have been green alone, and only the combined result is what would ship. Run it even when nothing merged, if anything at all was committed to `main`.
**If verify fails**: nothing is pushed, so nothing is broken in public. Identify the merge or commit that introduced it, report it, and offer to unwind to the SHA from step 1 with `git reset --hard <pre-land-sha>`. That is destructive, so it is offered and confirmed, never automatic. **Dispose of nothing while verify is red** — an unwound merge needs its branch and worktree to still exist.
### 9. Dispose of the junk
**Archive before deleting. Always, without being asked.**
```bash
git tag archive/<short-name> <branch>
```
A tag keeps the commit reachable permanently, costs nothing, never appears in `git branch`, and is not pushed unless someone asks. It is what makes deleting unmerged work a reversible act, and it is the difference between a cleanup and a loss. Name tags for the work, not the branch (`archive/cosmic-wind`, not `archive/claude-musing-sanderson`). Do this for every candidate marked **Delete**, including remote-only ones, before a single deletion runs.
Then:
```bash
git worktree remove <worktree-path>
git branch -d <branch> # merged branches
git branch -D <branch> # unmerged, only once archived and approved
git push origin --delete <branch>
git worktree prune
```
- `git worktree remove` **never** takes `--force`. If it refuses, the worktree is dirty: return to step 3's rule and ask. Note that the permission classifier may block a force-removal anyway, so a dirty worktree that must go is finished by Rob in his own terminal, not retried here.
- `git branch -D` is sanctioned **only** for a candidate that was archived in this step and explicitly approved for deletion. Everywhere else, `-d`, and its refusal is the safety net.
- Deleting a remote branch is the one outward-facing action in this skill. It removes work from GitHub, so it runs only against an approved, archived candidate, and every deletion is named in the report.
For every candidate marked **Keep**: change nothing. No commits, no deletions, no tidying of stray files. Name each in the report with its branch and worktree path so it stays a resume handle.
### 10. Report
- **Landed**: each branch or pile, its merge or commit, and a one-line summary of what it carried
- **Kept**: each one, why, and its resume handle
- **Deleted**: each one, its `archive/*` tag, and the recovery line — `git checkout -b <name> archive/<tag>`
- Conflicts resolved, how, and which were regeneration rather than a judgement call
- Prose the landed work invalidated, and the fix — or explicitly that nothing drifted
- The `npm run verify` result and the pre-land SHA as the unwind handle
- Branches and worktrees removed, local and remote
- Closing line: `main` carries unpushed commits and nothing deployed. Say `ship` to take it live, or leave it local.
## Guardrails
- **Never push, ever** — not `main`, not a branch. This skill ends local. Deploying is `ship`, always a separate ask
- **Never delete anything that was not archived first.** No `-D`, no remote deletion, no worktree removal without a tag already written
- Never remove a worktree with uncommitted changes, and never reach for `--force`, `git reset --hard`, or `git checkout -f` inside a worktree this session did not create. Those changes are the only copy, and another session may still be writing them
- Never land, commit, or delete anything not explicitly approved, and never read "clean up" as blanket permission to delete
- Never commit another session's uncommitted work to make a merge clean. Report it and stop
- Never hand-merge a generated file; never drop one side of a registry conflict; never resolve a source conflict silently
- Never commit `ga-analysis/output/`, `ga-analysis/service-account.json`, `.env*`, or anything credential-shaped, however it arrived in a merge
- If there are no candidates, say so and stop. A clean sweep is a valid outcome
releaseCuts an npm release of the component library: bumps the single source-of-truth version, runs the Release workflow in dry-run to prove a real consumer can install and build the tarball, publishes with signed provenance, then tags and writes the GitHub Release against the exact commit that shipped. Knows the two things that bite on release day: a version number can never be reused, and the registry lags a green publish by minutes.
release.mdmd---
name: release
description: Cut a new npm release of @robr0/design-system. Bump the version, dry-run, publish via the Release workflow, then tag the published commit. Use when asked to cut a release, publish a new version, or ship the package to npm.
---
# release
Cut a new npm release of `@robr0/design-system` — bump, dry-run, publish, tag.
## When invoked
Use this skill when asked to cut a release, publish a new version, or ship the package to npm — phrases like "cut a release", "publish the next version", "ship the package".
**This is the one workflow in this repo where a mistake is permanent.** npm never lets a version number be reused, even after unpublishing, so a botched publish burns that version forever. Read the guardrails before starting.
## Instructions
### 1. Decide the version
Read `PACKAGE_VERSION` in `scripts/package-manifest.mjs` — that constant is the *authoritative* version; two other files mirror it (see step 3) and `validate-package-exports.mjs` fails the build when they disagree (the root package.json version is a hand-maintained mirror; `dist/package.json` is generated from the manifest). Then pick the next version from what actually changed since the last release:
- **patch** — bug fixes, internal refactors, docs
- **minor** — new components, new exports, new tokens (additive)
- **major** — a renamed/removed prop, export, or token; anything a consumer must edit code for
Check what shipped since the last tag to justify the choice, and confirm it with Rob before bumping:
```bash
git log $(git describe --tags --abbrev=0)..HEAD --oneline
```
Be deliberate about breaking changes: components are exported both from the barrel and from `./components/*` deep paths, so a renamed component folder breaks consumers even if the barrel still exports the old name.
### 2. Pre-flight
- **Working tree must be clean and pushed.** The workflow builds from the repo, not your disk — anything uncommitted will not be in the release. `git status --short` and `git log origin/main..HEAD` should both be empty.
- **CI on `main` must be green.** A release from a red main ships known-broken code.
- Run `npm run verify` if anything at all is uncommitted or you haven't verified since the last change.
### 3. Bump and commit
**Three files carry the version — edit the first two, regenerate the third:**
1. `PACKAGE_VERSION` in `scripts/package-manifest.mjs` — the source of truth for what ships.
2. `"version"` in the root `package.json` — **must be kept in sync by hand.** No generator writes it. The root package.json stays `private: true` forever; only the generated `dist/package.json` is published, but the parity check still gates every build.
3. `package-lock.json` — npm records the version there too. Refresh it with `npm install --package-lock-only` and commit it with the bump; a stale lockfile dirties every later plain `npm install` (the 0.3.0 bump shipped without this and broke the worktree-based skills' cleanup).
`scripts/validate-package-exports.mjs` fails the build when any of the three disagree.
**Two hand-maintained release-history mentions ride along with the bump** — nothing generates or validates either, so this step is the only thing keeping them true: the release-history sentence in `CLAUDE.md` (CI & Local Verify section) and the history comment at the top of `.github/workflows/release.yml`. Add the new version, date, and a short what-shipped clause to both in the bump commit.
Then:
```bash
npm run validate-registry
```
This re-runs the three-way version parity check and regenerates the derived surfaces (no generated surface embeds the version literal itself, but the corpus mirrors any prose the bump edited, including that CLAUDE.md sentence). Commit the bump on its own (`chore(release): <version>`) and push — the commit you push here is the commit that will be published and tagged.
### 4. Dry run — never skip this
```bash
gh workflow run release.yml -f dry_run=true
```
Watch it to completion — but confirm you are watching the run you just dispatched, not the previous one. Registration lags the dispatch by a few seconds, so `--limit 1` immediately after `gh workflow run` can return the prior run (at step 5 that prior run is the green dry run, and watching it reads as a successful publish):
```bash
sleep 10
gh run list --workflow=release.yml --limit 3 --json databaseId,createdAt,displayTitle # newest first — check createdAt is after your dispatch
gh run watch <the-new-databaseId> --exit-status
```
The dry run does everything except upload: builds `dist/`, packs the tarball, installs it into a scratch Vite + React app and **builds that app without recharts installed** (the optional-peer path — the regression this catches), then prints the publish preview. It needs no npm token, so it is free to run as often as you like. Read the preview's file count and package size and sanity-check them against the previous release; a sudden jump means something got swept into the tarball. Expect `LICENSE` and `README.md` alongside the build output; anything else new is not deliberate.
### 5. Publish
```bash
gh workflow run release.yml -f dry_run=false
```
Watch it the same way (the same watch-the-right-run caution applies, and matters more here). The publish step runs `npm publish --access public --provenance --loglevel verbose` from `dist/` — the verbose flag is deliberate, because its log lines are the only visible evidence of the OIDC token exchange the auth guardrails below tell you to look for — and signs a provenance attestation tying the tarball to this repo, commit, and workflow run.
### 6. Verify — and do not panic at a 404
**The registry lags a successful publish by several minutes.** A `404` from `npm view` right after a green workflow is propagation, not failure. Confirm the workflow's publish step actually ran (`gh run view <id> --json jobs`) and look for `+ @robr0/design-system@<version>` in its log — if that line is there, it published. **Never re-run the workflow on a 404**; the version is already consumed and the rerun will fail with `EPUBLISHCONFLICT`.
Once it propagates, confirm the registry serves the new version:
```bash
npm view @robr0/design-system version --prefer-online
```
The real consumer-shaped check already ran before publish: `scripts/smoke-consumer.mjs` packs the tarball into a scratch Vite app and builds it (bare `node` can't import the barrel because components import their own CSS; that needs a bundler). To repeat it against the *published* artifact rather than the local tarball, scaffold a scratch Vite app, `npm install @robr0/design-system@<version>`, import the barrel plus `tokens/tokens.css`, and run its build.
### 7. Tag the published commit
Tag the commit that was **published**, not necessarily current HEAD — the provenance attestation names that commit, so the tag, attestation, and tarball should all agree:
```bash
git tag -a v<version> <published-sha> -m "v<version> — <one-line summary>"
git push origin v<version>
gh release create v<version> --verify-tag --title "v<version> — <short title>" --notes-file <notes>
```
Write the notes for a consumer, not a maintainer: what's new, anything breaking with the migration step spelled out, and the install snippet. The previous release is the format reference; the prose follows `content-design.md`.
### 8. Report
Version published, the npm URL, the tagged commit, the release URL, and anything a consumer must do to upgrade.
## Guardrails
- **Never** publish from a local machine (`npm publish` by hand) — releases go through the workflow so every release is provenance-signed and smoke-tested
- **Never** re-run the publish workflow after a successful publish, even if the registry 404s
- The version lives in **three** places and only one is authoritative: bump `PACKAGE_VERSION` in `scripts/package-manifest.mjs`, mirror it into the root `package.json` (nothing generates it), and refresh `package-lock.json` with `npm install --package-lock-only`. Never bump `package.json` alone — the manifest is what ships
- Never publish from a dirty tree, an unpushed commit, or a red CI
- **Auth is Trusted Publishing (OIDC) — there is no npm token to expire or rotate.** If the publish step fails to authenticate, the cause is one of: `actions/setup-node` was given a **`registry-url:`** (see below); the `id-token: write` permission was dropped from `release.yml`; the workflow file was **renamed or moved** (the trusted-publisher registration on npmjs.com is keyed to the filename `release.yml`); the npm CLI on the runner is older than 11.5.1; or the registration itself was removed. Rob owns anything that has to change on npmjs.com.
- **A `404` on `PUT` during publish is an AUTH failure, not a missing package.** npm returns 404 instead of 403 so it doesn't leak whether a package exists — the message even says "or you do not have permission to access it". Do not go hunting for a missing package; check the auth path.
- **Never add `registry-url:` to `actions/setup-node` in this workflow.** It writes an `.npmrc` with `_authToken=${NODE_AUTH_TOKEN}` *and* exports a placeholder `NODE_AUTH_TOKEN`, so npm thinks it is already authenticated, skips the OIDC exchange entirely, and gets rejected. The tell in the log: no mention of `oidc`/`trusted` anywhere, and `NODE_AUTH_TOKEN: XXXXX-XXXXX-XXXXX-XXXXX` in a step's env block. Provenance signing still succeeds in this state — that is GitHub→Sigstore and proves nothing about npm accepting the token.
- **A dry run never authenticates**, so it cannot prove OIDC is working — it exercises the build and the tarball, nothing else. After any change to `release.yml`'s auth, permissions, or filename, the only real proof is a genuine publish. Treat that as a reason to make the *next* release a small patch, not a reason to skip the dry run.
- The npm README is the package README — if the release changes install or usage, fix `README.md` in the same release, since it ships inside the tarball. Fix `src/stories/Configure.mdx` too: it carries the same install snippet on the Storybook landing page, deploys separately, and is where people evaluating a component arrive. `generate-readme-content.mjs` enforces that both mention the package name, but it cannot tell whether the *snippet* is still correct
drift-auditSweeps every place the repo describes itself (skills, CLAUDE.md, design.md, the README that ships to npm, and the website's own explanations of how it is built) and flags anything that no longer matches reality. Executes the commands and recipes the docs prescribe rather than just reading them, so a skill that would break on the next run is caught before someone runs it. Reports findings grouped by severity.
drift-audit.mdmd---
name: drift-audit
description: Comprehensive self-consistency audit after a structural or architectural change. Verifies every skill, doc, and website surface still describes the repo as it actually is. Use after big changes, or when asked whether the docs and skills are up to date.
---
# drift-audit
Verify that every self-description in the repo — skills, docs, website prose — still matches how the repo actually works.
## When invoked
Use this skill after any structural or architectural change (a new build step, a moved directory, a changed dependency model, a new published surface), or when asked whether the docs and skills are still accurate — phrases like "run a drift audit", "are the docs up to date", "check for gaps".
## The governing idea
Build validators already catch everything *mechanically checkable*. This audit exists for the layer beneath them: **prose that asserts something about the system, and instructions that only fail when someone follows them.** A skill telling you to run a deleted npm script passes every validator and every build — it fails silently, months later, for whoever runs it.
**Do not trust this file's own description of the architecture.** It deliberately contains no *inventory* facts — no counts, paths, component lists, or versions, only the command entry points needed to derive them — because inventory facts would rot too. Derive the current shape from the sources of truth (package.json scripts, registries, the validator chain, the exports map) every time.
**Verify by executing, not by reading.** The most valuable findings come from actually running what a doc prescribes. Reading a worktree recipe looks fine; running it surfaces that the bundler now rejects it.
## Instructions
### 1. Establish the current reality first
Before judging any prose, build an accurate picture of what is true *right now*:
```bash
npm run validate-registry # what the automated chain enforces, and what it prints
node -e "console.log(Object.keys(require('./package.json').scripts).join('\n'))"
node -e "console.log(JSON.stringify(require('./package.json').exports, null, 2))"
git log --oneline -20
```
(Heads-up: `validate-registry` **writes** — its leading generator scripts regenerate the derived surfaces; the script entry in the root `package.json` is the authoritative list of what. Check `git status` before and after, so regenerated output isn't mistaken for a finding.)
Read the root `package.json` (scripts, dependency model, workspaces), the validator scripts named in `validate-registry`, and the recent commits. The recent commits tell you *what kind* of drift to hunt for — a dependency-model change implicates install instructions everywhere; a renamed route implicates nav, sitemap, and cross-links.
If `validate-registry` fails, stop and report that first — the automated layer is broken and everything downstream is unreliable.
### 2. Mechanical cross-checks
These are checkable by grep and should be exhaustive. For each, the question is "does the thing this text references still exist?"
- **Every command referenced in prose exists.** Collect `npm run <script>` mentions across `*.md`, `.claude/skills/**`, and `website/src/**`, and diff against the real script list. A referenced-but-missing script is a broken instruction. **Check every workspace's scripts, not just the root** — a mention may be workspace-scoped (`--workspace <name>`, or preceded by a `cd`), which a naive grep reports as missing when it is perfectly valid.
- **Every file path referenced in prose exists.** Extract path-looking strings from README.md, every root spec (the doc list in `scripts/validate-doc-refs.mjs` is the authoritative set — it includes tracked specs that are not published to /blueprints; `FILES` in `scripts/sync-blueprints.mjs` is only the published subset), and every SKILL.md, and test each one. Moved or deleted files leave dangling references. Expect noise and filter it before reporting: bare filenames used conversationally (`globals.css`), scaffolding placeholders (`ComponentName.tsx`, `my-component/page.tsx`), date placeholders (`YYYY-MM-DD.md`), and shorthand for a pair (`tokens-light/dark.css`) are all fine. Only a path that *claims* to point at something real and doesn't is a finding.
- **Every import specifier in docs matches the real exports map.** Any `import … from "…"` in documentation or example code should resolve against the package's current `exports` (or be an obvious third-party import). Renamed aliases and scopes hide here.
- **Internal links resolve.** Route strings in website prose (`/foundations/...`, `/playground`) should correspond to real app directories, and the nav config should agree.
- **Counts come from registries, never literals.** Grep displayed numbers near countable nouns; each should be an imported constant.
- **Config still applies where it is declared.** A restructure can leave a config block sitting somewhere the tool no longer reads, and nothing warns you — it just silently stops taking effect. Check that declared intent matches installed reality: dependency `overrides`/`resolutions` (npm honours these **only** in the workspace root), engine constraints, lint and TS config inheritance, and bundler aliases. For dependency pins specifically, compare the declared range against what is actually installed (`npm ls <pkg>`) and run `npm audit` — pins are usually security fixes, so one that stops applying is a silent regression, not a style issue.
### 3. Prose surfaces — read against reality
For each surface, the test is: *if a stranger followed this exactly, would it work, and would what they believe afterwards be true?*
- **README.md** — highest stakes: it ships inside the npm tarball, so its install and usage instructions reach every consumer. Verify the install command, import examples, customization recipes, and local-dev steps against the real package.
- **CLAUDE.md** — the project's operating manual: structure diagram, quick start, command list, registries/generated surfaces, architecture invariants, infrastructure facts. Every generated surface must be listed with its markers and its generator.
- **design.md** — design language claims and per-component specs. Check that stated invariants are still enforced and that specs match the components.
- **content-design.md** — the content style guide. Check that its Register by Surface table still lists every prose surface that exists, that its pointers at skill-owned standards still land, and that no rule in it duplicates one CLAUDE.md owns (fact-architecture rules live in CLAUDE.md, style rules here — a rule restated in both is drift).
- **Website self-descriptions** — any page that explains how the system is built (the overview/pipeline, get-started and docs pages, foundations pages). These are public claims; treat inaccuracy as a bug.
- **Other root specs** — any tracked root spec beyond the three above, published or not (the doc list in `scripts/validate-doc-refs.mjs` is the authoritative set; `FILES` in `scripts/sync-blueprints.mjs` lists only the ones published to /blueprints). Each makes claims about code it describes; spot-check its heavily-referenced facts the same way, and check that its published-vs-repo-only status is stated where readers would assume otherwise.
- **Blueprint copies** — if the repo publishes copies of its own docs, confirm they are generated rather than hand-maintained, and that they regenerated.
### 4. Skills self-audit — the highest-yield section
Read **every** `SKILL.md`, not just the ones that seem related. For each, ask:
- Does every command it prescribes still exist and still do what it claims?
- Does every path it references still exist?
- Does it describe a workflow that a tooling change has since broken? **Where a skill scripts a multi-step recipe (worktrees, builds, deploys), actually execute it in a throwaway location and confirm it completes.** Clean up afterwards.
- Does it tell the reader to hand-edit something that has since become generated?
- Does it describe one-time setup that is now complete, or a future state that has since arrived?
- Does its section/category list omit anything added since it was written?
- Does it duplicate a fact that lives in a registry, instead of pointing at the registry?
Then check for **missing coverage**: is there now a repeated, consequential workflow with no skill? Recent commits are the evidence — a ritual performed manually twice is a skill-shaped hole, especially where mistakes are expensive or irreversible.
### 5. Consumer and privacy surfaces
- **What ships externally.** If the repo publishes a package, inspect the built artifact — its manifest, its file list, its size, and the docs inside it. Personal data hides in doc comments that become type declarations.
- **No secrets or personal data in public surfaces.** Sweep tracked files and the built artifact for credential-shaped strings, private emails, tokens, keys, and absolute local paths. Distinguish deliberately-public identifiers (an analytics measurement ID visible in page source by design) from genuine leaks, and say which is which rather than crying wolf.
### 6. Report
Group by severity, most actionable first. Every finding needs a file path (with line number where it applies), what it currently says, why that is wrong now, and the fix.
```
## Drift Audit
### Broken — following this would fail
- path/to/SKILL.md:42 — prescribes `npm run <deleted-script>`; replaced by X in <commit>
### Stale — inaccurate, would mislead
- CLAUDE.md:88 — structure diagram still lists a deleted directory
### Gaps — missing coverage
- No skill covers <repeated consequential workflow>
### Verified accurate
- <surfaces checked and found correct — say so explicitly, so the reader knows the scope>
### Summary
X broken · Y stale · Z gaps · N surfaces verified
```
State plainly what you **executed** versus what you only **read** — an unverified pass is weaker evidence, and the reader deserves to know which they are getting.
Then ask whether to apply the fixes. Do not fix silently as you go: the report is the deliverable, and some findings are judgement calls (a "gap" may be deliberate scope).
## Guardrails
- Never edit generated files to resolve a finding — fix the generator or its source, then regenerate
- Never weaken a validator to make a finding disappear
- If a finding is mechanically checkable and keeps recurring, the real fix is a **new validator in the `validate-registry` chain**, not a docs edit — recommend that explicitly (this repo's convention: anything countable or checkable gets build-enforced so it can never drift again)
- Report honestly when a surface was skipped or a check was inconclusive; silence reads as "verified"
component-doc-pageCreates a full-quality documentation page for a design system component on the website. Reads the component's props to generate a variant showcase grid (the Button page is the benchmark), writes all three page files, and adds the components-index card. The sidebar, sitemap and breadcrumbs derive from the component registry, so there is no navigation to wire.
component-doc-page.mdmd---
name: component-doc-page
description: Create a full-quality documentation page for a design system component on the website. Use when asked to document a component on the website, add a component docs page, or create the website page for a component.
---
# component-doc-page
Create a full-quality documentation page for a design system component on the website.
## When invoked
Use this skill when asked to document a component on the website, add a component page, or create docs for a component — phrases like "document [X] on the website", "add a docs page for [X]", "create the website page for [X]".
This is a more thorough, component-specific version of `new-page`. The Button page is the quality benchmark for static variant grids; for components whose value is interaction or streaming state (the `ai` category), the chat-message page (`website/src/app/components/chat-message/page.tsx`) is the exemplar — a `"use client"` page with small stateful demos instead of a grid.
## Instructions
1. **Gather requirements** if not already provided:
- Component name (PascalCase)
- Figma node URL (optional — ask Rob, or omit if unknown)
- Storybook path (optional — format: `/?path=/docs/components-<slug>--docs`)
2. **Read the source component** `src/components/ComponentName/ComponentName.tsx`:
- Extract all props from the TypeScript interface
- Identify all variant enumerations (e.g. `variant`, `size`, `status` props with union types)
- Understand the component's states (default, hover, active, disabled, loading, etc.)
- Note the BEM class names used for each variant/state
3. **Read the gold-standard reference:**
- `website/src/app/components/button/page.tsx` — study the variant showcase grid structure (rows = states, columns = variants), the `pageHeader` block, `introSection`, and how `PageLinks` is used
- `website/src/app/components/button/page.module.css` — CSS module structure
4. **Create `website/src/app/components/<component-slug>/page.tsx`:**
- Mirror the Button page's layout shell exactly — same components, same nesting, same class names, with your slug in the `getSidebarLinks` call. Don't improvise structure. Mirror the *structure*, not deprecated APIs: if an existing page still uses a prop marked `@deprecated` in the component source (the `priority` → `variant` rename is the precedent), write the current prop name.
- Invariants the exemplar can't teach:
- `subDisplay` is a *tagline* for the component (e.g. Button's "The main action element") — not the word "Components"; the breadcrumb already shows the section
- `introBody` is a clear 1–2 sentence description of the component's purpose, inferred from its props and JSDoc if available
- All copy on the page (tagline, intro, section labels) follows `content-design.md` — neutral, sentence case, no em dashes
- Import the component through the package, never a relative path into `src/`: `import { X } from "@robr0/design-system/components/X/X"` (recharts-backed charts come from `@robr0/design-system/charts`) — the website is an npm-workspace consumer of the published package's exports
- Include `PageLinks` with whichever Figma/Storybook URLs were provided
- **Variant showcase grid**: render the component in every meaningful combination of its variants and states. For components with discrete variants × states (like Button), render a proper grid. For simpler components, render one example per meaningful state/variant.
5. **Create `website/src/app/components/<component-slug>/page.module.css`:**
- Standard layout classes: `dsLayout`, `dsContent`, `pageHeader`, `pageTitle`, `subDisplay`, `introSection`, `introBody`
- Any additional classes needed for the variant showcase grid
- **Prose is never width-capped.** No `max-width` on `introBody`, section body text, or any paragraph class — page text fills the content column, exactly as the Button page does. A `max-width` is only legitimate on a *demo container* (a box that holds a rendered component, e.g. a drawer or form-control mount) where the component itself needs a bounded stage. If you find yourself capping a paragraph "for readability", don't — the column width is the layout's decision, not the page's.
- CSS custom properties only
- Mobile type and section rhythm collapse at the **token layer** (768px, system-wide) — do not add per-page `@media` overrides for tokenized values; when the showcase grid genuinely needs a breakpoint, use the canonical set in `design.md`'s responsive spec
6. **Create `website/src/app/components/<component-slug>/layout.tsx`** — it is exactly this, with no description of its own:
```tsx
import { componentPageMetadata } from "@/config/navigation";
export const metadata = componentPageMetadata("<component-slug>");
export default function Layout({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
```
Both the title and the description resolve from the component's entry in `src/components/registry.json` — that is the only place the one-line description lives, so do **not** pass one here. `scripts/validate-page-titles.mjs` requires this exact call and fails the build otherwise; a slug with no registry entry fails the website build with a message naming the fix.
7. **Do not touch `website/src/config/navigation.ts`.** `componentsSidebarLinks` is **derived** from the registry — there is no list to add to. Registering the component in `src/components/registry.json` (with its `label`, `slug` and `description`) is what puts it in the sidebar, and the sitemap, mega-nav and breadcrumbs follow from there. If the component is not yet registered, do that first; the `new-component` skill covers the entry's shape.
8. **Update `website/src/app/components/page.tsx`** (the components index) — the one surface still hand-maintained, because each card holds a bespoke preview:
- Add a `TocCard` in alphabetical order: `<TocCard href="/components/component-slug" title="Component Name">` wrapping a small preview — use the real component (imported from `@robr0/design-system`) where it reads well at miniature size, as most cards do, or a small inline-styled mockup where it doesn't (see the Accordion card)
9. **Sitemap is automatic** — `website/src/app/sitemap.ts` derives its routes from the shared sidebar configs, which for components derive from the registry. Do not edit `sitemap.ts` by hand. `scripts/validate-website-surfaces.mjs` build-enforces the showcase page, the step-8 `TocCard`, and the `design.md` spec section; the sidebar entry and its ordering are no longer checked because they can no longer drift. If the component has no `design.md` spec section yet, add one before building — see the `new-component` skill's spec step for the expected shape.
heuristic-analysisEvaluates a page or component against Nielsen's 10 Usability Heuristics. Takes screenshots in light and dark mode, reads the source code, then produces a structured findings table with severity ratings (Pass / Minor / Moderate / Critical) and specific fix suggestions.
heuristic-analysis.mdmd---
name: heuristic-analysis
description: Evaluate a page or component against Nielsen's 10 Usability Heuristics and produce a structured findings report. Use when asked for a heuristic analysis, UX review, or usability check.
---
# heuristic-analysis
Evaluate a page or component against Nielsen's 10 Usability Heuristics and produce a structured findings report.
## When invoked
Use this skill when asked to run a UX or usability review — phrases like "heuristic analysis of [page]", "UX review of [page]", "usability check on [component]", "how does [X] score on usability".
## Instructions
1. **Determine scope.** Accept one of:
- A website URL path (e.g. `/components/button`) → review the live page
- A component name (e.g. `AlertDialog`) → review the component source and its rendered output
2. **Gather visual evidence.** Start the preview server and screenshot the target in both light and dark mode (follow the `visual-review` skill pattern). Stop the server when done.
3. **Read the source code** for the page or component to understand the full implementation, not just what's visible in screenshots.
4. **Evaluate against each of Nielsen's 10 Heuristics.** For each, assign a severity:
- ✅ **Pass** — fully satisfied, no issues
- ⚠️ **Minor** — small gap, low user impact
- 🔶 **Moderate** — noticeable issue, degrades experience
- 🔴 **Critical** — breaks usability, must fix
**The 10 Heuristics:**
1. **Visibility of system status** — Does the UI communicate what's happening? (loading states, active states, progress indicators, feedback on interaction)
2. **Match between system and real world** — Do labels, icons, and concepts match the user's mental model? (plain language, familiar metaphors, no jargon)
3. **User control and freedom** — Can users undo, cancel, go back, or exit? (close buttons, undo actions, Escape key support on overlays)
4. **Consistency and standards** — Are patterns applied uniformly? (same component behaves the same way everywhere, no contradictory conventions)
5. **Error prevention** — Does the UI prevent mistakes before they happen? (confirmation dialogs for destructive actions, disabled states, validation hints before submission)
6. **Recognition rather than recall** — Are options visible rather than requiring memory? (labels on icon-only buttons, visible choices, no hidden commands)
7. **Flexibility and efficiency of use** — Can experienced users work faster? (keyboard shortcuts, compact modes, sensible defaults)
8. **Aesthetic and minimalist design** — Is every element necessary? (no redundant labels, no visual noise, appropriate information density)
9. **Help users recognise, diagnose, and recover from errors** — Are error messages plain, specific, and constructive? (not just "Something went wrong")
10. **Help and documentation** — Are tooltips, placeholder text, or inline guidance provided where genuinely needed?
5. **Produce a structured report:**
```
## Heuristic Analysis: [Page/Component Name]
| # | Heuristic | Severity | Finding |
|---|-----------|----------|---------|
| 1 | Visibility of system status | ✅ Pass | — |
| 2 | Match with real world | 🔶 Moderate | Submit button gives no feedback after click — add loading state |
...
### Findings requiring action
[Only Minor/Moderate/Critical items, each with a specific fix suggestion]
### Summary
X critical · Y moderate · Z minor · W passing
```
6. **Be specific.** Reference the exact element, prop, or file where possible. A finding like "the Dismiss button in AlertDialog has no visible focus ring (AlertDialog.css:47)" is more useful than "focus styles are missing".
accessibility-auditAudits a component or page against WCAG 2.1 AA criteria. Checks semantic HTML, ARIA usage, keyboard navigation, focus styles, and colour contrast via both source code analysis and live screenshots. Reports file and line-level findings with WCAG criterion and severity.
accessibility-audit.mdmd---
name: accessibility-audit
description: Audit a component or page for accessibility violations against WCAG 2.1 AA criteria. Use when asked for an accessibility audit, a11y check, WCAG compliance check, or "is X accessible".
---
# accessibility-audit
Audit a component or page for accessibility violations against WCAG 2.1 AA criteria.
## When invoked
Use this skill when asked to check accessibility, run an a11y audit, or find WCAG issues — phrases like "accessibility audit", "a11y check on [component/page]", "check WCAG compliance", "is [X] accessible".
## What is already automated — read this before auditing anything
**Axe runs on every Storybook story and fails the build.** `.storybook/preview.ts` sets `a11y.test: 'error'`, so `npm run test` (and therefore CI and `npm run verify`) already enforces WCAG 2.1 AA across the whole library. Start by running it:
```bash
npm run test
```
If that is green, every violation axe can detect is already absent — and re-checking icon-only button names, label association, `role="dialog"` naming or ARIA parent/child relationships by hand is duplicated effort.
**This skill exists for the three things that gate does not cover:**
1. **Colour contrast — partly excluded from the automated gate.** `color-contrast` is switched off in `.storybook/preview.ts` by a settled decision of Rob's. **Read that override's comment first**: it is the authoritative record of which pairs it covers and why, and it is the single place those details belong. What it names is out of scope — not a finding, not something to propose restyling, and not a reason to re-enable the rule. Skip them silently rather than restating them in a report. **Contrast everywhere else is the single highest-value thing to audit manually** — nothing else checks it.
2. **What axe cannot see.** Axe catches roughly a third of WCAG issues. It cannot tell whether alt text is *meaningful*, whether focus order makes sense, whether a Dialog *actually* traps focus, or whether a helper message *should* have been associated with its control. (Two such bugs shipped undetected until a manual survey found them: Dropdown announced neither its helper text nor its error state.)
3. **Anything outside the story suite** — the Next.js website pages, which axe never runs against.
Report a finding as **already-enforced** if `npm run test` would have caught it; that tells the reader the gate is working rather than implying a gap.
## Instructions
1. **Determine scope.** Accept one of:
- A component name (e.g. `Dropdown`) → audits `src/components/Dropdown/Dropdown.tsx` and its CSS
- A website page URL (e.g. `/components/button`) → audits the live rendered page
- `all-components` → audits all components in `src/components/`
2. **Read the source files.** For each component in scope, read the `.tsx` and `.css` files before taking screenshots.
3. **Structural audit (from source code).** Most items here are already enforced by axe — spend your effort on the ones marked **[manual]**, which it cannot evaluate:
**Semantic HTML & ARIA:**
- Interactive elements use correct roles (`button`, `link`, `checkbox`, etc.) — never a `<div onClick>` without `role` and `tabIndex`
- Icon-only `<button>` elements have `aria-label` describing their action
- **[manual]** `<img>` elements have *meaningful* `alt` text — axe only checks that the attribute exists; decorative images use `alt=""`
- Form inputs are associated with `<label>` via `htmlFor`/`id`, or have `aria-label`
- Modals and dialogs use `role="dialog"` and `aria-modal="true"`, with `aria-labelledby` pointing to the title
- Lists use `<ul>`/`<ol>` + `<li>`, not `<div>` stacks
- Heading hierarchy is logical — no h3 before h2, no skipped levels
**Keyboard Navigation:**
- **[manual]** All interactive elements are reachable by Tab, in an order that makes sense — axe cannot judge order
- Custom interactive components handle `onKeyDown` for Enter/Space (buttons), arrow keys (RadioButton, SegmentedControl, ToggleGroup)
- **[manual]** Modal/dialog *actually* traps focus while open and restores it to the trigger on close — axe sees the attributes, not the behaviour
- Escape key closes dismissible overlays (Tooltip, Popover, DropdownMenu, AlertDialog)
**Focus Styles:**
- Every interactive element has a `:focus-visible` rule in its CSS
- Focus ring uses the teal action token (`--color-action-primary-bg`) — per design.md, teal is reserved for primary CTAs and focus rings. Flag any `outline: none` without a visible replacement
**Motion** (axe evaluates none of this):
- **[manual]** Anything that animates for more than five seconds, or loops indefinitely, can be paused, stopped, or hidden (WCAG 2.2.2). CSS-token motion satisfies this through the `prefers-reduced-motion` guard in `tokens-motion.css`, which collapses every duration
- **[manual]** Animation driven from JavaScript is **outside that guard** — a `requestAnimationFrame` loop cannot be seen by CSS, so each one has to check `prefers-reduced-motion` itself. The site's ambient background is the standing example; confirm it still renders a single static frame under the preference rather than assuming the token layer covers it
- **[manual]** Motion triggered by interaction (parallax, cursor-reactive effects) is disabled under reduced motion, or is not essential (WCAG 2.3.3)
4. **Visual audit (from screenshots).** Start the preview server and screenshot the target in both light and dark mode (follow the `visual-review` skill pattern). Check:
- **Colour contrast (the priority — nothing automated covers this):** compute the ratio for every foreground/background pair actually rendered, not just body text. Flag anything below 4.5:1 for normal text or 3:1 for large text and UI components (WCAG 1.4.3 / 1.4.11). Note which token is used. The pairs named in `.storybook/preview.ts`'s rule override are out of scope: skip them silently rather than listing them, and do not name them in the report.
- **Text sizing:** No text visually below ~12px (WCAG 1.4.4)
- **Focus visibility:** Confirm focus rings are clearly visible in both light and dark themes
Stop the server when done.
5. **For each issue, report:**
```
src/components/Dropdown/Dropdown.tsx:84 — WCAG 4.1.2 Name, Role, Value [Critical]
Trigger button has no accessible name. Icon-only button needs aria-label="Open dropdown".
```
Severity:
- **Critical** — blocks keyboard or screen reader users entirely
- **Moderate** — degrades experience significantly
- **Minor** — best practice violation, low direct impact
6. **Summarise:**
- `X critical · Y moderate · Z minor`
- If clean: "No accessibility violations found. Component meets WCAG 2.1 AA."
api-consistencyReads all component Props interfaces and flags inconsistencies across the library: mixed boolean naming (disabled vs isDisabled), mismatched size enums, missing standard props (className, disabled), and structural mismatches within component families. Produces a grouped findings report prioritised by breaking impact.
api-consistency.mdmd---
name: api-consistency
description: Review component prop interfaces across the design system for naming inconsistencies, missing standard props, and pattern violations. Use when asked to review component APIs, audit prop naming consistency, or check TypeScript interfaces for API inconsistencies.
---
# api-consistency
Review component prop interfaces across the design system for naming inconsistencies, missing standard props, and pattern violations.
## When invoked
Use this skill when asked to review component APIs, check prop naming consistency, or audit TypeScript interfaces — phrases like "review component APIs", "prop consistency audit", "are our component props consistent", "check for API inconsistencies".
## Instructions
1. **Determine scope.** Accept one of:
- A list of specific components (e.g. `Button, CircularButton, ButtonGroup`) → compare those
- `all` → scan all components in `src/components/`
- A category description (e.g. "all button-like components", "all form inputs") → infer the relevant components
2. **Read every component's TypeScript interface.** For each `.tsx` file in scope, extract:
- All prop names, types, and whether they are required or optional
- Default values (from destructuring defaults in the function signature)
3. **First, check conformance to the published contract.** This is the highest-value part of the review and takes precedence over style preferences below. The contract is defined in the `new-component` skill and summarised in CLAUDE.md's **Component Anatomy** — read one of them rather than trusting this list, which exists to tell you *what to look for*, not to restate the rules:
- **`forwardRef` onto the primary DOM node**, plus a matching `displayName`. A component that cannot take a ref cannot be focused, measured, or registered by a form library.
- **`{...rest}` spread onto that same node**, placed first so the component's own attributes win.
- **Props extend the native element's type** — `Omit<React.ComponentPropsWithoutRef<'el'>, keyof OwnProps>`. Without it, `data-*`, `aria-*`, `autoComplete` and friends are unreachable.
- **`'use client'` present when the component is interactive, and absent when it is purely presentational.** A needless directive silently costs consumers Server Component rendering — flag both directions.
- **Native event signatures keep the standard names.** `onChange` must be a `ChangeEventHandler`, never `(value) => void`. The convenience callback is named for the value's shape: `onValueChange` (string/number), `onCheckedChange` (boolean), `onValuesChange` (array) — and both fire.
- **No Figma variant properties in the code API.** A `state` enum mixing `hover`/`active` (CSS pseudo-classes the browser owns) with `disabled` (real semantics) has two sources of truth. Prefer `variant` over `priority`/`kind`, and `disabled` as a real boolean.
- **Labelled form controls compose inside `Field`** rather than re-implementing label/helper/required/ARIA wiring.
- **Deprecations, not removals** — an old prop should still work and carry `@deprecated` naming its replacement.
Where a prop deliberately shadows a native attribute with different meaning (`size` vs the native character-width attribute, `title` vs the native tooltip), that is acceptable **only if the collision is documented in the prop's JSDoc**. Flag undocumented collisions.
4. **Then check these style-level inconsistencies:**
**Boolean prop naming:**
- Should follow `is*`/`has*` convention OR plain adjective — not both (e.g. `isDisabled` on one component, `disabled` on another doing the same thing)
- Flag: mixed usage within the same component family
**Event handler naming:**
- Must be `on*` (e.g. `onClick`, `onChange`, `onDismiss`)
- Flag: `handleClick`, `clickHandler`, `onClickHandler`, or similar
**Content prop naming:**
- `label` for display text, `children` for slot content
- Flag: `text`, `title`, `copy`, `content` used interchangeably across components for the same purpose
**Size enum values:**
- Should use a consistent vocabulary across components
- Flag: `"sm"/"md"/"lg"` on one component and `"small"/"medium"/"large"` on another, or `"compact"/"default"` on one and `"small"/"medium"` on another
**Missing standard props on interactive components:**
- All components rendering clickable/interactive elements should have `className?: string`
- All components with visual disabled states should have `disabled?: boolean`
- All form-like components should have `id?: string`
- `name` only belongs on a component that renders a **native** form control. Several controls here render a `div` with an ARIA role (Checkbox, RadioButton, Dropdown), where `name` cannot participate in form submission — on those it is a documented no-op, not a missing prop. Flag an *undocumented* `name`, not its absence.
**Family consistency:**
- Components in the same family (e.g. Button / CircularButton / ButtonGroup) should share `size` enum values
- If one component accepts `iconLeft`/`iconRight`, siblings in the same family should follow the same pattern
- Default values: if `size` defaults to `"default"` on Button, it should not default to `"medium"` on a related component
5. **Output a grouped findings report.** The component names in this example are **fictional by design** — findings about real components go stale the moment someone fixes them, so this block only demonstrates the format:
```
## API Consistency Report
### Contract violations (highest impact)
- Gadget — no forwardRef; a consumer cannot take a ref
- Sprocket — props do not extend ComponentPropsWithoutRef<'span'>; data-* unreachable
- MetricPod — has 'use client' but no hooks or handlers; blocks Server Component rendering
### Boolean prop naming
- Gadget: uses `disabled` (plain adjective)
- Doodad: uses `isDisabled` (is* prefix)
→ Standardise to `disabled` across all interactive components
### Size enum values
- Gadget: "compact" | "default" | "large"
- Whatsit: "small" | "medium" | "large"
→ Standardise to the enum of the most-used component in the family
### Missing className prop
- Doodad — no className passthrough
- Gadget — no className passthrough
### Summary
X naming inconsistencies · Y missing props · Z structural mismatches
```
6. **Prioritise fixes** by impact:
- **High:** Renames that would require consuming code changes — flag these clearly so Rob can decide whether to batch into a breaking release
- **Medium:** Missing props that are commonly needed by consumers
- **Low:** Style preferences with no breaking impact
growth-loopRuns one analytics-driven copy experiment end to end: pulls GA4 data, filters bot noise, forms a falsifiable hypothesis about the words on a page, implements the change on a branch in a temporary worktree, verifies the build, and writes a problem / hypothesis / solution report for approval. Runs itself every Monday, as one of the loops described on the Loops page.
growth-loop.mdmd---
name: growth-loop
description: Weekly GA-driven copy experiment loop for www.robertritacca.com. Analyze last month's GA data, find one copy problem, implement the fix on a local branch, and write a report for approval. Use when asked to run the growth loop. Never pushes, merges, or deploys.
---
# growth-loop
Weekly GA-driven copy experiment loop for www.robertritacca.com (this repo's `website/` deploys there via Vercel). Each run: analyze last month's GA data, find ONE copy problem, form a hypothesis, implement the fix on a local branch, and write a clear report for the user to approve. **Never push, merge, or deploy — the user approves every change.**
## When invoked
Run when asked to "run the growth loop" (`/growth-loop`) or by the `growth-loop-weekly` scheduled task.
## Scope guardrails (read first)
- **Copy only.** Headlines, body text, CTA/link labels, button text, page `metadata` titles/descriptions — all inside `website/src`. No CSS, no layout, no component structure, no new components, no dependencies.
- **One focused change per run.** One page, or one copy element (e.g. the same CTA wording) across a few pages. A reviewer should be able to read the diff in under two minutes.
- **Local branch only.** Never `git push`, never merge, never touch the user's checked-out branch or working tree (use a worktree — see step 4).
- Never read into version control or modify `ga-analysis/service-account.json` or `ga-analysis/output/`.
## The loop
### 0. Close the previous loop
Read the newest report in `ga-analysis/loop-reports/` (git-ignored, local-only). If a previous experiment was approved/merged, check whether its metric moved in this run's data and record the verdict (improved / no change / worse / too early to tell) in this run's report. If the previous branch was never merged, note that instead and don't count it as tested. Don't re-run a hypothesis a previous report already tested unless the report says the change was never merged.
### 1. Pull the data
```bash
cd "$(git rev-parse --show-toplevel)/ga-analysis" && ./.venv/bin/python pull_ga.py --days 28
```
Output lands in `ga-analysis/output/all.json`. If the venv is missing: `python3 -m venv .venv && ./.venv/bin/pip install -q -r requirements.txt`. FutureWarnings are harmless.
### 2. Analyze — with the ga-report skill's judgment calls
Apply every gotcha from the `ga-report` skill (`~/.claude/skills/ga-report/SKILL.md` — installed only on Rob's Mac, like the GA venv and credentials; this loop runs there, not on the Windows machine). That skill owns the bot-traffic list, the pagePath-vs-pageTitle rule, and the traffic-mix baselines — read it fresh each run rather than trusting a remembered copy, and subtract the bots it names before drawing conclusions.
Look for **copy-shaped problems**, e.g.: a high-traffic landing page with weak engagement or dwell; strong entry pages that don't lead anywhere (missing/weak CTA copy); case studies with good dwell but low reach (weak titles/descriptions); a mismatch between what a traffic source promises and what the page's headline says.
### 3. Pick ONE problem and write the hypothesis
The hypothesis must be falsifiable and name its metric:
> If we [specific copy change], then [specific metric for a specific page/segment] should [direction] over the next few weeks, because [reasoning grounded in the data].
If the data doesn't support a confident copy hypothesis this week, **say so and stop** — a no-op run with a short "nothing worth changing" report is a valid outcome. Don't invent a change to have something to ship.
### 4. Implement on a branch (via worktree)
Work in a temporary worktree so the user's working tree is untouched:
```bash
REPO=$(git rev-parse --show-toplevel)
WT=$REPO/../.growth-loop-worktree
BRANCH=growth/$(date +%F)-<short-slug>
git -C "$REPO" worktree add "$WT" -b "$BRANCH" main
```
Make the copy edits in `$WT/website/src/...` — new copy follows `content-design.md` (voice, register, banned words) — then verify the build (the repo is an npm workspace — one install at the worktree root wires everything, including the `@robr0/design-system` link back to the worktree's own `src/`; it's seconds thanks to the npm cache. Do **not** symlink `node_modules` from the main checkout — Turbopack rejects symlinks that point outside the project root):
```bash
cd "$WT" && npm install --no-fund --no-audit
cd "$WT/website" && npm run build
```
If the build fails because of your edit, fix it. Then commit in the worktree (conventional message, e.g. `experiment(growth): reword /work CTA — hypothesis in loop report 2026-07-20`). Commit scope: the website `prebuild` regenerates tracked files, and they stay out of the commit **unless your edit is what changed them** — with one standing exception that always qualifies: the site chat's corpus (`website/src/data/site-corpus.generated.ts`) is built from page prose, so a copy edit changes it by construction. Commit the regenerated corpus alongside your copy edits every time (a branch without it fails CI's drift guard, and `git worktree remove` refuses a dirty worktree); leave the other regenerated files (`website/src/data/skills-content.generated.ts`, `website/public/*.md`) out unless they actually changed. Then clean up:
```bash
rm -rf "$WT/node_modules" "$WT/website/node_modules"
git -C $REPO worktree remove "$WT"
```
The branch survives worktree removal and is ready for the user to review.
### 5. Write the report
Save to `ga-analysis/loop-reports/YYYY-MM-DD.md` **and** repeat it in full in the final message to the user. Plain English — the user is a designer, no analytics jargon. Format:
```markdown
# Growth loop — YYYY-MM-DD
## Last week's experiment
[Verdict on the previous change, or "none / not merged".]
## The problem
[What the data shows, with the actual numbers, after bot filtering.]
## The hypothesis
If we ..., then ... should ..., because ...
## The change (branch: growth/YYYY-MM-DD-slug)
[File(s) touched. Before → after for every copy string changed.]
## How we'll know
[Which metric to look at next run, and roughly what movement would count as a win.]
```
### 6. Hand off for approval
End by telling the user: the branch name, that the build passed, and that nothing is pushed or deployed. To approve they merge the branch (or say `ship` on it); to reject they delete the branch. That's the whole approval step.
site-updatesKeeps the Project journal timeline evergreen: reads every commit since the last curated bookmark, clusters them into themes, and writes one story entry (what was built, why, and the outcome in plain English), extending an existing arc when the work continues one. Runs biweekly on a schedule, builds on a branch in a temporary worktree, and hands the new entry over for approval. Nothing is pushed without a human merge.
site-updates.mdmd---
name: site-updates
description: Biweekly loop that keeps the /project-journal build-progression timeline current. Read the git history since the last curated commit, consolidate it into thematic story entries (what/why/outcome, never commit digests), update the data file on a local branch, and report for approval. Use when asked to update the project journal or run the site updates loop. Never pushes, merges, or deploys.
---
# site-updates
Biweekly curation loop for the `/project-journal` page (Project journal) — the evergreen timeline of the build's progression. Each run reads the commits since the last curated bookmark, consolidates them into at most a couple of thematic story entries, updates `website/src/data/site-updates.json` on a local branch, and writes a report for the user to approve. **Never push, merge, or deploy — the user approves every change.**
## When invoked
Run when asked to "update the project journal" (`/site-updates`) or by the `site-updates-biweekly` scheduled task.
## Editorial standard (the whole point — read first)
Entries are **thematic stories, not commit digests**:
- **One entry = one theme**, consolidating however many commits belong to it, even non-contiguous ones. A CI workflow commit plus its docs commits is ONE entry ("A real quality gate"), not four bullets.
- **Every entry answers three things in prose**: *what* was built, *why* it was needed (the problem or motivation — the human context), and *the outcome* (what's now true, guaranteed, or possible). No commit hashes, no conventional-commit prefixes, no "various fixes" — the validator rejects hash-like strings in bodies.
- **Continue arcs instead of fragmenting them.** If the period's work extends a theme an existing entry already tells (more components in a family, round two of a security scrub), extend that entry's body or write an explicit follow-on that references the arc — don't add a disconnected fragment.
- **A reader who has never seen the repo** should be able to read the timeline top to bottom and follow the build. Spell out names and stakes; write like the existing entries.
- **Omit themeless chores.** Typo fixes, dependency bumps, tiny tweaks with no story simply don't appear. The page shows the *largest* updates, not all of them.
- Point-in-time numbers inside a dated entry ("all 434 stories at the time") are fine; never write a *live* count that will drift — live counts belong to registries (see `CLAUDE.md`).
- **Sentence-level style follows `content-design.md`** — this section owns the story *shape* (themes, what/why/outcome); the content guide owns voice, banned words, and rhythm. Run its Self-Review Tests on each drafted entry.
## The loop
### 1. Gate — is there enough to say?
Read `asOf` from `website/src/data/site-updates.json`, then:
```bash
git log --format='%ad %s' --date=short <asOf.commit>..HEAD
```
If it has been fewer than ~12 days since `asOf.date`, or the new commits are only themeless chores, **stop** and tell the user "nothing worth recording yet" with a one-line summary of what was skipped. A no-op run is a valid outcome — don't invent an entry to have something to ship.
### 2. Curate
Cluster the new commits into themes and draft the entry (usually one, at most two) per the editorial standard. For context on what a commit actually was, read the touched files or `git show --stat` — the story should describe the change's substance, not its message. Check the existing entries first so a continuing arc extends rather than duplicates.
### 3. Implement on a branch (via worktree)
Work in a temporary worktree so the user's working tree is untouched:
```bash
REPO=$(git rev-parse --show-toplevel)
WT=$REPO/../.site-updates-worktree
BRANCH=site-updates/$(date +%F)
git -C "$REPO" worktree add "$WT" -b "$BRANCH" main
```
In `$WT/website/src/data/site-updates.json`: prepend the new entry (or extend an arc), and set `asOf` to the current `main` HEAD sha + today's date. Validate and build (the repo is an npm workspace — one install at the worktree root wires everything, including the `@robr0/design-system` link back to the worktree's own `src/`; it's seconds thanks to the npm cache. Do **not** symlink `node_modules` from the main checkout — Turbopack rejects symlinks that point outside the project root):
```bash
node $WT/scripts/validate-site-updates.mjs
cd "$WT" && npm install --no-fund --no-audit
cd "$WT/website" && npm run build
```
Fix anything your edit broke, then commit in the worktree (e.g. `content(site-updates): add "<entry title>" entry through YYYY-MM-DD`). The build regenerates tracked files, and your edit always changes one of them: the site chat's corpus (`website/src/data/site-corpus.generated.ts`) embeds the journal entries, so a new entry regenerates it by construction. Commit the regenerated corpus (and any other tracked file the build regenerated because of your edit) in the same commit — a branch without it fails CI's drift guard, and `git worktree remove` refuses a worktree with modified tracked files. Then clean up:
```bash
rm -rf "$WT/node_modules" "$WT/website/node_modules" # npm nests version-conflicting deps under website/, so both trees exist
git -C $REPO worktree remove "$WT"
```
The branch survives worktree removal and is ready for review.
### 4. Hand off for approval
The final message to the user IS the report — plain English:
- The full text of the new/extended entry (so approval needs no file-opening).
- Which commits it consolidates (date range and count, not a hash list).
- The branch name, that the validator and website build passed, and that nothing is pushed or deployed.
- To approve: merge the branch (or say `ship` on it). To reject: delete the branch.
seo-auditSweeps the technical SEO surface of the site: page titles and descriptions, canonical URLs, the sitemap and robots rules, social preview tags, and structured data. It checks the HTML the server actually sends rather than trusting the source, fixes what it finds on a branch for approval, and reports a clean pass when there is nothing worth changing.
seo-audit.mdmd---
name: seo-audit
description: "Behind-the-scenes SEO sweep of the website: page metadata, canonicals, sitemap, robots, social preview tags, structured data. Verifies the rendered HTML, fixes technical issues on a local branch, and a clean pass is a valid outcome. Use when asked to run the SEO audit or check the site's SEO. Never pushes, merges, or deploys."
---
# seo-audit
A recurring optimizer sweep over the website's technical SEO, in the same spirit as the growth loop: each run inspects everything a crawler or link unfurler sees, fixes what is safely fixable on a local branch, and reports for approval. **A run that finds nothing to fix is a valid outcome** — say so briefly and stop; never invent a change to have something to ship.
## When invoked
Run when asked to "run the SEO audit" (`/seo-audit`).
## Scope guardrails (read first)
- **Behind the scenes only.** Head metadata, crawl/index surfaces, link unfurl tags, structured data, redirects, internal-link integrity. Nothing a sighted visitor sees changes: no layout, no CSS, no component structure, no visible copy. (Meta titles and descriptions are in scope — they render in search results, not on the page.)
- **Crawl-policy changes are report-only.** Anything that changes what gets indexed or where the canonical site lives (robots rules, `noindex`, canonical host, redirect policy) gets proposed in the report, not implemented — unless it is an outright bug, like a page accidentally marked `noindex`.
- **Local branch only.** Never push, merge, or deploy; never touch the user's working tree. Follow the temporary-worktree recipe in `.claude/skills/growth-loop/SKILL.md` (step 4), with branch name `seo/YYYY-MM-DD-<slug>`. Skip the worktree entirely on a clean pass.
- **No hardcoded facts.** New metadata prose follows `content-design.md`; anything countable derives from a registry (see CLAUDE.md — never write a component count into a meta description).
## The sweep
### 1. Inventory the surfaces
The crawl/index surface lives in `website/src/app/`: the root `layout.tsx` (site-wide metadata and `metadataBase`), `sitemap.ts`, `robots.ts`, `manifest.ts`, `opengraph-image.tsx`, `icon.tsx` and `apple-icon.tsx`, and the `llms.txt` route — plus the structured data the layout injects, built in `website/src/lib/structuredData.ts` (its `sameAs` derives from `website/src/config/social.ts`). Per-page metadata comes from each page's `layout.tsx`; component pages derive theirs from the registry via `componentPageMetadata`. Read these fresh each run — the list above says where to look, not what is there.
### 2. Verify the rendered output, not the source
Build the site (`npm run build` in `website/`), then serve it via the `website-prod` entry in `.claude/launch.json` — it wraps `npm run start` and takes any assigned port, so it never collides with a running dev server; read the port from what it reports. Then fetch the served HTML — plain HTTP requests are enough; no browser needed. Sample every section of the site (at least one page per top-level route group, plus the home page and one component page), and fetch the sitemap, robots, and manifest routes directly. For each sampled page check the `<head>`:
- Title present, unique across pages, and following the site's title template.
- Meta description present, sensible length (~70–160 characters), and specific to the page.
- Canonical URL correct — one canonical host, no duplicate-content splits.
- Open Graph and Twitter card tags complete enough for a clean unfurl (title, description, image, type, url).
- No accidental `noindex`/`nofollow`; viewport and charset present.
- Structured data (JSON-LD) valid where present; note opportunities where a page type clearly warrants it (e.g. article markup on writing pages).
### 3. Cross-check the crawl graph
- Every public route appears in the sitemap, and every sitemap URL returns 200 from the running server. Registry-driven collections (components, case studies, writing) must be complete in it — if one is missing, the fix belongs in how `sitemap.ts` derives the list, not in a hand-added entry.
- Robots rules and the sitemap agree (nothing disallowed that the sitemap advertises).
- Internal links resolve: no anchors pointing at routes that 404.
- The 404 page itself returns HTTP 404, not 200.
### 4. Fix, verify, hand off
Apply the mechanical, clearly-safe fixes in one coherent batch on the branch; leave judgment calls (crawl policy, new structured-data strategy, description rewrites that change meaning) as proposals in the report. Verify the website build passes in the worktree before committing. Metadata edits are page prose, so the build regenerates the site chat's corpus (`website/src/data/site-corpus.generated.ts`) — commit the regenerated file in the same batch (see the commit-scope rule in the growth-loop recipe), then remove the worktree (the branch survives).
Where a finding is deliberately not a bug — a page intentionally out of the sitemap, an intentionally bare head — record why in a short comment at the site of the decision, so future runs read the reason instead of re-flagging it.
### 5. Report
Repeat the full report in the final message (no report file — the in-place comments from step 4 are the durable record). Plain English, findings grouped as **fixed on the branch** (with before → after), **proposed** (needs a decision), and **checked clean** (what was verified and passed). End with the branch name if one exists, confirmation the build passed, and the reminder that nothing is pushed or deployed: merging the branch (or saying `ship`) approves it; deleting it rejects it.
ga-reportPulls GA4 data for this site and analyzes it in plain English — with the judgment calls baked in: which traffic is bots, why page titles fragment, and which numbers are normal for a portfolio rather than problems. Lives in my personal skills folder rather than the repo, because it encodes analytics context instead of codebase conventions.
ga-report.mdmd---
name: ga-report
description: Pull Google Analytics (GA4) data for robertritacca.com and analyze it in plain English. Use when the user asks for a GA report, site traffic analysis, analytics summary, "how's the site doing", visitor/pageview breakdown, or to refresh GA numbers.
---
# GA report
Pulls GA4 data for **www.robertritacca.com** (property `•••••••••`) via the
`ga-analysis/pull_ga.py` script, then analyzes it in plain, non-jargon English.
## Where things live
- Script + venv: `~/Documents/Projects/design-system/ga-analysis/`
- Runner: `./.venv/bin/python pull_ga.py`
- Output: `ga-analysis/output/all.json` (+ one CSV per report). Git-ignored.
- Credentials: `ga-analysis/service-account.json` (git-ignored; already set up).
## Steps
1. **Pick a window.** If the user gave one (e.g. "last month", "this week", "90 days"),
use it. Otherwise default to `--days 30`. The script accepts `--days N` or
`--start YYYY-MM-DD --end YYYY-MM-DD`.
2. **Run the pull** from the ga-analysis folder:
```bash
cd ~/Documents/Projects/design-system/ga-analysis && ./.venv/bin/python pull_ga.py --days 30
```
If the venv is missing, create it first:
```bash
python3 -m venv .venv && ./.venv/bin/pip install -q -r requirements.txt
```
The Python-version FutureWarnings are harmless — ignore them.
3. **Read the data.** Load `ga-analysis/output/all.json`. It's large; if it exceeds
the read cap, use a small Python snippet with the venv interpreter to compute
aggregates instead of reading the whole file. Reports included: `daily_trend`,
`channels`, `top_pages`, `countries`, `devices`, `sources`, `landing_pages`, `events`.
4. **Analyze in plain English.** Lead with what matters, not raw dumps. Cover:
- Headline: total users, % new, sessions, pageviews, pages/session, engaged %.
- Traffic sources (channels + the `sources` detail — call out LinkedIn / Medium /
Reddit / Google / any AI-assistant referral by name).
- Best content (top_pages) with dwell time — case studies are the important ones;
component gallery pages naturally have short dwell, don't flag that as a problem.
- Geography + device split.
## Judgment calls / gotchas (apply these every time)
- **Bot / junk traffic:** flag any country or source with high user count but very low
engagement rate (historically **Singapore ~4%** engaged = bots). Spam referrers seen:
`ddvvff.org`, `snucm.com`. Subtract these when stating the "real" audience size.
- **Page-title fragmentation:** the same URL (e.g. `/` or `/work`) appears under several
different `pageTitle` values because of past SEO/metadata edits. Sum by `pagePath`,
don't treat each title as a separate page.
- **Direct is usually high (~75%)** for this portfolio — that's dark social (LinkedIn app,
DMs, résumé links), not an error.
- **Mobile engagement** tends to run lower than desktop here — worth mentioning if the gap
is large.
## Output
A tight written summary (the user is a designer, not an analyst — no acronym soup).
Offer, but don't auto-run: charting it as a visual, or filtering bots in GA.
Never commit `output/` or `service-account.json`.