Agent docs

Starting a new app on the kit

The mirror of 04-migrating-an-app.md.

The mirror of 04-migrating-an-app.md. Same evidence, read the other direction: that guide is a list of things two apps had to undo, and this one is a list of the same things not done in the first place.

Audience: an agent (or person) scaffolding a new app.

Install the kit first: packages/ui/README.md has the registry line, the token, the install and pin, the theme import, and the bundler contract. Then start from the template that matches the app's shape under apps/_templates/ (next, react-router-ssr for the speedway shape, vite-spa for the walmart shape; the folder's README says which is which) and follow its TEMPLATE.md for scaffolding. The rest covers what happens after the app runs and before it has any screens.

What you get free by starting here#

Worth naming, because each of these is a whole section of the migration guide that does not apply to you.

No parity ledger. There is no accumulated behaviour to preserve, so nothing can be silently purged. This is the single largest cost in a migration and you are exempt from all of it.

No name collisions. Import the kit's StatusDot and it is the only StatusDot. Keep it that way: the rule that prevents the migration guide's step 1 from ever applying to you is that a local component never takes a kit export's name. If you need a variant, name it for what makes it different.

A pin you chose. Take the current kit version deliberately, and know that a caret on a 0.0.x version is a patch pin, so you are choosing an exact version whether you meant to or not. Every canon rule is reachable at the latest; that stops being true the day you stop bumping.

Three decisions to make before the first screen#

None of these are reversible cheaply, and all three are decided by accident if nobody decides them.

Where filter and detail state lives. URL or component. The canon says URL params for a browsable list's filters (07 §A3, AP-10 row 15) and the two existing apps both do it by hand: speedway wires useSearchParams directly, walmart wrote a local useQSync hook. The kit's sync seam exists and works, and ships only a no-op adapter, so writing a real one is a genuine option that would also make the atomic-write rule structural instead of remembered. Pick one of the three, and write it down.

Whether you adopt ?hl=. The convention of returning from a create back to its list without losing position. Speedway owns the ?hl= name; walmart drives the same behaviour under ?job=. The canon calls ?hl= app-wide and it is not, which is a live inconsistency rather than settled law. Decide, and if you adopt it, say so somewhere a reader will find.

Which toolbar. The kit now ships FacetBar, the union of both product toolbars per the owner's 2026-08-14 ruling; its contract lives in packages/ui/docs/table.md. A greenfield app reaches for it first and becomes its first product caller; know that you are the proving ground. The older kit FilterBar and the two divergent app-local toolbars remain in the tree; the call-site ledger for all of them lives in 12-primitives-and-rules.md, cite it rather than restating counts here.

Build order#

The same outward-in order as a migration, for the same reason: a page built inside an unsettled shell has two layout owners.

  1. Shell. AppShell, Sidebar, Topbar.
  2. Page composition. Card, PageTitle, and the one-owner-of-inset rule (05 §A4, and reach for DataTable's bleed rather than hand-rolling negative margins).
  3. States, before any real page needs them. PageInfo, EmptyState, PageLoading, PageError, skeletons. Build the three-way branch once, truly empty against filtered-to-nothing against still-loading, and every later surface inherits it.
  4. The first real screen, from a recipe rather than from scratch.
  5. The app's own completeness, before anyone reviews a screen: a favicon (src/app/icon.svg), a title per route (metadata on every page.tsx, the app's name as the layout's title template), a not-found.tsx and an error.tsx in the kit's page-state idiom, the theme boot, and GET /build-info. None of these is a design decision and every one of them has been called out in an owner review after the fact (docs/GUIDEBOOK.md lapse row 10: no favicon or page titles at a release). The template carries them, and npm run doctor (scripts/app-doctor.mjs) names any that are missing with the fix, so an agent building the tenth screen is pushed to finish the app rather than reminded by this paragraph. Run it before the first review round and in CI. Its sibling npm run canon-check (the kit's bin) holds the canon feedback seam: a change touching UI paths adds a dated line to CANON-NOTES.md or carries a human overrule in the PR body, and the added lines are read for the deviation classes the retrospective names. The template ships both; keep both in CI (docs/plan/71-canon-feedback-loop-plan.md).

Build screens from the recipes, not from the component list#

The component docs answer "what props does this take". They do not answer "what does this screen need", and the gap between those two questions is where incompleteness comes from.

  • Asked for a list, table, queue, or index: 10-recipe-browsable-list.md. It is 27 rows of things that request brings along whether or not anyone said them.
  • Asked for a detail, preview, or drill-in: 11-recipe-record-detail.md. Answer its row zero, peek against modal against page, before writing anything.

Both recipes end in a do-not list mined from corrections on shipped code. Every entry is something that was built, shipped, and caught by a human looking at the screen. Reading those is the cheapest hour available to a new app.

Keep the canon reachable#

Two habits that cost nothing now and a lot later.

Bump the kit deliberately and regularly. The gap between what the canon documents and what your pin can reach is invisible until someone follows a rule that does not work. Walmart is the standing case: it declares ^0.0.24, and a caret on a 0.0.x version is a patch pin, so everything the kit added after that is unreachable in a clean install however current the canon reads. The pins live in the first paragraph of the repo's CLAUDE.md.

When you deviate, record why in your own surface's doc. Owner ruling D1 says a surface may break the canon's overlay tree when the break buys better and more consistent UX, and that the reason belongs in that surface's doc. Deviation is legitimate and never free. What the ruling retired was silent discretion, not discretion.

When you hit a rule the canon does not have, that is the canon's bug. Most gaps mined from the two existing apps were law that had never been written down. A real minority were not: the ledger names a cluster that cited a rule already in the canon and simply not applied (docs/evidence/20260812-ui-knowledge-plan/L0-ledger-walmart-A.md:31-37). So ask which kind you have hit. A third app will find more of both, and the docs only get cheaper for the fourth if what you learn goes back into docs/design-language/ rather than staying in your repo.

The first-run brief, in full#

packages/ui/USAGE.md carries the short form of this: eight steps that fit on a screen, plus a ninth that points at the defect vocabulary. This is the long form, with each step expanded and the house rulings listed once. The brief exists because two agents read the kit docs and still shipped screens that missed the canon, and the cause was that nothing made them follow it.

The steps#

  1. List the screens first. Before any code, write each screen into CANON-NOTES.md with its archetype (reading page or work surface, docs/design-language/09-page-composition.md), its recipe and its zones. If no recipe fits, as with a run or submit screen or a master-detail workbench, write that down and compose from packages/ui/docs/patterns.md.
  2. Search the kit for every noun. Run npx kit-inventory --find for each noun on each screen, and check packages/ui/docs/common-requests.md for the owner's own phrasing.
  3. Read the canon whole, not by search. Docs 02 buttons, 03 status language, 05 cards, 07 toolbars, 09 page composition and 13 forms, then 16 microcopy. Then read the contract and Banned list of each component you import, then docs/DO-NOTS.md.
  4. Apply the house rulings below even where a doc is silent. They are the owner's.
  5. Write the page frame once. One constant used by the pages and every skeleton, and one page-meta table that feeds navigation, titles and document.title.
  6. Build outward in. Shell, frame, states, then each screen from its recipe, never from the component list.
  7. Check after the first screen. Stop when it renders, run npm run check:render on it, and fix what it finds before building the second screen. Do not report a UI change done until npm run check exits 0 and the render check has run on every route and tab.
  8. File every gap. Anything the kit could not give you is a dated CANON-NOTES.md line. A workaround with no line is a defect.

The house rulings#

These come from the sitting-1 rulings of 2026-10-23 and the review rounds before them. The canon doc that owns each is named, so read it for the reasoning.

  • A bare button is quiet and carries an icon. Solid belongs to a surface's one primary action (doc 02 §A12).
  • The submit of a whole-page form sits at the bottom right of the form, in the card footer (doc 13 §D).
  • A screen has one save model, and a form freezes while a submit is in flight (doc 13 §D).
  • A list has one toolbar, with filters left and search right. Actions never sit in the filter row (doc 07 §A1).
  • Tabs carry icons, and a second tab level is a chip row or an inline pill track (doc 09 §D).
  • A state has one word and one colour on every surface, declared once in a status map (doc 03 §A5).
  • A small fact about a record is a Tag, not a bordered chip (doc 03 §A1).
  • A control that matters is visible without hover (the overview's ninth law).
  • The same thing looks the same everywhere (the overview's tenth law).
  • A destructive confirm has five parts, and the copy rules in doc 16 apply to every string.
  • A table column names what it holds and writes no widths (doc 06 §A5).

What is ruled but not in a pinned version#

The kit in source is ahead of what most apps pin (the first paragraph of the repo's CLAUDE.md has the current versions). A ruling above that changes kit behaviour, such as the quiet default button, defineStatusMap, Tag, FormModal, OptionCards and shaped table columns, is ruled and built in source but reaches an app only when it takes a deliberate upgrade past the last published release. Until then the brief does not promise the behaviour: write the ruled shape by hand and file the line in CANON-NOTES.md. Phone layout is ruled out of scope for the kit (17-phone-layout.md).

@versable-git/ui · reference, canon, and method, read in place