Skip to content

AGENTS

AGENTS.md — vendor-neutral agent guidance

Section titled “AGENTS.md — vendor-neutral agent guidance”

This file is the canonical entry point for any AI agent (Cursor, Copilot, Stitch, Claude Code, etc.) working in this repository. Claude Code users see CLAUDE.md — it is a thin wrapper around the same rules.

The single source of truth for every Rechaj product — designs, prototypes, PRDs, changelogs, API references, agent skills. Deployed to https://design.rechaj.com.

⛔ Creating or modifying a flow — MANDATORY ritual (read before writing any flow HTML)

Section titled “⛔ Creating or modifying a flow — MANDATORY ritual (read before writing any flow HTML)”

Every flow must match the platform’s source-of-truth app so designs stay consistent across the many people who author them. Do NOT hand-roll a shell, tabs, table, buttons, etc. from memory — you will drift and CI will reject the change.

Source of truth per platform (copy these apps’ components verbatim; PATTERNS.md indexes them, these files are ground truth):

Platform Canvas Source-of-truth apps
Web 1440, .sidebar+.topbar+.pg-wrap, document scrolls Polaris products/polaris/screens/dashboard.html (shell, .pseg tabs, .pstat KPIs, detail pages) + Caesar products/caesar/screens/dashboard.html (.tbl table system, .dlg dialogs, toast())
Mobile 390×844 .phone, dark backdrop Kit Assembler, Production QA (products/rechaj-kit-production-app/flows/…), Battery Onboarding Operator (products/rechaj-inventory-app/flows/battery-onboarding-operator/) — rsm-* scaffold, bottom sheets
Tablet 834×1194 .tablet + viewport-fit IIFE Production Manager, Procurement Manager, Inventory Manager — pm-*/im-*/km-* over rsm-*, bottom sheets

Full verbatim extractions of all seven: patterns/canon-source-extractions/.

The ritual — do all six, in order:

  1. Sync to current uat first — and again before you push: git fetch origin && git merge origin/uat (or run the /sync-uat skill). Everyone authors flows in parallel, so a branch that sat open is already behind canon and will collide at PR time. If a file you’re about to modify was rewritten on uat, do NOT blind-merge — open the current version and re-apply your change on top of it (see /sync-uat for the mechanical-vs-rebuilt distinction). CI branch protection is strict: a behind branch cannot merge.

  2. Start from the starter template, never a blank file: templates/{web,mobile,tablet}-flow-starter.html. It ships the canonical shell, tokens, tabs, table, dialog/sheet, toast and buttons — fill in content only.

  3. Read the platform’s Page Anatomy + atom specs in PATTERNS.md.

  4. Self-check every atom (checklist in PATTERNS.md) — shell, tabs, table, buttons, badges, KPIs, overlays — to canon, not “close enough”.

  5. Run the lint to 0 errors: node site/scripts/lint-canon.mjs <file>. CI runs it on every PR into uat/main; a file with canon errors cannot merge. The gate covers flows, products/*/screens/, and products/*/prototypes/ (screens/prototypes were previously unlinted — how the Swap Super Admin shipped a hand-rolled shell); it also flags a .sidebar/.topbar with no canonical .sb-item/.crumb-toggle. Sweep-staged rules are grandfathered by a baseline. Fourteen rules (atlas-shell-parity, atlas-logo-sub-category, web-no-drawer, web-inner-page-back, web-filt-bar-above-card, web-filt-bar-no-spacer, web-noncanon-form-field, atlas-login-not-starter, web-dead-pager, web-text-glyph-trend, web-card-title-echoes-page-title, web-login-gate-conditional-boot, web-page-title-subtext, web-pseg-in-table) describe drift that predates the canon sweep and still sits in ~24 flows. They are warnings up to the count recorded in site/scripts/canon-baseline.json for that (file, rule), and ERRORS above it — so a swept flow (baseline 0) fails CI the moment it regresses, and a backlog flow can’t get worse, without blocking anyone on drift they didn’t introduce. After you sweep a flow, run node site/scripts/lint-canon.mjs --all --update-baseline and commit the baseline so the flow graduates to zero-tolerance. Never run --all --update-baseline to do this — it grandfathers every unrelated violation in the repo (it once silently absolved 57 in a file the sweep never touched). Add the entries for the files you actually swept, and set the swept flow to 0 by hand. Never baseline a file that was already ERRORing — that silently weakens a gate which was doing its job; leave it failing.

    Two page-level rules worth knowing before you copy a page head. A top-level page carries no subtext — no .pg-sub under the .pg-title (web-page-title-subtext); an inner page, identified by its .pg-back, may keep one as record identity. And a tab strip in a table card head is a filter, so it takes .filt-select, not .pseg (web-pseg-in-table) — where tabs genuinely read better, such as “By zone / By rider” over one measure, declare it on the element with data-canon="allow-pseg-filter" so the decision is visible in review rather than argued twice. A flow’s design.md is linted too — not against the canon (it’s prose), but against its own sibling index.html. Naming a #pg-* page that no longer exists is an error; naming a dead .class, or prose describing a banned side drawer, is a warning. Both canon layers read the HTML and neither read the paragraph beside it, so docs went stale invisibly. When you change a flow, update its design.md in the same commit. To name a component deliberately without tripping the check, write the mention as a contrast — “never .tab-bar”, “.dw-row was replaced by .td-row”.

  6. Probe every view, then ratchet. Lint is only the file. Load the flow in the Browser pane and run the runtime/layout probe on every view — not the ones you touched:

    eval(await (await fetch('/_static/flow-probe.js')).text());
    flowViews().map(v => [v, flowProbe({ view: v })]); // all must be pass:true

    It asserts what no file-reader can see: a hidden element a class un-hides, undefined rendered by a var read before assignment, KPI cards that wrap or double-count, clipped nowrap headers, rows whose only target is the chevron, an unsorted or bar-less “ranking”, ragged text edges (painted glyph via a Range, not the cell box), grid-row cards of unequal height, a chart with no hover target, a popover wider than its trigger, a dialog that traps Escape, reflow on filtering. Not done while any view is pass:false.

    Then ratchet in the same commit: every finding either becomes a rule in site/scripts/canon-rules.mjs (pattern-detectable — staged:true + a baseline refresh when it is pre-existing drift) or a check in site/scripts/flow-probe.js (only visible after layout). Prove a new rule fires on a synthetic bad case and check its repo-wide count: zero hits usually means broken, not satisfied. Prose in DESIGN.md explains; it does not enforce.

Runtime + layout gate (free, automatic). probe-flows.yml runs site/scripts/probe-ci.mjs on every PR — headless, over every view of every changed flow, in light and dark — and blocks on any failure not recorded in site/scripts/probe-baseline.json. It asserts what no file-reader can see: a hidden element a class un-hides, undefined from a var read before assignment, KPI cards that wrap or double-count, clipped nowrap headers, unequal grid-row cards, a popover wider than its trigger, a chart with no hover target, a dialog that traps Escape, reflow on filtering. Locally: /probe-flow <file> in Claude Code, or PLAYWRIGHT_DIR=… node site/scripts/probe-ci.mjs <file>. Check the harness before believing a flood of findings — the first full-corpus run reported 405 failures and 146 were probe bugs (a tag-qualified view selector, and unrecognised view-switcher names reported as JS errors in the flow), not defects.

Archived flows are exempt from the canon gates. A flow marked archived: true in site/src/data/products.ts is skipped by lint-canon, flow-diff and probe-ci when they pick files automatically (CI’s diff scope, --all); naming the file still lints it, and every skip is printed. Without this a one-line fix to retired work would demand a full canon sweep of something nobody ships — the gates exist to stop drift entering LIVE work. The flag lives in the catalog rather than the file, so exempting a flow and hiding it from /flows are the same act, and un-archiving puts it straight back under the gate. Helper: site/scripts/archived-flows.mjs.

A Screens row is a sequence. A screen reached by clicking inside another screen is a frame in that screen’s row, in click order — only nav-named screens head a row, and any other candidate that will not drive at render time is dropped rather than shown as the wrong screen under a confident label. Children are found by CLICKING, not by reading handlers (openReceive('s-movement') names the screen to come back to). Frames grow to their content, so a long screen is not clipped to the device height. A state that needs typed input or a prior selection cannot be reproduced from a cold load and is deliberately omitted.

The /flows Screens canvas is DISCOVERED at runtime, so a flow can break it silently. site/public/_static/flow-canvas.js loads the flow and drives it to find each page’s states (the page, the detail a row opens, the dialogs it opens). Renaming a switcher, moving a view into a shared container, or wiring a dialog through a wrapper that needs a selected row breaks the canvas without failing lint or the probe. Audited across every product; the nav element, the switcher name, its arity, the page-container convention and the dialog-opening pattern all differ between flows, so nothing is assumed. The set is measured, not guessed: .rsm-nav-tab is the most common nav class in the repo (318 uses vs 216 for .sb-item) and leaving it out made every mobile flow report having no navigation; Caesar names one dedicated opener per page rather than passing a key to a switcher, and injects its modals into #modal-root so there is no element to find by id; hermes-web-app binds nav in a React listener, which no attribute scan can see. See the table in CLAUDE.md for the full list. After changing a flow’s navigation, views or dialogs: npm run build --prefix site && node site/scripts/canvas-audit.mjs --all — it replays every state from a cold load and exits nonzero on a recipe that does not land where it claims, a duplicate frame, an empty modal, or an unattributed screen.

Shared designs carry their own card, generated from the same discovery. /flows/ is one static file, so every design pasted into Slack previewed identically; each flow therefore has a real page at /s/<app>/<slug>/ with its own metadata, plus a committed 1200x630 card in site/public/og/flows/ built by scripts/og-shots.mjs (the homepage ProductCard.astro cover with the flow’s frame as the screen). npm run build does NOT refresh it, so after a redesign run PLAYWRIGHT_DIR=… node scripts/og-shots.mjs <product> <flow> against the built site. Three rules, each learned from a wrong card: never the login (recipe.isLogin); never a splash, identified by having no chrome inside the visible view (a document-wide search finds the app’s own tab strip sitting under the overlay and passes); and reload before the shot, because discovery has to click and those clicks leave the page dirty (atlas/cfsm shot as an empty board with a case panel hanging off the edge while the flow renders perfectly cold). Placeholder flows get no card and fall back to the generic image.

The web starter LINKS the design system. templates/web-flow-starter.html and the reference flow products/atlas/flows/starter-template/index.html load /tokens/tokens.css + /components/rechaj-ds.css instead of inlining ~170 rules each. Keep both links in a new flow and put only flow-local CSS in <style> — a linked flow cannot have a half-transplant, and a canon fix is one edit instead of a 36-file sweep. Migrating an EXISTING flow is not a codemod: linking full canon is additive, so a flow that diverged by omission gains properties it never had (canon .tbl is table-layout:fixed; one flow on the auto default lost 263px of table width and started clipping headers). Use scripts/link-ds-css.mjs then site/scripts/probe-ab.mjs and keep it only when NEW is 0.

Tokens are generated and enforced. tokens/tokens.css / tokens.ts / tailwind.preset.js are built from tokens/tokens.json by node scripts/build-tokens.mjs — never hand-edit them, and --check blocks the PR if they drift. The token-value-drift rule flags a flow that redeclares a DS token with a different value; if a surface needs a different colour, give it its own name rather than redefining --bg.

Two-layer canon gate. The lint (Layer 1) only catches pattern-detectable drift. Judgment-level issues — a custom class re-skinned to look canonical instead of the real component, congested/overflowing tables, misaligned atoms, oversized/non-circular icons, tinted status cards, wrong modal/KPI/tabs — need Layer 2: the design-reviewer agent (.claude/agents/design-reviewer.md). Run /design-review <file> before opening the PR (sweeps every atom against the live reference apps, fixes with the real canonical components, no re-skins, no feature changes). Layer 2 is local-only — there is no CI design review. The PR workflow was removed on 2026-08-25 (it billed one shared Claude token for the team). canon (lint-canon + flow-diff) is the only gate that runs on a PR, so run /design-review <file> yourself before opening one. Promote each new pattern-detectable finding to a rule in site/scripts/canon-rules.mjs (the ratchet).

Layer 0 — the gate runs BEFORE the PR exists, on the author’s machine. Everything above runs after a PR is opened, which is too late twice over: the author has context-switched, and a red check is only advice if nothing blocks on it. So the same canon job also runs locally and refuses to let broken work leave the machine. scripts/canon-gate.sh runs the same five steps as lint-canon.yml, in the same order, over the same diff scope (build-tokens --check → lint-canon → flow-diff → lint-flow-titles → probe-copy parity) and is called from two triggers: .githooks/pre-push (any git push, including from a terminal, an IDE, or a teammate not using an agent) and, for Claude Code, a PreToolUse hook in the tracked .claude/settings.json that intercepts git push / gh pr create / gh pr ready and exits 2 on a finding, so the agent reads the findings, fixes them, and retries — the PR is never created broken. Run it yourself with bash scripts/canon-gate.sh. Step 0 exists because a DELETED design is invisible to every other check: CI and the gate are diff-scoped with --diff-filter=d, which excludes deletions, so a branch whose only effect is that a flow is gone has nothing to lint and nothing to diff — PR #166 squash-merged into uat on 2026-08-27 and silently took 12 files with it (the whole atlas/field-validation-engineer flow, the whole products/bulwark product, two PRDs, three share cards), only 3 of which showed as removed in its own file list; opt in with CANON_ALLOW_DELETE=1. Four more properties to preserve: it exits 0 silently when no canon-gated file changed (~2s for one flow); it installs itself via SessionStart because scripts/install-hooks.sh was opt-in and a fresh clone therefore had no hooks at all — the same failure mode as a label-gated review nobody labels; it fails open on plumbing, closed on findings, since a hook that blocks every command because of its own bug gets disabled within the hour; and git stash push is neutralised before matching, because sessions sharing a checkout stash constantly. Layer 2 (the browser probe) is deliberately excluded — minutes per run, and a slow pre-push hook gets deleted. Bypass with CANON_GATE_SKIP=1, which buys a push, not a merge.

Nine gap-closers (2026-08-27). Auditing atlas/revenue-assurance-lead (8 merged PRs, canon green every time, four kinds of wrong) showed the gate was blind in nameable ways. Hard errors: card-side-accent-inline (a stripe in an inline style= — card-side-accent reads CSS rule blocks only), card-shape-side-accent (a card by SHAPE under any name — both card rules key off the substring card in the selector, so .leak-tile was invisible), web-flow-default-theme (<html data-theme="dark"). Staged + baselined: web-flow-links-ds (56), web-stacked-tables-not-tabbed (2), font-weight-700-label (79), icon-container-not-circular (48), em-dash-in-copy (227, promoted from a plain warning). spacing-off-grid is a WARNING only because the DS sheet itself carries three off-grid declarations. Four traps, all caught by linting the REFERENCE rather than trusting a count: (1) a staged rule must be declared severity:'warn' — lint-canon only ever UPGRADES a staged finding above baseline and never downgrades, so error+staged errors unconditionally (5 rules shipped that way and broke 3 swept flows; 8 pre-existing staged rules are also mis-declared); (2) a rule the DS itself fails is a broken rule — canon ships .pg-title{font-weight:700} and .icon-action{border-radius:var(--r-md)} deliberately, so both rules now ban DIVERGING from canon via canonDecl(); (3) segment on real view containers, not on “any div with an id”, or the starter template’s four cards in four views read as four stacked in one page; (4) never strip <script>/<style> with '' when you still need line numbers — blank to spaces and keep the newlines. Fixing (3) and (4) took one rule from 17 hits to 2.

Key canon: web tabs = .pseg (never .tab-bar/.ptab); web tables = .tbl-card>.tscroll>.tbl; web KPIs = .pstat; buttons = .btn-pri (green + #111) / .btn-danger-out (outline red, solid red only in a .dlg confirm); confirmations = centered .dlg on web, bottom sheets on mobile/tablet; mobile/tablet filter tabs + chips = white active pill on a grey track + green text (never a dark/black active pill or underline tabs); mobile headers = centered .rsm-page-head title (dashboard = .rsm-hero), never a left-aligned title bar; detail opens on row click, never pinned open; cards have no visible border; no emoji; no em dashes (use ·).

When asked to design, modify, or reason about any UI or service, follow this order. Stop at the first source that answers the question.

  1. PATTERNS.md — per-platform widget recipe book. Read this FIRST when starting any new design. Identify the form factor (web 1440 / tablet 834 / mobile 390) and pull the canonical widget classes from that section. Eliminates 80% of iteration on widget choice.
  2. DESIGN.md — global tokens, typography, spacing, hard rules, cross-form-factor invariants.
  3. tokens/tokens.json — full machine-readable token export (W3C DTCG).
  4. components/ — per-component documentation. The 2.6 MB components/index.html is the production component library — grep, never full-read. Search for the specific class (.dlg, .ds-btn-primary, .tab-bar, .sb-item, .lp-*, etc.).
  5. patterns/ — cross-product interaction patterns (bottom-sheet, list-picker, stat-grid).
  6. products/<name>/DESIGN.md — product-scoped overrides.
  7. products/<name>/screens/ — real in-context usages. Tie-breaker for composition. NOT canon — precedent screens may have drifted from PATTERNS.md.
  8. Ask the user — only if PRD is ambiguous about scope (persona, format), never about widget choice.

Hard rules that override anything you might infer

Section titled “Hard rules that override anything you might infer”
  • Never invent tokens, colors, components, or patterns that are not documented in PATTERNS.md, DESIGN.md, or tokens/tokens.json.
  • Color — palette only (mandatory). Every color is a palette token (tokens/tokens.json / color foundations page); no ad-hoc/off-palette colors. Only bare hex allowed: neutrals #111/#fff/#000 + documented status-chip hexes; else var(--token).
  • Typeface — General Sans only (mandatory). Body/UI = General Sans (or inherit); serials/code = SF Mono stack. No other font-family. Enforced by the non-canon-font lint rule.
  • No decorative gradients (mandatory). Flat palette-token fills only; the only allowed gradients are functional (progress rings conic-gradient, sticky-footer scrim to transparent).
  • Brand mark is the exact two-tone SVG in DESIGN.md — never substitute, recolor, or redraw.
  • Brand-green button = #01E46F background with dark #111111 text. NEVER white-on-green for utility buttons.
  • Confirmations on mobile + tablet use bottom sheets. Confirmations on web use centered .dlg modals — never bottom sheets on web.
  • Mobile prototype shells contain ONLY the device on a dark backdrop. No titles, labels, legends, or external chrome.
  • All icon containers are circular (border-radius: 50%). Rounded squares are reserved for photo/media thumbnails. Exception: canonical .rsm-profile-row-ic uses 10px rounded square (Kit Assembler precedent).
  • All DS icons are OUTLINE style — fill:none; stroke:currentColor (stroke-width ~1.8–2), never filled. Covers every icon: nav, KPI/tonal frames, list rows, and icons inside buttons. Give each icon <svg> an inline stroke="currentColor" (Polaris convention) so a size-only CSS rule can’t drop it. An icon svg with no stroke renders wrong (invisible or a black blob) → an empty icon frame. Every icon container shows a real glyph; avatars show initials. Only the two-tone brand mark and multi-color logos (e.g. Google “G”) are filled.
  • Spacing on a strict 4px grid. No 7/11/15px arbitrary values.
  • Weight discipline. Headings and titles are semibold (600). 700 is for display numerals only (KPI/stat digits, wallet/balance amounts, hero numbers) plus splash/welcome/success display titles and the sidebar wordmark. Never 700 on a text label (a stat-row value holding a word, a table/column header, a kanban/card title → 600). b/strong render at 600.
  • Badges (any form factor): 12px / 500 / 6px radius / NO uppercase / letter-spacing -0.01em. The canonical .ds-badge widget.
  • Cards have NO borders — box-shadow: var(--sh-card) only. Status hero on detail screens = white card + small 36×36 tonal icon, never a full-card tinted background.
  • No emojis anywhere in the UI.
  • Mobile flows = 390px. Tablet = 834×1194. Web/desktop = 1440px.
  • Flow titles in site/src/data/products.ts are short + single-line — aim for ~3 words, hard cap 30 chars. Long names wrap in the /flows catalog cards and sidebar. Put descriptive detail in description, never the name. Enforced diff-scoped by site/scripts/lint-flow-titles.mjs (CI canon job): new/changed titles over the cap fail; the existing backlog is grandfathered.
What Where
New web/desktop/back-office flow products/atlas/flows/<persona-slug>/index.html — Atlas is the umbrella product for every Rechaj web surface. Do NOT create a new top-level products/<some-web-tool>/ folder. Assign a sidebar sub-category — set category: 'onboarding' | 'swap' | 'tricycle-hardware' | 'support' on the flow’s entry in site/src/data/products.ts. If which one isn’t obvious, ask the user before placing it.
New web/desktop static mockup or dashboard products/atlas/screens/<kebab-case>.html
New web/desktop multi-state prototype products/atlas/prototypes/<flow>.html
New mobile / tablet screen / mockup HTML products/<product>/screens/<kebab-case>.html
New mobile / tablet canonical flow products/<product>/flows/<persona-slug>/index.html
New PRD section products/<product>/docs/prd.md
Changelog entry products/<product>/changelog.md (Keep a Changelog format)
New shared pattern patterns/<pattern>.md
New token tokens/tokens.json (and reference it from DESIGN.md)
New feature spec specs/<year>/<slug>/{proposal,design,tasks}.md

Different phases of design work suit different models. See agents/routing.md:

  • ideate → Claude (open exploration)
  • mockup → Gemini (design-system implementation)
  • refine → GPT (iteration under tight constraints)
  • drift-check → any (deterministic verification)

Skills in .claude/skills/ declare their preferred phase.

  • Figma MCP — only to read specific frames during transition. Never fetch full files. The bet is that DESIGN.md + interactive HTML are replacing Figma; new design work originates here.
  • GitHub — PR / issue context for the rechaj org.
  • Metabase — live product metrics (drift-check, metrics.md updates).

MCP server config is in agents/mcp/mcp.json.