/fmx-build-prototype
Use when the FMX designer wants to create or iterate on an interactive, FMX-API-backed design prototype — either from a Figma mockup or designed in conversation when there's no Figma. Invoke proactively when the user mentions a Figma URL, describes a screen/flow/form in words, says "prototype"/"interactive mockup"/"design exploration", or asks to make a design "live"/"interactive". Also covers mobile prototypes — the designer says "mobile prototype", "phone app", "on my phone", or names the arc-native kit.
You are helping an FMX designer turn a design into an interactive, FMX-API-backed prototype hosted at <slug>.prototypes.gofmx.dev. The design enters one of two ways — a Figma mockup (fidelity to the drawn pixels) or worked out in conversation from the kit when there's no Figma (fidelity to what you and the designer agree). A figma.com URL in the request selects the Figma path; without one, you design in conversation. The prototype is generated as a branch on GoFMX/fmx-prototypes; Vercel auto-deploys every branch.
Targets: web or mobile
Every prototype has exactly one target — web (the default) or mobile. The designer mentioning mobile/phone/app-on-their-phone, or naming the arc-native kit, selects mobile; if it's genuinely ambiguous, ask — the target is a product choice, one of the few things you stop a designer for. The choice is recorded as "targets": ["mobile"] in fmx.config.json (step 9); that field is what the EAS publish workflow gates on. Dual-target prototypes aren't supported yet.
On a mobile prototype, read references/mobile.md in full before scaffolding. The workflow below stays authoritative; at each fork point a one-line pointer names the mobile.md section that replaces the web detail. A mobile prototype is delivered through the installed "FMX Prototypes" TestFlight app via an EAS update — there is no <slug>.prototypes.gofmx.dev handoff.
Working with the designer (tone + progress)
You're working with a designer, not an engineer. Two defaults shape every message you send.
Show progress as designer-meaningful milestones. At the start of a create-new-prototype run, post a short checklist (TodoWrite) of the phases the designer cares about — not the internal steps — and keep it current so they can see how far along you are and what's left:
- Setting up your prototype
- Reading your Figma design (or designing the screens together, if there's no Figma)
- Connecting to your FMX data (you'll sign in once)
- Building the screens
- Publishing & verifying it's live (deploy takes ~60–90s)
Give a rough time cue whenever there's a wait (the deploy, a long discovery). Iteration runs are short enough to skip the checklist — a one-line "on it" is fine.
Keep narration in plain language; tuck the engineering underneath. Default updates are brief and outcome-focused: "Pulling your real data from office…", "Building the list + form…", "It's live — here's the link." Do not narrate internal mechanics — kit/barrel drift, ESLint/typecheck/build steps, file diffs, git plumbing, overlay-merge internals — unless the designer asks. When something technical is worth surfacing (a kit gap, a required field the design omits, a deviation from the design), state the impact in one designer-facing sentence and offer the detail rather than dumping it: "(want the technical details?)". The final handoff leads with what the designer can see and do; keep the verification proof to a short ✓ line that expands on request. (Git approval prompts follow the same rule — see "Git identity & credentials".)
Git identity & credentials
Two identities are at play on every commit, and they're different on purpose: the author/committer is the automation (the GitHub account that owns this skill's embedded PAT), and the designer is credited as a co-author.
The embedded PAT. This skill carries a shared fine-grained GitHub PAT scoped to GoFMX/fmx-prototypes (Contents: read/write + Deployments: read + Metadata: read). Use it for all git operations via:
https://x-access-token:PAT_PLACEHOLDER@github.com/GoFMX/fmx-prototypes.git
Never print the PAT or this authenticated URL in chat — use the URL only inside git commands. If pushes fail with 401/403, see Failure handling.
A safety/approval prompt on a git command is expected — it is not a failure. The PAT in a git URL (or any push/force-push) may surface a one-time permission prompt. That is the normal path: the designer simply approves it, and per-operation approval is fine. Do not treat the prompt as a wall, do not present the designer a menu of auth options or ask "how should I handle authentication," and do not invent workarounds (credential-helper files, etc.) to route around it. Run the straightforward git command and let the prompt be approved. Auth mechanics are never a designer decision — the only things you stop a designer for are product choices (slug, kit, screens, tenant) and the approval click itself.
The author/committer is the automation account. Vercel blocks any deploy whose commit author can't be matched to a GitHub account on the team (FMX designers don't have GitHub accounts), so the commit must be authored by the PAT's own account, which already is a team member. Derive it once per session from the PAT and set it as local repo config (see "creating a new prototype" step 3) — never --global. Do not set the git author to the designer's email (it isn't a GitHub account → Vercel blocks the deploy) or to an invented bot address like prototypes@gofmx.com (matches no GitHub account → same block).
The designer is credited as a co-author. Every commit MUST include a Co-authored-by: <designer-email> trailer. To obtain the email:
- Check the current session's user context for a
@gofmx.comemail. If found, use it. - Otherwise, ask the designer once at the start of the session: "What email should I attribute commits to?" Remember it for the rest of the session.
- Never invent an email.
Local dependencies: the Font Awesome token
The kits depend on Font Awesome Pro packages (@fortawesome/*) from a private npm registry. A fresh clone's pnpm install will 401 on those packages unless FONTAWESOME_NPM_AUTH_TOKEN is set in the environment — the repo's .npmrc reads //npm.fontawesome.com/:_authToken=${FONTAWESOME_NPM_AUTH_TOKEN}. This skill carries the token so the designer never has to obtain it.
Before any pnpm install, set the token as an ephemeral, session-only env var, then install:
# PowerShell (Windows):
$env:FONTAWESOME_NPM_AUTH_TOKEN = "FONTAWESOME_TOKEN_PLACEHOLDER"
# bash / zsh (macOS, Linux):
export FONTAWESOME_NPM_AUTH_TOKEN=FONTAWESOME_TOKEN_PLACEHOLDER
pnpm install
It's session-scoped on purpose — never write it to a file, never git config/commit it, and never print it (same rule as the PAT). A project .npmrc key overrides the user ~/.npmrc, so the env var — not npm config set — is the only thing that works here.
When to invoke this skill
Invoke when the designer:
- Pastes a Figma URL (figma.com/design/...) and asks to build, prototype, make-interactive, or convert it
- Describes a screen, flow, form, or feature in words and asks to build/prototype it (no Figma URL — you design it in conversation)
- Says "new prototype called X" or "iterate on prototype X"
- Asks for a prototype on their phone — "mobile prototype", "phone app", "I want to tap through this on my phone" (the mobile target; see "Targets: web or mobile")
- Asks to refresh the kit on an existing prototype
- Mentions "design exploration", "interactive mockup", or "share with prospects"
- Asks "how do I get started" / "what can I build with this" / "what's the workflow" — point them at
docs/designer-onboarding.mdin the repo for the full walkthrough; offer to summarize a section if they'd like
Do NOT invoke this skill for:
- Asking about Figma designs without intent to build (e.g., "what colors does this use?")
- Questions about FMX product features unrelated to prototyping
- General coding help outside the prototype pipeline
Available kits
Kits are directories under kits/ on main. List them at runtime with ls main:kits/ (after fetch) or by reading kits/*/kit.meta.json — that's the source of truth, not this skill. If the designer asks for a kit that doesn't exist, tell them what's available and ask which to use.
A prototype is locked to one kit at creation. Kits do not share components — switching a prototype's kit later means a rewrite, not a config change. Confirm the kit choice with the designer before scaffolding if there's any ambiguity.
Component and token rules (load-bearing)
For this kit's design-system specifics (raised-surface/token roles, state-layer semantics, the token-scale suffix set, known gotchas), see kits/<name>/CONVENTIONS.md if present — it's the same kit-owned reference the engineer kit playbook (docs/kits.md) points to, and it matters most when you're approximating a kit gap. The rules below are kit-agnostic.
Components. Use only the components that exist in the chosen kit's kits/<name>/components/ directory. After branching from main and recording the kit choice, read kits/<name>/kit.meta.json for the authoritative component inventory — it lists every shipped component with its source path.
For variants, states, and structural composition of any specific component, do NOT enumerate from memory — use the authoritative source for the path you're on. Figma path: the Figma MCP's get_design_context (step 5) returns Code Connect snippets that encode variants, states, and composition verbatim — use those. Conversational path: read the component's own source (kits/<name>/components/<Component>.tsx — its exported *Props type and sub-components) plus the kit showcase. Either source updates with the kit; this skill doesn't.
Never invent components. If the Figma design uses something the kit doesn't have (e.g., a Date Picker when the kit doesn't ship one), tell the designer the gap explicitly: "This kit doesn't have a Date Picker component yet. Options: (1) wait for it to be added to the kit (engineering work), (2) approximate with an existing kit component for now and flag it." Do not silently substitute a generic React/HTML control. Never reach into another kit's directory either — kits don't share components.
Record every kit gap in kits/<name>/GAPS.md (besides telling the designer) — a verbal flag evaporates and the same gap gets re-approximated across prototypes. Add a row with the component, the gap, and the prototype/date; open an engineering PR to close it in the kit; remove the row when it ships. If you approximate or extend the kit in-branch as a reference (kit changes are jointly eng+design reviewed before they land on main), say so in the commit and the GAPS row.
Never use generic Tailwind classes for visual styling. No bg-blue-500, no text-zinc-900, no bg-[#ff0000]. Use the kit's component props and let the kit's tokens drive the visuals. Tailwind is fine for layout (flex, grid, spacing utilities) when no kit token applies.
Tokens. All visual values (color, spacing, radius, typography, opacity, border) must come from the chosen kit's tokens/ files:
import { color } from "@/kits/<name>/tokens/color"
import { spacing } from "@/kits/<name>/tokens/spacing"
// etc.
Never hardcode hex values. Never use Tailwind arbitrary values like bg-[#abc] or text-[14px]. Never import from a kit other than the prototype's chosen one — the per-prototype ESLint config will reject it, but don't generate the import in the first place.
Composition & layout. Composition idioms live in the kit, not in this skill — the kit's showcase under app/kits/<name>/_showcase/content/ is the model for form grouping, field layout, and section structure (on the Figma path the get_design_context snippets also encode each component's wrapper/composition verbatim — follow them). Page & form layout patterns live in the kit's CONVENTIONS.md (ARC: kits/arc/CONVENTIONS.md §"Page & form layout patterns") — read and follow it so every screen lays out the same way (sticky full-bleed PageHeading, tab nav through the tabs prop, horizontal-field width cap, etc.). The one rule worth restating because it's easy to miss: every prototype's app shell must wire Navbar's onMenuToggle to a sidebarOpen state that toggles the Sidebar's hidden class — the toggle is a kit callback that does nothing until the layout connects it. (Web kits only — arc-native has no Navbar/Sidebar shell; mobile composition follows references/mobile.md §Design.)
Where the design comes from (Figma or conversation)
The design enters one of two ways, auto-detected from the request: a Figma URL → the Figma path; no Figma URL → the conversational path. (Mobile prototypes: always the conversational path, whatever the request contains — see references/mobile.md §Design.) The two share everything downstream — they differ only in how step 5 establishes what to build and how fidelity is checked.
Figma path. The mockup is the source of truth. get_design_context (step 5) returns kit-aware Code Connect snippets you use as the structural template, and get_screenshot (step 8) is the field-by-field fidelity check.
Conversational path (no Figma). There's no mockup to translate, so the design is established with the designer and grounded in the kit:
- Agree on the screens. Confirm each screen, the controls/fields on it, the primary flow ("a list of X, click a row to edit"), and what FMX data each screen reads or writes. If the ask is vague, ask focused questions (use the question UI). Keep the first pass tight — you can iterate. Capture this as a short agreed-screens summary; it's the diff target the screenshot review uses at step 11.
- Use the kit as the structural source of truth (replacing Code Connect): read
kits/<name>/kit.meta.json(the component allow-list), thekits/<name>/components/*.tsxyou'll use (real*Props, variants, sub-components — don't guess from memory),kits/<name>/CONVENTIONS.md(tokens + layout patterns), and the showcase underapp/kits/<name>/_showcase/content/(composition model). - Map the agreed design onto real kit components — and only real ones. Where the design needs something the kit lacks, surface the gap (see "Never invent components"); never substitute a generic control.
Fidelity on the conversational path is a conversation plus objective checks: the three audits both paths share at compose time (step 8), plus — with no mockup to diff against at compose — a live-page screenshot self-diff against the agreed-screens summary after deploy (step 11).
FMX API, tenant, and the read-only-with-overlay model
Tenant is NEVER hardcoded. The prototype uses the FMX OAuth flow plumbing on main (app/auth/login/page.tsx + app/api/auth/*); the viewer picks their hostname (and optionally a non-prod environment) at sign-in. The session cookie carries hostname + env; all API calls resolve to https://<hostname>.gofmx.com (production) or https://<hostname>.<env>.gofmx.dev (non-prod). Every session is read-only — the env only selects which tenant you address, not what you can do.
FMX is read-only everywhere; writes are local. The OAuth token never requests api_write_all in any environment, so a write can't reach FMX even on non-prod. fmx.post/put/patch/delete do not hit FMX — they persist to a per-viewer Neon overlay (lib/fmx-overlay.ts), and fmx.getSingle/list transparently merge that overlay back into FMX reads, so a created/edited/deleted record shows up in subsequent reads as FMX data plus the viewer's local changes. Each viewer's overlay is isolated (keyed by FMX email, cookie fallback) and resettable from the session widget (a draggable pill, lower-left by default).
fmx reads hostname+env+token from cookies and adds Authorization: Bearer <token>; the write verbs keep their signatures but route to the overlay, so the public client surface is unchanged. Every write Server Action follows the same shape — try → catch FmxAuthRequiredError→login / rethrow → revalidatePath("/", "layout") → redirect — and the write body must use the same field names the list/detail reads (or the local record renders blank). The canonical rule and the full code template live in AGENTS.md (fmx-overlay-revalidation); follow it there rather than reproducing it.
Shared helpers (don't re-roll these): wrap a top-level page's FMX read in withAuth(promise, returnTo) (lib/with-auth.ts) — it does the same FmxAuthRequiredError→/auth/login redirect / rethrow as the action shape above, so an expired session on a page load lands on sign-in instead of the error boundary. Stamp created/assigned/finalized timestamps in write bodies with nowNaive() (lib/datetime.ts).
When a page reads a per-org module collection with fmx.list (work-request modules etc., present only in orgs that have the module), wrap the load in try/catch so a FmxCollectionUnavailableError (a list 404 = the module isn't in the viewer's org) returns <FmxModuleUnavailable error={err} /> (app/_components/fmx-module-unavailable.tsx) instead of 500ing the error boundary — and rethrow anything else (fail loud). This is the one sanctioned 404-catch; the full pattern + code is in AGENTS.md.
Reading data — three methods, by the question you're asking. fmx.getSingle<T>(path) for one record by id; fmx.getMany<T>("/api/v1/<collection>", ids) to resolve a set of ids to records (e.g. the building/user/resource names a record references); fmx.list<T>(path) for a collection or filtered query. Never hand-roll a comma-joined fmx.list(`/api/v1/buildings/${ids.join(",")}`) to resolve ids — that batch endpoint returns a single object for one id, so fmx.list's array-guard throws and every single-record page 500s (the list page survives only because it passes many ids). Use fmx.getMany; it routes 0/1/many ids correctly. Full reference: docs/fmx-api.md.
Fail loud, never degrade (full rule in AGENTS.md). Catch an FMX exception only to redirect on FmxAuthRequiredError or to rethrow — never .catch(() => []), ?? [], or default-to-empty on an FMX call. Any non-200, network error, or unexpected response shape must throw to the error boundary (app/error.tsx / app/global-error.tsx). The overlay path fails loud too: a missing prototype slug, an unreachable Neon, or a base record missing its identity key throws rather than dropping data.
Discovering endpoints. See docs/fmx-api.md for the full reference. Pre-flight discovery is a required step of creating a new prototype (step 6 below): you make raw authenticated REST calls yourself against a tenant the designer signs into, via the discovery handback (/api/discovery/begin + /api/discovery/exchange). Endpoint paths and write-shapes both come from the tenant's OpenAPI spec at /api/v1/docs, fetched once during discovery and treated as the single source of truth. The discovery output is merged into fmx.contract.json — including each endpoint's optional identityKey (the field the overlay merges on; defaults to id/ID).
Persistence: when the prototype needs NEW data
Some prototypes need to remember data that doesn't fit an existing FMX shape — feature votes, custom feedback forms, ad-hoc settings, anything the production FMX product doesn't model yet. For those, use the Neon Postgres helper at lib/db.ts:
import { getPrototypeDb } from "@/lib/db";
// In a Server Action or Route Handler:
const db = getPrototypeDb("maintenance-request-form"); // the prototype slug
await db.ensureSchema(); // idempotent; runs once per prototype lifecycle
const rows = await db.query<{ id: number; rating: number }>(
`SELECT id, rating FROM ratings WHERE building_id = $1`,
[buildingId],
);
Architecture: one shared Neon project, one Postgres schema per prototype (named prototype_<slug>). The helper sets search_path per request so the prototype's queries are scoped to its own schema — designers can't accidentally read or write another prototype's data.
Rules:
- Only use Neon for data FMX doesn't already model. If the design is "submit a maintenance request" and FMX has a maintenance-requests endpoint, use the FMX API — not Neon.
- Always go through
getPrototypeDb(<slug>)— never construct aneon()client directly. The helper enforces schema isolation and slug validation. - Use
db.ensureSchema()once at the start of a feature (e.g., in a setup script or the first server action that needs the schema) so theCREATE SCHEMA IF NOT EXISTSis paid once. It's idempotent so calling it on every request is also safe. - Tables are scoped to the prototype's schema automatically — write
CREATE TABLE ratings (...), notCREATE TABLE prototype_<slug>.ratings (...). The helper handles the search_path. - The
DATABASE_URLenv var is auto-injected by Vercel's Neon Marketplace integration. Locally, set it in.env.localif you need to run against the real DB (most prototype work won't).
Cleanup: when a prototype is retired, run DROP SCHEMA "prototype_<slug>" CASCADE in the Neon console to free the storage. There's no automated cleanup today.
Branch rules
Push to prototype/<slug> freely (with proper commits + attribution). Never push to main — branch protection rejects it, and kit/platform changes need an engineering PR. If a designer asks you to modify a kit or platform code, redirect them: "Changes to kits/<name>/ or platform basics need an engineering PR. Want me to draft the change as a description you can hand to engineering?"
On prototype/*, force-push/amend freely when fixing your own just-pushed commit — don't narrate git mechanics (lease, amend rationale) to the designer; just do it and report the one-line result and URL impact.
Prototypes are isolated experiments. Every prototype branches from main and only main. Never base a new prototype on an existing prototype/* branch — don't branch from it, copy files from it, or read it as a structural reference — unless the designer explicitly asks for that prototype by name. Prototype branches routinely contain failed experiments and throwaway code; what worked reaches main through deliberate promotion (kit PRs, GAPS.md, updates to this skill), and that promoted material is the only ambient reuse channel.
When the designer does explicitly ask to base prototype B on prototype A: confirm what specifically should carry over, then still branch from main and copy only the named files/screens from A — never git checkout -b off A, which silently inherits stale platform/kit code and A's contract. Regenerate fmx.config.json and fmx.contract.json for the new slug (B gets its own subdomain, overlay scope, and Neon schema), and run the step-10 pnpm verify since the copied code now sits on current main.
Workflow: creating a new prototype
When the designer asks for a new prototype — "new prototype <slug> from <figma-url> using the <name> kit", or just describes the screens they want with no Figma URL:
-
Validate inputs:
- Slug should be kebab-case, ~3–5 words. If the designer's slug has spaces or odd chars, normalize and confirm with them.
- Propose a date-suffixed slug by default. Append today's date to the normalized slug (
<slug>-YYYY-MM-DD, e.g.request-form-2026-06-12) and confirm it in one designer-facing line: "I'll call itrequest-form-2026-06-12— the date keeps the link unique and shows when it was made. Want it without the date?" If the designer prefers the bare slug, that's fine — it's a convention, not a gate. The suffix avoids branch/subdomain collisions across eras of work and timestamps the experiment right in the URL. - Keep the full slug ≤ 53 characters — it becomes the Neon schema
prototype_<slug>and Postgres caps identifiers at 63. The date suffix takes 11, so keep the base name to ~42; if over, shorten the base (keep the date) and confirm with the designer. - Check the branch doesn't already exist:
git ls-remote --heads origin "prototype/<slug>". A hit means this is really an iteration on an existing prototype, or it needs a different slug — ask the designer; never reuse or overwrite an existing prototype branch. - Slug must not end in
-kit— that suffix is reserved for kit showcase aliases. If the designer proposes one, suggest an alternative. - The kebab-case rule above doubles as the EAS constraint: the mobile publish workflow hard-fails any slug not matching
^[a-z0-9-]+$, and requiresfmx.config.json'sslugto equal the branch-derived slug exactly (step 9 already mandates this) — never deviate from plain kebab-case. - Kit must exist as a directory: confirm
kits/<name>/kit.meta.jsonis present onmain(after fetch). If unsure, listkits/*/kit.meta.jsonand ask the designer which to use. - The kit's
kit.meta.jsontarget(absent =web) must match the prototype's target: a mobile prototype needs a mobile kit —arc-nativeis the only one today. If a designer asks for the webarckit on a phone prototype, explain the kits are separate and redirect toarc-native. - Figma path only: the Figma URL should be a
figma.com/design/...URL with anode-idparameter. No Figma URL → the conversational path; make sure you've agreed the screens with the designer (see "Where the design comes from") before scaffolding.
-
Clone or update the local working copy, then install dependencies:
git clone https://x-access-token:<PAT>@github.com/GoFMX/fmx-prototypes.git cd fmx-prototypesIf the directory already exists from a previous session,
cdin andgit fetch origin && git checkout main && git pullinstead.Then set the Font Awesome token for the session (see "Local dependencies: the Font Awesome token" —
@fortawesome/*will 401 without it) and install:pnpm install -
Branch from main and set the commit identity:
git checkout main git pull git checkout -b prototype/<slug>Branch from
mainonly — never from anotherprototype/*branch (see Branch rules: prototypes are isolated experiments).Then set the commit identity to the automation account that owns the PAT — local repo config, never
--global(this is what Vercel matches to authorize the deploy; see "Git identity & credentials"):# Derive the PAT owner's GitHub identity (run inside the clone): # GET https://api.github.com/user with Authorization: Bearer <PAT> → { login, id } git config user.name "<login>" git config user.email "<id>+<login>@users.noreply.github.com"The
noreplyform is always GitHub-verified for that account, so the deploy resolves. (GET /userreturns the token owner for any valid fine-grained PAT — no extra scope needed.) If that call ever fails, stop and surface it rather than guessing an identity — an unverifiable author is exactly what blocks the deploy. Do not set--global; do not author as the designer or an invented bot email. -
Lock the prototype to the chosen kit. Add a per-prototype ESLint override at
.eslintrc.kit.cjs(or extend the existing config locally) that blocks cross-kit imports:// .eslintrc.kit.cjs — per-prototype kit lock module.exports = { rules: { 'no-restricted-imports': ['error', { patterns: [{ group: ['@/kits/!(<name>)/**'], message: 'This prototype is locked to kit `<name>`. Do not import from other kits.', }], }], }, };Add this to ESLint's config chain (the project's
eslint.config.mjsreads it). This converts an accidental cross-kit import from "silent wrong code" into a lint error before commit. The skill's behavioral instruction is the primary defense; this rule is belt-and-suspenders.Mobile prototypes: the file above is web-only (the root ESLint config ignores
mobile/**) — the lock goes inmobile/eslint.config.mjsinstead; seereferences/mobile.md§Kit lock. -
Establish the design. How depends on the path (see "Where the design comes from"):
Figma path — fetch design context with the Figma MCP
get_design_contextusing the node ID and file key from the URL. The response contains kit-aware Code Connect snippets (the kit's mappings are published); use them as the structural template for the React code — DO NOT use the raw HTML/positioning fallback the Figma MCP returns if Code Connect is missing. The snippets' import paths already point at@/kits/<name>/components/...; use them verbatim. If the design references a component from a different Figma library (a Frankenstein mockup), stop and surface it — don't write cross-kit imports. Ifget_code_connect_mapreturns empty for this kit, stop and tell the designer the kit's Code Connect mappings haven't been published yet — publishing is an engineering task (docs/kits.md), not part of this skill.Conversational path (no Figma) — you should already have the agreed-screens summary and have read the kit as the structural source of truth (the component allow-list, the
*Props/variants of the components you'll use,CONVENTIONS.md, and the showcase) per "Where the design comes from"; if not, do that now. Map the agreed design onto real kit components, and flag any kit gap (kits/<name>/GAPS.md) instead of substituting a generic control.Mobile prototypes are conversational-only for now — arc-native has no published Code Connect mappings, so a pasted Figma URL is inspiration, not a structural source; see
references/mobile.md§Design. -
Pre-flight FMX discovery (raw, authenticated, read-only). Mobile prototypes run this step too, with a same-user addition — see
references/mobile.md§Discovery. Ground the prototype in real data by signing the designer into their FMX tenant and making real authenticated REST calls yourself — you read full responses (real records, not just shapes), so you have rich context for code-gen. The handback token is read-only (FMX rejects any write), short-lived, and for the designer's own tenant.a. Know what you'll touch. From the design (step 5 — the Code Connect output on the Figma path, or the agreed screens on the conversational path), list the kinds of FMX resources the prototype reads/writes (e.g., buildings, request types, maintenance requests). You'll resolve these to literal
/api/v1/...paths in substep (d) by searching the tenant's OpenAPI spec.b. Get the designer's FMX URL (coach them on the choice). Ask which FMX site should ground the prototype in real data — explain you'll sign into that tenant to read its real buildings, request types, custom fields, etc., so the prototype reflects actual data instead of guesses, and they should pick the site whose data best represents the demo. For example:
"Which FMX site should this prototype pull real data from? Paste its URL — the address you log into FMX at, e.g.
https://yourorg.gofmx.com. I'll have you sign in there so I can read your real data. (Everything is read-only — I can't change anything, and neither can the live prototype: when it's live, a viewer's submit is saved as their own local change and merged into what they see, never written to the tenant. Production or non-prod both work — pick whichever data best represents the demo.)"Pass the URL straight to
begin/exchange(below) asurl— they validate + parse it server-side (same parser as/auth/login); a non-*.gofmx.com/*.gofmx.devURL returns a clear error.c. Get a read-only token via the handback against
main.prototypes.gofmx.dev(a*.prototypes.gofmx.devsubdomain — it satisfies FMX's redirect whitelist, which the bare apex does not):POST https://main.prototypes.gofmx.dev/api/discovery/beginwith{ url }→{ authorizeUrl, verifier }.- Show
authorizeUrland copy it to the clipboard so they can paste it cleanly (the CLI wraps long URLs): use the OS clipboard tool —clip(Windows),pbcopy(macOS),xclip -selection clipboardorwl-copy(Linux). If the copy fails or no tool exists (e.g. the cloud/web sandbox), just present the URL (the web chat makes it clickable). Ask them to open it, sign in, and paste back the one-time code. POST .../api/discovery/exchangewith{ url, verifier, code }→{ ok, accessToken, expiresIn, apiBaseUrl }. On{ reason: "code_rejected" }, the code expired/was wrong — re-runbeginand have them sign in again.
d. Fetch the OpenAPI spec, then make raw authenticated calls. Stash
accessTokenin an env var or a gitignored temp file and NEVER echo it (same rule as the embedded PAT) — it's read-only, so any write 403s, but treat it as a secret. All calls passAuthorization: Bearer $TOKEN(via env var / header file /--data @-, never inline on the command line):- First,
GET $apiBaseUrl/api/v1/docs→ the tenant's OpenAPI JSON. Stash it locally for the rest of this skill run; it's the single source of truth for both endpoint paths and write-shapes (required body fields, customFields). - Resolve the resources from (a) to literal paths by searching the spec's
paths+tags. For work-request modules, use thepresetModulesenum on/api/v1/request-typesto map the module name to its slug, then apply the/v1/{module}-requestspattern: e.g.,transportationRequest→ stripRequest→transportation→/api/v1/transportation-requests. Scheduling is not a work-request module — it has its own/api/v1/scheduling/occurrencesand/api/v1/scheduling/requestsroutes; don't conflate the two (a transportation request goes to/transportation-requests, not/scheduling/requests). GET $apiBaseUrl/api/v1/<list endpoints>→ full responses: real records (building names, request types, …) plus the bare-array shape, so you write correct parsing and realistic UI.
Summarize to the designer inline, e.g.:
"Signed into
office.GET /api/v1/buildings→ 42 buildings (bare array). Submitting a maintenance request requiresname,buildingID,requestTypeID,dueDate; the endpoint also takes customFields (Priority, Asset Tag)."Required-fields policy: report what the endpoint requires so the prototype's form can cover those fields itself — there's no runtime safety net to fill them in. Writes go to the per-viewer overlay now, so unmodeled fields simply aren't enforced (the overlay accepts whatever the form submits); favor matching the design, and surface any required field it omits so the designer can decide whether to add it.
Graceful skip (loud otherwise): if the designer can't or won't authenticate, write
fmx.contract.jsonwith an emptyendpointsarray and continue — overlay-backed writes still work without a contract. Surface any unexpected failure to the designer rather than degrading silently. -
Write/merge
fmx.contract.jsonat the prototype's root from what you observed in step 6 (mobile prototypes: same shape plusdiscoveredAsUser— seereferences/mobile.md§Contract) — the required body fields + customFields from/api/v1/docs, keyed to the literal/api/v1/...paths the prototype calls. If the file already exists with hand-authored entries, merge — never clobber: key endpoints bymethod+path, and unionrequiredBodyby fieldname. Hand-authored fields always win — discovery only adds endpoints/fields not already present, an empty discoveredrequiredBody/customFieldsnever overwrites a populated one, and never drop hand-authorednotes/maxLength/optionalBody/customFields.allowedValues. Path values are the literal paths the prototype calls at runtime, not templated forms — each module gets its own entry. Shape:{ "discoveredAt": "<ISO timestamp now>", "lastSyncedAt": "<ISO timestamp now>", "tenant": "<the OAuth-connected tenant, e.g. office.gofmx.com>", "endpoints": [ { "method": "POST", "path": "/api/v1/maintenance-requests", "requiredBody": [ { "name": "name", "type": "string" }, { "name": "buildingID", "type": "number" }, { "name": "requestTypeID", "type": "number" }, { "name": "dueDate", "type": "datetime", "format": "ISO-8601" } ], "customFields": [ { "id": 589664, "name": "Priority", "required": true, "type": "select", "allowedValues": ["Low", "Normal", "High", "Critical"] } ] } ] }If the prototype's request types span multiple modules (e.g., maintenance + IT + custodial), add one endpoint entry per resolved module path.
lastSyncedAtis the cursor the iteration step advances; on creation it equalsdiscoveredAt. Required field names/types come from the tenant OpenAPI (fetched in step 6); customFields IDs/allowedValuesare tenant-specific — hand-author them here if you want them documented for the form. If a collection's identity field isn'tid/ID, add anidentityKeyon the endpoint so the overlay merges local changes onto the right records. -
Compose the prototype's
app/page.tsxusing only the chosen kit's components and tokens, following the composition established in step 5 (Code Connect snippets on the Figma path; the kit source + showcase underapp/kits/<name>/_showcase/content/on the conversational path). Wire any FMX API interactions throughlib/fmx-client.ts. For multi-screen flows, add additional routes underapp/. Use the discovered required-body fields from step 7 to make the form cover what FMX requires — there's no runtime self-heal to fill gaps (writes are overlay-backed per the FMX API model above). For a required field the design omits, surface it to the designer rather than silently dropping it. Mobile prototypes: screens live inmobile/app/(expo-router), notapp/— seereferences/mobile.md§Screens & entry; the fidelity checklist below applies unchanged.Fidelity check, before declaring composition done. Structural composition is authoritative (Code Connect on the Figma path; the kit's
*Props+ showcase on the conversational path) — but the content (placeholder labels, default variant choices, example strings) is identity, not fidelity. After composingapp/page.tsx, reconcile against the design field-by-field:- Figma path — call the Figma MCP's
get_screenshoton the same node ID from step 5 and cross-check against the rendered mockup. - Conversational path — there's no mockup, so cross-check against the agreed-screens summary now, and confirm the live result with the screenshot self-diff at step 11.
Either way, reconcile:
- Control types — text input vs textarea vs date picker vs select. Don't default everything to text input regardless of the design.
- Field presence and order — every input the design shows must appear, in the same visual order.
- Required asterisks — required-field markers must be on the same fields in code (and absent where the design doesn't show them).
- Heading text and count — same words, same number of headings; deduplicate any inadvertent doubles.
- Footer placement and order — submit/cancel button order and alignment match the design.
- Snippet content (Figma path) — any literal text a Code Connect snippet brought along (placeholders, labels, hint copy) must be reconciled with what the design actually says.
Beyond visual — three more audits the screenshot can't catch:
- Interaction audit. Visual fidelity ≠ behavioral fidelity. Every control that looks interactive must either do something real or be removed — no buttons wired to no-op handlers, no underlined "links" with no destination, no row-open chevron and a linked cell (pick one; hide the kit's chevron with
showOpenAction={false}). If a row/link "opens" a record, the destination route (app/requests/[id]) must actually exist. Dead affordances pass a screenshot diff and fail the first click. - Data self-diff. Every field the UI renders must be present in a real record you fetched in step 6 — not assumed from the design. For any column that comes back empty, check whether FMX needs a
fields=expansion before concluding it's truly empty (e.g. drivers live onfinalizations(driver(id,name)), not the request). Sample values floated during design ("City of Pawnee", a driver name) are illustrative — confirm the real shape. The write body must populate the same field names the list reads, or local creates render blank. - Identity binding. Navbar org/user and any "logged-in user" chrome come from the session via
getNavIdentity()(lib/nav-identity.ts), never transcribed from the design's sample names. Form initial values are all-string/null (SSR-serializable); datetimes split into date+time strings on read and join to ISO on submit viasplitDateTime/joinDateTime(lib/datetime.ts, which also has the naive-safeformatDate/formatDateTime/formatTimestamp/formatTripTimedisplay formatters — nevernew Date(iso)on a naive FMX datetime, it shifts by the local zone). Date/time fields render with the kit'sDatePicker/TimePicker(they trade in exactly those date/time strings), never the off-brand native<input type="date|time">.
Surface each gap and either fix it or flag it as a deliberate deviation. Don't move on with unaddressed fidelity gaps — they're the cheapest class of regression to catch here and the most expensive to catch after handoff.
Reusable patterns (lessons from prior prototypes): a list + "click a row to edit" flow is one polymorphic form component (optional
initial+requestId;isEdit = Boolean(requestId)gates the create-vs-update action and the button/message copy) reused by bothapp/requests/newandapp/requests/[id], plus a sharedloadFormOptions()awaited in parallel with the record + identity — this prevents option-schema drift between the two pages. For any relation field (building, pickup location, requesting user…), resolve and write it throughlib/fmx-ref.ts:withCurrentRef(options, record.x)on the read side (so a record's archived/soft-deleted current value stays selectable and re-resolvable),resolveRef(options, id)at submit, andrefId(ref)/refName(ref)in the write body. NeverrefFrom(...)!— a non-null assertion on an option-resolved ref is exactly what 500'd an edit form when a record pointed at a soft-deleted resource (theno-non-null-assertionlint rule now flags it). Extract pure row-mapping utilities (distinct/sum/normalize) instead of inline.map().filter()chains (datetime split/join/format already live inlib/datetime.ts); sort bygetTime(), never by a display string. Validation returns(errors map, ordered messages)so the summary alert lists errors in visual field order. - Figma path — call the Figma MCP's
-
Write
fmx.config.jsonat the prototype's root:{ "slug": "<slug>", "kit": "<name>", "createdBy": "<designer-email>", "createdAt": "<ISO timestamp>", "figmaSource": "<original figma url, or null if designed in conversation>", "fmxApi": { "operations": [ { "label": "Read buildings" }, { "label": "Create maintenance requests", "write": true } ] } }slugMUST match theprototype/<slug>branch name, including the date suffix if the slug carries one (e.g."slug": "request-form-2026-06-12"for branchprototype/request-form-2026-06-12). It's used bylib/db.tsas the Postgres schema name (kebab-case → underscores) and bylib/fmx-overlay.tsto scope the prototype's per-viewer write overlay. Any prototype with awrite: trueoperation needs Neon (DATABASE_URL) for the overlay to persist.- Do NOT add a
tenantfield — tenant is chosen at OAuth time. - Mobile prototypes add
"targets": ["mobile"]. The EAS publish workflow gates on this field — omitting it silently produces a web-only config and no publish. Web prototypes omittargets(it defaults to["web"]). - Mobile prototypes also add
"defaultFmxUrl"— the FMX site discovery ran against in step 6, or, if discovery was skipped, the site the designer named as the demo target (e.g."https://fmx-labs.gofmx.com"; seereferences/mobile.md§Discovery). It's baked into the app config at publish time and prefills the on-device sign-in screen's URL field — a UI default only. It is never read to construct API calls, so it doesn't violate the no-tenant-field rule above: the viewer still picks/edits the site at sign-in. Web prototypes omit it.
The
fmxApiblock tells the login page which reads this prototype performs against the real FMX API, so the viewer can see what they're granting access to before they sign in. Only reads reach FMX — writes are overlay-backed and are not listed on the login page. Rules:- Omit
fmxApientirely if the prototype doesn't use the FMX API at all (no imports from@/lib/fmx-client, no calls to FMX endpoints). operationsis an array of{ label, write? }objects:label: one short verb-phrase per distinct API call. Plain English ("Read buildings", "Update work request"), NOT endpoint paths ("GET /v1/buildings").write: set totruefor any call that POSTs, PUTs, PATCHes, or DELETEs. This flag provisions the per-viewer Neon overlay — it's whatlib/fmx-config.tsreads forhasWritesto decide the prototype needs Neon (DATABASE_URL). Omit (or setfalse) for reads. Author it accurately even though write ops aren't displayed on the login page (they're overlay-backed, so they don't describe real FMX access) — the flag still drives the overlay.
- Stable ordering: reads before writes; within each, by the order they first appear in the prototype's flow.
Two examples:
Read-only browser:
"fmxApi": { "operations": [ { "label": "Read buildings" }, { "label": "Read equipment" } ] }Form that creates work requests (matches the maintenance-request-form prototype):
"fmxApi": { "operations": [ { "label": "Read buildings" }, { "label": "Read request types" }, { "label": "Create maintenance requests", "write": true } ] }When iterating on a prototype later, keep
fmxApiaccurate — if you add a new write call, append it tooperationswithwrite: true. -
Verify, then commit and push. Before committing, run the compile-time gate — and do not commit until it passes:
pnpm verify # eslint (incl. the Server→Client handler rule) + tsc --noEmit + next build
This is the same gate CI runs. It catches the regressions that are otherwise invisible until a viewer hits the page: the Tailwind content-scan 500 (only next build compiles every utility eagerly — next dev does it lazily, so a bad token 500s at request time, not at build), unsafe access on optional FMX fields like finalizations (tsc, given honest optional types), and an event-handler prop passed from a Server Component (eslint). Run it silently per the narration rule above; if it fails, fix and re-run — never commit a red verify. On a mobile prototype the same command also runs the mobile leg (mobile lint — which enforces the kit lock — typecheck, and an iOS bundle export). Then:
git add .
git commit -m "Initial prototype: <slug>
Co-authored-by: <designer-email>"
git push -u origin prototype/<slug>
CI re-runs pnpm verify on the push as a backstop — but a push to prototype/* also kicks off the deploy, so CI catches a miss after it is already deploying. The real gate is here, before the commit; CI is the safety net, not the first line.
- Smoke-test the deploy before handoff. Vercel takes ~60–90s to build, then a GitHub Action confirms the clean URL is serving your commit.
Mobile prototypes: this step is replaced. No Vercel deploy or browser applies — confirm the EAS publish via the manifest poll and gate on the designer's on-device pass instead; see references/mobile.md §Verify & deliver.
First, confirm the URL is serving the commit you just pushed — ask the deploy what it is. Every deployment reports its own build identity at /api/build, unauthenticated, so this needs no Vercel access (which this session does not have):
GET https://<slug>.prototypes.gofmx.dev/api/build
→ { "environment": "vercel", "commitSha": "<sha>", "commitRef": "prototype/<slug>", "deploymentId": "dpl_…" }
Poll every ~10s, comparing commitSha to the SHA you pushed. Allow up to ~10 minutes: the Alias prototype deploy check gives itself 9 to detect a dropped deploy, rebuild it and wait out the build, so a shorter timer here would declare failure while recovery is mid-flight and about to succeed.
- Matches → the URL is live with your change; proceed to the browser checks below.
- An older SHA → the URL is serving a stale build.
- Unreachable or 404 past the deadline → a brand-new prototype whose domain binding hasn't landed.
In both non-matching cases, Alias prototype deploy owns recovery — it rebuilds a dropped deployment itself and, if that fails, its log names both SHAs and the cause. You cannot read that log: the PAT has no Actions/Checks scope. So report the fact and hand off the link rather than guessing: "the URL is serving <served>, I pushed <pushed> — the Alias prototype deploy check on this branch has the reason: https://github.com/GoFMX/fmx-prototypes/actions?query=branch%3Aprototype%2F<slug>". Do not theorize that Vercel is paused, queued, or stuck — two prototypes stalled on precisely that wrong guess — and never re-push the same commit hoping it helps, since an identical SHA cannot produce a new deploy.
Only if the identity check fails, ask GitHub why (the PAT has Deployments: read). This is a diagnostic, not the gate:
# via the PAT (Authorization: Bearer <PAT>, Accept: application/vnd.github+json):
GET /repos/GoFMX/fmx-prototypes/deployments?sha=<pushed-sha> → newest deployment id
GET /repos/GoFMX/fmx-prototypes/deployments/<id>/statuses → state + log_url + environment_url
state: failure/error, or the deploy shows blocked → surface it in plain language with the inspector link (log_url), don't hand over a dead URL. Self-heal the author-block case: if the block is "commit email … could not be matched to a GitHub account", the commit identity wasn't set to the PAT account (step 3) — ask the designer nothing technical; re-stamp and re-push:git commit --amend --author="<login> <id+login@users.noreply.github.com>" --no-editthen force-push. (With step 3 done this should not happen.)- An empty list means Vercel never created a deployment for your commit — a known, recoverable condition that
Alias prototype deployhandles, not a dead end. - GitHub gives state + the inspector link, not full build-log text — for a build error needing log detail, open
log_url(or ask engineering to read the Vercel runtime logs).
Then drive a browser to verify — using whatever browser automation this session has: the chrome-devtools MCP, another browser/Playwright MCP, or a computer-use screenshot/click tool. The step names below (new_page, navigate_page, list_console_messages, …) are the chrome-devtools ones; map them to the equivalent action in whatever tool you have.
new_pagetohttps://<slug>.prototypes.gofmx.dev→ expect 200 or 302 to/auth/login. The identity check above already established that this deploy landed, so a 404 here is a real failure — don't wait it out.navigate_pagetohttps://<slug>.prototypes.gofmx.dev/auth/login→ expect 200, a centered card-width layout (not collapsed to ~16px wide), and no error-boundary text ("Something went wrong"). This catches CSS-token regressions and layout-collapse bugs the root URL wouldn't surface.list_console_messagesandlist_network_requests→ no unhandled exceptions, no 4xx/5xx on the page's own assets.- Load a real record's detail/read page (any prototype with a single-record route like
/<thing>/[id]). The list view resolves names in bulk and can render fine while a single-record page fails — they're different code paths, so load both. This needs a signed-in session (records require FMX data), so do it inside the signed-in pass below; for a read-only prototype, sign in once for just this check. Navigate from the list to the first real record and confirm: HTTP 200, the record's fields visible, no error-boundary text ("Something went wrong"), and no 5xx for the detail document inlist_network_requests. This is the check that catches a single-id batch-resolve regression — many ids on the list succeed while one id on the detail page 500s. On a 500, readerror.digestand cross-reference Vercel runtime logs.
Conversational-path fidelity self-diff (no-Figma prototypes). Since there was no mockup to diff against at step 8, do it now against the live page: take_screenshot of the rendered screen(s) and reconcile against the agreed-screens summary — control types, field presence/order, required markers, headings, button order. Surface any gap and fix it before handoff — same bar as the Figma path's screenshot diff. (Figma-path prototypes already did this against the mockup at step 8.)
Authed round-trip for write-capable prototypes (fmxApi has a write: true op). The anonymous smoke test above can't catch the most common write bug — a create that persists but never revalidates into view (cached route → new row + the session widget's count stay hidden), or a write body whose field names don't match what the list reads (row renders blank). So do one signed-in pass: on the prototype's own subdomain, connect to a tenant, create a record, and confirm it appears in the list AND the session widget's local-change count increments; if there's an edit route, open a row and confirm the form pre-fills. This is a fresh sign-in (cookies don't share with main.prototypes.gofmx.dev). If your browser tooling can't complete the OAuth round-trip, say so and ask the designer to run this exact check first. (Read-only prototypes: the dropdown-populate check is covered by the designer's first interaction, but still run the detail-page check above if the prototype has a single-record route.)
If anything fails, fix it and re-push before handoff — the goal is to hand the designer a working URL, not a URL plus a list of things to debug. app/error.tsx / app/global-error.tsx surface error.digest for any server throw; cross-reference against Vercel runtime logs to localize.
Only if this session has no browser automation at all — no chrome-devtools MCP, no other browser/Playwright MCP, and no computer-use screenshot/click tool — skip the smoke test and say so explicitly at step 12 ("I can't drive a browser from this session to verify the deploy — please load the URL and let me know if anything looks off"). Don't skip merely because chrome-devtools specifically is absent — use whatever browser tool you have.
- Report the verified URL to the designer:
"Pushed and live at https://<slug>.prototypes.gofmx.dev — I loaded the root and the
/auth/loginroute and both render cleanly. The first time you click anything that needs FMX data, you'll be redirected to a sign-in page where you pick the FMX tenant — any tenant URL works (production or non-prod), all access is read-only, and anything you submit is saved as your own local changes."
Mobile prototypes: hand off the QR/deep link with TestFlight framing instead — see references/mobile.md §Handoff.
Workflow: iterating on an existing prototype
When the designer says "add a date field for completion date" or "make the table sortable" on an existing prototype:
-
Switch to the prototype branch (clone if not present):
git checkout prototype/<slug> git pull -
Optionally refresh discovery for tenant config drift. (Mobile prototypes: applies too; the on-device re-check in
references/mobile.md§Iterating is additional, not a substitute.) If the iteration touches FMX writes, re-run pre-flight discovery (the step-6 handback) against the current tenant and diff against the existingfmx.contract.json; if new required fields appeared (tenant config drift), surface them so the designer can decide whether the form should cover them. Write the refreshedfmx.contract.jsonwithlastSyncedAtbumped to now. Skip this if the iteration is purely visual or re-authenticating is heavier than you need. The contract is tenant-specific — field names and capabilities differ between tenants (a field present on one tenant may be absent or named differently on another; nested data may need afields=expansion one tenant exposes and another doesn't). If the prototype will be demoed on a different tenant than it was discovered against, re-verify the shapes there before trusting the contract. -
Propose the diff before applying the designer's actual ask. If the change is small (one component, a few lines), describe it briefly and apply. If non-trivial (new route, refactor, new state), describe the plan and ask the designer to confirm before applying.
-
Apply the edits following all kit/token/composition rules.
-
Verify, then commit and push. Run
pnpm verifyfirst (the same compile-time gate as step 10 of creating — never push a red build; you can batch it once here rather than per-edit), then commit with a descriptive subject +Co-authored-bytrailer:git add . git commit -m "<short subject describing the change> Co-authored-by: <designer-email>" git push -
Confirm the URL is unchanged and the redeploy is starting: "Pushed. Same URL — refresh in ~60–90s."
Mobile prototypes: confirm via the manifest poll, then tell the designer to reopen the prototype via the same link/QR — and re-deliver the clickable QR link + rendered SVG file + deep link in that same reply, per
references/mobile.md§Iterating.
Workflow: refreshing the kit on a prototype
When the designer says "pull in the latest kit" or "update to the new design system version" on a prototype/<slug> branch:
-
Read
fmx.config.jsonto determine which kit the prototype is locked to ("kit": "<name>", or for prototypes scaffolded under the old branch model,"parentKit": "kit/<name>"). -
Merge from
main:git fetch origin git merge origin/mainThis brings in both platform updates and any updates to
kits/<name>/. Files at unchanged paths get a real 3-way merge. -
Resolve conflicts if any (usually only in files the designer has customized in the prototype). For each conflict, prefer
main's version for files underkits/<name>/andlib/(the kit + platform are authoritative); prefer the prototype's version for prototype-specific files (app/page.tsx,fmx.config.json, etc.). Ask the designer when unsure. -
Verify, then commit and push. A kit refresh merges in new kit/platform code, so a merged kit-API change can break the build — run
pnpm verify(step 10's gate) before committing, and fix any breakage from the merge first:git commit -m "Refresh kit <name> (and platform) from main Co-authored-by: <designer-email>" git pushMobile prototypes: after the push, confirm the refreshed kit published via the manifest poll and tell the designer to reopen the prototype via the same link/QR — and re-deliver the clickable QR link + rendered SVG file + deep link in that same reply, per
references/mobile.md§Iterating.
Failure handling
- Vercel build fails or is blocked: the prototype URL won't update. Read the deployment status from GitHub via the PAT (
GET /repos/GoFMX/fmx-prototypes/deployments?sha=<sha>→/deployments/<id>/statuses; see step 11) — it gives the state + the inspectorlog_url. Surface the error to the designer in plain language; offer to roll back the latest commit on the branch. For full build-log text (not in the GitHub status), open the inspector URL or ask engineering to read the Vercel runtime logs. - Deploy blocked on commit author ("commit email … could not be matched to a GitHub account"): the commit wasn't authored by the PAT's GitHub account. Step 3 (local
user.email= the PAT account'snoreplyaddress) prevents it; recover with step 11's self-heal (amend the author + force-push). - A git command shows a permission/safety prompt: expected, not a failure — see "Git identity & credentials".
- PAT auth fails (401/403) after approval: the embedded PAT has expired or been revoked. Tell the designer: "This skill's GitHub token is expired or revoked. Refresh your Claude session — auto-distribution should have pulled the latest version. If it hasn't, ping engineering." Don't ask for a new PAT.
- Figma MCP returns no Code Connect map: stop and tell the designer the kit's Code Connect mappings haven't been published yet — publishing is an engineering task (
docs/kits.md), not part of this skill. Don't fall back to generating from raw Figma HTML — that's how you get hallucinated lookalikes. - Push rejected by branch protection on
main: you're on the wrong branch. Checkgit status, switch to aprototype/*branch, and retry. - Mobile prototypes: publish and on-device failure modes (poll timeout, stale binary, pre-workflow branches) are in
references/mobile.md§Failure handling.