Purpose of this file: This is an architecture reference for AI agents working in this codebase. It covers how the system connects (skills, MCP, Figma, the extract plugin) — not the details of each spec type.
Two rendering paths:
- Figma-native path (
create-*skills + Figma MCP): each spec type writes its own annotation frame into Figma next to the component. Figma is the source of truth.- Component markdown path (
figma-plugin/+create-component-md+extract-*skills): the plugin emits a single_base.json; interpreter skills turn it into a standalone.mdspec. The.md— not Figma — becomes the implementation source of truth.What belongs here:
- System architecture and data flow for both paths
- Template registry pattern and extensibility (Figma path)
- Figma plugin phase map and
_base.jsoncontract (MD path)- Cross-cutting utilities (cloning, error handling)
- Build, test, and documentation setup
- Reference file pointers
What does NOT belong here:
- Per-spec-type JSON schemas and examples — those live in each skill's instruction file (e.g.,
screen-reader/agent-screenreader-instruction.md,component-md/agent-component-md-instruction.md)- Hardcoded template component keys — those are configured via the
firstrunskill and stored inuspecs.config.json- The
_base.jsonfield reference — that lives infigma-plugin/docs/base-json-schema.mdAdding a new spec type: Author the new skill in the platform-neutral
skills/<name>/SKILL.mdsource (use{{skill:name}}and{{ref:path}}tokens for cross-references — see the Skill Source and CLI section). Add any shared docs toreferences/<area>/. Update the skills table, template type table, and reference files table below. Document the spec's schema, examples, and template structure in its own instruction file. If the new spec should participate in the component markdown path, pair it with a read-onlyextract-<type>sibling skill and wire it intocreate-component-md's Step 6 fan-out. Bumppackages/cli/package.jsonand rebuild so existing installs can pick up the new skill vianpx uspec-skills update.Operator's manual. This file is the architecture reference. For the human-facing release process — when to bump versions, how to publish to npm, troubleshooting failed publishes, and the smoke-test workflow — see
maintaining.md.
- Overview — the two rendering paths and supported spec types
- Skills — host directories, full skill list, source-of-truth rules
- Figma MCP Tools — Console vs Native MCP, complete tool mapping
- Native MCP Page Context — required
setCurrentPageAsyncpattern - Font Loading in
use_figma—loadAllFontsrecipe and when to call it - SLOT Node Handling — slot mutation ordering, default child workaround
- Console MCP Tools / Native MCP Tools — per-provider tool catalog
- Native MCP Page Context — required
- Architecture — host-specific configuration and template keys
- Components — render pipeline, two-tier extraction, template infrastructure
- Skill and Agent Instruction Architecture — SKILL.md vs instruction file split
- Template Key Config —
uspecs.config.jsonshape and fields
- Cloning Logic — shared template clone-fill-remove pattern
- Stability — multi-call splitting to avoid Figma timeouts
- Component Markdown Pipeline —
_base.json→.mdspec flow- Figma plugin (
figma-plugin/) — phase map A–I create-component-mdorchestrator — workflow steps 1–10.5.uspec-cache/layout — per-component cache files
- Figma plugin (
- Skill Source and CLI (
packages/cli/) — token rewriting, render engine, CLI commands- Tokenized cross-references —
{{skill:}}and{{ref:}}rewrite rules - CLI package layout — files inside
packages/cli/ - Commands —
init/install/update/doctor - Publish safety — three layers preventing wrong-registry publishes
- Tokenized cross-references —
- Documentation Site — Mintlify, deploy flow, file structure
- Reference Files — full file inventory by area
uSpec generates documentation specifications for UI components. The system ships two rendering paths that share the same interpretation patterns but produce different artifacts:
- Figma-native path.
create-*skills extract data via a Figma MCP (Console or native) and render annotations directly in Figma using Plugin API JavaScript. Each spec type has its own template frame. The motion skill is an exception — it reads pre-computed data from an After Effects export script rather than inspecting Figma components. - Component markdown path. The
figma-plugin/Figma Desktop plugin walks a component locally and emits a single_base.json. Thecreate-component-mdorchestrator runs four read-onlyextract-*interpreter skills against that file, reconciles their outputs, and renders one self-contained.mdspec. The.mdbecomes the implementation source of truth; Figma is only the source of extraction.
Spec types currently supported (either path unless noted):
- Anatomy - Numbered markers on a component instance with an attribute table (Figma-native only)
- Property - Variant axes and boolean toggles with instance previews (Figma-native only)
- Screen Reader Specs - Accessibility specifications for VoiceOver, TalkBack, and ARIA
- Color Annotation - Design token specifications for component colors
- API Overview - Component property documentation with configuration examples
- Structure Specification - Dimensional properties documentation (spacing, padding, density variants)
- Motion Specification - Animation timeline documentation from After Effects export data (pre-computed segments, no raw keyframes) (Figma-native only)
- Component Markdown - Single standalone
.mdthat bundles API, Structure, Color, and Voice into one implementation source of truth (component markdown path only)
Agent workflows are defined as skills. Each skill has a SKILL.md with frontmatter (name, description), inputs, an MCP Adapter mapping table, a step-by-step workflow, and Plugin API code blocks. The skill content is identical across hosts and MCP providers — only the directory location, invocation syntax, and tool call names differ (handled by the MCP Adapter section in each skill).
| Host | Skill directory | Invocation |
|---|---|---|
| Cursor | .cursor/skills/ |
@skill-name in chat |
| Claude Code | .claude/skills/ |
/skill-name or natural language (auto-discovered by description) |
| Codex | .agents/skills/ |
$skill-name to invoke explicitly, or natural language (matched by description) |
Figma-native (render into Figma):
| Skill | Trigger Keywords | Purpose |
|---|---|---|
create-anatomy |
anatomy, component anatomy, create anatomy | Numbered markers and attribute table |
create-property |
property, properties, create property | Variant axes and boolean toggle exhibits |
create-voice |
voice, voiceover, screen reader, talkback, aria | Screen reader spec generation |
create-color |
color, color annotation, tokens | Color annotation generation |
create-api |
api, props, properties, component api | API overview generation |
create-structure |
structure, structure spec, dimensions, spacing, density, sizing | Structure spec generation |
create-motion |
motion, motion spec, animation spec, timeline | Motion specification from AE export (JSON paste, file ref, or Figma destination link) |
Component markdown path (consume _base.json, render .md):
| Skill | Trigger Keywords | Purpose |
|---|---|---|
create-component-md |
component md, component markdown, spec md, source of truth, migrate to md | Orchestrator: validates a plugin-produced _base.json, runs the four extract-* interpreter skills, reconciles their outputs, and renders a standalone components/{componentSlug}.md. See the Component Markdown Pipeline section below. |
extract-api |
(sub-skill, invoked by create-component-md) |
Read-only: interpret properties, sub-components, and configuration examples from _base.json. Produces the shared API dictionary that steers the three downstream specialists. Runs inline in the orchestrator's parent context. |
extract-structure |
(sub-skill, invoked by create-component-md) |
Read-only: interpret variant axes, dimensions, sub-component variant walks (Phase I output), slot contents, and cross-variant diffs from _base.json. |
extract-color |
(sub-skill, invoked by create-component-md) |
Read-only: interpret per-element fills/strokes/effects, axis classification, boolean delta, variable-mode detection, and rendering strategy from _base.json. |
extract-voice |
(sub-skill, invoked by create-component-md) |
Read-only: interpret focus order, merge analysis, per-state VoiceOver/TalkBack/ARIA tables, and slot insertion plans from _base.json. |
Setup:
| Skill | Trigger Keywords | Purpose |
|---|---|---|
firstrun |
firstrun, first run, setup, setup library, configure templates | First-time environment setup and template library configuration |
Usage: Mention trigger keywords in your prompt (e.g., "Create voice spec for this button"). In Cursor, you can also reference directly with @create-voice.
Skill source of truth. The platform-neutral skills/<name>/SKILL.md files at the repo root are the source of truth — they contain {{skill:name}} and {{ref:path}} tokens that the CLI rewrites at install time. Per-platform directories (.cursor/skills/, .claude/skills/, .agents/skills/) and the bundled packages/cli/templates/ directory are generated artifacts — never edit them directly. See the Skill Source and CLI section below.
Editing skills: Edit skills/<name>/SKILL.md (and any references/ files), then cd packages/cli && npm run build to refresh the bundled templates/ copy. Run npx uspec-skills update from any consumer project to pick up the changes.
uSpec supports two Figma MCP providers, configured via mcpProvider in uspecs.config.json:
| Provider | Value | Description |
|---|---|---|
| Figma Console MCP (Southleft) | figma-console |
Requires Desktop Bridge plugin. Provides dedicated tools for navigation, screenshots, variables, styles, and component search. |
| Figma MCP (Native) | figma-mcp |
Official Figma MCP with write access. Uses use_figma for Plugin API execution and dedicated tools for screenshots, metadata, and design system search. |
Each skill has an MCP Adapter section at the top that maps operations to the correct tool calls for either provider. The Plugin API JavaScript in all figma_execute / use_figma code blocks is identical — no code changes are needed between providers. The only per-call differences on the native path are supplying fileKey and description parameters.
| Console MCP | Native MCP | Notes |
|---|---|---|
figma_execute(code) |
use_figma(fileKey, code, description) |
JS code is identical. Both support top-level await + return. Native requires fileKey + description. |
figma_get_status |
(none) | Connection is implicit on native. Verify by making any call. |
figma_navigate(url) |
(none) | Not needed — use_figma takes fileKey directly. Agent extracts fileKey from URL. |
figma_take_screenshot |
get_screenshot(fileKey, nodeId) |
Functionally equivalent. Native requires explicit fileKey + nodeId. |
figma_capture_screenshot |
get_screenshot(fileKey, nodeId) |
Same mapping. |
figma_get_file_data |
get_metadata(fileKey, nodeId) or get_design_context(fileKey, nodeId) |
get_metadata returns structural XML. get_design_context is richer (includes code + screenshot). |
figma_get_component |
get_metadata(fileKey, nodeId) |
Replacement for component inspection. Can also use use_figma for full detail. |
figma_get_component_for_development |
get_design_context(fileKey, nodeId) |
Replacement for component data + visual reference. |
figma_get_variables |
use_figma script: figma.variables.getLocalVariableCollectionsAsync() |
Console returns file-wide collections. Native get_variable_defs is node-scoped only — use a script for file-wide access. |
figma_get_token_values |
use_figma script reading variable values per mode |
Same gap as above. Mode/value data needs use_figma scripts. |
figma_get_styles |
search_design_system(query, fileKey, includeStyles: true) |
Console lists all styles directly. Native requires a search query, or use a use_figma script: figma.getLocalPaintStyles(). |
figma_search_components(name) |
search_design_system(query, fileKey, includeComponents: true) |
Functionally equivalent. Native also searches variables/styles in the same call. |
figma_get_selection |
use_figma script: figma.currentPage.selection |
No direct tool equivalent on native. |
use_figma resets figma.currentPage to the first page on every call. In multi-step workflows where a script accesses a node from a previous step via getNodeByIdAsync(ID), the page content may not be loaded — findAll, findOne, and characters will fail with TypeError until the page is activated. Insert this page-loading block immediately after getNodeByIdAsync:
let _p = node; while (_p.parent && _p.parent.type !== 'DOCUMENT') _p = _p.parent;
if (_p.type === 'PAGE') await figma.setCurrentPageAsync(_p);This walks up to the PAGE ancestor and loads its content. Once the page is loaded, findAll, findOne, and other traversal methods work normally. Console MCP does not need this — figma_execute inherits the Desktop page context from figma_navigate.
Both providers serialize the code argument through JSON before the Figma sandbox runs it. Two compounding layers of escaping make ' inside a '...' JS string the most common cause of SyntaxError from these calls.
Rule: when authoring note text, descriptions, or any string content that contains apostrophes (e.g., clear button copy like "Visible when value is non-empty", design-intent notes, contracted English), wrap the JS string in "..." (double quotes) or use template literals (backticks). Never reach for \' inside a '...' string.
// Good
const note = "Visible when value isn't empty";
const note2 = `Visible when value isn't empty`;
// Bad — produces SyntaxError after JSON re-escapes
const note = 'Visible when value isn\'t empty';This applies to every skill that ships note-bearing rows through __ROWS_JSON__ or its equivalents.
getRangeAllFontNames is not available in the use_figma sandbox and will throw TypeError. Use tn.fontName instead, which returns { family, style } for uniformly-styled text or figma.mixed for mixed-font text.
Collecting fonts from existing text nodes — use this pattern in all skills:
const textNodes = frame.findAll(n => n.type === 'TEXT');
const fontSet = new Set();
const fontsToLoad = [];
for (const tn of textNodes) {
try {
const fn = tn.fontName;
if (fn && fn !== figma.mixed && fn.family) {
const key = fn.family + '|' + fn.style;
if (!fontSet.has(key)) { fontSet.add(key); fontsToLoad.push(fn); }
}
} catch {}
}
await Promise.all(fontsToLoad.map(f => figma.loadFontAsync(f).catch(() => {})));Loading fonts from component instances — component instances may use fonts not present in the template's static text nodes (e.g., a component using "Uber Move" when the template uses "Inter"). After creating a component instance via createInstance(), and after any operation that may reveal new text nodes (setProperties, appendChild for slot content, directUnhide), call loadAllFonts on the instance before performing further mutations.
Slot safety: Component instances containing SLOT nodes cannot be traversed with findAll or findOne — these methods crash with "Node with id ... not found" when they encounter default slot children with compound IDs (see Slot mutation ordering constraint above). The loadAllFonts function uses a manual recursive collector with per-node try-catch instead of findAll to handle this safely:
async function loadAllFonts(rootNode) {
const textNodes = [];
function collect(node) {
try {
if (node.type === 'TEXT') textNodes.push(node);
if ('children' in node && node.children) {
for (const c of node.children) { try { collect(c); } catch {} }
}
} catch {}
}
collect(rootNode);
const fontSet = new Set();
const fontsToLoad = [];
for (const tn of textNodes) {
try {
const fn = tn.fontName;
if (fn && fn !== figma.mixed && fn.family) {
const key = fn.family + '|' + fn.style;
if (!fontSet.has(key)) { fontSet.add(key); fontsToLoad.push(fn); }
}
} catch {}
}
await Promise.all(fontsToLoad.map(f => figma.loadFontAsync(f).catch(() => {})));
}Call loadAllFonts(instance) at these critical points:
- After
createInstance()— the instance may contain text nodes with non-template fonts - After
setProperties()— toggling booleans or swapping variants may reveal hidden text nodes with different fonts - After
appendChild()into a SLOT — the inserted child may bring new fonts - After
directUnhide()— making hidden nodes visible may expose text with unloaded fonts
This prevents "unloaded font" errors when Figma tries to reflow auto-layout after a mutation.
Loading a font by family name — font style names vary per file ("SemiBold" vs "Semi Bold"). Use listAvailableFontsAsync to discover exact style strings:
async function loadFontWithFallback(family, preferredStyle, fallbackStyle) {
fallbackStyle = fallbackStyle || 'Regular';
const allFonts = await figma.listAvailableFontsAsync();
const familyFonts = allFonts.filter(f => f.fontName.family === family);
const match = familyFonts.find(f => f.fontName.style === preferredStyle);
if (match) { await figma.loadFontAsync(match.fontName); return match.fontName; }
const fallback = familyFonts.find(f => f.fontName.style === fallbackStyle);
if (fallback) { await figma.loadFontAsync(fallback.fontName); return fallback.fontName; }
if (familyFonts.length > 0) { await figma.loadFontAsync(familyFonts[0].fontName); return familyFonts[0].fontName; }
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
return { family: 'Inter', style: 'Regular' };
}See Figma MCP server guide — text-style-patterns for the upstream reference.
Figma introduced Slots as a native component property type (currently in open beta). A SlotNode (type: 'SLOT') is a child frame of a component that allows freeform content editing in instances — designers can add, remove, and rearrange children without detaching the instance. This mirrors how components work in code (e.g., React children or named slots in Vue/Svelte).
SLOT is a fifth component property type alongside BOOLEAN, TEXT, INSTANCE_SWAP, and VARIANT:
type ComponentPropertyType = 'BOOLEAN' | 'TEXT' | 'INSTANCE_SWAP' | 'VARIANT' | 'SLOT'Creating slots — ComponentNode exposes createSlot():
const slot = comp.createSlot(); // returns SlotNode, also creates a SLOT property in componentPropertyDefinitionsSlots can also be created via addComponentProperty('name', 'SLOT', ...). The editComponentProperty method supports preferredValues (array of InstanceSwapPreferredValue — curated components suggested when adding content to the slot) and description (string — only SLOT properties support descriptions). deleteComponentProperty also supports SLOT type.
Traversal — skills that walk component trees to find meaningful children must handle SLOT nodes. The resolveChildContainer pattern used in anatomy treats a single-child SLOT the same way it treats a single-child auto-layout FRAME — as a transparent wrapper:
// After walking through single-child auto-layout FRAMEs:
if (cc.children.length === 1 && cc.children[0].type === 'SLOT') {
cc = cc.children[0];
}When iterating childContainer.children, SLOT children appear as regular SceneNode entries — INSTANCE, TEXT, FRAME, etc. The SLOT itself only appears when it is a direct child of the container being iterated.
Populating slot content — use appendChild to insert content into a SLOT node on a component instance:
const slotNode = compInstance.findOne(n => n.type === 'SLOT');
const contentInstance = contentComponent.createInstance();
slotNode.appendChild(contentInstance);
await loadAllFonts(compInstance); // inserted child may bring new fonts — see Font Loading aboveInserting into a SLOT triggers auto-layout reflow on the parent. Re-read dimensions and bounding boxes after population if marker placement or sizing depends on them.
Slot mutation ordering constraint — after appendChild into a SLOT, the child instance's internal nodes receive compound IDs (e.g., I6291:650;6015:5301). These compound references are inaccessible via findOne, findAll, and setProperties — calls will crash with "The node with id ... does not exist". All mutations on a child instance must happen before appendChild into the slot:
// CORRECT: mutate first, then adopt into slot
const child = contentComponent.createInstance();
await loadAllFonts(child);
child.setProperties({ 'show subtext#6015:7': true }); // works — child is free-floating
await loadAllFonts(child);
const textNode = child.findOne(n => n.type === 'TEXT' && n.name === 'subtext'); // works
if (textNode) textNode.characters = 'Supporting context';
slotNode.appendChild(child); // compound IDs assigned here — no more mutations on child internals
await loadAllFonts(parentInstance);// INCORRECT: mutate after adoption — will crash
const child = contentComponent.createInstance();
slotNode.appendChild(child);
child.setProperties({ ... }); // may crash — compound IDs
child.findOne(n => n.type === 'TEXT'); // crashes: "node with id ... does not exist"Default slot children (instances already inside a slot when the parent instance is created) have compound IDs from the start. Calling setProperties or findOne on them after any mutation may crash or leave references stale. The workaround is to remove the default child and insert a fresh instance with pre-applied overrides:
const slot = parentInstance.findOne(n => n.type === 'SLOT' && n.name === 'title slot');
// Remove inaccessible default child
while (slot.children.length > 0) slot.children[0].remove();
// Insert fresh instance with overrides applied before adoption
const fresh = titleContentComponent.createInstance();
await loadAllFonts(fresh);
fresh.setProperties({ 'show subtext#6015:7': true });
await loadAllFonts(fresh);
const tn = fresh.findOne(n => n.type === 'TEXT' && n.name === 'subtext');
if (tn) tn.characters = 'Supporting context';
slot.appendChild(fresh);
await loadAllFonts(parentInstance);Resetting slots — slotNode.resetSlot() reverts slot content to the main component's default. Useful for cleanup or undo scenarios.
Reading slot properties — componentPropertyDefinitions entries with type: 'SLOT' expose:
preferredValues— array of{ type: 'COMPONENT', key: string }pointing to recommended components for the slotdescription— string describing the slot's purpose (only SLOT properties support this field)
Boolean bindings — a slot's componentPropertyReferences.visible may point to a boolean property that controls slot visibility. Read it to detect hidden/conditional slots:
const cpRefs = slotNode.componentPropertyReferences || {};
if (cpRefs.visible) {
// cpRefs.visible is the raw key of the controlling boolean property
}Current skill support: create-anatomy handles SLOT nodes with dedicated extraction, classification, preferred-instance resolution, and rendering logic. create-structure detects SLOT properties with preferredValues and generates dedicated slotContent sections per preferred component — measuring contextual dimensions (padding, constraints, alignment) when each preferred component is placed inside the slot across all parent sizes. At render time, slotContent previews mirror create-anatomy's slot-population approach: the parent inst is sourced at each column's parent size, the SLOT node is located by name (SLOT_POPULATION.slotName from Step 11a), and an instance of the preferred component is nested via slotNode.appendChild(prefInst) (with a 0.6-opacity ghost-overlay fallback at the slot's bbox if appendChild fails). This makes the preview a faithful reference for the table — the SLOT's contextual padding, sizing, and spacing are live in the inst tree, so canvas measurement overlays drawn on the preview correctly reflect the slot-imposed values the table documents. create-api extracts slotProps with preferred instances and default children for Pattern A sub-component tables. create-property extracts slotProps for informational completeness and detects boolean-to-slot linkage — when a boolean controls a SLOT's visibility, the property exhibit description reads "Controls slot" with preferred content names instead of the generic "Controls layer". SLOT properties do not produce their own property chapters (slot content is freeform). create-voice deep-recurses into SLOT nodes during extraction, resolves preferredValues, records default slot children, and reads slotVisibility so the AI can reason about slot-hosted focus stops. When the documented focus order depends on preferred slot content rather than the default child, voice carries a slot insertion plan into artwork rendering and populates the slot before marker resolution. create-color uses the slot-safe loadAllFonts pattern for extraction (Step 4b boolean enrichment) and rendering (Step 11 preview instances). The AI reasoning layer (Step 4c) evaluates sub-component token ownership — entries with subComponentName from slot-hosted sub-components are filtered based on whether the parent or the sub-component owns the color (see the instruction file's token ownership framework). Other skills interact with slot-based components at the design-pattern level (composable children, container detection) without SLOT-specific node handling.
For the latest Console MCP tools and usage, see: https://docs.figma-console-mcp.southleft.com/tools
| Tool | Purpose |
|---|---|
figma_navigate |
Open a Figma URL to start monitoring |
figma_take_screenshot |
Capture visual of component and variants |
figma_get_file_data |
Get component structure, variant axes, properties |
figma_get_component |
Get detailed component metadata |
figma_get_component_for_development |
Get component data + visual reference in one call |
figma_get_variables |
Get variable collections and token definitions |
figma_get_token_values |
Get variable values organized by collection and mode |
figma_get_styles |
Get color, text, effect styles |
figma_get_design_system_summary |
Get overview of entire design system |
figma_search_components |
Find components by name |
| Tool | Purpose |
|---|---|
use_figma |
Execute Plugin API JavaScript (equivalent of figma_execute) |
get_screenshot |
Capture visual of a node |
get_metadata |
Structural XML inspection of a node |
get_design_context |
Rich node context with code, screenshot, metadata |
search_design_system |
Search components, variables, and styles by query |
get_variable_defs |
Variable definitions bound to a specific node |
| Spec Type | Key Operations |
|---|---|
| Anatomy / Property | Plugin JS execution (extraction, template import, rendering), screenshot (validation) |
| Screen Reader | Screenshot, file data (for states/variants), Plugin JS execution (template import, rendering) |
| Color Annotation | Variables, token values, styles, Plugin JS execution (template import, rendering) |
| API Overview | File data (variant axes), component metadata (properties), Plugin JS execution (template import, rendering) |
| Structure Spec | Token values, Plugin JS execution (for measurements, template import, rendering) |
| Motion Spec | Plugin JS execution (template import, rendering), screenshot (validation) |
uSpec supports two Figma MCP providers. The mcpProvider field in uspecs.config.json determines which tool calls the agent uses. Each skill's MCP Adapter section translates generic operations to the correct provider-specific calls.
┌─── Figma Console MCP ──── figma_execute ───┐
AI Agent (Cursor / Claude / ───┤ ├──> Figma
Code / Codex) └─── Figma MCP (Native) ─── use_figma ──────┘
Agent Host Figma MCP Figma
| | |
|-- get context ---->| |
|<-- component data -| |
| | |
|-- execute JS ------|----------------->|
| (import template, | |-- render annotation
| create instances,| |-- place markers
| fill tables) | |-- build exhibits
| Host | Project instructions | MCP config | Skill directory |
|---|---|---|---|
| Cursor | .cursor/rules/ |
.cursor/mcp.json (user-level) |
.cursor/skills/ (all skills) |
| Claude Code | CLAUDE.md |
.mcp.json (project root) |
.claude/skills/ (only firstrun until user runs it) |
| Codex | AGENTS.md |
.codex/config.toml |
.agents/skills/ (only firstrun until user runs it) |
Most skills extract component data via MCP, then render annotations directly in Figma using Plugin API JavaScript (figma_execute on Console MCP, use_figma on native MCP). Each skill imports its documentation template (by component key from uspecs.config.json), detaches it, and fills text fields, clones sections, and builds tables programmatically. The motion skill is different: its data comes from an After Effects export script (motion/export-timeline.jsx) that pre-computes segments, easing values, formatted labels, and composition.durationMs. Raw keyframes are stripped from the output — the JSON contains only segments. The agent passes segment data and pxPerMs to the Figma code, which computes bar positions at render time.
The anatomy and property skills share a single template (anatomyOverview); anatomy clones and fills its sections first, then property re-uses the same detached frame to build its own chapters. The voice, color, API, structure, and motion skills each have their own template and render independently.
Template component keys are stored in uspecs.config.json and configured via the firstrun skill. Skills read the key for their template type and import it via figma.importComponentByKeyAsync:
| Config key | Template |
|---|---|
screenReader |
Screen reader spec |
colorAnnotation |
Color annotation |
anatomyOverview |
Anatomy annotation template |
apiOverview |
API overview |
structureSpec |
Structure specification |
propertyOverview |
Property overview |
motionSpec |
Motion specification |
All skills render directly in Figma via Plugin API JavaScript (figma_execute on Console MCP, use_figma on native MCP), following a shared pattern:
- Extract — Gather component data via MCP tools and AI reasoning (motion skill reads pre-computed data from AE export JSON instead)
- Import template —
figma.importComponentByKeyAsyncwith the skill's template key (fromuspecs.config.json), create instance, detach, and place on the component's page to the right of the component (see Spec Placement below) - Fill header — Set component name, description, and header text
- Build content — Clone template sections, fill text fields, build tables, create component instances where needed
- Validate — Screenshot to verify output (
figma_take_screenshotorget_screenshot) - Completion link — Print a clickable Figma deep-link URL to the rendered spec frame in chat:
https://www.figma.com/design/{fileKey}/?node-id={frameId}(with:replaced by-in the node ID)
Template keys: All template keys are stored in uspecs.config.json under the templateKeys object and configured via the firstrun skill. Each skill has its own template key.
Variant matching (Anatomy/Property): When creating component instances for a specific property value, the skill first attempts an exact match across all variant axes. If no exact match exists, it falls back to the best partial match.
Variant selection (Anatomy): Step 3 uses the default variant for extraction. If the default variant produces 0 elements after wrapper traversal (e.g., an unchecked checkbox whose default state has an empty structure frame), the script falls back to the richest variant (most descendant children). The selected variant's ID is returned as selectedVariantId and reused by Step 8 for rendering, ensuring the artwork matches the extraction data.
Marker positioning (Anatomy & Voice): After placing the component instance in the artwork, the skill re-reads actual child positions from the instance using absoluteTransform rather than relying on extraction-time positions. Marker placement uses the nearest-edge + collision avoidance algorithm: for each element, score all four sides by distance from the element's edge to the component boundary, pick the shortest, then check for overlap with already-placed markers (8px minimum gap). If overlap exists, apply perpendicular offset; if offset exceeds artwork bounds, try the next-best side. In Anatomy, when multiple sides tie at the same distance, a tiebreaker prefers top/bottom over left/right (producing cleaner vertical connector lines for wide components). Connectors are always straight lines from the marker to the element's nearest edge; when a perpendicular collision-avoidance offset is applied, the anchor point on the element edge shifts by the same offset so the line stays axis-aligned. Anatomy also supports inline markers for elements nested inside other annotated elements — these sit on the nearest edge with a short stub line (16px) and are excluded from the perimeter collision pool.
Slot preferred instances (Anatomy): The Step 3 extraction script reads componentPropertyDefinitions for SLOT-type properties with preferredValues, resolves component keys via local page traversal, and reads componentPropertyReferences.visible for boolean bindings. Step 4 enriches slot notes with preferred component names, marks hidden/empty slots for artwork population, and sets section eligibility. Step 8 inserts preferred component instances directly into the SLOT node via appendChild; if slot insertion fails, it falls back to a ghost instance overlay. Step 8b creates sub-component anatomy sections for eligible preferred instances, deduplicating against existing default slot children.
Canvas measurements (Structure): create-structure Step 11c annotates each preview instance with native Figma measurement overlays via figma.currentPage.addMeasurement(...) (Plugin API; available in Dev Mode for both Console and native MCPs). The annotation plan is built strictly from the section's rendered ROWS in Step 11a — overlays can only ever reflect what the table documents. A hard allowlist limits annotations to padding sides, itemSpacing, and min/max width/height; an explicit blocklist keeps cornerRadius, borderWidth, sizing modes, typography, icon refs, fixed dimensions, and group headers in the table only. freeText carries the token name when the row is token-bound. Hardcoded padding rows are anchored to the child whose edge sits on the container's inner-content edge for that side (within a 0.5-px epsilon of paddingTop/paddingBottom/paddingLeft/paddingRight) so Figma's default numeric label naturally matches the autolayout value the table documents; when no child aligns to that edge — e.g., a horizontal capsule whose children are HUG-sized and counterAxisAlignItems=CENTER — the line falls back to the first/last visible child but carries a freeText override of the autolayout value so the label still matches the table. Hardcoded gap/itemSpacing rows continue to let Figma's default numeric label show through (consecutive children sit edge-to-edge with the gap by definition). Min/max constraints prefix with "min N" / "max N". Per-instance idempotency is provided by getMeasurementsForNode + deleteMeasurement before each annotation pass. Annotation scope is one of two values, decided per section type, not per row group: rootOnly for variant / density / shape / composition / behavior / state-conditional / boolean-toggled sections (the table documents the root container's own auto-layout settings, so overlays draw on the inst root only); fullTree for subComponent and slotContent sections (the table documents the inst's internal structure, so the walker recurses through the inst, drawing on every matching FRAME and stopping at nested INSTANCE boundaries — those have their own spec sections). Two preview-fidelity prerequisites make the overlays honest: (a) slotContent previews nest the preferred component inside the actual SLOT node (see "Current skill support" in SLOT Node Handling), and (b) every section type except boolean-toggled runs a recursive nested-boolean enabler (mirrors create-color's pattern) after setProperties so all documented optional children are visible. Step 11c returns { success, section, measurementCount, plannedColumns } for verification — measurementCount === 0 while plannedColumns > 0 indicates the inst was missing or hidden and the section needs re-rendering. Sections whose tables contain only blocklisted properties (cornerRadius / borderWidth / typography / sizing modes / icon refs / etc.) are expected to return plannedColumns === 0 and need no follow-up. Measurements are a canvas overlay and do not appear in figma_take_screenshot / get_screenshot output.
Property extraction (Property): create-property uses a two-tier extraction model. Tier 1 (deterministic scripts): Steps 3, 3a, 3c, and 3d are figma_execute scripts that extract properties, resolve variant-gated booleans, link controlling booleans to child components by node ID, and normalize the data (coupled axes, unified slot chapters, sibling boolean collapsing). Tier 2 (AI reasoning): Step 3b (variable mode search) requires AI judgment for collection matching, and Step 3e is a validation layer that cross-checks the deterministic output for semantic mismatches, structural anomalies, and combination count sanity before rendering.
Structure extraction (Structure): create-structure uses the same two-tier extraction model. Tier 1 (deterministic scripts): Steps 4b (enhanced extraction: dimensions, tokens, sub-components, collapsed dimensions) and 4d (cross-variant dimensional comparison) are figma_execute scripts that measure every variant, resolve token bindings, walk sub-component trees, and build the raw comparison data. Tier 2 (AI reasoning): Step 6 is an AI interpretation layer that builds the section plan, writes design-intent notes, detects anomalies, and judges completeness before the deterministic rendering step fills the template. The rendering step (Step 11c) also draws native canvas measurement overlays per section — see Canvas measurements (Structure) for the contract.
Color extraction (Color): create-color uses the same two-tier extraction model. Tier 1 (deterministic script): Step 4b is a single consolidated figma_execute script that walks the component tree, resolves color variable bindings (paint/stroke style names take precedence over variable bindings), classifies variant axes by token fingerprint, detects boolean-gated elements (with nested boolean enablement), tags sub-component instances with their parent component set name, discovers mode-controlled collections, and detects composite paint styles (2+ visible fill layers) — emitting a compositeDetail object with layer stacking order, blend modes, opacities, and gradient stops. Tier 2 (AI reasoning): Step 4c interprets the extraction output — chooses the rendering strategy (Strategy A vs B via the two-gate model), builds the variant plan, resolves mode-specific token aliases, maps elements to tokens, and constructs compositeChildren breakdowns for multi-layer styles.
Spec placement: The import template step places the spec frame on the same page as the source component, positioned to its right with a 200 px gap. The script resolves the component node via getNodeByIdAsync, walks up to its PAGE ancestor, calls setCurrentPageAsync to activate that page, then positions the frame at compNode.x + compNode.width + 200, compNode.y. This works identically for both MCP providers — the page-loading block is harmless on Console MCP where the page is already active. For skills that accept a cross-file destination URL (anatomy, property, structure, motion), the cross-file branch keeps the existing viewport-center placement; the component-relative placement only applies when the spec stays in the same file as the component.
Completion link: After the final validation step, the agent constructs a Figma deep-link URL from the fileKey (extracted from the user's input URL) and the frameId (returned by the import step), replacing : with - in the node ID. The agent prints this URL in chat so the user can click directly to the rendered spec.
Clone visibility: All cloned sections explicitly set visible = true after cloning, since template sources are hidden. Some templates (e.g., color's #variant-template and #hierarchy-indicator) default to visible = false; rendering scripts only flip sub-elements to visible = true when needed (e.g., hierarchy indicators on composite child rows) rather than hiding them after the fact.
| Skill | Template | Sections Generated |
|---|---|---|
create-anatomy |
Anatomy | Component structure with numbered markers and attribute table |
create-property |
Property | One chapter per variant axis (with instance previews) and per boolean toggle |
create-voice |
Screen reader | Focus order, per-state platform sections (VoiceOver, TalkBack, ARIA) with property tables |
create-color |
Color annotation | Per-variant sections with element-to-token mapping tables (Strategy A for ≤6 variants; Strategy B consolidates states into columns for >6). Composite paint styles render as nested child rows with hierarchy indicators. |
create-api |
API overview | Main property table, sub-component tables, configuration examples |
create-structure |
Structure spec | Per-section dimensional tables with dynamic columns for size/density variants |
create-motion |
Motion spec | Timeline bars with easing-colored segments (bar positions computed in Figma code), detail table from pre-computed segments |
Each skill has two layers:
-
SKILL.md is the orchestration layer — it defines WHAT to do and WHEN. It contains the step-by-step workflow, MCP adapter mapping, executable Figma Plugin API scripts, script output contracts, intermediate data structures, and template mechanics.
-
Agent instruction file (e.g.,
structure/agent-structure-instruction.md) is the domain knowledge layer — it defines HOW to think and decide. It contains interpretation guidance for extraction output, decision frameworks, property naming conventions, value formatting rules, worked examples, common mistakes, edge cases, and validation checklists.
SKILL.md should not re-teach domain concepts that the instruction file covers. The instruction file should not describe script implementation details or workflow step numbers. The script output contract in SKILL.md (what each script returns) is the bridge between the two layers — SKILL.md describes the data shapes, the instruction file teaches the agent what to do with them.
See the Reference Files table at the bottom for each spec type's instruction file.
Template component keys are stored in uspecs.config.json at the project root and configured via the firstrun skill. Each skill reads its key from this file and imports the template via Plugin API JavaScript calling figma.importComponentByKeyAsync.
The template infrastructure uses a config file for extensibility:
Template key config (uspecs.config.json):
{
"mcpProvider": "figma-mcp",
"environment": "cursor",
"extractionSource": "plugin",
"fontFamily": "Inter",
"templateKeys": {
"screenReader": "key-from-firstrun",
"colorAnnotation": "key-from-firstrun",
"anatomyOverview": "key-from-firstrun",
"apiOverview": "key-from-firstrun",
"propertyOverview": "key-from-firstrun",
"structureSpec": "key-from-firstrun",
"motionSpec": "key-from-firstrun"
},
"reconciliation": {
"autoRetry": true
}
}mcpProvider: Determines which Figma MCP the create-* skills use. Values: "figma-console" (Southleft Console MCP with Desktop Bridge) or "figma-mcp" (native Figma MCP with write access). Set by the firstrun skill. Each skill reads this value and follows the matching tool-call path in its MCP Adapter section. The component markdown path does not use the MCP for measurements — it reads _base.json from disk — but the field is still consulted if a sub-skill's optional Step 3-delta triggers.
extractionSource: Signals where the component markdown path expects its input to come from. Current value: "plugin" (produced by figma-plugin/). Non-plugin extraction sources are not currently supported; the orchestrator's Step 1 aborts if the input cannot be validated against figma-plugin/docs/base-json-schema.md.
reconciliation.autoRetry: Toggles the create-component-md orchestrator's Step 8.5 bounded serial retry loop. When true, typed disagreements between extract-structure / extract-color / extract-voice trigger up to N targeted re-runs of the offending specialist with the mismatch payload attached. When false, mismatches are recorded into reconciliation.json and carried through to the final audit without retries.
fontFamily and the __FONT_FAMILY__ placeholder: The fontFamily value (detected by firstrun from the template library) is used in rendering scripts that create text labels in Figma. Skills declare const FONT_FAMILY = '__FONT_FAMILY__'; at the top of each rendering script, and the agent replaces __FONT_FAMILY__ with the value from uspecs.config.json before execution. This follows the same __PLACEHOLDER__ convention used for all dynamic values in figma_execute scripts.
Adding a new template type requires:
- Add a new key to
uspecs.config.jsonundertemplateKeys - Create a new SKILL.md in the platform-neutral source at
skills/<name>/SKILL.mdwith the MCP Adapter preamble and a workflow that reads the key and usesfigma.importComponentByKeyAsyncto import the template. Use{{skill:name}}and{{ref:path}}tokens for any cross-references to other skills or shared docs. - Update the
firstrunskill (inskills/firstrun/SKILL.md) to search for and extract the new template's component key - Rebuild the CLI bundle with
cd packages/cli && npm run buildso the new skill ships in the publishedtemplates/artifact, then bump the package version and publish (npm publish --access public). Existing installs pick up the new skill vianpx uspec-skills update. - Add the new skill to the tables in
CLAUDE.md,AGENTS.md, and this file
All skills follow a shared "clone from pristine template, fill, hide/remove original" pattern implemented inline within each Plugin API call (figma_execute / use_figma):
- Find template — Locate the hidden template node by name (e.g.,
#section-template,#variant-template,#row-template) - Clone per data item — For each item in the data array, call
template.clone()and append the clone to the template's parent - Set visible — Each clone sets
visible = true(templates are hidden by default) - Fill content — Load fonts, set text fields, configure properties on each clone
- Remove or hide original — After all clones are created, either
template.remove()(for row-level templates) ortemplate.visible = false(for section-level templates). Templates that default tovisible = false(e.g., color's#variant-template) skip this step.
This pattern nests at multiple levels. For example, the screen reader skill clones state templates, then within each state clones platform section templates, then within each section clones table templates, then within each table clones row templates. Each nesting level follows the same clone-fill-remove pattern within a single Plugin API call.
Each skill splits work across multiple Plugin API calls (figma_execute / use_figma) to avoid timeouts — typically one call per section, variant, or state. This keeps each call's execution time short and lets Figma process between calls. Complex specs (e.g., structure with many sections, screen reader with many states) benefit most from this pattern.
The component markdown path produces a single standalone .md spec per component. It bypasses the Figma MCP for measurements — data comes from a locally-installed Figma Desktop plugin that runs inside the Figma plugin sandbox and writes a _base.json file to disk. Interpreter skills then read that file and render the .md.
Each create-* Figma-native skill costs roughly 100k tokens per run because the majority of the weight is the Figma render pass: setProperties, createInstance, loadFontAsync, layout math, cloning templates, placing markers. The extract-* skills strip all of that. Because the plugin produces a single shared _base.json, the interpreters also stop calling the MCP for measurements — they read that file from disk. Per-spec token cost drops into the low tens of thousands and the parent orchestrator only holds one-line summaries from each specialist. The .md artifact is also easier to diff, review, and hand to downstream code-generation tools than seven separate Figma frames.
A local Figma Desktop plugin installed via Plugins → Development → Import plugin from manifest…. Not published to Figma Community — the source lives in-repo under figma-plugin/ and is built with esbuild.
The plugin walks the selected COMPONENT or COMPONENT_SET (a selected variant is auto-promoted to its component set) through a fixed sequence of phases and emits {componentSlug}-_base.json. Every variant is walked — no default-variant sampling — so cross-variant diffs are computed in the sandbox rather than reconstructed by the agent.
| Phase | File | Purpose |
|---|---|---|
| A | phaseA.ts |
Meta, axes, component property definitions |
| B | phaseB.ts |
Local variable collections + resolved values per mode |
| C | phaseC.ts |
Style resolution with inline-sample fallback when library styles are unresolvable |
| D | phaseD.ts |
Library-linked variable resolution (name, codeSyntax, alias chains, remote collection metadata) via figma.variables.getVariableByIdAsync |
| E | phaseE.ts |
Per-variant walker: dimensions, hierarchical tree, color walk, post-walk validation. extractDims is exported for reuse by Phase I. |
| F | phaseF.ts |
Cross-variant diffs + axis classification |
| F′ | childComposition.ts |
First-guess classification for each top-level child instance (constitutive / referenced / decorative). Designer confirms or flips each guess in the plugin UI before extraction completes. Forwards Phase E's typed componentProperties snapshot into every _childComposition.children[*] entry for the renderer's referenced-component override table. |
| G | phaseG.ts |
Revealed trees + slot host geometry |
| H | phaseH.ts |
Ownership hints (which element "owns" a given color / dimension) |
| I | phaseI.ts |
Constitutive sub-component variant walks: enumerates each constitutive child's own variant axes (cross-product capped at 20 combos per sub), measures dimensions + treeHierarchical per combo, emits subComponentVariantWalks keyed by subCompSetId. Fixes the case where a parent-variant walk captures a child only in its embedded configuration and misses the child's own size/density/etc. axes. |
Designer-in-the-loop composition. Phase F′ pre-classifies children using node metadata (name, main component set, variant axes), but the plugin UI surfaces each top-level child for designer review. Confirmed or flipped guesses land in _childComposition.children[*] with classificationEvidence: ["user-selected"]. The orchestrator's Step 4.5 review short-circuits to a confirmation-only pass when every child carries that evidence. Each entry also carries a typed componentProperties snapshot (mirroring Figma's InstanceNode.componentProperties shape with the #… clean-key suffix stripped) for INSTANCE entries — null for FRAMEs, vectors, layout wrappers, and slot-preferred entries. The create-component-md renderer reads this snapshot exclusively for its referenced-component override table; the legacy booleanOverrides field is now a backward-compat projection over the typed snapshot.
Defensive accessors. src/safe.ts provides safeLen, sg, and sidStr wrappers that let the walker tolerate GROUP and SLOT nodes whose property reads would otherwise throw under the plugin sandbox's strict mode. src/safe.ts also exports snapshotComponentProperties (typed InstanceNode.componentProperties snapshot consumed by Phase E and the slot-default-child branch in code.ts) and getSlotPropName (authoritative SLOT → slot-property binding via componentPropertyReferences.mainComponent with a name-based fallback; replaces an earlier Object.values(cpRefs)[0] lookup that was picking up the visible binding instead of the slot binding).
Inline font capture. Text style IDs are recorded, but inline font family + style + size + weight are also captured on every text node so typography data survives even when a library-linked text style cannot be resolved.
Schema + validator. The full _base.json shape is documented in figma-plugin/docs/base-json-schema.md. An Ajv validator lives at figma-plugin/scripts/validate-base.mjs and is shell-executed by the orchestrator's Step 1 — non-zero exit aborts the run with the validator's FAIL output.
Inputs:
baseJsonPath(required) — path to the plugin's output. Missing → abort with "run the uSpec Extract plugin".figmaLink(optional) — only consulted if an interpreter's Step 3-delta MCP call fires._meta.fileKey/_meta.nodeIdfrom_base.jsonwin when the two disagree; the parent logs aMETA_DISAGREES_WITH_LINKwarning.optionalContext(optional) — free-form design intent forwarded verbatim to every sub-skill._base.json._meta.optionalContextwins when both are set.
Workflow (abridged — the canonical checklist lives in .cursor/skills/create-component-md/SKILL.md):
- Preflight. Read
uspecs.config.json, load_base.json, run the Ajv validator. Extract_meta.{fileKey, nodeId, componentSlug, optionalContext, extractionSource}. - Resolve
componentSlugand the output path (default./components/{componentSlug}.md). Create./components/(tracked) and.uspec-cache/{componentSlug}/(gitignored). - Announce the plan. One-line summary of what will be generated.
- Stage
_base.jsoninto.uspec-cache/{componentSlug}/_base.json. - Run
extract-apiinline in the parent. Produces{componentSlug}-api.jsonandapi-dictionary.json. The dictionary lands in the parent so downstream specialists can read it. - Parallel fan-out. Dispatch
extract-structure,extract-color,extract-voiceas threegeneralPurposesubagents in a single batch. Each subagent holds its own_base.json+api-dictionary.jsoncontext; the parent keeps only the returned one-line summary + cache-file path from each. - Reconciliation (Step 8.5). Compare the three specialist artifacts for typed disagreements (e.g., same element classified as
constitutivein structure butreferencedin voice; variant axis present in one artifact and absent in another). Whenreconciliation.autoRetry === true, re-run the offending specialist with the mismatch payload attached, up to a bounded retry count. Write the final verdict toreconciliation.json. - Render the
.mdpercomponent-md/agent-component-md-instruction.mdusing all four cache files +api-dictionary.json. - Integrity check (Step 9.5). Validate every cache file's shape, assert axis-name consistency across artifacts, assert voice state platform coverage, assert the
coverageMatrixartifact fromextract-structureiscomplete === true, and recountframesWalkedindependently. Abort on failure. - Audit + summary. Emit a one-line run summary.
- Recursion manifest (Step 10.5). Emit a manifest of constitutive children so the caller can fan out to generate per-child
.mdspecs without re-walking_base.json.
Produced per component by the orchestrator. .uspec-cache/ is gitignored.
.uspec-cache/{componentSlug}/
├── _base.json staged copy of plugin output
├── {componentSlug}-api.json from extract-api
├── {componentSlug}-structure.json from extract-structure
├── {componentSlug}-color.json from extract-color
├── {componentSlug}-voice.json from extract-voice
├── api-dictionary.json shared dictionary that steers structure/color/voice
└── reconciliation.json Step 8.5 verdicts + retry log
Shared shape across all four:
- Read-only. No MCP calls except an optional Step 3-delta ping if a measurement is missing from
_base.jsonandfigmaLinkwas passed. The delta path writes tiny_deltaExtractionsentries into the cache artifact so the orchestrator can surface them in the audit. - Paired instruction file. Each
extract-<type>references the canonicalagent-<type>-instruction.mdfor domain rules (same instruction file the Figma-nativecreate-<type>skill uses). The skill teaches the read-path over_base.jsonfields; the instruction file teaches the interpretation rules. - Deterministic output paths.
{componentSlug}-<type>.jsonunder the component's cache directory, plus any_deltaExtractionsrequests. - Provenance flags. Every row / cell carries a
provenancetag (measured,inferred,delta, or"—"with a reason) so the orchestrator and downstream readers can trust or challenge values without re-running the pipeline.
extract-structure is the largest of the four. It consumes the Phase I subComponentVariantWalks block to populate per-column values for constitutive sub-components across their own variant axes (e.g., Input size=large|medium|small), replacing the old "—" placeholders. When a matching walk entry exists, cells are sourced from variants[*].dimensions (or treeHierarchical for hierarchical properties) with provenance: 'measured'; when it is missing or the walk was skipped, a _deltaExtractions gap is emitted instead of a silent "—".
The Figma-native skills treat the Figma file as the source of truth for the component spec and write annotation frames beside the component. create-component-md inverts that relationship: the .md file is the source of truth for implementation, and Figma is only the source of extraction. Downstream code generators, documentation sites, and review tools should consume the .md; regenerating it is cheap because the plugin + interpreter chain is deterministic given the same _base.json.
Skills, references, and the CLI that installs them are versioned together but live in separate trees:
skills/<name>/SKILL.md platform-neutral source of truth, with {{skill:}} / {{ref:}} tokens
references/<area>/*.md shared instruction and reference files
packages/cli/ the uspec-skills npm package (CLI + render engine)
packages/cli/templates/ bundled copy of skills/ and references/ — built artifact
.cursor/skills/ generated per-host artifact (DO NOT edit)
.claude/skills/ generated per-host artifact (DO NOT edit)
.agents/skills/ generated per-host artifact (DO NOT edit)
Because the same SKILL.md is rendered into three different per-host directories whose relative paths to references/ differ (e.g., .cursor/skills/<name>/SKILL.md reaches references via ../../references/..., while .claude/skills/<name>/SKILL.md reaches them via ../../../references/...), bare relative paths inside a SKILL.md cannot work across hosts. The CLI's render engine rewrites two token forms at install time:
| Token | Rewritten to | Purpose |
|---|---|---|
{{skill:other-skill}} |
host-specific invocation phrasing (e.g., @other-skill for Cursor, "the other-skill skill" elsewhere) |
Cross-skill references |
{{ref:area/file.md}} |
host-correct relative path to references/area/file.md |
Pointers to shared instruction files |
Authors should always use these tokens in the source skills/<name>/SKILL.md. Bare relative paths to references/... will resolve correctly only on Cursor and break on Claude Code / Codex.
packages/cli/
├── package.json published to npm as "uspec-skills"
├── src/
│ ├── cli.ts command dispatcher
│ ├── render.ts token rewrite + per-host writer
│ ├── paths.ts source-dir resolution (dev vs production)
│ ├── config.ts uspecs.config.json reader/writer
│ ├── version.ts reads CLI version into config.cliVersion
│ └── commands/
│ ├── init.ts interactive setup; bootstraps empty dirs
│ ├── install.ts non-interactive (re-)install for a platform
│ ├── update.ts wraps install — re-render after package upgrade
│ ├── doctor.ts verify install + report drift
│ └── render.ts internal render driver
├── scripts/
│ ├── build.mjs esbuild → dist/index.js + copy templates/
│ └── check-registry.mjs prepublishOnly safety guard (blocks non-public registries)
├── templates/ built copy of /skills and /references (gitignored)
└── .npmrc pins this package's registry to npmjs.org
| Command | Purpose |
|---|---|
npx uspec-skills init |
Interactive setup: prompts for platform + MCP, installs skills + references, writes uspecs.config.json. Bootstraps a fresh project when no .git/, package.json, or uspecs.config.json is found above the current directory. |
npx uspec-skills install [--platform p] |
Non-interactive (re-)install. Reads environment from uspecs.config.json if --platform is omitted. Idempotent. When called with --platform for a secondary host, preserves the primary environment field already in the config. |
npx uspec-skills update |
Re-renders skills against the currently installed CLI version. Run after upgrading the package. |
npx uspec-skills doctor |
Verifies install: checks uspecs.config.json exists and has environment, the platform's skills directory is populated, all .md-relative links resolve, and reports CLI version drift. |
paths.ts → resolveSourceDirs() looks for skills and references in two places:
- Production —
<package>/templates/skillsand<package>/templates/referencesinside the installed npm package. The build script copies the repo'sskills/andreferences/intotemplates/on every build. - Development — when running directly from a checkout with no
templates/dir, walks up from the CLI module location until it finds siblingskills/andreferences/directories.
Production wins when both are present, so a published package never accidentally serves files from a stale checkout.
Three defensive layers prevent accidentally publishing this package to a non-public registry:
package.json → publishConfig.registrypins the publish target tohttps://registry.npmjs.org/- A local
packages/cli/.npmrcoverrides any user-level registry config for this directory prepublishOnly → scripts/check-registry.mjsruns before everynpm publishand aborts with a non-zero exit if the resolved registry isn'tregistry.npmjs.org
Always publish from inside packages/cli/ so the local .npmrc is honored.
| Field | Written by | Notes |
|---|---|---|
environment |
init (always) and install only when no value already present |
Primary host: cursor | claude-code | codex |
mcpProvider |
init (always) |
figma-mcp | figma-console |
cliVersion |
init, install, update |
Used by doctor to surface drift between recorded version and installed CLI |
templateKeys, fontFamily |
firstrun skill (not the CLI) |
Filled by the agent on first run after install |
The uSpec docs are hosted at https://docs.uspec.design using Mintlify.
- Reference the Mintlify writing rule (
.cursor/rules/mintlify.mdcin Cursor, or read it directly in other hosts) so the agent uses the correct writing style and Mintlify components - Edit the MDX files in
docs/(and updatedocs/docs.jsonif adding or removing pages) - Push to
main— Mintlify auto-deploys within 1–2 minutes
docs/
├── docs.json # Site config: theme, navigation, colors, metadata
├── index.mdx # Homepage
├── getting-started.mdx # Setup and first spec guide
├── how-it-works.mdx # System overview
├── specs/ # Specification type docs
├── help/ # Troubleshooting, contribute, changelog
├── images/ # Screenshots, videos, and spec output demos
├── logo/ # Logo files (light.svg, dark.svg)
└── favicon.svg
- Writing rule:
.cursor/rules/mintlify.mdc— component syntax, writing style, content standards - Mintlify MCP:
SearchMintlifytool — look up component syntax, configuration, and best practices - Local preview:
npx mintlify devfromdocs/directory (port 3000) - Site config schema: https://mintlify.com/docs.json
Every skill is authored once at the repo root in skills/<name>/SKILL.md with {{skill:name}} and {{ref:path}} tokens. The CLI's render engine rewrites those tokens per host at install time. Always edit these source files, never the per-host generated copies.
Figma-native path:
| File | Content |
|---|---|
skills/create-anatomy/SKILL.md |
Anatomy: extraction, marker rendering, attribute table |
skills/create-property/SKILL.md |
Property: variant axis and boolean toggle exhibits |
skills/create-voice/SKILL.md |
Screen reader: merge analysis, platform sections, property tables |
skills/create-color/SKILL.md |
Color: consolidated extraction (style-over-variable token resolution, composite style detection, axis classification, boolean gating, sub-component tagging, mode discovery), AI strategy selection, element-to-token mapping tables, composite breakdown with hierarchy indicators |
skills/create-api/SKILL.md |
API: main table, sub-component tables, configuration examples |
skills/create-structure/SKILL.md |
Structure: dynamic columns, hierarchy indicators, dimensional tables |
skills/create-motion/SKILL.md |
Motion: timeline bars, pre-computed easing segments, detail table |
Component markdown path:
| File | Content |
|---|---|
skills/create-component-md/SKILL.md |
Orchestrator: preflight + schema validation, Step 4.5 composition review, extract-api inline run, parallel fan-out of extract-structure / extract-color / extract-voice as subagents, Step 8.5 reconciliation, Step 9 .md render, Step 9.5 integrity check, Step 10.5 recursion manifest |
skills/extract-api/SKILL.md |
Interpret API / properties / sub-components / examples from _base.json; emit {slug}-api.json + shared api-dictionary.json |
skills/extract-structure/SKILL.md |
Interpret variant axes, dimensions, subComponentVariantWalks, slot contents, cross-variant diffs; emit {slug}-structure.json with coverageMatrix audit artifact |
skills/extract-color/SKILL.md |
Interpret per-element fills/strokes/effects, axis classification, boolean delta, variable-mode detection, rendering strategy; emit {slug}-color.json |
skills/extract-voice/SKILL.md |
Interpret focus order, merge analysis, per-state VoiceOver/TalkBack/ARIA tables, slot insertion plans; emit {slug}-voice.json |
Setup:
| File | Content |
|---|---|
skills/firstrun/SKILL.md |
First run: reads mcpProvider and environment from uspecs.config.json (already written by npx uspec-skills init), verifies the Figma MCP connection, then either writes the built-in Uber template keys or extracts component keys from the user's library, detects the font family, and writes templateKeys + fontFamily to uspecs.config.json |
These directories are produced by npx uspec-skills install/update from the platform-neutral source above. They are gitignored in consumer projects and never edited directly:
.cursor/skills/— Cursor host. Cross-skill references rendered as@skill-name; relative paths toreferences/use../../..claude/skills/— Claude Code CLI host. Cross-skill references rendered asthe skill-name skill; relative paths toreferences/use../../../..agents/skills/— Codex CLI host. Same rendering as Claude Code.
In this monorepo we keep a checked-in copy of .cursor/skills/ because the development repo itself uses uSpec; consumer projects only have the directory matching their chosen host.
| File | Content |
|---|---|
CLAUDE.md |
Claude Code project instructions and skill index |
AGENTS.md |
Codex agent instructions and skill index |
.mcp.json |
Shared MCP config (Claude Code reads this by default) |
.codex/config.toml |
Codex MCP config |
.cursor/mcp.json |
Cursor MCP config (gitignored — user configures locally) |
Each domain owns one canonical instruction file that both the Figma-native create-<type> skill and the read-only extract-<type> sub-skill reference. The component-md instruction file is unique to the markdown path.
All instruction files live under references/<area>/ at the repo root and are shipped to consumer projects by the CLI as ./references/<area>/.... From inside a SKILL.md, link to them via the {{ref:area/filename.md}} token (the render engine inserts the host-correct relative path).
| File | Content |
|---|---|
references/anatomy/agent-anatomy-instruction.md |
Anatomy annotation: extraction validation checklist, note-writing guidelines, property-aware unhide decisions, nearest-edge marker placement, inline marker detection, slot preferred instance enrichment |
references/screen-reader/agent-screenreader-instruction.md |
Screen reader spec: data schema, platform reference (VoiceOver/TalkBack/ARIA), merge analysis guidance. Consumed by both create-voice and extract-voice. |
references/screen-reader/voiceover.md |
iOS accessibility properties reference |
references/screen-reader/talkback.md |
Android semantics and roles reference |
references/screen-reader/aria.md |
ARIA roles and states reference |
references/color/agent-color-instruction.md |
Color annotation: strategy selection (A vs B), token resolution rules (styles over variables), element-to-token mapping, composite style breakdown, hierarchy indicator rendering, rendering decisions. Consumed by both create-color and extract-color. |
references/api/agent-api-instruction.md |
API overview: property classification rules, sub-component patterns (slot vs fixed), naming conventions, validation checklist. Consumed by both create-api and extract-api. |
references/api/api-library.md |
API documentation reference patterns |
references/structure/agent-structure-instruction.md |
Structure spec: interpretation guidance, section planning, dimensional comparison rules, sub-component variant walk read-path, anomaly detection. Consumed by both create-structure and extract-structure. |
references/motion/agent-motion-instruction.md |
Motion spec: JSON schema (with pre-computed segments), rendering rules, timeline layout |
references/motion/export-timeline.jsx |
After Effects export script: keyframe extraction, segment computation, cubic-bezier conversion, value formatting, keyframe stripping |
references/property/agent-property-instruction.md |
Property exhibit interpretation: variant axis classification, boolean-to-slot linkage, validation. Consumed by create-property. |
references/component-md/agent-component-md-instruction.md |
Component markdown renderer: section plan, how to compose API + Structure + Color + Voice into a single .md, referenced-child disclosure rules, recursion manifest format |
references/component-md/component-md-template.md |
Reference template for the final .md output (headings, table shapes, disclaimers) |
| File | Content |
|---|---|
figma-plugin/README.md |
Install, use, validate, and hand-off instructions for the uSpec Extract plugin |
figma-plugin/docs/base-json-schema.md |
Canonical _base.json field reference, phase map, audit checklist |
figma-plugin/manifest.json |
Figma plugin manifest |
figma-plugin/scripts/build.mjs |
esbuild bundler (writes dist/code.js, dist/ui.html) |
figma-plugin/scripts/validate-base.mjs |
Ajv schema validator — shell-executed by create-component-md Step 1 |
figma-plugin/src/code.ts |
Sandbox entry point; orchestrates all phases |
figma-plugin/src/ui.html + src/ui.ts |
Plugin UI iframe (checklist for child classification, download, clipboard) |
figma-plugin/src/types.ts |
Shared types between sandbox and iframe |
figma-plugin/src/safe.ts |
Defensive property accessors (safeLen, sg, sidStr), typed componentProperties snapshot helper (snapshotComponentProperties), and authoritative SLOT → slot-property binding (getSlotPropName) |
figma-plugin/src/phaseA.ts … phaseH.ts |
Extraction phases A–H (see the phase map above) |
figma-plugin/src/phaseI.ts |
Phase I — constitutive sub-component variant walks; uses extractDims exported from phaseE.ts |
figma-plugin/src/childComposition.ts |
Phase F′ — first-guess child classification |