Skill v1.0.0
currentAutomated scan100/100name: figma-from-code description: Orchestrates the full code-to-Figma rebuild workflow for a web application. Thin dispatcher that delegates all substantial work to subagents and reads only small summary files. Runs eight tracked phases (phase0a, phase0b, phase1, phase2, phase2_5, phase3, phase4, phase5). Never calls use_figma or get_screenshot directly. model: claude-sonnet-4-5 metadata: version: "1.0.0"
Skill: Build Figma from Code (Orchestrator)
Thin dispatcher. Delegates every phase to a subagent, reads only small summary files, and tracks progress in state.json. Never calls use_figma or get_screenshot directly.
Required Inputs
fileKey: Figma file keyresume(optional):trueto skip completed phasesconfig(optional): per-project overrides; any omitted key falls back to the generic default shown below
{"devServerUrl": "http://localhost:5173","devServerStart": "npm run dev","sourceDir": "src", // app source root"componentsRoot": ["src/components"], // array of component directories (supports monorepos); populated after Phase 0a discovery"pagesRoot": "src/pages", // where page/screen sources + figma-screen.json live"cssPath": "src/index.css", // CSS custom-properties file for token extraction"tailwindConfigPath": "tailwind.config.js", // or null for Tailwind v4 / vanilla CSS projects"iconLibrary": "lucide-react", // or null to skip icon extraction"skillRoot": "plugins/figma-from-code/skills/figma-from-code" // where this skill tree lives; ${CLAUDE_PLUGIN_ROOT}/skills/figma-from-code when running as an installed plugin}
Hard Gates — Files the Orchestrator Must Never Read
The orchestrator is a thin dispatcher. It must not open, read, or load any sub-skill SKILL.md files. Those files are for subagents only.
Forbidden reads (orchestrator):
| File | Why forbidden | |
|---|---|---|
1-discovery-components/SKILL.md | Subagent reads it to execute Phase 0a | |
2-discovery-assets/SKILL.md | Subagent reads it to execute Phase 0b | |
3-setup-tokens/SKILL.md | Subagent reads it to execute Phase 1 | |
4-setup-structure/SKILL.md | Subagent reads it to execute Phase 2 | |
5-precapture/SKILL.md | Subagent reads it to execute Phase 2.5 | |
6-icon-preamble/SKILL.md | Subagent reads it to execute Phase 3 icon preamble | |
7-build-component/SKILL.md | Subagent reads it to execute per-component build + review/fix (steps 1–7) | |
8-build-screens/SKILL.md | Subagent reads it to execute Phase 4 | |
9-validate/SKILL.md | Subagent reads it to execute Phase 5 | |
10-validator/SKILL.md | Subagent reads it for validation logic and Component App Map |
What the orchestrator reads instead: only small summary/output files under .temp/figma-from-code/ (e.g., discovery-summary.json, tokens-summary.json, structure-summary.json).
If you find yourself opening a sub-skill SKILL.md to understand inputs, outputs, or workflow steps — stop. That information belongs in the dispatch table and per-phase notes in _this_ file. If those notes are missing, ask the user before proceeding.
Subagent Dispatch Pattern
When dispatching a subagent:
- Give the subagent only: the skill filepath +
fileKey+ any state fields it needs (listed in the table below) - Instruct the subagent to read its own skill file for full instructions
- Tell the subagent to report back only:
{ "success": true/false, "outputFile": "<path>" } - Read the output file yourself after the subagent completes
The orchestrator never re-explains what a skill does — the skill file is the source of truth. The orchestrator never opens a sub-skill's SKILL.md file — see Hard Gates above.
Config Placeholder Convention
Sub-skill files reference config values as {devServerUrl}, {sourceDir}, {componentsRoot}, {pagesRoot}, {cssPath}, {tailwindConfigPath}, {iconLibrary}, {skillRoot}, etc. Subagents resolve these placeholders from state.json → config before executing any commands. The orchestrator also substitutes resolved values from state.config when composing dispatch prompts so subagents always receive concrete paths and URLs.
`componentsRoot` is an array. Most config references pass the full array to subagents. Subagents that write .figma/ tracking files derive the write path from each component's sourcePath (in component-map.json) rather than from a single componentsRoot prefix. For synthetic components (icons, assets) that have no source file, use the first entry in the array.
State Ledger
Maintain .temp/figma-from-code/state.json:
{"fileKey": "{fileKey}","startedAt": "ISO timestamp","config": {"devServerUrl": "http://localhost:5173","devServerStart": "npm run dev","sourceDir": "packages/client/src","componentsRoot": ["packages/client/src/components"],"pagesRoot": "packages/client/src/pages","cssPath": "packages/client/src/index.css","tailwindConfigPath": "packages/client/tailwind.config.js","iconLibrary": "lucide-react","skillRoot": "plugins/figma-from-code/skills/figma-from-code"},"phases": {"phase0a": "complete|in_progress|pending","phase0b": "complete|in_progress|pending","phase1": "complete|in_progress|pending","phase2": "complete|in_progress|pending","phase2_5": "complete|in_progress|pending","phase3": "complete|in_progress|pending","phase4": "complete|in_progress|pending","phase5": "complete|in_progress|pending"},"tierProgress": { "tier1": "complete|complete_with_failures|in_progress|pending" },"buildOrder": { "tierCount": 0, "tiers": [{ "tier": 1, "label": "...", "components": ["..."] }] },"figmaNodes": {"foundationsPageId": "...","componentsPageId": "...","screensPageId": "...","foundationsFrameId": "...","iconsFrameId": "...","screensFrameId": "...","tier1FrameId": "...","tier2FrameId": "..."},"existingCollections": [],"existingPages": [],"variableMapPath": ".temp/figma-from-code/variables.json","builtComponents": {},"builtScreens": {},"preExistingComponents": {},"preExistingScreens": {},"screenBodySize": { "w": 1440, "h": 900 },"iconDiscovery": { "iconCount": 0, "icons": [], "assetCount": 0, "assets": [] }}
Subagents do not modify state.json. Each writes its own output file; the orchestrator reads it and updates state.
figmaNodes uses the pattern tier{N}FrameId for each tier (e.g., tier1FrameId, tier2FrameId, ...). The number of keys matches buildOrder.tierCount.
Fresh Run: Initialize state.json
Config detection (runs once, before Phase 0a). On a fresh run the orchestrator detects project-specific values before writing state.json:
- Read
package.json(root and any workspace roots) — find thedevorstartscript and extract the port from--port,PORT=, or Vite/CRA defaults; derivedevServerUrl. - Set
componentsRootto[](empty array). Component directories are discovered by Phase 0a and confirmed by the user at the Wave 1 pause. SetsourceDirto the most likely app source root (e.g.,src,packages/client/src) based onpackage.jsonworkspace structure. - Locate the pages/screens directory: look for
pages,screens, orroutesundersourceDir. - Locate the CSS custom-properties file: prefer
src/index.css,packages/*/src/index.css, or any*.cssfile containing--custom properties. - Detect
tailwindConfigPath: check fortailwind.config.js,tailwind.config.ts,tailwind.config.cjsat the repo root and likely package roots; set tonullif absent (Tailwind v4 / vanilla CSS project). - Detect
iconLibrary: greppackage.jsondependencies forlucide-react,react-icons,@heroicons/react,@radix-ui/react-icons; use the first match ornull. - Present the resolved config to the user in a compact table. Note that
componentsRootwill be populated after Phase 0a discovery. Ask for confirmation or corrections before proceeding. - Persist the confirmed object as
configinstate.json. Onresume, read config fromstate.json— never re-detect.
After confirming config, create .temp/figma-from-code/state.json from the template above. Set fileKey and startedAt to current ISO timestamp; set config to the confirmed object; all phases values to "pending"; all other fields empty.
Pre-Existing Components Rule
preExistingComponents is an immutable snapshot from Phase 0a. Never update it after Phase 0a.
Hard rule: Pause and get explicit user authorization before any action that modifies, replaces, or deletes a node in preExistingComponents. This includes rebuilding, running the icon preamble over existing icons, or any Phase 3 build that resolves to a pre-existing node.
Phase Dispatch Table
All skill files live under {skillRoot}/. Shell steps (normalization after Phase 0b, aggregation scripts after Phase 3 tiers and Phase 4) are in Per-Phase Notes. After each subagent succeeds, set phases.{phase}: complete in addition to the listed state fields.
Important: Each subagent does all of its work inline — no subagent spawns further subagents. Phase 3 dispatches one subagent per component (fresh context each time). The validation subagent (Phase 5) processes all screens sequentially.
| Phase | Skill file | Inputs to pass | Output file to read | State fields to update | Skip if | |
|---|---|---|---|---|---|---|
| 0a | 1-discovery-components/SKILL.md | fileKey; config: devServerUrl, sourceDir | discovery-summary.json | buildOrder, builtComponents, preExistingComponents, preExistingScreens; also extract figma.variableCollections → existingCollections and figma.pages → existingPages from the summary | phase0a: complete | |
| 0b | 2-discovery-assets/SKILL.md | config: sourceDir, iconLibrary | icons-summary.json | iconDiscovery | phase0b: complete | |
| 1 | 3-setup-tokens/SKILL.md | fileKey, existingCollections; config: cssPath, tailwindConfigPath | tokens-summary.json | variableMapPath | phase1: complete AND tokens-summary.json AND variables.json AND resolved-colors.json all exist under .temp/figma-from-code/ | |
| 2 | 4-setup-structure/SKILL.md | fileKey, existingPages | structure-summary.json | figmaNodes (foundationsPageId, componentsPageId, screensPageId, foundationsFrameId, iconsFrameId, screensFrameId) | phase2: complete AND page/icons/screens IDs in figmaNodes are non-null | |
| 2.5 | 5-precapture/SKILL.md (one subagent) | fileKey only — subagent owns and builds all manifests itself from component-map.json; config: devServerUrl | precapture-all.json, precapture-screens.json | phases.phase2_5 | phase2_5: complete | |
| 3 pre | 6-icon-preamble/SKILL.md | fileKey, figmaNodes; subagent reads builtComponents.json from disk; config: devServerUrl, componentsRoot (array) | icon-preamble-results.json | merge created into builtComponents; rewrite builtComponents.json | all icons already in builtComponents | |
| 3 tiers | 7-build-component/SKILL.md (one subagent/component) | fileKey, componentName, tier, tierFrameId, componentsPageId (from figmaNodes); subagent reads builtComponents.json from disk; config: devServerUrl, componentsRoot (array) | per-component: build-results/{Name}.json | builtComponents (after each component); figmaNodes.tier{N}FrameId; rewrite builtComponents.json after each; set phases.phase3: complete after all tiers | component already in builtComponents | |
| 4 | 8-build-screens/SKILL.md (one subagent/screen) | fileKey, figmaNodes, preExistingScreens; per-screen: screenName, route, pageSourceFile, keyComponents (read from component-map.json → routes/tree); subagent reads builtComponents.json from disk; config: devServerUrl, pagesRoot | build-screens.json | phases.phase4 | phase4: complete | |
| 5 | 9-validate/SKILL.md | fileKey, figmaNodes, preExistingScreens, buildOrder; config: devServerUrl | validation-summary.json | phases.phase5 | always runs |
Phase Execution Order
Phases have dependency constraints but several can overlap. Follow this wave structure to minimize wall-clock time:
Wave 1 (parallel): Phase 0a || Phase 0b- 0a: static route enumeration + browser crawl + interaction pass(interactions.json reveals dialogs/menus/edit modes) + Figma inspection- 0b: static icon/asset scan (no Figma, no browser needed)After Phase 0b completes: run normalization script (before the pause)⏸ PAUSE — Component Directory Confirmation (see below); then askuser to continue or stopWave 2 (as soon as normalization + Phase 0a are both done):Phase 1 || Phase 2 (parallel — both only need 0a outputs)⏸ PAUSE — write progress.md, ask user to continue or stopWave 3 (starts when normalization + Phase 1 + Phase 2 are ALL done):Before dispatching Phase 2.5, start the shared browser server:node {skillRoot}/scripts/browser-server.js &(Background process. Scripts fall back to per-script browsers if itisn't running. Phase 5 (9-validate) kills it via pw-server.pid.)Phase 2.5 — dispatch ONE subagent with unified manifest (allcomponents in a single sorted batch; chunked at 15 entries perscript run to stay within the 60s timeout)Subagent owns and builds all manifests; read precapture-all.json and precapture-screens.json after complete.⏸ PAUSE — write progress.md, ask user to continue or stopWave 4 (sequential): Phase 3 preamble → Phase 3 tiers (one tier at a time)Each tier waits for the prior tier; preamble runs before Tier 1.Within a tier, one subagent per component (fresh context each time).⏸ PAUSE after preamble⏸ PAUSE after EACH tier — write progress.md, ask userWave 5 (parallel): Phase 4 screens — dispatch ALL in parallelScreens only instance built components, never modify masters.⏸ PAUSE — write progress.md, ask user to continue or stopWave 6 (sequential): Phase 5 validation⏸ PAUSE — write final progress.md (build complete)
Wave 1 Pause: Component Directory Confirmation
At the Wave 1 pause, read componentDirectories and excludedDirectories from discovery-summary.json and present them to the user for confirmation before proceeding:
One directory found:
I found 1 component directory with {count} components:-{path}({count} components, subdirs: {list})Is this the correct directory to use?
Multiple directories found (monorepo or multi-app):
I found {N} component directories across your project:| # | Directory | Components | Subdirs ||---|-----------|------------|---------|| 1 |{path1}| {count1} | {subdirs1} || 2 |{path2}| {count2} | {subdirs2} || … | … | … | … |Which directories should be included? (Select all that apply, or provide a custom list.)
Zero directories found:
No component directories were automatically detected. Please provide the path(s) to your component directories.
Excluded directories (always shown if any):
The following directories were excluded by default:-{path}— {reason} (e.g., "shadcn primitives (ui/ under components/)", "test directory")Let me know if any should be included.
After the user confirms:
- Update
state.json → config.componentsRootwith the confirmed array of directory paths - Derive
sourceDirfrom the common ancestor of all confirmed roots if it's not already set correctly (e.g., if all roots are underpackages/client/src/, setsourceDirtopackages/client/src; if roots span multiple packages, keepsourceDiras the repo root or the user-confirmed value) - Write
progress.md
Dependency summary:
- Phase 1 needs: Phase 0a (
existingCollections) - Phase 2 needs: Phase 0a (
existingPages) - Phase 2.5 needs: normalization done (normalized
component-map.jsonforcapturedata — exact URL, selector, fallbacks, interaction replay per component); Phase 1 + Phase 2 must also be done before Phase 3 can start, but Phase 2.5 itself only needs normalization - Phase 3 needs: Phase 1 (
variables.json), Phase 2 (figmaNodes), Phase 2.5 (screenshots) - Phase 4 needs: Phase 3 (
builtComponents), Phase 2 (figmaNodes.screensFrameId) - Phase 5 needs: Phase 4 (all screens built)
Per-Phase Notes
After Phase 0b: Normalization (Phase 0b+)
Run the normalization script, then re-read discovery-summary.json to refresh buildOrder in state. Skip if phase0b: complete AND phase3 is in_progress or complete (normalization already applied).
node {skillRoot}/scripts/normalize-component-map.js \.temp/figma-from-code/component-map.json \.temp/figma-from-code/icons.json \--write
The script rewrites component-map.json in place with normalized names and regenerates discovery-summary.json from the normalized map. Re-read discovery-summary.json and update buildOrder in state.
Phase 2.5: Reading pre-capture results
After the pre-capture subagent completes, read both output files and verify that screenshot file counts are non-zero. There is no aggregation script — the orchestrator reads each file directly.
precapture-all.json— component screenshots; schema:{ "components": [{ "name", "selector", "screenshotFile", "status": "captured|failed|skipped" }] }precapture-screens.json— full-page screen screenshots; schema:{ "screens": [{ "screenName", "route", "pageSourceFile", "keyComponents": [], "appScreenshot", "textFile", "status": "captured|failed|skipped" }] }
Before Phase 3 (preamble): Materialize builtComponents.json
builtComponents.json is the on-disk pass mechanism for all Phase 3 and Phase 4 subagents — they read it directly rather than receiving builtComponents inline. Keep it in sync with state.builtComponents before every dispatch.
Write current state.builtComponents to disk before dispatching the preamble subagent:
node -e "const s=JSON.parse(require('fs').readFileSync('.temp/figma-from-code/state.json','utf-8')); require('fs').writeFileSync('.temp/figma-from-code/builtComponents.json', JSON.stringify(s.builtComponents||{},null,2));"
After preamble completes, merge new icons into state.builtComponents and rewrite builtComponents.json. The file persists through all of Phase 3 and Phase 4 as the live registry of built components.
Phase 3 Tiers: Post-tier aggregation
build-tier{N}.json schema:
{"tier": 1,"tierFrameId": "<figma-node-id>","status": "complete|complete_with_failures","completed": [{ "name": "...", "nodeId": "...", "variants": 1, "matchPct": 95 }],"failed": [{ "name": "...", "status": "failed", "reason": "..." }]}
After reading build-tier{N}.json, extract tierFrameId and store it as figmaNodes.tier{N}FrameId in state. Then run:
node {skillRoot}/scripts/collect-tier-results.js --tier {N} --components "{comma-separated}" --tier-frame-id "{tierFrameId}"
Read stdout for the one-line JSON summary. Then follow the End-of-Phase Pause Protocol: update state.json, write progress.md, and ask the user whether to continue to the next tier or stop. Each tier is a full pause point — the user may stop and a new orchestrator agent can resume from the next tier.
Phase 4: Post-screen aggregation
After all screen subagents complete:
mkdir -p .temp/figma-from-code/build-results/screensnode {skillRoot}/scripts/collect-screen-results.js --screens "{comma-separated}"
Progress File
After every pause point, the orchestrator writes .temp/figma-from-code/progress.md — a human-readable narrative that allows a fresh orchestrator agent (with no memory of prior work) to understand what happened and where to continue.
progress.md is the handoff document. state.json is the machine-readable truth. Both must exist and stay in sync.
Template
# Figma-from-Code Build Progress**File Key:** {fileKey}**Last Updated:** {ISO timestamp}**Status:** Paused after {phase/tier description}## Completed Phases| Phase | Description | Key Output | Completed At || ----- | -------------------- | ------------------------------------------------ | ------------ || 0a | Component discovery | {component count} components, {tier count} tiers | {timestamp} || 0b | Asset/icon discovery | {icon count} icons, {asset count} assets | {timestamp} || ... | ... | ... | ... |## Current State-**Built components:** {count} / {total}-**Tiers completed:** {N} / {total tiers}-**Screens built:** {count} / {total}-**Figma nodes created:** {list key node IDs}## Next Step**Phase to execute:** {phase ID and name}**Prerequisites:** {what must be true — e.g., "dev server running on {devServerUrl}"}**What it does:** {one-sentence description}**Estimated dispatches:** {number of subagents or script runs}## Warnings / Partial State-{any incomplete tiers, failed retries, or pre-existing component conflicts}
Startup Protocol
On every invocation, before doing any work:
- Check if
.temp/figma-from-code/progress.mdexists - If it exists:
- Read
progress.mdin full - Present a concise summary to the user: which phases are done, what comes next
- Ask: _"A previous build was paused after {phase}. Resume from {next phase}?"_
- If user confirms → read
state.json, validate it matchesprogress.md, skip to next incomplete phase - If user declines → ask if they want a fresh start (which deletes
.temp/figma-from-code/) or manual phase selection
- If it does not exist:
- Check if
state.jsonexists (orphaned state without progress file) - If
state.jsonexists → warn user, offer to reconstruct progress from state or start fresh - If neither exists → initialize fresh run (create
state.jsonfrom template, proceed to Phase 0a)
End-of-Phase Pause Protocol
After completing any pause point (see Pause Points below), the orchestrator MUST:
- Update `state.json` — mark the phase/tier complete, update all relevant state fields
- Write `progress.md` — regenerate the full file from current
state.jsonusing the template above - Report to user — show key metrics (counts, node IDs, any warnings)
- Ask the user:
> _"{Phase/tier} complete. {Brief metric summary}. Continue to {next phase/tier}, or stop here so a new agent can resume later?"_
- If user says stop → confirm
progress.mdis written, say "Build paused. A new agent can resume by invoking this skill." and terminate - If user says continue → proceed to the next dispatch
The orchestrator never auto-continues to the next pause point without explicit user confirmation.
Pause Points
Every wave boundary and every Phase 3 tier is a mandatory pause point:
| Pause Point | After | Before | |
|---|---|---|---|
| Wave 1 → 2 | Phase 0a + 0b complete (+ normalization) | Phase 1 + Phase 2 | |
| Wave 2 → 3 | Phase 1 + Phase 2 complete | Phase 2.5 | |
| Wave 3 → 4 | Phase 2.5 complete | Phase 3 preamble | |
| Phase 3 preamble | Icon preamble complete | Tier 1 | |
| Phase 3 Tier N | Tier N complete | Tier N+1 (or Wave 5 if last tier) | |
| Wave 4 → 5 | Phase 3 all tiers complete | Phase 4 screens | |
| Wave 5 → 6 | Phase 4 screens complete | Phase 5 validation | |
| End | Phase 5 complete | — (build finished) |
Error Handling
| Scenario | Action | |
|---|---|---|
| Dev server not running | Halt before Phase 2.5, tell user to start it | |
Subagent reports success: false | Report error, offer retry — never silently skip | |
| State inconsistency | Trust per-tier build JSON files; rebuild state from outputs | |
| Pre-existing component needs modification | Pause, apply authorization protocol, wait for user |