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.
What this repo is
Section titled “What this repo is”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:
-
Sync to current
uatfirst — and again before you push:git fetch origin && git merge origin/uat(or run the/sync-uatskill). 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 onuat, do NOT blind-merge — open the current version and re-apply your change on top of it (see/sync-uatfor the mechanical-vs-rebuilt distinction). CI branch protection isstrict: a behind branch cannot merge. -
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. -
Read the platform’s Page Anatomy + atom specs in PATTERNS.md.
-
Self-check every atom (checklist in PATTERNS.md) — shell, tabs, table, buttons, badges, KPIs, overlays — to canon, not “close enough”.
-
Run the lint to 0 errors:
node site/scripts/lint-canon.mjs <file>. CI runs it on every PR intouat/main; a file with canon errors cannot merge. The gate covers flows,products/*/screens/, andproducts/*/prototypes/(screens/prototypes were previously unlinted — how the Swap Super Admin shipped a hand-rolled shell); it also flags a.sidebar/.topbarwith 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 insite/scripts/canon-baseline.jsonfor 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, runnode site/scripts/lint-canon.mjs --all --update-baselineand commit the baseline so the flow graduates to zero-tolerance. Never run--all --update-baselineto 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-subunder 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 withdata-canon="allow-pseg-filter"so the decision is visible in review rather than argued twice. A flow’sdesign.mdis linted too — not against the canon (it’s prose), but against its own siblingindex.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 itsdesign.mdin the same commit. To name a component deliberately without tripping the check, write the mention as a contrast — “never.tab-bar”, “.dw-rowwas replaced by.td-row”. -
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:trueIt asserts what no file-reader can see: a
hiddenelement a class un-hides,undefinedrendered by avarread 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 aRange, 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 ispass: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 insite/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.ymlrunssite/scripts/probe-ci.mjson every PR — headless, over every view of every changed flow, in light and dark — and blocks on any failure not recorded insite/scripts/probe-baseline.json. It asserts what no file-reader can see: ahiddenelement a class un-hides,undefinedfrom 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, orPLAYWRIGHT_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: trueinsite/src/data/products.tsis skipped bylint-canon,flow-diffandprobe-ciwhen 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.jsloads 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-tabis 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-rootso there is no element to find by id;hermes-web-appbinds 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 insite/public/og/flows/built byscripts/og-shots.mjs(the homepageProductCard.astrocover with the flow’s frame as the screen).npm run builddoes NOT refresh it, so after a redesign runPLAYWRIGHT_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/cfsmshot 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.htmland the reference flowproducts/atlas/flows/starter-template/index.htmlload/tokens/tokens.css+/components/rechaj-ds.cssinstead 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.tblistable-layout:fixed; one flow on theautodefault lost 263px of table width and started clipping headers). Usescripts/link-ds-css.mjsthensite/scripts/probe-ab.mjsand keep it only when NEW is 0.Tokens are generated and enforced.
tokens/tokens.css/tokens.ts/tailwind.preset.jsare built fromtokens/tokens.jsonbynode scripts/build-tokens.mjs— never hand-edit them, and--checkblocks the PR if they drift. Thetoken-value-driftrule 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-revieweragent (.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 insite/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
canonjob also runs locally and refuses to let broken work leave the machine.scripts/canon-gate.shruns the same five steps aslint-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(anygit push, including from a terminal, an IDE, or a teammate not using an agent) and, for Claude Code, aPreToolUsehook in the tracked.claude/settings.jsonthat interceptsgit push/gh pr create/gh pr readyand exits 2 on a finding, so the agent reads the findings, fixes them, and retries — the PR is never created broken. Run it yourself withbash 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 wholeatlas/field-validation-engineerflow, the wholeproducts/bulwarkproduct, two PRDs, three share cards), only 3 of which showed as removed in its own file list; opt in withCANON_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 viaSessionStartbecausescripts/install-hooks.shwas 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; andgit stash pushis 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 withCANON_GATE_SKIP=1, which buys a push, not a merge.Nine gap-closers (2026-08-27). Auditing atlas/revenue-assurance-lead (8 merged PRs,
canongreen every time, four kinds of wrong) showed the gate was blind in nameable ways. Hard errors:card-side-accent-inline(a stripe in an inlinestyle=—card-side-accentreads CSS rule blocks only),card-shape-side-accent(a card by SHAPE under any name — both card rules key off the substringcardin the selector, so.leak-tilewas 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-gridis 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 declaredseverity:'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 viacanonDecl(); (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 ·).
Lookup order — never invent
Section titled “Lookup order — never invent”When asked to design, modify, or reason about any UI or service, follow this order. Stop at the first source that answers the question.
- 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.
- DESIGN.md — global tokens, typography, spacing, hard rules, cross-form-factor invariants.
- tokens/tokens.json — full machine-readable token export (W3C DTCG).
- components/ — per-component documentation. The 2.6 MB
components/index.htmlis the production component library — grep, never full-read. Search for the specific class (.dlg,.ds-btn-primary,.tab-bar,.sb-item,.lp-*, etc.). - patterns/ — cross-product interaction patterns (bottom-sheet, list-picker, stat-grid).
- products/<name>/DESIGN.md — product-scoped overrides.
- products/<name>/screens/ — real in-context usages. Tie-breaker for composition. NOT canon — precedent screens may have drifted from PATTERNS.md.
- 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; elsevar(--token). - Typeface — General Sans only (mandatory). Body/UI =
General Sans(orinherit); serials/code =SF Monostack. No other font-family. Enforced by thenon-canon-fontlint rule. - No decorative gradients (mandatory). Flat palette-token fills only; the only allowed gradients are functional (progress rings
conic-gradient, sticky-footer scrim totransparent). - Brand mark is the exact two-tone SVG in DESIGN.md — never substitute, recolor, or redraw.
- Brand-green button =
#01E46Fbackground with dark#111111text. NEVER white-on-green for utility buttons. - Confirmations on mobile + tablet use bottom sheets. Confirmations on web use centered
.dlgmodals — 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-icuses 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 inlinestroke="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/strongrender at 600. - Badges (any form factor): 12px / 500 / 6px radius / NO uppercase / letter-spacing -0.01em. The canonical
.ds-badgewidget. - 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.tsare short + single-line — aim for ~3 words, hard cap 30 chars. Long names wrap in the /flows catalog cards and sidebar. Put descriptive detail indescription, never thename. Enforced diff-scoped bysite/scripts/lint-flow-titles.mjs(CIcanonjob): new/changed titles over the cap fail; the existing backlog is grandfathered.
Where new work goes
Section titled “Where new work goes”| 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 |
Phase-aware model routing
Section titled “Phase-aware model routing”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.
When to consult external sources
Section titled “When to consult external sources”- 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.