Skip to content

Night Watch Figma Librarian

Cindy Zhang edited this page Jun 23, 2026 · 1 revision

Night Watch — Figma Librarian

Assigned to: Ernest's Navi (ernestt)

Goal: Keep the Astryx OSS Figma Library in sync with the code in packages/core/src/.

Schedule: Once per day at 12pm PST, Mon–Fri. Requires Ernest's Mac with Figma open and Claude Code connected via MCP. If the laptop is off (PTO, etc.), the run skips and catches up on the next available day — drift accumulates harmlessly.

GChat notification space: https://chat.google.com/room/AAQAAo12MnA?cls=7


Why This Role Exists

The Astryx Figma library and the Astryx code library are two representations of the same design system. When a component gets a new prop, variant, or size in code, the Figma library should reflect that. Today this sync is manual and drifts silently — a designer builds with outdated Figma components, ships a handoff, and the engineer discovers the mismatch during implementation.

The Figma Librarian closes this loop automatically.


Scope

What it does

  • Read new unresolved comments on the Figma library file since last run (via REST API)
  • Detect code changes to component prop interfaces since last run
  • Classify each change (new prop, removed prop, renamed prop, new variant value, new component)
  • Read the current Figma component state via use_figma MCP tool
  • For existing components with prop changes: modify the Figma component in-place (add/remove/rename variant axes, boolean toggles, text properties, instance swap slots)
  • For brand new components: run the full CC Figma Builder 4-phase workflow (Research → Check Dependencies → Plan Variants → Build → Verify)
  • Respect human overrides — if a designer manually adjusted a Figma component, preserve their changes unless there's a direct conflict with code
  • Log every action taken with before/after state
  • Send a summary to GChat after each run

What it does NOT do

  • Visual design judgment (that's the Designer role)
  • Code review or PR review (that's Reviewer)
  • CI fixes (that's QA)
  • Issue triage (that's PM)
  • Component code auditing (that's Component Auditor)
  • Publish the Figma library — humans publish

Architecture

┌──────────────────────────────────────────────────────────────┐
│  STEP 0: READ FIGMA COMMENTS                                 │
│                                                               │
│  GET /v1/files/c9CR0F4jJQwhue60Ec7qOz/comments?as_md=true   │
│  (Figma REST API — works from sandbox, no MCP needed)         │
│                                                               │
│  → Filter to unresolved comments created since last run       │
│  → Classify each comment:                                     │
│                                                               │
│    QUESTION ("Does Button have xl?")                          │
│      → Answer from code via Figma reply                       │
│                                                               │
│    REQUEST with no code match ("Add compact density")         │
│      → Do NOT action. Surface in GChat for human review       │
│      → Code is king — Figma reflects code, not vice versa     │
│                                                               │
│    PARITY ("Code has ghost variant, Figma doesn't")           │
│      → Verify against code. If confirmed, queue as a          │
│        sync action in STEP 4. Reply in Figma after fixing.    │
│                                                               │
│  → Track seen comment IDs in state to avoid duplicates        │
│  → Never resolve or delete comments — humans do that          │
└───────────────────────────┬──────────────────────────────────┘
                            │
                            ▼
┌──────────────────────────────────────────────────────────────┐
│  STEP 1: DETECT CODE CHANGES                                 │
│                                                               │
│  git log --since="{lastRunAt}" --name-only -- packages/core/  │
│  → list files changed under packages/core/src/{Component}/    │
│  → for each changed component, read Astryx{Component}.tsx        │
│  → extract the props interface:                               │
│    • variant union types (e.g. ButtonVariant)              │
│    • size union types (e.g. ButtonSize)                    │
│    • boolean props (isDisabled, isLoading, isIconOnly)        │
│    • string/text props (label, tooltip, placeholder)          │
│    • ReactNode/slot props (icon, children, endContent)        │
│    • event handlers (skip — no Figma representation)          │
│    • HTML passthrough props (skip — infrastructure)           │
└───────────────────────────┬──────────────────────────────────┘
                            │
                            ▼
┌──────────────────────────────────────────────────────────────┐
│  STEP 2: CLASSIFY CHANGES                                     │
│                                                               │
│  Compare extracted props against last-known state:            │
│                                                               │
│  NEW_COMPONENT   — component dir exists in code, not in Figma │
│  PROP_ADDED      — new variant value, boolean, text, or slot  │
│  PROP_REMOVED    — variant value or prop deleted from code    │
│  PROP_RENAMED    — prop name changed (heuristic: add+remove   │
│                    in same commit with similar type)           │
│  VALUES_CHANGED  — union type expanded/contracted             │
│  NO_FIGMA_IMPACT — only event handlers, internal refactor,    │
│                    style-only changes, test changes            │
│                                                               │
│  Skip components that are infrastructure-only:                │
│  Layer, SizeContext, Field (internal), Center, Stack, Layout, │
│  Grid, AspectRatio — these have no meaningful Figma component │
└───────────────────────────┬──────────────────────────────────┘
                            │
                            ▼
┌──────────────────────────────────────────────────────────────┐
│  STEP 3: READ FIGMA STATE (via use_figma MCP)                │
│                                                               │
│  For each component that needs attention:                     │
│                                                               │
│  a) Find the component set:                                   │
│     figma.root.findOne(n =>                                   │
│       n.type === "COMPONENT_SET" && n.name === "Astryx{Name}")   │
│                                                               │
│  b) Read current properties:                                  │
│     cs.componentPropertyDefinitions                           │
│     cs.children.map(c => c.name) // variant names             │
│                                                               │
│  c) Read last-known agent state from state file               │
│                                                               │
│  d) Compute three-way diff:                                   │
│     CODE state (current)                                      │
│     FIGMA state (current, from MCP)                           │
│     AGENT state (last time agent touched it)                  │
│                                                               │
│  This three-way diff enables human override detection:        │
│  If FIGMA ≠ AGENT but CODE hasn't changed → human edited it  │
│  If FIGMA ≠ CODE and FIGMA = AGENT → code changed, update    │
│  If FIGMA ≠ CODE and FIGMA ≠ AGENT → conflict, see rules     │
└───────────────────────────┬──────────────────────────────────┘
                            │
                            ▼
┌──────────────────────────────────────────────────────────────┐
│  STEP 4: APPLY CHANGES                                        │
│                                                               │
│  For EXISTING components (update in-place):                   │
│                                                               │
│  PROP_ADDED / VALUES_CHANGED:                                 │
│    → use_figma: add new variant to component set              │
│    → use_figma: add new boolean/text/swap property            │
│    → use_figma: get_screenshot → verify                       │
│                                                               │
│  PROP_REMOVED:                                                │
│    → use_figma: remove variant from component set             │
│    → use_figma: remove property definition                    │
│    → use_figma: get_screenshot → verify                       │
│                                                               │
│  PROP_RENAMED:                                                │
│    → use_figma: rename property/variant axis                  │
│    → use_figma: get_screenshot → verify                       │
│                                                               │
│  For NEW components (full build):                             │
│    → Run CC Figma Builder 4-phase workflow:                   │
│      Phase 1: Research code source (read component file)      │
│      Phase 1.5: Check sub-component dependencies exist        │
│      Phase 2: Plan variant matrix                             │
│      Phase 3: Build in Figma via use_figma (incremental)      │
│      Phase 4: Screenshot verification (MANDATORY)             │
│                                                               │
│  IMPORTANT: Work incrementally. One use_figma call per        │
│  logical step. Validate after each step. Never try to build   │
│  an entire component in one script.                           │
└───────────────────────────┬──────────────────────────────────┘
                            │
                            ▼
┌──────────────────────────────────────────────────────────────┐
│  STEP 5: RECORD & REPORT                                      │
│                                                               │
│  a) Update state file with new AGENT state for each component │
│  b) Log all actions to memory/xds-night-watch/{date}.md       │
│  c) Send GChat summary to notification space                  │
│  d) If any conflicts were detected, include them in summary   │
│     with recommendation for human review                      │
└──────────────────────────────────────────────────────────────┘

Human Override Policy

The Figma library is a shared artifact. Designers may manually adjust Figma components for visual reasons that don't map cleanly to code props. The agent MUST respect this.

Three-Way Diff Model

The agent maintains three states per component:

State Source Purpose
CODE Parsed from Astryx{Component}.tsx props interface What the code says the component should be
FIGMA Read from Figma via use_figma MCP at runtime What the Figma component actually looks like right now
AGENT Last state the agent wrote to Figma (from state file) What the agent last set it to

Decision Matrix

CODE changed? FIGMA = AGENT? Action
Yes Yes Auto-update. Code changed, Figma matches what agent last set. Safe to update.
Yes No Conflict. Code changed AND someone manually edited Figma since agent's last run. See conflict resolution below.
No Yes No action. Nothing changed.
No No Human override. Code didn't change but Figma was manually edited. Preserve the human's changes — update AGENT state to match current FIGMA (so we don't flag this next run).

Conflict Resolution (CODE changed + Human edited Figma)

When both code and a human have changed the same component:

  1. Additive code changes (new variant value, new prop): Apply the code change alongside the human's edits. Adding a new size="xl" variant doesn't conflict with a designer adjusting padding on the size="md" variant.

  2. Direct conflicts (code removed a prop that the human modified, or code renamed something the human customized): Apply the code change — the code is the source of truth for what props exist. But log the conflict prominently in the run report and flag it in GChat so a human can review.

  3. Ambiguous conflicts (human changed a property value that code also changed — e.g., both changed default variant spacing): Do not auto-apply. Log the conflict and skip. Let a human decide.

What Counts as a "Human Edit"

Any change to a Figma component property or variant that wasn't made by the agent. Detected by comparing current FIGMA state against stored AGENT state. If they differ and the agent didn't run since the change, a human (or another tool) made the edit.


Prop-to-Figma Mapping

How TypeScript props map to Figma component concepts:

Code Pattern Figma Representation Example
Union type prop (variant?: 'primary' | 'secondary') Variant axis variant = primary, variant = secondary
Size union (size?: 'sm' | 'md' | 'lg') Variant axis size = sm, size = md, size = lg
Boolean prop (isDisabled?: boolean) Boolean component property Toggle in properties panel
String prop (label: string) Text component property Editable text in properties panel
Single fixed-type ReactNode prop (icon?: ReactNode) Instance swap property (INSTANCE_SWAP) User picks from compatible components
Freeform ReactNode prop (children?: ReactNode) Figma Slot (SLOT property + SlotNode) Freeform content area — users add/remove/rearrange layers without detaching
Repeating ReactNode prop (items?: ReactNode[]) Figma Slot (SLOT property + SlotNode) Slot with repeating instances inside
Mixed content area (endContent?: ReactNode) Figma Slot or Instance swap Slot if content varies freely; instance swap if it's always a single component type

Props that do NOT map to Figma

Code Pattern Why no Figma mapping
Event handlers (onClick, onChange) Behavior, not visual
Ref (ref?: Ref<HTMLElement>) Infrastructure
HTML attributes (className, style, id) Infrastructure
xstyle (StyleX override) Theme-level, not component-level
as (polymorphic) Rendering detail
href, target, rel Link behavior, not visual

When to Use Slots vs Instance Swap

Figma Slots (SLOT property type + SlotNode) are a specific Figma feature (currently in open beta) — not a generic named layer. A slot is a frame within a component that allows freeform content editing in instances without detaching. This maps directly to how children and flexible ReactNode props work in React.

Decision matrix:

Code Pattern Use Slot Use Instance Swap Rationale
children?: ReactNode Content is freeform — could be anything
items: ReactNode[] (repeating) Instances can add/remove items in the slot
icon?: ReactNode Content is always a single component of a known type
endContent?: ReactNode Could be a badge, icon, text, or anything
startContent?: ReactNode Same as endContent — freeform
trigger?: ReactNode (e.g. Popover) Trigger could be button, link, or custom element
header?: ReactNode Freeform layout area

Rule of thumb: If the code prop accepts arbitrary JSX children that could be multiple elements, use a Slot. If it expects a single component of a constrained type (like an icon), use Instance Swap.


Figma Slots — API Reference

Slots are a first-class component property type in Figma's Plugin API. The agent MUST use the native Slot API — never approximate slots with plain named frames.

Creating a Slot via createSlot() (Recommended)

The simplest way. Call createSlot() on a ComponentNode to create a SlotNode child and automatically register a SLOT property on the component.

// Inside a use_figma call on a ComponentNode
const comp = figma.createComponent();
comp.name = "size=md, variant=primary";
comp.layoutMode = "VERTICAL";
comp.primaryAxisSizingMode = "AUTO";
comp.counterAxisSizingMode = "AUTO";

// Create a slot — this creates both the SlotNode AND the SLOT property
const slot = comp.createSlot();
slot.name = "Content";  // visible in the Layers panel
slot.layoutMode = "VERTICAL";
slot.primaryAxisSizingMode = "AUTO";
slot.counterAxisSizingMode = "FILL";
slot.itemSpacing = 8;

// Optionally add default content inside the slot
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
const placeholder = figma.createText();
placeholder.characters = "Add content here";
slot.appendChild(placeholder);

return { compId: comp.id, slotId: slot.id };

Creating a Slot via addComponentProperty() (For more control)

Use this when you need to name the SLOT property explicitly or set preferred instances upfront.

const slotKey = comp.addComponentProperty('Content', 'SLOT', '');

Note: addComponentProperty with type 'SLOT' does NOT accept a defaultValue — pass an empty string. The slot's default content is whatever layers are inside the SlotNode in the main component.

Editing Slot Properties

// Set preferred instances (guide designers toward recommended components)
comp.editComponentProperty('Content#1:0', {
  preferredValues: [
    { type: 'COMPONENT', key: 'abc123' },
    { type: 'COMPONENT_SET', key: 'def456' }
  ],
  description: 'Add action buttons, cards, or other content'
});

editComponentProperty for SLOT supports:

  • name — rename the slot property
  • preferredValues — set suggested components (array of InstanceSwapPreferredValue)
  • description — help text shown to designers

Does NOT support for SLOT:

  • defaultValue — default content is the layers inside the SlotNode, not a property value

SlotNode Properties

A SlotNode behaves like a FrameNode — it supports:

  • Auto-layout (layoutMode, itemSpacing, padding, alignment)
  • Fills, strokes, effects, corner radius
  • Min/max width/height constraints
  • Variable bindings
  • Children (any node type)
  • resetSlot() — resets slot content back to main component default

It does NOT support:

  • Layout grids (cannotApplySlotPropertyToFrameWithGrid error)
  • Being the top-level layer of a component (must be nested)
  • Component properties on layers directly inside the slot of a main component (but component instances placed INTO a slot in an instance DO retain their own properties)

Detecting Existing Slots

// Find all slots in a component
const slots = comp.findAll(n => n.type === 'SLOT');

// Check component property definitions for SLOT type
const propDefs = comp.componentPropertyDefinitions;
const slotProps = Object.entries(propDefs)
  .filter(([key, def]) => def.type === 'SLOT');

Deleting a Slot

// WARNING: Destructive — resets all instance slot overrides
comp.deleteComponentProperty('Content#1:0');

Anti-Patterns

  • Using a plain Frame named "Slot" — this is NOT a slot. Users can't add content without detaching.
  • Using Instance Swap for freeform content — instance swap restricts to a single component. Slots allow any content.
  • Adding component properties to layers inside a slot in the main component — they'll be stripped. Use variable bindings instead, or let inserted instances carry their own properties.
  • Putting a slot at the top level of a component — slots must be nested within a component, not the root layer.

Extraction Strategy

For each component:

  1. Find the exported props interface (export interface Astryx{Name}Props)
  2. Parse each field's type:
    • Literal union → variant axis with those values
    • boolean → boolean property
    • string → text property
    • ReactNode (single, constrained type like icon) → instance swap (INSTANCE_SWAP)
    • ReactNode (freeform content like children, endContent) → Figma Slot (SLOT)
    • Everything else → skip
  3. Also find the variant/size type aliases (e.g., ButtonVariant = keyof ButtonVariantMap) and resolve to concrete values
  4. Note default values from the function signature destructuring

State Schema

{
  "lastRunAt": "2026-04-22T10:00:00-07:00",
  "lastCodeSHA": "abc123def",
  "components": {
    "Button": {
      "figmaNodeId": "201468:12345",
      "figmaPageId": "201468:404593",
      "lastSyncedAt": "2026-04-22T10:00:00-07:00",
      "lastSyncedCodeSHA": "abc123def",
      "status": "synced",
      "agentState": {
        "variantAxes": {
          "variant": ["primary", "secondary", "ghost", "destructive"],
          "size": ["sm", "md", "lg"]
        },
        "booleanProps": ["isDisabled", "isLoading", "isIconOnly"],
        "textProps": ["label", "tooltip"],
        "instanceSwapProps": ["icon"],
      "slotProps": ["children", "endContent"]
      },
      "humanOverrides": []
    },
    "Toast": {
      "figmaNodeId": null,
      "status": "missing",
      "lastSyncedAt": null,
      "lastSyncedCodeSHA": null
    }
  },
  "pendingConflicts": [],
  "seenCommentIds": ["123456", "789012"],
  "runLog": []
}

Figma Comments Check

Before any code diffing, the agent reads new comments on the Figma library file. This surfaces designer feedback that may be relevant to the sync work or needs human attention.

API

GET https://api.figma.com/v1/files/c9CR0F4jJQwhue60Ec7qOz/comments?as_md=true
Authorization: Bearer {FIGMA_API_TOKEN}

Returns an array of Comment objects:

{
  "id": "123456",
  "message": "The ghost variant hover state looks off",
  "created_at": "2026-04-29T14:30:00Z",
  "resolved_at": null,
  "user": { "handle": "Ruby Cheung", "id": "..." },
  "client_meta": { "node_id": "201468:12345", "node_offset": { "x": 100, "y": 200 } },
  "parent_id": null,
  "order_id": "1"
}

Filtering

  • Only process comments where resolved_at is null (unresolved)
  • Only process comments where created_at is after lastRunAt from state
  • Skip comments whose id is in seenCommentIds in state
  • After processing, add all seen IDs to seenCommentIds

Matching Comments to Components

Use client_meta.node_id to identify which component the comment is on. Match against the known Figma node IDs in the state file's components.{name}.figmaNodeId. If a comment is on a node that's a child of a component set, walk up to find the parent component.

What the Agent Does with Comments

Comments are classified into three categories with distinct handling:

1. Questions → Answer them

If a comment is asking a question about the component ("Should this use a slot?", "What's the default size?", "Is there a ghost variant?"), the agent replies with a factual answer derived from the code. The code is the source of truth — cite the specific prop interface, default value, or variant list from Astryx{Component}.tsx.

Example comment: "Does Button support an xl size?"
Agent reply: "Currently Button supports sm, md, and lg sizes 
(defined via ButtonSize in packages/core/src/Button/Button.tsx, 
line 135). There is no xl variant in code."

The agent MAY reply to questions via POST /v1/files/:key/comments with comment_id set to the parent comment (threaded reply). Keep replies factual, concise, and always cite the code source.

2. Requests not in the codebase → Do not action, surface only

If a comment requests something that doesn't exist in code ("Can we add a compact density?", "We need an xl size"), the agent does NOT take action. Code is king — Figma should reflect the code, not the other way around. Include the request in the GChat summary so a human can decide whether to implement it in code first.

GChat summary:
  📋 Feature request (no code match):
  • @ruby on Button: "Can we add compact density?" — no compact/density prop in code

3. Parity comments → Action them

If a comment points out that the Figma component doesn't match what's in the code ("The code has a ghost variant but Figma doesn't", "Button has endContent in code but not in Figma", "The sm size padding looks wrong compared to the token"), this is a parity issue. The agent should:

  • Verify the claim by reading the code
  • If confirmed, treat it as a sync action during STEP 4 (same as a detected code drift)
  • Prioritize it — a human noticed the gap, so it's higher signal than automated detection
  • Log the comment as the reason for the change in the run report
GChat summary:
  ✅ Parity fix (from comment):
  • @cindy on Button: "code has ghost variant, Figma missing" — confirmed, added ghost variant

Comment Classification Heuristic

Signal Classification Action
Contains ? or starts with "is", "does", "what", "how", "why", "can" Question Answer from code
Requests something not in the code ("add", "we need", "can we get", "would be nice") AND the feature doesn't exist in Astryx{Component}.tsx Request (no code match) Surface in GChat, do not action
Points out a gap between code and Figma ("code has X but Figma doesn't", "this doesn't match", "missing", "wrong", "out of date") AND the code confirms the discrepancy Parity Fix in Figma, cite comment
Ambiguous Surface in GChat Let human classify

Reply Policy

  • Questions: Reply via Figma comments API (threaded reply). Keep factual and cite code.
  • Requests: Do not reply in Figma. Surface in GChat only.
  • Parity: Reply in Figma after fixing ("Fixed — added the ghost variant to match code"). Then resolve is up to the human.
  • Never resolve or delete comments — that's for humans.

Figma Connection Guard

Before running any Figma operations, the agent must verify that:

  1. The Mac CLI node is connected
  2. Claude Code is available on the Mac
  3. The Figma MCP server is connected and responding

If any of these fail, the agent:

  • Sends a message to GChat: "Figma Librarian: Can't reach Figma MCP. Is Figma open with the MCP server connected? Skipping today's run."
  • Logs the skip in state
  • Does NOT attempt partial work

The nightly detection step (reading code changes) can still run on the sandbox without Figma.


Non-Visual Components (Skip List)

These components in packages/core/src/ have no meaningful Figma representation:

Component Reason
Layer z-index management utility
SizeContext React context provider
Field Internal form field wrapper
Center Layout utility
Stack Layout utility
Layout Page layout utility
Grid Grid layout utility
AspectRatio Ratio container utility

The agent maintains this list in state and skips these automatically. If a new component is added that looks like infrastructure (no visual props, no variant types, only children + utility props), classify it as NO_FIGMA_IMPACT and add to skip list.


CC Figma Builder Integration

For NEW_COMPONENT builds, the agent invokes the CC Figma Builder skill's 4-phase workflow. Key rules from that skill:

  1. Reuse existing sub-components — never recreate Button, Icon, etc. Use figma.root.findOne() or importComponentByKeyAsync()
  2. Icons from XMDS Meta Icons Library — never draw manually
  3. Always bind variables — never hardcode hex/spacing values. Use the known Variable IDs:
    • Colors: accent(1:3), bgSurface(1:7), textPrimary(1:13), border(1:34), etc.
    • Spacing: 0(1:652) through 10(1:664)
    • Size: SM/28(1:668), MD/32(1:669), LG/36(1:670)
    • Radius: None(1:674), Inner(1:675), Element(1:676), Container(1:677), Full(1:679)
  4. Use native Figma Slots for ReactNode/children props — call comp.createSlot() or addComponentProperty('Name', 'SLOT', ''). Never use a plain named frame as a fake slot. See the Figma Slots API Reference section above for the full API.
  5. Component sizing: use 'HUG' for layoutSizingHorizontal/layoutSizingVertical — set AFTER appendChild
  6. Screenshot verification is MANDATORY after every build

Component Frame Placement (MANDATORY)

Every new component MUST be built inside a Component Frame — never as a loose node on the page. This is the standard wrapper structure used by every existing component in the library.

Component Frame template: Node 266:35317 on the Components page. Clone it and rename.

Structure:

FRAME "Astryx{ComponentName}" (1600 × auto, VERTICAL layout)
  ├── INSTANCE "Document Header #quality_disable"  ← title/description header
  └── FRAME "Body" (VERTICAL layout)
       └── COMPONENT_SET "{ComponentName} / Astryx{ComponentName}"

How to create a new component's frame via Plugin API:

// 1. Clone the template frame
const page = figma.root.children.find(p => p.name === 'Components');
await figma.setCurrentPageAsync(page);
const template = await figma.getNodeByIdAsync('266:35317');
const compFrame = template.clone();
compFrame.name = 'Astryx{ComponentName}';

// 2. Position in the correct category row
compFrame.x = {column} * 1700;  // columns at x = 0, 1700, 3400, 5100, 6800
compFrame.y = {row_y};           // y from the category section

// 3. Build component set inside the Body frame
const body = compFrame.findOne(n => n.name === 'Body');
body.appendChild(componentSet);

Sub-components (e.g., ChatMessage, CommandPaletteItem) go inside the SAME Component Frame as their parent component — not in a separate frame. The Body frame holds both the main component set and any sub-component sets.

Placement Grid — Category Rows

Components are organized in a grid on the Components page (3:12315). Each category has a header frame, followed by rows of Component Frames at x = 0, 1700, 3400, 5100, 6800 (1600px wide + 100px gap).

New components MUST be placed in the correct category row, matching the ordering in the code's packages/core/src/index.ts export order:

Category Section Header ID Row y-positions Existing components
Call to Action (top of page) y≈254 Button, Link, SegmentedControl, ToggleButton
Communication 86:350115 y≈4008, y≈7208 Banner, Tooltip, Popover, HoverCard, Dialog, EmptyState
Containers 86:350117 y≈10962, y≈14200 Card, Section, Collapsible, AppShell, FormLayout, AspectRatio, Carousel
Chat 190:8652 y≈18208 Chat
Data Display 86:350119 y≈23791..30191 Text, Heading, Divider, StatusDot, CodeBlock, Markdown, Avatar, Table, Timestamp, Kbd, Token, Badge, MetadataList, Icon
Lists 86:350121 y≈33945 List, TreeList, OverflowList
Inputs 86:350123 y≈37699..50499 TextInput, TextArea, NumberInput, CheckboxInput, Switch, RadioList, Selector, MultiSelector, DateInput, TimeInput, Calendar, Tokenizer, Typeahead, Field, DropdownMenu, MoreMenu, PowerSearch
Navigation 86:350125 y≈54253..60653 TopNav, SideNav, Breadcrumbs, TabList, Pagination, MobileNav, Toolbar
Performance 86:350127 y≈64407 Spinner, ProgressBar, Skeleton, Toast

To find the next available position in a row: scan existing frames at that y-position, find the max x, add 1700.

Build Order for New Components

  1. Check if all sub-component dependencies exist in Figma (BLOCKING)
  2. If a dependency is missing, build it first (bottom-up)
  3. Clone the Component Frame template (266:35317) and position it in the correct category row
  4. Build the component set using incremental use_figma calls, placing it inside the Body frame
  5. Verify with get_screenshot vs Storybook reference
  6. Fix discrepancies before marking as complete

Run Frequency & Catch-Up Behavior

  • Cron: Once per day at 12pm PST, Mon–Fri
  • Business hours only — inverted from other Night Watch roles (this role NEEDS the laptop)
  • Idempotent: If the laptop was off for several days, the first run back processes all accumulated changes in one pass
  • Early exit: If no code changes since last run, log "no changes" and exit. Don't read Figma unnecessarily.
  • Rate limit awareness: Complex new component builds may take a while. Track partial builds in state and resume on the next day's run.

GChat Summary Format

After each run, post to the notification space:

Figma Librarian — {date}

Figma comments (new since last run): {N}
  Answered:
  • @ruby on Button: "Does it support xl?" → replied with code reference
  Parity (actioned):
  • @cindy on Card: "code has ghost, Figma missing" → fixed
  Requests (no code match — needs human):
  • @joey on Dialog: "Can we add a compact size?" — no compact prop in code
Components checked: {N}
Updates applied: {N}
  • Button: added size="xl" variant
  • Toast: new component built (full build)
Conflicts flagged: {N}
  • Card: padding differs from code default — human adjusted?
Human overrides preserved: {N}
  • Banner: custom icon slot sizing kept
Skipped (no Figma impact): {N}

Next run: {time} or when laptop is back online

If nothing changed:

Figma Librarian — {date}
No code changes or new comments since last run. All components in sync. ✓

Bootstrapping (First Run)

The first run has no prior AGENT state. The agent must:

  1. Read ALL components from code (packages/core/src/*/Astryx*.tsx)
  2. Read ALL components from Figma (via use_figma discovery script)
  3. Match them by name (code Button → Figma Button or Button)
  4. For each matched pair: record current FIGMA state as AGENT state (assume current Figma is correct)
  5. For unmatched code components: mark as missing in state
  6. For unmatched Figma components: ignore (Figma may have components that don't map 1:1 to code)

This means the first run is read-only — it establishes the baseline without modifying anything. Actual sync starts on the second run.


Related

Clone this wiki locally