Create Component Markdown (Orchestrator)
This skill consumes a _base.json produced by the uSpec Extract Figma plugin (figma-plugin/), runs four read-only interpretation skills (extract-api, extract-structure, extract-color, extract-voice), and renders their combined output into one self-contained Markdown file. The .md is the artifact; Figma is only the source of extraction.
Do not call the create-* skills from here. They render Figma frames that overlap and do not compose into a single file.
Why this orchestrator exists
The four create-* skills each cost ~100k tokens per run because the majority of their weight is Figma rendering (setProperties, createInstance, loadFontAsync, layout math). The extract-* skills strip all rendering. Because the Figma plugin produces a single shared _base.json, the four interpretation skills also stop calling Figma — they read that file from disk. This removes most of the Figma-side work and keeps the orchestrator's parent context small by discarding each phase's detail after its one-line summary lands.
Token model (approximate):
| Phase | Peak context in parent |
|---|---|
| extract-api (runs first, inline) | instruction + _base.json read + interpretation |
| parallel fan-out (structure + color + voice, subagent each) | three one-line summaries (subagents hold their own context) |
| reconciliation (Step 8.5) | mismatch lists + api dictionary (small) |
| rendering | 4 JSON cache files + template + instruction |
extract-api runs first in the parent so its dictionary can steer the three downstream specialists. After the dictionary lands, the parent dispatches extract-structure, extract-color, and extract-voice as three parallel subagents (subagent_type=generalPurpose, single batch). Each subagent holds its own _base.json + dictionary context; the parent keeps only the returned one-line summaries and cache-file paths.
Inputs Expected
baseJsonPath(required): absolute or workspace-relative path to the_base.jsonfile produced by the uSpec Extract Figma plugin. Must validate against figma-plugin/docs/base-json-schema.md. If this is not provided, abort and instruct the user to run the uSpec Extract plugin (seefigma-plugin/README.md).figmaLink(optional): URL to the component set or standalone component. Acceptfigma.com/design/:fileKey/...and branch URLs (/branch/:branchKey/). Only consulted if an interpretation skill needs a Step 3-delta MCP call.optionalContext(optional): free-form guidance (e.g., "this is a compact variant only", "skip error states"). If the plugin already captured it in_meta.optionalContext, that wins; otherwise the value passed here is used. Forwarded verbatim to every sub-skill.
No output path is required — the default is ./components/{componentSlug}.md in the current working directory.
Workflow
Copy this checklist and update as you progress:
Task Progress:
- [ ] Step 1: Preflight — read config, load + validate _base.json, resolve metadata from _meta
- [ ] Step 2: Resolve componentSlug and output path
- [ ] Step 3: Announce the plan
- [ ] Step 3.5: Composition classification (reasoning gate — internalize before Step 4.5 review)
- [ ] Step 4: Stage _base.json into cachePath
- [ ] Step 4.5: Post-extract review — confirm _childComposition (user-selected classifications skip override pass)
- [ ] Step 5: Run extract-api (reads _base.json, no Figma), flush, verify cache + api-dictionary.json
- [ ] Step 6: Parallel fan-out — dispatch extract-structure, extract-color, extract-voice as three subagents in a single batch; join on all three summaries
- [ ] Step 8.5: Reconciliation — typed disagreement handling with bounded serial retries
- [ ] Step 9: Render the .md (follow {{ref:component-md/agent-component-md-instruction.md}})
- [ ] Step 9.5: Integrity check — validate all cache files and reconciliation artifact before rendering
- [ ] Step 10: Audit output and return a one-line summary
- [ ] Step 10.5: Emit recursion manifest (constitutive children only)
Step 1: Preflight
Read uspecs.config.json at the project root. Extract:
mcpProvider(figma-consoleorfigma-mcp). Only used iffigmaLinkis also provided AND an interpretation skill's Step 3-delta triggers.environment(used only if you need to emit provider-specific guidance).
Load and validate _base.json. baseJsonPath is required. If it is missing, abort with a one-line diagnostic: "run the uSpec Extract plugin in Figma and rerun with baseJsonPath=<path> — see figma-plugin/README.md."
- Read the file.
- Run the Ajv schema check at
figma-plugin/scripts/validate-base.mjs(shell out withnode figma-plugin/scripts/validate-base.mjs <path>) — any non-zero exit aborts with the validator's FAIL output. - On success, read
_meta.fileKey,_meta.nodeId,_meta.componentSlug,_meta.optionalContext,_meta.extractionSource. - If the caller also passed
optionalContextand_meta.optionalContextis null, stamp it onto the loaded base. - The MCP connection check is deferred — it is not needed unless a sub-skill triggers a delta.
If figmaLink is also passed alongside baseJsonPath, parse it and stash {fileKey, nodeId} for potential delta use — but trust _meta as the source of truth when they disagree and log a META_DISAGREES_WITH_LINK warning for the final summary.
Step 2: Resolve componentSlug and output path
- Resolve the component name from
_meta.componentSlug(pluscomponent.componentNamefor display). No MCP call needed. - Resolve
outputPath:- If user passed an explicit path, use as-is.
- Otherwise,
./components/{componentSlug}.mdin cwd. Create the./components/directory (recursive mkdir) if it does not exist — thecomponents/folder is tracked in version control (it holds the source-of-truth.mdspecs).
- Resolve
cachePath = .uspec-cache/{componentSlug}/. Create it (recursive mkdir)..uspec-cache/is gitignored.
Do not proceed until all of componentSlug, outputPath, cachePath are resolved. Every subsequent step reads from them.
Step 3: Announce the plan (non-blocking)
Print a single informational message to the user and then immediately continue to Step 4. Do not call AskQuestion. Do not wait for input. The chain runs start-to-finish without interruption.
Generating Markdown source of truth for {ComponentName} (
{nodeId}).Running 4 interpretation passes (API → Structure → Color → Voice) against the provided
_base.json. Cache:{cachePath}. Output:{outputPath}.No phase touches Figma unless an interpretation skill's Step 3-delta fires (read-only). Every pass flushes to disk to stay under token budget.
If the user wants to correct the target node or the extraction context, they must cancel this run and start a new one with different inputs. There is no mid-chain correction point.
Step 3.5: Composition classification (reasoning gate)
Before you interpret any of the extracted data, you must internalize how to classify every top-level child instance of the component being spec'd. This classification decides how each child appears in the final .md and whether a follow-up create-component-md run is needed. The mechanics of reading _childComposition happen at Step 4.5 (after _base.json lands). This step is the reasoning model you carry into that review.
Ask this question, in order, for each top-level child instance you will see in variants[<default>].treeHierarchical:
Q1. If I removed this child, would the component still be the thing the user is asking me to spec?
- No → constitutive (part-of). It is part of what this component is. Spec it.
- Yes → continue to Q2.
Q2. Does this child have a name/identity that a different consumer would recognize and use independently in a different context?
- Yes → referenced (uses-a). It is a self-contained component this one happens to embed. Name it and document the configuration passed to it; do not re-spec it.
- No → decorative (has-no-identity). A vector, glyph, or layout frame. Fold into structure or color inline.
Patterns to match against, with one illustration each
The column you should pattern-match on is Pattern — the shape of the relationship. The Illustration column is one concrete example so you can anchor the shape; the agreement is with the pattern, not the specific names. When you spec any component, ask: which of these patterns does each child match?
| # | Pattern | Classification | Illustration (one of many) |
|---|---|---|---|
| P1 | Child is a fixed, named anatomical part of the parent. Remove it and the parent loses a defined slot in its own anatomy, not just a feature. | constitutive | A form control's "helper text" row. Without the row, the control's anatomy no longer matches its own definition. |
| P2 | Child's name is only meaningful inside the parent (e.g., ParentName + role suffix like Item, Row, Cell, Step, Tab, Segment, Panel). The child was designed to live inside this parent and nowhere else. |
constitutive | A list-like parent whose repeated child is named after the parent's role taxonomy (e.g., the repeated "item" of a list-navigation component). |
| P3 | Child is a component the parent reuses from elsewhere to fulfill one of its own anatomical roles — the parent is not a button / icon / input, but it contains one because its definition requires that role to be filled. The child has its own spec. | referenced | A composite control that hosts a standard button in its action row. The composite is not a kind of button; it uses one. |
| P4 | Child is a generic system primitive (icon, divider, loading indicator, avatar, badge) that multiple unrelated components embed. Its identity is orthogonal to the parent. | referenced | Any component that embeds an instance from the design system's icon set. The icon has its own spec; the parent documents only which glyph and when. |
| P5 | Child is a peer component the parent composes alongside its own behavior — the parent could exist without it, but in this usage chooses to embed it. | referenced | A layout/form-style component that embeds N independent controls. The parent documents the layout contract; each embedded control keeps its own spec. |
| P6 | Child is a pure vector, text layer, or auto-layout frame with no component identity at all. | decorative | A decorative glyph drawn directly on the canvas (not an instance of anything). Document its fill/size inline. |
| P7 | Child is an instance-swap target (INSTANCE_SWAP property) whose concrete fill is consumer-provided. The parent defines the slot contract but does not own any particular instance. |
referenced (document the contract) | Any component with a swap-able icon/avatar/thumbnail slot. Parent documents "accepts: Icon (size=M)"; each concrete consumer passes a different instance. |
| P8 | Child is a component whose name could stand alone in the design system catalog without referencing the parent. If you found this child on its own page, would you still know what it is? Yes → it is a referenced component, not a constitutive one, even if the current parent depends on it. | referenced | Any widely-reused primitive — buttons, inputs, menus, tooltips, popovers — embedded into a more specific composition. |
How to apply the patterns
Most children match exactly one pattern, and the classification follows. For the stubborn cases:
- P1 vs. P2. Both are constitutive. Distinction matters only for rendering: P1 children that are not themselves component sets fold into sub-component tables; P2 children (which usually are component sets, repeated) emit a recursion entry.
- P2 vs. P8. This is the sharpest judgment call. Run the standalone-catalog test: "If I saw this child's main component in the design system catalog with no parent context, would its name and purpose be self-explanatory?" If yes → P8 (referenced). If it would be confusing without parent context → P2 (constitutive). Name suffixes like
Item,Row,Cell,Step,Tab,Segmentare strong P2 signals; names that match common design-system primitives are strong P8 signals. - P3 vs. P5. Both are referenced. The difference is intent: P3 says "the parent's anatomy requires this role to be filled by some reusable component" (the role is inherent to the parent); P5 says "the parent embeds arbitrary other components as its content" (like a modal hosting whatever the consumer passes). When unclear, default to P5 — it makes the fewest ownership claims.
- P4 vs. P6. If the child is an
INSTANCEwhose main component has a name, it's P4 (referenced). If it is a raw vector / frame / text node with no main-component reference, it's P6 (decorative).
Anti-patterns — failure modes to catch yourself doing
A1. Waiting for the color-extraction "container hint." The _containerRerunHint emitted by extract-color is a symptom (all visible colors live on sub-components), not a cause. By the time it fires you have already processed API, Structure, and Color under the wrong mental model and are one step away from writing property tables that belong to children. Classify at Step 3.5 (and confirm at Step 4.5) so every downstream interpretation runs with the right frame.
A2. Type-based shortcutting. Do not classify on the COMPONENT_SET / COMPONENT type alone. Many referenced primitives (P3, P4, P8) are also component sets, and many constitutive parts (P1) are not. The type is evidence, not the decision — always run Q1 and Q2.
A3. Copying child properties into the parent API. When a child is referenced (P3/P4/P5/P7/P8), do not lift its property table into the parent's ## API section. The parent documents the configuration it passes to the child, not the child's full surface. The child's own spec is the source of truth for its properties.
A4. Over-recursing. Just because a child is itself a COMPONENT_SET does not mean the parent's spec is incomplete without a dedicated child-spec run. If the child is referenced, the child already has (or will have) its own spec — one independent of this parent. Only constitutive children (P1/P2) drive the recursion manifest.
A5. Under-recursing on name coincidence. Do not skip recursion on a constitutive child just because its name matches a generic word. A child named Row nested inside a specific tabular parent, with no existence outside it, is still P2 — even though "Row" sounds generic. Run the standalone-catalog test (see "P2 vs. P8"), not the word-recognition test.
What each classification produces
- Constitutive → If the child is not itself a
COMPONENT_SET, fold it into the parent's sub-component tables (current Step 5 behavior). If the child is aCOMPONENT_SET, emit a "recurse" entry in Step 10.5's manifest. Either way, its properties are part of the parent's API section. - Referenced → Emit a
Referenced componentssubsection in the parent.md's API body. One entry per referenced component containing: name, variant/props passed, Figma node ID of the referenced component set, and a link to its spec (expected path:./{referenced-slug}.md). Do not include its property table. The referenced component's.mdis its own source of truth. - Decorative → No entry. Shows up naturally in Structure dimensions and Color tokens.
What to do with ambiguity
If Q1 and Q2 produce contradictory answers, default to referenced and log the reasoning in _base.json._childComposition.ambiguousChildren[] (Step 4.5 persists this back to disk). Over-referencing is safer than over-spec'ing — a referenced child's spec can be promoted to constitutive later; a child whose property table has been mistakenly copied into the parent is much harder to disentangle.
Step 4: Stage _base.json into the cache
Copy the file at
baseJsonPathto{cachePath}/{componentSlug}-_base.json. If the source is already inside{cachePath}(user already moved it), skip the copy.If the caller passed an
optionalContextand_meta.optionalContextis null, update the copied file's_meta.optionalContextin place. This is the only mutation the orchestrator may do to_base.jsonat this step; the Step 4.5_childCompositionrewrite is the other.Emit a one-line summary:
base: variants=<V>, bytes=<B>, warnings=<W> → {cachePath}/{componentSlug}-_base.jsonVandBcome fromvariants.lengthand the serialized byte count;Wfrom_extractionNotes.warnings.length.Flush phase context. Keep only:
_base.jsonpath + the one-line summary.Verify the file has
_meta,component,variantAxes,propertyDefinitions,variants[],ownershipHints[],_childComposition.crossVariantis allowed to benull(single-variant-axis component). If any required top-level key is missing, abort with a diagnostic asking the user to re-run the uSpec Extract plugin.If
_extractionNotes.warnings.length > 0, surface the warnings to the user before continuing. Common codes includeHIERWALK_MISSING_CHILDRENand walk-validation warnings like "Walked tree is missing children for constitutive instance(s)" — both mean the.mdmay be incomplete and the user should re-extract with a corrected selection.Render-meta freshness check. Spot-check
_base.json.variants[<defaultVariantName>].layoutTree: if it is an object whose root entry has noidfield (orid === undefined), the file was produced by a pre-render-meta plugin build. Render-meta will still be emitted at Step 9, but everysectionTargets[*].nodeIdandgroupTargets[*][*].nodeIdwill benulland the Step 9.5 integrity gate will fail those rules. Surface a warning right here so the user has time to re-extract before the run completes: "render-meta:_base.jsonwas produced by a pre-render-meta plugin build (noidonlayoutTreenodes); re-extract with the current uSpec Extract plugin to populatesectionTargets[*].nodeIdandgroupTargets[*][*].nodeId. The.mdwill still render, but the downstreamcreate-*skills consume this render-meta to resolve sections/groups/markers — without the node ids they must name-walk the live layer tree (a degraded, ambiguous path), so re-extract to get reliable ids." Note: thecreate-*skills no longer have a full-extraction fallback — they require the.mdand fail fast (plus a bounded per-skill whitelist of minimal reads), so missing render-meta ids degrade their identity resolution rather than triggering a re-extraction.
Step 4.5: Post-extract review (composition classification)
Read _base.json._childComposition. The plugin UI asks the designer to resolve every top-level INSTANCE child at extraction time, so every entry in children[] with nodeType === "INSTANCE" carries classificationEvidence: ["user-selected"] and ambiguousChildren[] is empty by construction. Non-INSTANCE entries (layout-wrapper FRAMEs with evidence ["layout-wrapper"], raw vectors/text with evidence ["not-instance"]) are auto-classified as decorative by the plugin and are not surfaced for designer confirmation.
Treat the designer's choice as authoritative and skip the override pass entirely. Briefly cross-check by running Q1 from Step 3.5 on each INSTANCE entry — if any obviously wrong assignment jumps out (e.g., a generic Icon instance marked constitutive), raise it as a warning to the user in the Step 10 summary but leave the _childComposition untouched. The designer's choice wins; the orchestrator's job is to surface, not override.
If _childComposition.children[] contains any entry where nodeType === "INSTANCE" and classificationEvidence does not include "user-selected", or if ambiguousChildren[] is non-empty, the file is malformed (the plugin UI did not reach the designer's confirmation step). Flag it as a warning and apply the Step 3.5 reasoning model to resolve each affected INSTANCE entry before continuing. Non-INSTANCE entries (wrappers / vectors / text) keep their plugin-assigned decorative classification and are skipped by the override pass:
- For each child in
children[], readclassificationand run Q1/Q2 from Step 3.5. If the classification is wrong, override it and append"agent-override"toclassificationEvidence[]. - Walk
ambiguousChildren[]. For each entry, make a decision via Q1/Q2 and the P1–P8 patterns, then move the entry intochildren[]with the resolvedclassification. On uncertainty, default toreferencedand note the reasoning inclassificationReason. - Persist the revised
_childCompositionback to{cachePath}/{componentSlug}-_base.json. This is the one explicit exception to the "_base.jsonis immutable" invariant.
Flush the review's working context. Keep only: the final classification counts (constitutive=N, referenced=M, decorative=K, ambiguous=0) as a one-line summary. Downstream interpretation skills will read the updated _childComposition from disk.
If constitutive > 0 and every constitutive child has a non-null subCompSetId, this is a composition-heavy parent — Step 10.5's manifest will emit follow-up runs. If referenced > 0, the rendered .md will include a ### Referenced components subsection (see Change 4 / {{ref:component-md/agent-component-md-instruction.md}}).
Step 5: Run extract-api (inline, in the parent)
Follow .cursor/skills/extract-api/SKILL.md inline (not as a subagent — the parent needs the resulting dictionary directly). Pass cachePath, componentSlug, optionalContext, deltaAvailable (true only when the user provided a figmaLink), and — when deltaAvailable is true — fileKey, nodeId, and mcpProvider (from uspecs.config.json). When the input is plugin-only (no figmaLink), explicitly instruct the skill that delta calls are not available — any gap surfaces as _deltaExtractions[] with unavailable: "no-figma-link".
The skill:
- Reads
_base.jsonfrom disk (no fresh Figma walk). - Optionally issues a tiny Step 3-delta call when
deltaAvailableis true and a fact is genuinely missing. - Writes
{cachePath}/{componentSlug}-api.json. - Writes
{cachePath}/{componentSlug}-api-dictionary.json(Step 7.5 ofextract-api) — the canonical vocabulary projected from api.json. - Returns a single-line summary ending with
(+ dictionary at <path>).
Flush phase context. Keep only: the two paths + one-line summary. Verify both files exist and have _meta + data. Abort with a diagnostic if the dictionary file is missing — the parallel fan-out requires it.
Set apiDictionaryPath = {cachePath}/{componentSlug}-api-dictionary.json for use in Step 6.
Step 6: Parallel fan-out (extract-structure + extract-color + extract-voice)
With the API dictionary on disk, dispatch three subagents in a single batch using the Task tool, subagent_type="generalPurpose". Each subagent runs exactly one extract skill, holds its own _base.json + dictionary context, and returns a single-line summary to the parent. The parent keeps only those three strings, never three full contexts.
Mandatory invocation shape (all three tool calls go in one assistant message — do NOT dispatch sequentially):
For each specialist in [extract-structure, extract-color, extract-voice]:
description— 3–5 words (e.g.,"Structure pass for Button").subagent_type—"generalPurpose".run_in_background—false. The parent must wait for all three returns before proceeding.prompt— a fully self-contained instruction with these seven fields (no prior chat context is available to the subagent):- The absolute path to the extract skill's SKILL.md file (e.g.,
.cursor/skills/extract-structure/SKILL.md). Instruct the subagent to read it and follow every step. componentSlug(string).cachePath(string).apiDictionaryPath— the path produced by Step 5.optionalContext— the orchestrator'soptionalContextstring (or"none"when absent).mcpProvider— the value fromuspecs.config.json.deltaAvailable— boolean.falsewhen the user provided only abaseJsonPath.
- Instruct the subagent to return a single-line summary matching the skill's declared return format, followed by the written cache path. Nothing else — not the full payload, not checklists, not screenshots.
- The absolute path to the extract skill's SKILL.md file (e.g.,
Expected return shapes (the parent parses these):
- Structure:
Structure extracted: N sections, M sub-components, K slot contents → <path> - Color:
Color extracted: strategy=<A|B>, N sections, M unique tokens, modes=[...] → <path> - Voice:
Voice extracted: N focus stops, M states, platforms=[VoiceOver, TalkBack, ARIA] → <path>
Join before proceeding. Wait for all three subagent returns. Do NOT advance to Step 8.5 until every return has landed. If any subagent fails (returns an error or no path), abort with a diagnostic naming the failing specialist — partial completion is never silently tolerated.
Container hint collection. After all three subagents return, open {cachePath}/{componentSlug}-color.json with a targeted read and check data._containerRerunHint. If non-null, surface it to the user using the neutral, authoritative framing below. Do not describe the parent's color spec as informational, provisional, placeholder, or pending — the parent's tokens are flattened from colorWalk[] and the parent .md is shippable as-is.
Constitutive sub-components detected (
<subCompSetNames joined>). The parent .md fully documents the parent's own color tokens (authoritative, measured fromcolorWalk[]). The recursion manifest in Step 10.5 lists each constitutive child as an optional follow-up if you also want a per-child canonical color spec; running them is not required for the parent .md to be complete.
Collect the hint for inclusion in the Step 10 Follow-ups (not Known gaps). Continue past this — do not abort.
Flush phase context. Keep only: the three cache-file paths + three one-line summaries. The subagents already discarded their internal context when they returned; the parent was never exposed to it.
Step 8.5: Reconciliation (typed disagreement handling)
After the three specialist caches land and before the Step 9.5 integrity gate, run a deterministic reconciliation pass. This step is typed dispatch, not reasoning — it recognizes exactly 4 classes of disagreement (3 actionable + 1 benign) between the dictionary and the specialist outputs, and dispatches the right response for each.
Load the dictionary + the three mismatch lists.
- Read
{cachePath}/{componentSlug}-api-dictionary.json(ApiDictionary). - Read the
data._extractionArtifacts.dictionaryMismatches[]arrays from the three specialist caches (structure.json,color.json,voice.json). Missing or empty arrays are fine — they indicate the specialist agreed with the dictionary. - Read
reconciliation.autoRetryfromuspecs.config.json(default:truewhen the key is absent). Whenfalse, skip retries and surface coverage gaps directly ashighKnown-gaps entries. Still run auto-rewrites for vocabulary drift.
Classify every mismatch into one of four buckets:
| Class | Detection rule | Response |
|---|---|---|
| Vocabulary drift | Dictionary has {axis.name, value.figmaValue, value.runtimeCondition}; specialist emitted a column / label that matches figmaValue but the dictionary canonical is runtimeCondition (or vice versa — a simple rename). Or the specialist's sub-component name matches a dictionary subComponents[].name modulo casing / whitespace / suffix. |
Auto-rewrite in place. Open the specialist's cache file, rewrite the drifted labels/columns/section names to the dictionary canonical, add a reconciliation log entry. No retry. |
| Coverage gap (scope miss) | Dictionary lists a value/sub-component/state that has no corresponding row/column/section in the specialist's cache AND the value exists somewhere in _base.json as real evidence (variant option, revealed sub-component, etc.). |
Re-dispatch the specialist as a single subagent with optionalContext = "create-component-md retry: <comma-list of missing items>" prepended to the original optionalContext. Max 1 retry per specialist per invocation. After the retry returns, re-run this entire Step 8.5 from the top — a successful retry may resolve gaps that would otherwise trigger further retries. |
| Benign value-extra | A kind: "value-extra" mismatch (specialist documented something the dictionary's axes[] does not list) where the observed item resolves to a real part of the API surface: it matches a dictionary.booleanProps[] entry by camelCase name (a top-level boolean like isDisabled/isLoading), or a dictionary.states[] entry by figmaValue / apiAssignments key, or a _base.json variant axis option. The specialist was correct to document it; the only "gap" is that booleans/decomposed states are intentionally absent from axes[]. |
Log to reviewedBenign[] and move on. No rewrite, no retry, NO Known-gaps entry. This is the disposition for the systemic false-positive that the specialist-side value-extra guard (extract-* Step 2.5) now prevents at the source; this bucket catches any that still arrive from older or standalone caches. |
| Semantic conflict | Dictionary and specialist disagree on a fact that cannot be auto-rewritten AND the observed item resolves to nothing in the dictionary or _base.json (e.g., API declares size: small | medium | large; Structure measured compact + regular which have no figmaValue in the dictionary and no backing axis option). |
Surface as high Known gap immediately. NEVER auto-resolve. NEVER retry (a retry will not fix a semantic disagreement — it needs human judgment). |
A value-extra that does NOT resolve to booleanProps[] / states[] / a variant axis is a semantic conflict, not benign — it goes to high. Benign classification requires the observed item to trace to a real API-surface element; never use reviewedBenign[] as a dumping ground for unexplained extras.
Retry loop (bounded, serial, no declared specialist priority):
Maintain a retry counter per specialist (structureRetries, colorRetries, voiceRetries), each starting at 0 and capped at 1.
while (any specialist has unresolved coverage gaps AND its retryCount < 1 AND reconciliation.autoRetry is true):
pick the next specialist with unresolved coverage gaps, in the original run order
(Structure → Color → Voice); this is iteration order, NOT a priority relationship
re-dispatch ONE specialist as a generalPurpose subagent (serial — never parallel)
prompt includes the same invocation contract as Step 6, plus
optionalContext = "create-component-md retry: <missing items>\n\n<original optionalContext>"
wait for its return
increment that specialist's retryCount
re-read the specialist's cache (it overwrote its own file)
re-run Step 8.5 classification on all three specialists
— a successful retry may resolve gaps in other specialists too,
shortening the retry chain
loop back
After the loop terminates (no specialist has unresolved coverage gaps OR every specialist with one has already retried once):
- Any remaining coverage gap becomes a
highKnown-gaps entry (the auto-retry couldn't recover it). - Every semantic conflict is already a
highKnown-gaps entry. - Every vocabulary drift has already been auto-rewritten in place; it becomes a
low-severity informational entry in the auto-reconciled list (see renderer). - Every benign value-extra is already logged to
reviewedBenign[]; it produces NO Known-gaps entry and NO confidence downgrade — it is an audit-trail note only.
Write the reconciliation artifact. Every pass (auto-rewrite, retry, unresolved) is logged to {cachePath}/{componentSlug}-reconciliations.json. Envelope:
{
"_meta": {
"schemaVersion": "1",
"generatedAt": "<ISO 8601 timestamp>",
"componentSlug": "<slug>",
"autoRetryEnabled": <value read from uspecs.config.json reconciliation.autoRetry; default true>
},
"data": {
"autoReconciled": [
{
"class": "vocabulary-drift",
"specialist": "structure" | "color" | "voice",
"before": "<drifted label>",
"after": "<dictionary canonical>",
"location": "<cache path + JSONPath>",
"source": "<dictionary axis / sub-component / state reference>"
}
],
"retries": [
{
"specialist": "structure" | "color" | "voice",
"missingItems": ["..."],
"attemptedAt": "<ISO 8601>",
"outcome": "resolved" | "still-missing",
"retryCount": 1
}
],
"unresolved": [
{
"class": "coverage-gap" | "semantic-conflict",
"specialist": "structure" | "color" | "voice",
"detail": "<scannable summary ≤160 chars>",
"severity": "high"
}
],
"reviewedBenign": [
{
"class": "value-extra",
"specialist": "structure" | "color" | "voice",
"observed": "<the value-extra the specialist emitted>",
"resolvesTo": "booleanProps:isDisabled" | "states:loading" | "variantAxis:<name>=<option>",
"note": "<why it is part of the API surface and not a gap; ≤160 chars>"
}
]
}
}
reviewedBenign[] is the formal home for the Benign value-extra class — it is an audit trail only, never a defect list. The renderer does NOT surface it in Known gaps. When there is nothing benign to log, emit reviewedBenign: []. Do not invent ad-hoc keys (e.g. a leading-underscore _reviewedBenign) — reviewedBenign[] is the schema-defined bucket and Step 9.5 validates its shape.
When data.retries[].outcome === "resolved", the corresponding mismatch disappears from the specialist's cache on the next classification pass — that is how the re-run loop confirms success. When "still-missing", the item is promoted into unresolved[] with severity high.
Abort conditions:
- A subagent returns a failure line → abort the whole orchestrator with a diagnostic naming the failing specialist and retry number. Do NOT continue past Step 8.5 with a half-broken cache set.
- A specialist's retry overwrites its cache file with a malformed envelope (missing
_meta/data) → abort with the same diagnostic.
Flush the reconciliation working context. Keep only: the path {cachePath}/{componentSlug}-reconciliations.json + counts (auto-rewrites=<A>, retries=<R>, unresolved=<U>, benign=<B>).
Render-meta handoff note. Step 9's render-meta builder consumes data.autoReconciled[] to do drift-aware name lookups when resolving sectionTargets[*].nodeId / groupTargets[*][*].nodeId against _base.json.variants[<default>].layoutTree. When a section/group label was rewritten here (e.g., "Clear button" → "clear (X) button"), render-meta retries the layer lookup with both entry.before and entry.after so a successful auto-rewrite never silently breaks ID resolution. This is a downstream-consumer relationship only — render-meta does NOT itself trigger further reconciliation, and Step 8.5 owns every rewrite that lives in the cache files.
Step 9: Render the Markdown
Now the parent's context should contain:
- 4 domain cache-file paths (api, structure, color, voice).
_base.jsonremains on disk at{cachePath}/{componentSlug}-_base.jsonfor targeted reads only during rendering — see the expanded narrow-read list below. Everything else renderable was already lifted into the four domain JSONs. fileKey,nodeId,componentSlug,outputPath,cachePath,optionalContext.- Any container-rerun hints or delta-extraction records collected during Steps 5–8.
- Not much else.
Read:
- All four domain JSON cache files in full.
_base.json— only the narrow fields the renderer consumes (do not load wholesale):_meta(full) — for render-metaextractedAt,fileKey,nodeId,sourceHashrecomputation_childComposition— for Composition subsection + Referenced components + Known gaps + render-metasubComponents[]_extractionNotes.warnings— for Known gapscomponent(full) — for cross-check + render-metacomponentvariantAxes+defaultVariant— for variant-axes summary + render-metavariantAxes/variantAxesDefaultspropertyDefinitions(full) — for render-metapropertyDefs+booleanDefs+slotContentsslotHostGeometry— for render-metaslotContents.preferredComponentsenrichmentvariants[<defaultVariantName>].layoutTree(full walk) — for render-metasectionTargets[*].nodeId+groupTargets[*][*].nodeIdresolutionvariants[<defaultVariantName>].treeHierarchical(full walk) — for the{{ANATOMY_SCAFFOLD}}layer-composition tree (mechanical pass-through; seeagent-component-md-instruction.md> ## Anatomy scaffold)variants[*]._selfCheck.missingChildren— for Known gapssubComponentVariantWalks(when present) — for render-metasubComponents[*].subCompVariantAxesDefaults- This widening from the prior
_childComposition+_extractionNotes.warnings+component.componentName+_selfCheckset is intentional: render-meta needs the canonical pre-interpretation shape and the four domain caches do not preserve it. Read these fields once, then re-flush.
{cachePath}/{componentSlug}-reconciliations.json— for render-meta name-lookup retries (readdata.autoReconciled[]forbefore/afterpairs). When the file is absent, treat as empty (no rewrites).{{ref:component-md/component-md-template.md}}.{{ref:component-md/agent-component-md-instruction.md}}.
Follow agent-component-md-instruction.md section by section to produce:
{{COMPONENT_NAME}},{{FIGMA_URL}},{{GENERATED_AT}},{{OPTIONAL_CONTEXT}},{{NODE_ID}},{{FILE_KEY}},{{CACHE_PATH}}(mechanical).{{OVERVIEW_PARAGRAPH}}(synthesis — 2–4 sentences drawn from all fourdataobjects).{{VARIANT_AXES_SUMMARY}}(one-liner from the structure axes).{{COMPOSITION_SUBSECTION}}(rendered from_base.jsontop-level_childComposition).{{ANATOMY_SCAFFOLD}}(mechanical pass-through ofvariants[<default>].treeHierarchicalinto a### Anatomytree at the top of the Structure section — seeagent-component-md-instruction.md> ## Anatomy scaffold).{{API_BODY}},{{STRUCTURE_BODY}},{{COLOR_BODY}},{{VOICE_BODY}}(per-section renderers). API body's Referenced components subsection consumes_base.json._childComposition.children[]filtered toclassification === "referenced". The Voice body ends with the hidden<!-- voice-render-meta v=1 ... -->focus-stop layer-name carry (seeagent-component-md-instruction.md> Voice body rendering step 5) socreate-voicecan resolve focus markers from the.mdalone.{{CROSS_SECTION_INVARIANTS}}and{{CROSS_REFERENCES}}(computed from the_extractionArtifactsblocks).{{RENDER_META_JSON}}— built peragent-component-md-instruction.md > ## RENDER_META_JSON. Source:_base.json(the narrow fields above) + structure cachedata.sections[]— modern caches stampsection._anchorand group-headerrow._layerName/row._layerIddirectly, which the resolver consumes as mechanical pass-through (preferred path). Reconciliations cache is consulted only by the legacy name-walk fallback for caches produced before identity stamping. Render-meta is mechanical pass-through — never read it fromapi.jsoneven where the fields overlap.
Write the result to outputPath using UTF-8.
Do not skip the audit checklist at the end of `agent-component-m
…(truncated)