page-pass
The one skill for taking a page to done: its agent surface and its UI/UX, in one pass. Use when assigned pages or routes to pass, on `/page-pass <route>`, or when a campaign brief says 'do the page pa
SKILL.md.claude/skills/page-pass/SKILL.md
GitHubpage-pass — take one page to done
<!-- rules:stamp -->Rules from
common-docs/systems/platform/ui-shell/FEATURE.md— edit there, never here (python3 common-docs/meta/scripts/one_home.py --write).
- Delete means archive, everywhere. A delete that does not archive is a defect.
- A menu regroup ships as a side-by-side demo page with a what-goes-where list, never as a direct change.
- Every surface mounts the canonical menu. Use
NonEditableContextMenuorEditableContextMenufromfeatures/context-menu-v3/. Never use a bespokeDropdownMenuor a rawcontextmenuhandler, and never keep both. - A menu without a source is a defect. A menu with no
contentSource,entityorsurfaceNameopens with every verb dark. Fix these before unwired surfaces. - Lists built on
EntityListPageinherit the row menu throughItemContextMenu. They need their own wiring only for surface-specific sections. - A window or overlay mounts its own menu. It never delegates to the page under it.
- Reuse the domain's action registry. Never create a second one.
- Secret-bearing surfaces follow
VaultContextMenu. PassgetApplicationScope, forceselection: "", put names and field keys in the content (never values or notes), and pass noentity. - Done means the menu opens clean. The console shows no
INERT MENUorVALUE MAPPING GAPscream. - Chat is a universal agent host. Never bind a page-agent roster to it.
- Anything with an id shows it through the one canonical id component (display plus one-click copy).
- Flagship and first-run screens get a bespoke elevated stage, not the 28px app-chrome controls.
- Before a link goes to Arman, every screen on its path passes the Steve Jobs bar (walked and fixed by an Opus reviewer).
- Admin tables filter, search and sort on the server, and people are picked by name or email, never by id.
- Wasted vertical space is the worst sin; every status shown is backed by real state.
- A new output style or menu layout is a NEW function used only by the new path; never thin a shared builder.
- A page fetches only what it shows, each thing once; every removed fetch ships its on-engagement load.
- Floating things never move the page. Pills, assists, badges, alarms, notices, toasts and the
chat dock float over it; when one would cover something, it moves itself (lifts, rests above
the bar, yields). No page pads, shrinks or re-centres for one. Only the empty scroll room after a
page's last line may grow. Guard:
pnpm check:overlays-never-move-page(matrx-frontend). - Every feature has the same pages (front door,
/all,/new, record, sub-feature) and derived breadcrumbs: feature pages.<!-- rules:stamp-end -->
Version 1 (2026-09-27). Built with Arman and tested on real pages; every
miss found in testing becomes a rule here. This file plus
page-types.md is everything you read up front. Each rule
below is complete as written. The skill named after a rule holds the step-by-step
procedure — open it only when you are doing that job or the check fails.
What you do with a page — seven steps, in order
- Look first. Run
pnpm page:look --route <route>(every route the page has, with a real record id): it signs in as the test admin and writes desktop 1280×800 and phone 375×812 screenshots in light and dark, pluslook.json— console errors, failed requests, anything under the header, charts/images drawn at zero size, how much of the first screen holds content, headings, text under 12px, emoji, small phone targets. Add--fullto see below the fold,--signed-outfor what a visitor sees,--as memberfor an ordinary (non-admin) person,--freshfor a first visit (no remembered organization or view prefs),--org "<name>"to pick the workspace first,--views desktop-light,phone-lightto go faster, and--click '[right:]SELECTOR=>TEXT'(repeatable; chain steps on one load with>>) — TEXT matches the START of the text or aria-label ("About Theme" needs=>About) to open each menu, dialog and tab and capture it. A slow page needs--settle 12000. Read every screenshot yourself; the numbers are places to look, not verdicts. The dark views force the dark class, so a theme control reading "Light" there is the tool, not the page. For a longer interaction, write a throwaway Playwright script in your own TMPDIR modeled onscripts/page-look.mjs— never a dev server. Use it the way a person would: click every control, open every menu, dialog and tab. Before you read any rule, write down everything that looks wrong or wasteful. This list is yours; the rules must end up covering every line of it. - Name the page's type from
page-types.md. The type says which core rules change and what it adds. Unsure between two types → pick the one whose "recognize it" line fits, and say why in your report. - Run the core (below), then your type's additions. Every rule gets a
verdict:
pass·fixed(what) ·na(one-line why) ·deferred-visual(code done; name the exact look still owed) ·arman(question filed) ·blocked(what blocks). - Fix what you found — the cause, not the symptom. Never remove, rename or
hide a feature, or a designed look another page relies on, to make a rule pass. Database changes are part of the job
(functions, grants,
client_callable_doorrows, per CLAUDE.md; new tables only viaplatform.create_entity_table). Pick ONE path: through the Supabase MCP with NO migration file, or a file you write and apply withpnpm db:apply(ledgered). Never both — an MCP-applied change with a file left beside it reads as unapplied drift. A DROP/CREATE of a function loses its grants and itsclient_callable_doorrow: restore both in the same transaction. A shared-link fix often reaches past the page into the shared execution stack (org gate, bindings, the server's guest path) — follow it there. If the fix belongs in a shared component or package, first list every page that uses it (search its imports in every repo). A behavior or bug fix that leaves the look unchanged: fix it and prove it live on a few of those pages. A change to how it LOOKS: ask Arman first, one simple question with the full page list and before/after links (law:common-docs/policies/no-dead-ends.md); if it suits your page and not the others, make it an option for your page, never the new default. Run that component's tests (npx jest <its dir>; menus:npx jest @ai-matrx/context-menu, which runs the heading check). If it belongs on the server (a read wrongly gated, a 4xx/5xx the page can't fix), fix it in../aidreamunder that repo's CLAUDE.md, same commit rules, and name both commits in your report. - Prove it live. Every fix seen working on the real page; every main action carried through to its real saved result as the page's real audience (signed out for a shared link or promotional page — a guest run, not an admin run), then re-checked (console and Error Inspector clean after, not just on load).
- Look again as a stranger — the final look. Needs your changes live: push
(or have your coordinator push), then wait for the release with
until pnpm -s page:look --route <r> --views desktop-light --commit <sha> --out /tmp/<you>/w; do sleep 120; done(exit 3 = not live yet). On the live result, runpage:look --full(and--signed-outwhere visitors come) and review it as a demanding customer who never saw your work: list every flaw you can still see, rule or no rule. Look at EVERY mode and state the page has (each view/edit mode, each tab, empty/loading/error, the phone path to each — in light AND dark; a third-party stylesheet once made one view's text near-black on dark), not just its default screen: a notes pass once graded its Read view and missed a stock third-party Write view with no app menu. Fix them, or report each as open. Blind reviewers grade the page after you; a flaw they find that your final look missed counts against the pass. - Record it. One Change Log line in the owning feature's
FEATURE.md(no FEATURE.md for this route? the nearest one that owns its code) in the same commit (page-pass <date>: type <x>, posture <x> after <product>, fixed …), then the report below.
If the live site or database is down (504s on several pages, db.matrxserver.com
reads timing out), stop the live steps, write INCIDENT: <time, what failed> in
your report, keep doing static work, and resume the live steps when it answers —
an outage is never a page finding and never yours to chase.
You decide; you never interview. Product questions only — does a feature
exist, should a surface move in the hierarchy, a size or policy outside what
this file allows — go to Arman through ask-arman; keep working on everything
else. Posture skills that say "interview first" mean a person live in the
session, never you.
Where you work — pick your lane once
| Local lane (a Mac session) | Cloud lane (a cloud container) | |
|---|---|---|
| Server | none needed to look; pnpm preview:start (the ONE shared server, http://<session>.localhost:3001) only for a rapid fix-and-look loop on UI you are changing right now |
none — a container cannot run a dev server and a type check together |
| Look and prove | TMPDIR=/tmp/pl-<you> pnpm page:look --route <route> --commit <sha> against https://aimatrx.com (admin routes are served from https://manage.aimatrx.com — check the commit there) after the release carrying your commit (about hourly; exit 3 = not live yet — work on something else) |
same |
| Agent surface proof | TMPDIR=/tmp/pp-<you> pnpm surface:probe --surface <name> --route <route> --commit <sha> --out <file> (a window/dialog surface: add --open 'SELECTOR=>TEXT' so it is opened before the read) (+ --agent '<request>' for write targets) |
same |
| A view you cannot see | deferred-visual with the exact step |
same |
Both lanes:
- A hot shared file holds someone else's uncommitted hunks (the surface
registry,
route-to-surface.ts)? Commit only your hunks through a private index, then re-sync the real index for those paths:git diff -- <file> > /tmp/<you>.patch # edit it down to your hunks only GIT_INDEX_FILE=/tmp/<you>.idx git read-tree HEAD GIT_INDEX_FILE=/tmp/<you>.idx git apply --cached /tmp/<you>.patch GIT_INDEX_FILE=/tmp/<you>.idx git commit -m "page-pass(<route>): …" git reset -q -- <file> # index only; the worktree keeps their hunks - Admin routes redirect to
manage.aimatrx.comby themselves; pass--base https://manage.aimatrx.comonly if a look lands on the wrong host. - A page that needs an organization or record opens with it in the URL
(
/hr/settings/employer?org=<slug>,/crm/<id>): pass that full route topage:lookand the probe. pnpm page:checks <your files or dirs>runs every check this skill names (six at a time, ~3 min) and prints CLEAN / FINDINGS / ERROR per check for YOUR files only; red on other files is not yours. The per-area check lists below say what each covers.- Type check:
pnpm type-check(TypeScript 7, whole repo, ~30 s; the machine-wide queue folds your call into any other waiting one). Never a focused tsconfig and never a direct compiler call — both bypass the queue and cannot be shared. Errors in files you didn't touch are not yours; list them. - Every commit parses. Before each commit run
pnpm -s check:parse(~20s, whole tree; red outside your files is not yours) — a quote inside a string once broke a manifest on main between two commits. Committing early never means committing unparsed. - Commit right after each coherent edit, before running checks — a sync sweeps the shared checkout every ~30 minutes and commits any dirty file under its own message, so a file left uncommitted loses your authorship and message. It can still win the race by seconds. If it swept your edit, that is fine: name its commit as yours in the report and move on — never rewrite it.
- Never delete code to clear a check. A flow a guard flags is fixed, not removed; deleting needs Arman's written word (unfinished-work alarm). Check
git logfirst: a worker once deleted a login flow another lane had hardened 16 minutes earlier. - Prove a test fails on a scratch copy, never by breaking the real file — the sync sweep can commit the broken version in the seconds it sits there. Copy the file (or its old version from git) into your TMPDIR and point the test at it.
- zsh does not split a variable holding several paths: use an array
(
F=(a.ts b.tsx); git add -- $F) or list the paths literally. - Commit by path per page (push unless your coordinator says it pushes):
git add <files>→git commit --only -m "page-pass(<route>): …" -- <files>→ push. One shared checkout onmain: no branches, no worktrees, no tree-wide git, never format a file you didn't create, never runrelease.sh. After a pull, re-runpnpm install --frozen-lockfileif the lockfile changed. - Test data you create is obviously named (
PP test — …) and listed in your report.
Content a person authored is not the page. An app's own layout, a document's text, a user's template body are judged by their author; the rules here judge our chrome around them (header, menus, run controls, states, agent surface). Say which parts you judged as authored content.
The core — seven areas, every page
1 · Agents can work on the page
- Registered: a surface manifest in
features/surfaces/manifests/whose values describe THIS page (a record's sub-views — run, code, settings — may share the record's family surface when every value is true on each; a sub-view with data of its own gets a child surface) (a list page never borrows its parent's one-record surface — give it its own), label = the page's human name, route mapped inroute-to-surface.ts, DB mirror synced. - Sees everything: every piece of data the page loads and a person could
point at is a declared value, emitted by one pure scope module from state the
page already rendered (
getScopenever fetches). Not loaded yet → omit the key; loaded and empty →[]/0/""; a failed load reports its status (aload_error-style value the agent can see), never counts, rows or a value that contradicts what the person sees. Keep existing value names (stored bindings use them). - Sees what its jobs need up front — the context budget. An agent that
lacks what it needs fails or spends more tokens looking it up, so send it —
up to 10,000 chars per page in total, spent by the page's shape: a focused
record ~10,000; a record with its comments/folders ~7,000 + the rest shared;
a list page's condensed visible list ~4,000; a broad page (SEO, dashboards)
only a ~2,000-3,000 overview and a guide for discovery. Pack it as ONE XML
bundle with
../aidream/apps/shared/chat/src/surfaces/runtime/context-bundle.ts— never raw JSON rows. The bundle is what the agent reads up front; the page's individual values stay declared too (bindings and write twins use their names) at the default size. Worked bundles:features/research/browse/surface.ts(list),features/flashcards/components/home/deckSurface.ts(list); older manifests that inline raw arrays are not examples to copy. Over budget needs Arman's approval. Procedure:surface-write-targetsStep 4. - An agent sees the choices of anything it can set — a stage, rating or
role target ships its options as a value (
<field>_options: id + label), and a write the agent makes shows where the person looks (the tile that lists it). - Every person-action has an agent twin — count them. List every create / edit / archive control on the page (New deal, Add file, a toggle) and the write target that does the same job; put that mapping in your report. A control with no target is a gap you close or explain.
- Can change what makes sense: every record type the page lists gets
create_/update_/delete_<plural>over lists (one set per type, built withcollectionWriteHandlers), plus<record>_draftwhen the page has a "New ___" dialog, anddrafttargets for authored fields on editors. Allask. Handlers save through the page's own save function, validate the whole value before the approval card, and return what landed with ids. Never targets for ids/ownership, credentials, billing, permissions, or derived numbers (scores, counts). - Told how: the intro names which target does which job; a page with more than one record type or rules the descriptions can't hold gets a guide.
- Right-click menu: one menu per pane, delegated per row (a
MatrxDataTabledoes it withcontextMenu={{ resolveRowContext }}+createTableRowMenuDescriptor— worked:lib/entity-list/components/EntityListTable.tsx;EntityListPagelists get it for free), wrapper carriessourceFeature+surfaceName+getApplicationScope; the runtime provider goes AROUND the menu. A page showing a real record passes that record's owncontentSource(andentitywhen it can be attached or shared);{type:"raw"}only when there is no primary text. Items are short verbs with no subtext — the one exception is a disabled item, whosedescriptionsays why it is off and where it works. The menu's last entry (this page's label) is added by the platform fromsurfaceName; if it reads "This page" or another page's label, the surface mapping is wrong. - Agents menu: every AI job the page already runs appears once in the top Agents menu; nothing is added to the visible page to announce it, and no AI feature is invented to fill it. A mandate opens in place.
- Agent feedback read first:
pnpm surface:feedback --surface <name>prints SQL — run it through the Supabase MCP; fix what agents reported or say why not. - Proven with a real agent: the probe reads the page (every visible value
supplied, nothing undeclared, no
INERT MENU/VALUE MAPPING GAP); with write targets,--agent '<a real request>'creates two, updates one, archives one, gets one refused before the card — and a read-only SQL check shows created rows complete, matching one made by hand. - Checks:
pnpm check:surface-drift·check:surface-routes·check:surface-impact <name>·check:context-menu·check:menu-naming·check:agent-disclosure. - Procedures:
surface-write-targets(Steps 0-4: targets, handlers, inline tiers, guide, live agent test) ·surface-authoring/references/campaign-worker.md§2 "Rules that bite" and §3 sync ·context-menu-v3·agent-disclosure.
2 · It actually works
- Every button, link and menu item does something real — no dead controls, no toast stubs, no disabled-looking controls that work or working-looking ones that don't.
- A write is proven by the saved row, never by the absence of an error.
For every save/create/delete the page offers, follow the call to the
database function it reaches and confirm that function accepts it (a
generic upsert may refuse the kind you send); then, live, save a
PP test —value and read the row back with SQL, and restore it. A button that "works" and saves nothing is the worst dead control. - Read the field that exists. Reading a property the type does not have
(
request.errorMessagewhen the field isrequest.error) makes every failure silent — let the type check catch it (no casts), and show the real reason. - An empty read is not a success. A lookup that returns no row where one must exist (access refused, wrong id) is an error the person and the agent see — never a silent return that leaves the page "still loading" forever.
- A control is never enabled while what it needs is loading only to refuse on press — it shows a pending state, or waits and then acts.
- A server render never waits on the database without a limit. Every read
a page's server render (and
generateMetadata) makes has a timeout and a fallback — metadata falls back to the generic title, the page to an honest error — so a slow database is never a platform 504. - An error affordance appears only in an error state — an error menu, red
icon or retry never renders beside a healthy "Saved" (
ErrorAlchemyMenugoes inside the error branch; it reads the error box it sits in). - A vague error has a real cause — find it. "Couldn't load …" is a symptom:
read the recorded failure (
errorsMCP tool, orops.system_errorvia the Supabase MCP) and fix the cause. - Every load ends: data, an honest empty state, or a visible error with a retry after a bounded wait. Never an endless skeleton or spinner.
- Loading says what is loading:
SuspenseLoaderwith amessagefor compact states; aSkeletonfrom@ai-matrx/design-systemshaped like the content for content areas. Never plain "Loading…" or a bare pulsing box. - Errors surface; nothing is swallowed. AI work streams into the live-run window, never a spinner.
- Every record the page names (a person, agent, document, a count over our records) opens — open, new tab, peek or window. Every detected problem carries its one-click fix.
- What a person typed is never lost — on every page, window and dialog:
closing, navigating or a refresh keeps the draft (or asks first). Any
non-empty text counts — a minimum length before saving a draft loses short reports.
Unsaved edits in a form are guarded against in-app navigation too (header
Back, links, browser Back): use
lib/navigation/useUnsavedChangesGuard.ts, never a page-localbeforeunload, and give edit mode a Discard. - A custom editing field keeps everything a plain field gives — replacing a textarea/input with a rich or chip editor must keep: undo/redo for every change (inserts and auto-formatting included), plain-text paste, the caret never landing inside a non-editable piece, Home/End/Enter at the edges, and the standard field toolbar (voice and the page-agent door). Test each one live by typing — every one of these failed on first build.
- A Stop / Cancel / Undo is proven by the server's state, not the client's. Stopping a run means the server stops generating (reopen it: no finished answer), not that the screen hid it; after a stop the screen says "Stopped" and offers the next step.
- A control's saved value must change what it claims to change. Prove it by the effect (switch the model off → it is gone from the picker), never by the saved row alone: a settings switch once wrote a list nothing read.
- Catalog text written for engineers never reaches a person ("Fires when…", "Audience:", spec tags) — fix it at the source; a display filter is only the stopgap while the source rewrite lands.
- An empty value is still a supplied value: a context inspector or a check
that treats
nullas "missing" raises a false alarm. - Desktop keeps every action visible; only the phone folds. Folding into a "More"/tools menu is a phone fix — applying it at every width hid features and Arman had it reverted (2026-09-27). On desktop, fit actions into fewer rows, never behind a menu.
- A control the device cannot run is absent (screen capture on a phone), never a button that fails.
- The screen shows what was saved. After a save the view re-reads (or applies the returned row) — never the pre-save copy. Saving an untouched form changes nothing: a save writes only the fields the person changed, and every field round-trips exactly (a toggle labeled "Private" never writes another visibility; a blank type is never saved as a default).
- A count on a button equals what it opens ("Review 81 due" opens 81, not 29).
- An automatic AI job re-runs only when its inputs meaningfully change — never on page open, a view switch, or an empty record it created itself.
- Numbers are plausible. Sanity-check every computed figure against the
rows behind it (a study time of 57 days, a count that disagrees with its
list, a 0 shown beside content that exists are defects).
A count or total built from a plain list read stops silently at 1000 rows —
read it with
readAllRowsor count on the server (an admin "in DB" count read 1 where the database held 31). Color carries meaning only when it varies: a green badge on nearly every row is noise. - Any change that loosens a gate is reviewed before it ships — dropping an
organization requirement, a new grant or door, a row-security or auth
exemption: dispatch an independent reviewer (standard lane) to trace every
call path to the end (the org-free
/browsedeclaration broke every Microsoft browse five calls down). Say so in your report. - First ask whether it needs an organization at all. A person's own records (their connections, their settings, their history) never do — if the server demands one for them, fix the server declaration, don't hold the page. A page re-runs its loads when the header organization changes.
- A request that needs an organization is held, never failed. With no
organization selected, the page shows the person's memberships inline and
proceeds once one is chosen (
lib/organization/organization-gate.ts); an automatic AI job waits for its organization rather than failing on load — never an error box, and never an internal key (app.some_mandate,execution_error) on screen. A page opening in an error state before the person did anything is a first-look failure. - Edit rights follow the person's access (
iam.has_access_for), never "created_by = me" alone. - Nothing that decides what a person may do is writable by that person.
When the page reads a plan, grant, quota, usage, role or approval, confirm
the browser cannot write it:
select has_table_privilege('authenticated', '<schema.table>', 'UPDATE')(and INSERT/DELETE) must be false. If it is true, the fix isclient_read_only = trueon itsplatform.entity_typesrow, theniam.apply_table_grants(<schema>, <table>, <its rls_variant>), proven by a refused write as the test user (the D355 pattern). - Reads and writes go straight to the database through the feature's service;
a list treated as complete uses
readAllRows. - A browser component never imports a value from server-only code (a loader
using the server database client,
next/headers,"server-only") — share constants from a plain module. The type check cannot see this; the build breaks. - Checks:
pnpm check:dead-ends·check:unwired·check:static-loader·check:client-server-only. - Procedures:
real-loading-states·no-dead-ends.
3 · The first screen is right
- The title stands alone. One title, in the page header, with no description or subtitle under it and no second title or hero in the body. No welcome text, no "this page lets you…". Help is one short sentence next to the thing it helps with, or a tooltip. The same holds for every section and card: a heading, not a heading plus an explanatory sentence.
- Empty is compact. A thin record never shows a wall of "—" rows or a stack of empty cards: empty fields collapse into one "Add …" affordance, and an empty section is one line with its create action, not a full card.
- Nothing sits under the header. The header is glass over the page. Content
that must stay visible — buttons, toolbars, card grids, banners — starts
below it with
pt-[var(--shell-header-h)](never a hand-typedpt-12), and at rest nothing interactive or readable sits inside the glass or its fade. Content that scrolls freely may pass behind it while scrolling. - No wasted space. At 1280×800 the real work (the table, the editor, the list) is on screen without scrolling and uses the width. No empty bands, no oversized headings, no cards holding one label and an icon, no tiles that show one small number, no box inside a box, no narrow centered column unless it is long reading text.
- The floating assistant never covers content. The element that actually scrolls the page's last content reserves bottom space for the fixed chat composer / assist dock, so the final item scrolls fully above it at desktop and phone widths (check each content mode).
(core)routes: header via<PageHeader>; bodyh-full overflow-hidden; neverh-screen,100vh,h-page; no fake title bar in the body.- Density: sharp by default — clean, compact, generous only where it helps
reading (Linear, Stripe); dense for all-day power pages — tables, tight
rows, many columns (Airtable, Datadog). Name the posture and the real product
you matched in the Change Log line; open
ui-sharp/ui-denseonly if unsure. - Checks:
pnpm check:page-headers·check:scroll-chain:strict· live look at the top edge on desktop and phone. - Procedures:
core-route-headers·.claude/ui-skills/shared/application-ui-copy-and-hierarchy.md·ui-sharp/ui-dense.
4 · It works on a phone
- Every desktop action exists on a phone (header actions → bottom sheet);
functionality is gated with
useIsMobile(), never just hidden with CSS. - Finger-sized: the page's section roots and every
DialogContentcarrymatrx-touch-targets(44px on touch, desktop stays compact — but a control's ownh-*/min-h-*class overrides the floor, so check the rendered size); a checkbox, radio or switch getsmatrx-tap-areaon its label. - Nothing hover-only — including a tooltip that carries real information (a
limit, a reason): on touch it opens on tap (a popover), or the text is shown.
A control revealed with
opacity-0 group-hover:opacity-100also carriespointer-coarse:opacity-100. - A plain
<Dialog>becomes a bottom sheet by itself; hand-roll a Drawer only for a drag handle or a genuinely different layout. dvhnevervh;pb-safeon anything fixed to the bottom; one scroll area; no tabs as phone navigation; tables reflow; popups capped with internal scroll; fields authoredtext-base, never an inline font size under 16px.- Long-press opens the same menu as right-click.
- Checks:
pnpm check:phone-layout· live look at 375×812, tapping everything. - Procedure:
ios-mobile-first.
5 · It looks like one product
- Only our standard parts. Buttons, icon buttons (TapButtons from
@ai-matrx/tap-target/buttons), inputs, pickers, dialogs, tables and lists come from the design system. A hand-styled<button>or one-off control that does what a standard one does is a finding — replace it. - Tables:
MatrxDataTable, sort and filter on every column. Lists:EntityListPagewith saved view preferences and an archive control wherever records can be archived. Pickers over a growable list offerCreate "what you typed". - Dialogs open through their typed opener (
useOpenX()), neverdispatch(openOverlay(...)). No browserconfirm/alert/prompt;toastonly from@/lib/toast. With a layer open, the page behind stays usable. - Lucide icons only; no emoji; no Sparkles for AI;
INTELLIGENCE_ICONonly for Intelligence,AGENT_ICONfor Agents. - Text a person reads is 12px or larger; 10px only for small all-caps section
labels and keyboard hints. Machine labels (
some_key) are humanized. - Show what a person recognizes, never an internal key — ours or the
data's (import paths like
Biology::Cells, slugs, codes). A value is shown as its display form (proper case, platform named, a link when it is a link), never its normalized/dedupe key, slug, raw id or JSON. A person never types JSON: structured input gets real fields (an address is address fields, a state is a state picker, a fixed set is a select). - One name per thing. The header, tab title, buttons, placeholders, empty and error states use the same noun (a "deck" is never also a "set").
- Nothing internal reaches a person. No test/fixture product names, no engineering notes ("no read path…", ticket codes), no placeholder copy. An unbuilt part is absent, or a Coming Soon entry.
- Semantic color tokens only; right in light AND dark.
- A link is a link. Navigation is an
<a>/Link(new tab, copy, crawl), never a button callingrouter.push. A status is never shaped like a button — a state ("Premium is on") is a badge or a line of text, not a full-width filled pill where an action would be. - One door per action. No button that repeats navigation the header or section nav already gives (a Back beside the section tabs). One create button per page (not a header "+", a toolbar button AND a create card); one org/scope control (the header's switcher — a page never adds a second organization picker).
- A destructive or expensive click says what it will cost before it happens. A person's records are archived (restorable), never permanently deleted from a page — Arman's standing rule, never a question; if the feature's reads do not yet hide archived rows, adding that filter to every read is part of the fix; a destructive option is never pre-checked; a one-click trash with no confirm is a defect.
- Color means something. Success color only for success, alarm color only for a real alarm (a zero is never green; a routine control is never red).
- Charts are readable and honest: axes and units visible and not clipped, a scale a person can read, they render on a phone, and a line never smooths across periods with no data (mark empty periods; points carry their count).
- One fact, once. The same list or number never appears twice on a page.
- A section's scope is its title. A section that covers only part of what the page covers (one study mode of many) says so, or is widened to match.
- Tab title leads with the specific word (on a record page, the record's
name); follow the
route-metadata-faviconshelper's order for a record's sub-views. The route has a favicon entry. - Checks:
pnpm check:ui-primitives·check:one-table-law·check:archived-items-law·check:picker-add·check:canonical-pickers·check:browser-dialogs·check:blocking-dialogs·check:popover-sizing·check:reserved-icons·check:new-tab-icon·check:theme-color-literals·check:route-metadata:strict·check:favicon-letters·node .claude/skills/light-dark-integrity/scripts/detect-light-dark.mjs <paths> --strict. - Procedures:
efficient-tap-button-migration·table-drill-down·picker-custom-entry·overlay-system·no-emojis-in-ui·light-dark-integrity·route-metadata-favicons·compact-nav-menus.
6 · Copy and AI are everywhere
- Nothing is shown that can't be copied — field, row, record, page — through
the canonical
CopyButtonsmenu (the Alchemy copy menu; Copy and Copy for AI live inside it). - Every box a person writes in is
ProTextarea/ProInputwith the microphone and the page's agents (surfaceName+getApplicationScopepassed). A bare box is only for a raw value (code, JSON, slug), marked// ui-exception: <reason>;pnpm check:writing-boxesenforces it. - A field that expects a syntax (formula, pattern, cron, JSON, filter) offers "Help with this…".
- A friction point gets an assist chip before anyone invents a manual button.
- Checks:
pnpm check:copy-everywhere. - Procedures:
agent-copy·components/official/ProTextarea.tsxdocstring.
7 · Proof and record
- Live proof for this page: desktop light, desktop dark, phone light, phone dark, the right-click menu, the probe output — with the URL and commit.
FEATURE.mdChange Log line in the same commit; manifestreadinesshonest (partialwith a note naming what is unproven;verifiedonly when everything is); anything Arman should see →agent-review-queuerow; unrelated defects →FOUND_DEFECTS.md.
Running it as a loop (coordinator)
The tested loop: a worker per page (loop/worker-brief.md), then a blind judge
per page who has never seen the worker's report (loop/judge-brief.md). The
coordinator sends the judge's findings back as the next iteration, gives each
shared defect one owner, and turns every miss into a rule here.
Shared defects — report, don't skip, don't collide
A defect you see on your page that lives in a shared piece (the shell header,
the org picker, the list shell, a shared section, the right-click menu) is
never "not mine". If it is small and nobody else is on that file, fix it at the
source (step 4's second-page proof applies). Otherwise put it in your report
under SHARED: with file, symptom and evidence; the coordinator gives each
shared defect exactly one owner so parallel workers never edit it twice.
When YOU change a shared piece (or a sibling route's code that renders into another page), look at every page that renders it, not just yours — a top bar added for the public route once doubled the title on the signed-in run page.
When the page hosts content it does not own (a person's Applet, an
embedded document), its quality limits are still yours to report: give the
content the full width and the restored input through the host contract; what
only the content's author can change goes in the report as CONTENT-OWNED:.
Report — one per page
<route> — type <x>, posture <x> after <product>, commit <sha>
My first look (step 1): <every problem I wrote down> → each: fixed / covered by rule / not fixed (why)
Core 1-7 and type additions: <n> <verdict> — <what>
Proof: <lane, URL, screenshots / probe file>
Test rows created: <table, ids>
PERSON→AGENT MAP: <each create/edit/archive control → its write target, or why none>
SHARED: <shared-component defects seen: file, symptom, evidence>
Left open: <exact remaining work, or none>
Checks no script runs yet: candidate-checks.md.
A problem from your first look that no rule covered goes in its own line:
RULE GAP: <problem>. Those lines are how this skill improves.