figma-relai 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -83,6 +83,8 @@ file precedent attached — "…is on purpose" · the edit backs off
83
83
 
84
84
  The panel's Memory row lists every entry; delete any of them any time. Nothing is recorded silently.
85
85
 
86
+ **Kits.** One designer usually runs one product, and its files share the same law. Save this file’s rules under a name and other files can follow it. Figma drops file-carried law on branch merges; a followed kit puts it back — one click, or automatically if you flip that kit’s auto-restore on.
87
+
86
88
  **AI no-go zones** fence off whole pages — brand masters, legal, the one that is already pixel-perfect. Writes into a guarded page are rejected before they run, with the reason on the receipt, and only you edit the guard list.
87
89
 
88
90
  **Confirmation** is one level with four stops — OPEN · RISK · BULK · ALL. At RISK, the default, only dangerous operations ask (deleting variables or styles, detach, flatten); everything else keeps moving. Set it in the panel; agents can't move it.
@@ -98,7 +100,7 @@ The plugin is the designer's side of the deal: a live activity feed of everythin
98
100
  ```
99
101
  AI (any MCP client)
100
102
  ↕ stdio
101
- MCP server 32 tools · analysis · verification
103
+ MCP server 33 tools · analysis · verification
102
104
  (embedded relay) WebSocket room hub on 127.0.0.1:9055
103
105
  ↕ WebSocket
104
106
  Figma plugin executes Plugin API calls
@@ -118,7 +120,7 @@ Ports are fixed by Figma's plugin sandbox: the manifest allowlists `ws://localho
118
120
  | Read | `get_node_data` (summary / tree / full / css / variables) |
119
121
  | Create & edit | `create_node` · `set_properties` · `set_text` · `edit_structure` |
120
122
  | Components | `manage_components` |
121
- | Design system | `get_design_system` · `manage_variables` · `manage_styles` · `import_from_library` · `manage_conventions` |
123
+ | Design system | `get_design_system` · `manage_variables` · `manage_styles` · `import_from_library` · `manage_conventions` · `manage_rulesets` |
122
124
  | Document | `manage_pages` · `navigate` |
123
125
  | Assets | `export_asset` · `add_image` |
124
126
  | Annotations | `annotate` |
@@ -12,7 +12,7 @@ function createServer() {
12
12
  return new McpServer(
13
13
  {
14
14
  name: "Relai",
15
- version: "0.4.2"
15
+ version: "0.5.0"
16
16
  },
17
17
  {
18
18
  instructions: `
@@ -160,6 +160,19 @@ var FIGMA_COMMANDS = [
160
160
  "audit_readiness",
161
161
  "audit_ghosts",
162
162
  "snapshot_variables",
163
+ "list_rulesets",
164
+ "save_ruleset",
165
+ "delete_ruleset",
166
+ "link_ruleset",
167
+ "unlink_ruleset",
168
+ "ruleset_status",
169
+ "restore_from_ruleset",
170
+ "push_to_ruleset",
171
+ "promote_precedent",
172
+ "export_ruleset",
173
+ "import_ruleset",
174
+ "list_write_log",
175
+ "propose_rule",
163
176
  // Components
164
177
  "get_local_components",
165
178
  "get_instance_overrides",
@@ -667,7 +680,7 @@ var RelayCore = class {
667
680
  };
668
681
 
669
682
  // src/tools/index.ts
670
- import { z as z22 } from "zod";
683
+ import { z as z23 } from "zod";
671
684
 
672
685
  // src/tools/core/create.ts
673
686
  var create_exports = {};
@@ -1948,27 +1961,76 @@ async function fetchLibraryCatalog(libraryFileUrl) {
1948
1961
  };
1949
1962
  }
1950
1963
 
1964
+ // src/tools/core/rulesets.ts
1965
+ var rulesets_exports = {};
1966
+ __export(rulesets_exports, {
1967
+ register: () => register17
1968
+ });
1969
+ import { z as z18 } from "zod";
1970
+ var ACTIONS2 = {
1971
+ list: ["list_rulesets", []],
1972
+ save: ["save_ruleset", ["name", "conventions", "fromFile", "autoRestore", "provenance"]],
1973
+ delete: ["delete_ruleset", ["name"]],
1974
+ link: ["link_ruleset", ["name", "restore"]],
1975
+ unlink: ["unlink_ruleset", []],
1976
+ status: ["ruleset_status", []],
1977
+ restore: ["restore_from_ruleset", []],
1978
+ push: ["push_to_ruleset", []],
1979
+ promote: ["promote_precedent", ["id"]],
1980
+ export: ["export_ruleset", ["name"]],
1981
+ import: ["import_ruleset", ["markdown", "name"]]
1982
+ };
1983
+ function register17(server, sendCommand) {
1984
+ server.tool(
1985
+ "manage_rulesets",
1986
+ "Kits (rulesets) \u2014 named common-ancestor law the plugin keeps on the designer's machine, spanning every file it opens (one designer usually runs one product; its files share the same law). The panel shows this as the KIT row in the connection cluster \u2014 when speaking to the designer, call it a kit, not a ruleset. A file LINKS to one ruleset; the file's own carried conventions stay the working copy. Figma DISCARDS file-carried law on branch\u2192main merges \u2014 a linked ruleset is the recovery: status shows 'file-empty' after a merge, restore (or the set's autoRestore switch) re-seeds it. Actions: list / status (this file's state: in-sync, file-empty, drifted\u2026) / save (name + conventions, or fromFile:true to capture this file's law) / link (optionally restore:true) / unlink / restore (ruleset\u2192file, also seeds promoted precedents) / push (file\u2192ruleset \u2014 the file's law becomes the ancestor) / promote (id \u2014 copy one precedent into the linked ruleset so every linked file inherits it) / delete / export (markdown package with provenance frontmatter) / import (markdown). Pass only the fields the action needs.",
1987
+ {
1988
+ action: z18.enum(Object.keys(ACTIONS2)),
1989
+ name: z18.string().optional().describe("Ruleset name (save/link/delete/export; import fallback)"),
1990
+ conventions: z18.string().optional().describe("save: full conventions markdown (or use fromFile)"),
1991
+ fromFile: z18.boolean().optional().describe("save: capture this file's current conventions"),
1992
+ autoRestore: z18.boolean().optional().describe("save: restore without asking when a linked file's law is found wiped"),
1993
+ provenance: z18.string().optional().describe("save: where this law comes from (author/repo)"),
1994
+ restore: z18.boolean().optional().describe("link: immediately restore the ruleset into the file"),
1995
+ id: z18.string().optional().describe("promote: precedent id (list_precedents shows them)"),
1996
+ markdown: z18.string().optional().describe("import: a ruleset package exported earlier")
1997
+ },
1998
+ async (args) => {
1999
+ try {
2000
+ const [command, fields] = ACTIONS2[args.action];
2001
+ const params = Object.fromEntries(
2002
+ fields.map((f) => [f, args[f]]).filter(([, v]) => v !== void 0)
2003
+ );
2004
+ const result = await sendCommand(command, params);
2005
+ return jsonResult(result);
2006
+ } catch (error) {
2007
+ return errorResult(error);
2008
+ }
2009
+ }
2010
+ );
2011
+ }
2012
+
1951
2013
  // src/tools/batch.ts
1952
2014
  var batch_exports = {};
1953
2015
  __export(batch_exports, {
1954
- register: () => register17
2016
+ register: () => register18
1955
2017
  });
1956
- import { z as z18 } from "zod";
2018
+ import { z as z19 } from "zod";
1957
2019
  function textResult2(text) {
1958
2020
  return { content: [{ type: "text", text }] };
1959
2021
  }
1960
- function register17(server, sendCommand) {
2022
+ function register18(server, sendCommand) {
1961
2023
  server.tool(
1962
2024
  "batch_execute",
1963
2025
  "Execute multiple commands in a single round-trip. Use when performing 3+ similar operations (e.g., styling multiple nodes, creating many elements). Commands run sequentially. Pass dryRun:true to preview the exact command list without touching the canvas \u2014 useful for showing the designer a plan before a large batch.",
1964
2026
  {
1965
- commands: z18.array(
1966
- z18.object({
1967
- command: z18.string().describe("Command name"),
1968
- params: z18.record(z18.string(), z18.unknown()).optional().describe("Command parameters")
2027
+ commands: z19.array(
2028
+ z19.object({
2029
+ command: z19.string().describe("Command name"),
2030
+ params: z19.record(z19.string(), z19.unknown()).optional().describe("Command parameters")
1969
2031
  })
1970
2032
  ).describe("Array of commands to execute sequentially"),
1971
- dryRun: z18.boolean().optional().describe("Preview only \u2014 return the plan, execute nothing")
2033
+ dryRun: z19.boolean().optional().describe("Preview only \u2014 return the plan, execute nothing")
1972
2034
  },
1973
2035
  async ({ commands, dryRun }) => {
1974
2036
  try {
@@ -1999,10 +2061,10 @@ function register17(server, sendCommand) {
1999
2061
  // src/tools/v2/context.ts
2000
2062
  var context_exports = {};
2001
2063
  __export(context_exports, {
2002
- register: () => register18,
2064
+ register: () => register19,
2003
2065
  styleCountsFrom: () => styleCountsFrom
2004
2066
  });
2005
- import { z as z19 } from "zod";
2067
+ import { z as z20 } from "zod";
2006
2068
 
2007
2069
  // src/semantic/response.ts
2008
2070
  function standardResult(opts) {
@@ -2194,7 +2256,7 @@ function styleCountsFrom(styles) {
2194
2256
  }
2195
2257
  return { ...counts, total: counts.paint + counts.text + counts.effect + counts.grid };
2196
2258
  }
2197
- function register18(server, sendCommand) {
2259
+ function register19(server, sendCommand) {
2198
2260
  server.tool(
2199
2261
  "get_document_overview",
2200
2262
  "Get the full document structure: pages, component/style/variable counts. Always call this first to understand the file. Follow with get_selection_context or search_nodes.",
@@ -2304,7 +2366,7 @@ function register18(server, sendCommand) {
2304
2366
  "get_node_details",
2305
2367
  "Get a single node's full properties, CSS, token bindings, and children. Use after search_nodes or get_selection_context to inspect a specific node. Follow with update_node or analyze_* tools.",
2306
2368
  {
2307
- nodeId: z19.string().describe("The node ID to inspect")
2369
+ nodeId: z20.string().describe("The node ID to inspect")
2308
2370
  },
2309
2371
  { readOnlyHint: true },
2310
2372
  async ({ nodeId }) => {
@@ -2363,9 +2425,9 @@ function register18(server, sendCommand) {
2363
2425
  "search_nodes",
2364
2426
  "Search for nodes by name and/or type within a scope. Returns matching node IDs, names, and types. Use to find specific elements before inspecting or modifying them. Follow with get_node_details or update_node.",
2365
2427
  {
2366
- query: z19.string().optional().describe("Name substring to search for"),
2367
- types: z19.array(z19.string()).optional().describe("Node types to filter: FRAME, TEXT, COMPONENT, INSTANCE, RECTANGLE, etc."),
2368
- parentId: z19.string().optional().describe("Scope search to children of this node (default: current page)")
2428
+ query: z20.string().optional().describe("Name substring to search for"),
2429
+ types: z20.array(z20.string()).optional().describe("Node types to filter: FRAME, TEXT, COMPONENT, INSTANCE, RECTANGLE, etc."),
2430
+ parentId: z20.string().optional().describe("Scope search to children of this node (default: current page)")
2369
2431
  },
2370
2432
  { readOnlyHint: true },
2371
2433
  async ({ query, types, parentId }) => {
@@ -2448,7 +2510,7 @@ function register18(server, sendCommand) {
2448
2510
  "screenshot",
2449
2511
  "Take a PNG screenshot of a node or the current view. Returns an image visible to the AI. Use to visually verify changes or inspect design details. Works for both context inspection and verification after modifications.",
2450
2512
  {
2451
- nodeId: z19.string().optional().describe("Node ID to screenshot (default: current view)")
2513
+ nodeId: z20.string().optional().describe("Node ID to screenshot (default: current view)")
2452
2514
  },
2453
2515
  { readOnlyHint: true },
2454
2516
  async ({ nodeId }) => {
@@ -2504,9 +2566,9 @@ function extractTokenName(boundVars, prefix) {
2504
2566
  var analysis_exports = {};
2505
2567
  __export(analysis_exports, {
2506
2568
  collectColorIssues: () => collectColorIssues,
2507
- register: () => register19
2569
+ register: () => register20
2508
2570
  });
2509
- import { z as z20 } from "zod";
2571
+ import { z as z21 } from "zod";
2510
2572
 
2511
2573
  // src/semantic/health.ts
2512
2574
  var WEIGHTS = { color: 0.3, layout: 0.25, components: 0.2, accessibility: 0.25 };
@@ -2559,7 +2621,7 @@ var ASPECT_TOOL = {
2559
2621
  components: "analyze_component_health",
2560
2622
  accessibility: "analyze_accessibility"
2561
2623
  };
2562
- function register19(server, sendCommand) {
2624
+ function register20(server, sendCommand) {
2563
2625
  const aspectHandlers = /* @__PURE__ */ new Map();
2564
2626
  const interceptor = new Proxy(server, {
2565
2627
  get(target, prop, receiver) {
@@ -2578,10 +2640,10 @@ function register19(server, sendCommand) {
2578
2640
  "analyze_design",
2579
2641
  `Audit the design from one aspect: color (token coverage, unbound fills/strokes), layout (auto-layout quality, spacing consistency), components (detached instances, component health), accessibility (WCAG contrast incl. large-text thresholds, touch targets, minimum text sizes), tokens (hardcoded values that match an existing variable \u2014 each finding names the variable to bind; fix in one shot with manage_variables action:tokenize fix:true), overall (runs color/layout/components/accessibility and returns a weighted 0-100 health score \u2014 good for audits and reports), voice (the file's statistical fingerprint: radius/spacing/type signatures, tokenized-paint and instance rates \u2014 what "sounds like this file"), readiness (0-100 agent-readiness score with top gaps: conventions, precedents, semantic tokens, components), or ghosts (stale references to soft-deleted variables \u2014 they still render but are invisible in pickers and dead on publish). Defaults to the current selection; tokens/voice/ghosts default to the current page.`,
2580
2642
  {
2581
- aspect: z20.enum(["color", "layout", "components", "accessibility", "tokens", "overall", "voice", "readiness", "ghosts"]),
2582
- nodeId: z20.string().optional().describe("Root node to analyze (default: current selection)"),
2583
- pageIds: z20.array(z20.string()).optional().describe("voice/ghosts: pages to scan (default: current page; ghosts auto-chunks any count)"),
2584
- platform: z20.enum(["desktop", "mobile"]).optional().describe("accessibility: target-size profile \u2014 desktop 24px (default), mobile 44px")
2643
+ aspect: z21.enum(["color", "layout", "components", "accessibility", "tokens", "overall", "voice", "readiness", "ghosts"]),
2644
+ nodeId: z21.string().optional().describe("Root node to analyze (default: current selection)"),
2645
+ pageIds: z21.array(z21.string()).optional().describe("voice/ghosts: pages to scan (default: current page; ghosts auto-chunks any count)"),
2646
+ platform: z21.enum(["desktop", "mobile"]).optional().describe("accessibility: target-size profile \u2014 desktop 24px (default), mobile 44px")
2585
2647
  },
2586
2648
  { readOnlyHint: true },
2587
2649
  async ({ aspect, nodeId, pageIds, platform }) => {
@@ -2763,7 +2825,7 @@ function registerAnalysisTools(server, sendCommand) {
2763
2825
  "analyze_color_usage",
2764
2826
  "Audit color consistency and design token coverage for a node tree. Reports which fill/stroke colors are not bound to variables. Use after get_selection_context to diagnose token gaps. Follow with bind_tokens to fix issues.",
2765
2827
  {
2766
- nodeId: z20.string().optional().describe("Root node to analyze (default: current selection)")
2828
+ nodeId: z21.string().optional().describe("Root node to analyze (default: current selection)")
2767
2829
  },
2768
2830
  { readOnlyHint: true },
2769
2831
  async ({ nodeId }) => {
@@ -2849,7 +2911,7 @@ function registerAnalysisTools(server, sendCommand) {
2849
2911
  "analyze_layout_quality",
2850
2912
  "Check layout quality: auto-layout usage, magic numbers in spacing/padding, and sizing consistency. Use after get_selection_context to find layout improvements. Follow with set_auto_layout to fix issues.",
2851
2913
  {
2852
- nodeId: z20.string().optional().describe("Root node to analyze (default: current selection)")
2914
+ nodeId: z21.string().optional().describe("Root node to analyze (default: current selection)")
2853
2915
  },
2854
2916
  { readOnlyHint: true },
2855
2917
  async ({ nodeId }) => {
@@ -2902,7 +2964,7 @@ function registerAnalysisTools(server, sendCommand) {
2902
2964
  "analyze_component_health",
2903
2965
  "Check component system health: detached instances, unused components, and override consistency. Use to audit design system adherence. Follow with create_component or update_instance to fix issues.",
2904
2966
  {
2905
- nodeId: z20.string().optional().describe("Scope to analyze (default: current page)")
2967
+ nodeId: z21.string().optional().describe("Scope to analyze (default: current page)")
2906
2968
  },
2907
2969
  { readOnlyHint: true },
2908
2970
  async ({ nodeId }) => {
@@ -2955,8 +3017,8 @@ function registerAnalysisTools(server, sendCommand) {
2955
3017
  "analyze_accessibility",
2956
3018
  "Check accessibility: text contrast ratios against backgrounds, interactive target sizes (WCAG 2.5.8: 24px desktop default, 44px with platform:mobile). Use to ensure designs meet WCAG guidelines. Follow with update_node to fix contrast or sizing issues.",
2957
3019
  {
2958
- nodeId: z20.string().optional().describe("Root node to analyze (default: current selection)"),
2959
- platform: z20.enum(["desktop", "mobile"]).optional().describe("Target-size profile: desktop 24px (default), mobile 44px")
3020
+ nodeId: z21.string().optional().describe("Root node to analyze (default: current selection)"),
3021
+ platform: z21.enum(["desktop", "mobile"]).optional().describe("Target-size profile: desktop 24px (default), mobile 44px")
2960
3022
  },
2961
3023
  { readOnlyHint: true },
2962
3024
  async ({ nodeId, platform }) => {
@@ -3003,9 +3065,9 @@ function registerAnalysisTools(server, sendCommand) {
3003
3065
  "diff_nodes",
3004
3066
  `Compare two nodes and list their differences (fills, strokes, size, layout, etc.), or audit changes over time with checkpoints: checkpoint:"save" snapshots nodeIdA's key properties; checkpoint:"compare" later diffs the current state against that snapshot \u2014 useful before/after an editing session to show the designer exactly what changed.`,
3005
3067
  {
3006
- nodeIdA: z20.string().describe("First node ID (or the checkpoint target)"),
3007
- nodeIdB: z20.string().optional().describe("Second node ID (omit when using checkpoint)"),
3008
- checkpoint: z20.enum(["save", "compare"]).optional().describe("save: snapshot nodeIdA now; compare: diff current vs saved snapshot")
3068
+ nodeIdA: z21.string().describe("First node ID (or the checkpoint target)"),
3069
+ nodeIdB: z21.string().optional().describe("Second node ID (omit when using checkpoint)"),
3070
+ checkpoint: z21.enum(["save", "compare"]).optional().describe("save: snapshot nodeIdA now; compare: diff current vs saved snapshot")
3009
3071
  },
3010
3072
  { readOnlyHint: true },
3011
3073
  async ({ nodeIdA, nodeIdB, checkpoint }) => {
@@ -3301,16 +3363,16 @@ function compareField(diffs, field, a, b) {
3301
3363
  // src/tools/v2/verification.ts
3302
3364
  var verification_exports = {};
3303
3365
  __export(verification_exports, {
3304
- register: () => register20
3366
+ register: () => register21
3305
3367
  });
3306
- import { z as z21 } from "zod";
3307
- function register20(server, sendCommand) {
3368
+ import { z as z22 } from "zod";
3369
+ function register21(server, sendCommand) {
3308
3370
  server.tool(
3309
3371
  "verify_changes",
3310
3372
  "Verify that a node's current properties match expected values. Use after update_node, set_auto_layout, or batch_execute to confirm changes applied correctly. Returns match/mismatch for each checked field.",
3311
3373
  {
3312
- nodeId: z21.string().describe("Node ID to verify"),
3313
- expected: z21.record(z21.string(), z21.unknown()).describe("Expected property values, e.g. { fill: '#3B82F6', cornerRadius: 8 }")
3374
+ nodeId: z22.string().describe("Node ID to verify"),
3375
+ expected: z22.record(z22.string(), z22.unknown()).describe("Expected property values, e.g. { fill: '#3B82F6', cornerRadius: 8 }")
3314
3376
  },
3315
3377
  { readOnlyHint: true },
3316
3378
  async ({ nodeId, expected }) => {
@@ -3361,8 +3423,8 @@ function register20(server, sendCommand) {
3361
3423
  "validate_design_rules",
3362
3424
  "Run design quality rules on a node: token coverage, auto-layout usage, naming conventions, and accessibility basics. Use after modifications or during design audits. Returns pass/fail for each rule with fix suggestions.",
3363
3425
  {
3364
- nodeId: z21.string().optional().describe("Root node to validate (default: current selection)"),
3365
- platform: z21.enum(["desktop", "mobile"]).optional().describe("Target-size profile for the touch rule: desktop 24px (default), mobile 44px")
3426
+ nodeId: z22.string().optional().describe("Root node to validate (default: current selection)"),
3427
+ platform: z22.enum(["desktop", "mobile"]).optional().describe("Target-size profile for the touch rule: desktop 24px (default), mobile 44px")
3366
3428
  },
3367
3429
  { readOnlyHint: true },
3368
3430
  async ({ nodeId, platform }) => {
@@ -3493,8 +3555,8 @@ function register20(server, sendCommand) {
3493
3555
  "verify_visual",
3494
3556
  "The write\u2192see\u2192assert loop in one call: screenshots a node AND (optionally) checks expected property values. Returns the image plus structured pass/fail. Use after visual edits instead of separate screenshot + verify_changes calls.",
3495
3557
  {
3496
- nodeId: z21.string().describe("Node to verify"),
3497
- expected: z21.record(z21.string(), z21.unknown()).optional().describe("Expected property values, e.g. { cornerRadius: 8, width: 320 }")
3558
+ nodeId: z22.string().describe("Node to verify"),
3559
+ expected: z22.record(z22.string(), z22.unknown()).optional().describe("Expected property values, e.g. { cornerRadius: 8, width: 320 }")
3498
3560
  },
3499
3561
  { readOnlyHint: true },
3500
3562
  async ({ nodeId, expected }) => {
@@ -3578,6 +3640,7 @@ var moduleCategories = [
3578
3640
  [styles_exports, "design-system"],
3579
3641
  [library_exports, "design-system"],
3580
3642
  [design_system_exports, "design-system"],
3643
+ [rulesets_exports, "design-system"],
3581
3644
  [pages_exports, "document"],
3582
3645
  [navigate_exports, "document"],
3583
3646
  [assets_exports, "assets"],
@@ -3591,7 +3654,7 @@ function registerRoomTool(server, joinRoom, listRooms) {
3591
3654
  "join_room",
3592
3655
  "Connect to a specific Figma plugin instance. Usually unnecessary \u2014 pairing is automatic when one plugin is connected. Call with no arguments to LIST the connected plugins (room + file name) instead of typing the room by hand, then call again with the room you want.",
3593
3656
  {
3594
- room: z22.string().optional().describe("Room name shown in the Figma plugin. Omit to list connected rooms.")
3657
+ room: z23.string().optional().describe("Room name shown in the Figma plugin. Omit to list connected rooms.")
3595
3658
  },
3596
3659
  async ({ room }) => {
3597
3660
  try {
@@ -3638,7 +3701,7 @@ function registerEventsTool(server, consumeEvents, getSessionLog) {
3638
3701
  "get_events",
3639
3702
  "Activity since the last check. scope 'designer' (default): the designer's selection/node/page changes (also piggybacked as designer_events on command results). scope 'agent': this session's own command log \u2014 every plugin command sent, with success/duration \u2014 useful for summarizing what was changed. 'all': both.",
3640
3703
  {
3641
- scope: z22.enum(["designer", "agent", "all"]).optional()
3704
+ scope: z23.enum(["designer", "agent", "all"]).optional()
3642
3705
  },
3643
3706
  { readOnlyHint: true },
3644
3707
  async ({ scope = "designer" }) => {
@@ -3710,7 +3773,7 @@ var design_system_first_default = '# Design-system-first building\n\nThe differe
3710
3773
  var janitorial_cleanup_default = '# Janitorial cleanup\n\nThe chores designers postpone for months \u2014 layer renaming, token drift, spacing normalization \u2014 are where an agent earns trust fastest. These jobs are big, mechanical, and easy to verify. Do them conservatively and loudly.\n\n## Ground rules\n\n- **Preview before bulk.** Use `dryRun:true` on `batch_execute` / `set_properties`, or report findings first (`analyze_design`), and tell the designer the scale ("about 140 renames on this page \u2014 go?"). If the plugin\'s approval gate is on, big writes will ask them anyway.\n- **Scope tightly.** Work page by page (or within the designer\'s selection). Never sweep the whole file unasked.\n- **Never rename or restyle library instances\' internals** \u2014 only top-level frames/groups the team owns.\n\n## Recipes\n\n### Rename layers by content\n`Frame 427` tells nobody anything. Walk the target scope with `execute_figma` + `relai.query`, and derive names:\n- A frame whose only text child says "Submit" \u2192 `Button/Submit` (match the file\'s existing naming pattern from `get_design_system`, don\'t invent one).\n- Text nodes \u2192 their content, truncated (`Heading: Pricing plans`).\n- Skip nodes that already have deliberate names (anything not matching `/^(Frame|Group|Rectangle|Ellipse|Vector|Line) \\d+$/`).\nReturn the rename list; apply via one `batch_execute` of `rename_node` commands.\n\n### Tokenize hardcoded values\n`analyze_design aspect:"tokens"` finds hardcoded colors/numbers that visually match existing variables; `manage_variables action:"tokenize" fix:true` binds them in one pass. Report anything close-but-not-matching (deltaE just over tolerance) instead of silently snapping it.\n\n### Normalize spacing\nIn auto-layout scopes, collect `itemSpacing`/padding values; flag off-scale values (not on the file\'s 4/8pt grid or spacing variables) and propose the nearest on-scale value. Apply only after the designer confirms the mapping.\n\n### Sweep detached instances\n`analyze_design aspect:"components"` lists likely-detached instances. Offer to re-link obvious ones (matching component still exists) and just report the rest \u2014 re-attaching wrong is worse than detached.\n\n## Report format\n\nEnd with counts and jump-points: what changed, what was skipped and why, node ids for spot-checking (the designer can click entries in the plugin feed).\n\n---\n\n*Tool parameter contracts are deliberately not duplicated in this document \u2014 every tool is self-describing over MCP, and `npx figma-relai docs <tool>` (or the generated `docs/manifest.json`) is the always-current reference.*\n';
3711
3774
 
3712
3775
  // ../../docs/skills/memory-and-precedents.md
3713
- var memory_and_precedents_default = '# File memory & precedents\n\nEvery Figma file this plugin touches can carry its own case law: **precedents** \u2014 single adjudications the designer made, stored inside the file (shared plugin data) so they reach every future session from any AI client. Conventions are the statutes; precedents are the case law. Together they are the file\'s law, and `manage_conventions action:get` returns both.\n\nWhen a write touches something a precedent references, **the file speaks**: the ruling arrives attached to your tool result. Treat it as the designer\'s voice from a past session calling a halt \u2014 the four-act shape is always the same: they said it once, the file kept it, you came to "fix" it knowing nothing, their words stop you. Back off before you undo something intentional.\n\n## Read before you work\n\nAt the start of any session that will modify the file, call `manage_conventions action:get`. Treat what comes back the way you treat user instructions:\n\n- `content` \u2014 the conventions doc (statutes).\n- `precedents` \u2014 recent adjudications (case law), newest first, with `precedentCount` for the total.\n\nA precedent outranks your own judgment about what "looks wrong". If a precedent says a deviation is intent, do not "fix" it \u2014 and do not re-ask about it.\n\n## When to record\n\nRecord a precedent (`action:record_precedent`) when the designer **rules** on something durable. The signal is adjudication language:\n\n- "This is intentional, not drift" \u2192 `kind: "intent"`\n- "Approved \u2014 but never restructure this table again" \u2192 `kind: "decision"`\n- "Don\'t do it that way; do X instead" \u2192 `kind: "correction"`\n\nOne precedent = one sentence (\u2264280 chars), written so a future session with zero context understands the ruling. Attach `refs` \u2014 the variable IDs, node IDs, or page IDs the ruling concerns \u2014 because refs power in-band surfacing: any future write that touches them gets the precedent attached to its result automatically.\n\n**Always say in your reply that you recorded it** ("\u5DF2\u8BB0\u5165\u6587\u4EF6\u8BB0\u5FC6: \u2026"). Recording silently breaks the designer\'s trust in what the file knows.\n\n## When NOT to record\n\n- Transient, one-off choices ("make this one red for the screenshot").\n- Anything already in the conventions doc (point to it instead; propose a conventions edit if it deserves statute status).\n- Secrets, personal information, or anything about people rather than the file.\n- Your own inferences the designer has not confirmed. Precedents are the DESIGNER\'s rulings, not your observations. When unsure, ask: "\u8BB0\u4E3A\u5224\u4F8B\u5417?"\n\n## Promote rulings into enforcement\n\nWhen conventions or repeated precedents describe something ENFORCEABLE, propose the promotion in chat \u2014 never enact it silently:\n\n- "Never touch page X" (in conventions or a repeated ruling) \u2192 offer: "\u8981\u628A X \u8BBE\u4E3A AI \u7981\u533A\u5417?(manage_pages action:guard)" \u2014 set it only after the designer agrees.\n- Repeated identical adjudications ("this is intent" three times on the same token family) \u2192 offer to consolidate them into one conventions line, then remove the superseded precedents.\n\nGuards belong to the designer: never guard/unguard on your own judgment, and mention any guard change in your reply.\n\n## Reporting style as trust matures\n\nEarly sessions: report every change. Once the designer clearly stops reading full receipts (they say "\u7B80\u62A5\u5C31\u597D" or approve digests), switch to digest mode: lead with **the two changes you are least sure about** and anything a precedent or guard touched, then one line for the rest ("\u5176\u4F59 N \u5904\u6309\u89C4\u7EA6"). Full detail must always remain one question away \u2014 digest is a summary, never a omission. Never digest away errors, guard rejections, or precedent hits.\n\n## Maintenance\n\n- The designer sees and can delete every entry in the plugin panel (Memory section). Their record, their rules.\n- Memory holds 200 entries / 64KB. When recording fails with a capacity error, offer a consolidation pass: `list_precedents`, merge superseded rulings into fewer summary entries (`record_precedent` the summaries, `remove_precedent` the merged ones), preserving every still-active ruling. Show the merge plan before applying it.\n- If a precedent contradicts current reality (the token it references is gone, the ruling was reversed), ask the designer whether to update or remove it \u2014 never silently drop case law.\n';
3776
+ var memory_and_precedents_default = '# File memory & precedents\n\nEvery Figma file this plugin touches can carry its own case law: **precedents** \u2014 single adjudications the designer made, stored inside the file (shared plugin data) so they reach every future session from any AI client. Conventions are the statutes; precedents are the case law. Together they are the file\'s law, and `manage_conventions action:get` returns both.\n\nWhen a write touches something a precedent references, **the file speaks**: the ruling arrives attached to your tool result. Treat it as the designer\'s voice from a past session calling a halt \u2014 the four-act shape is always the same: they said it once, the file kept it, you came to "fix" it knowing nothing, their words stop you. Back off before you undo something intentional.\n\n## Read before you work\n\nAt the start of any session that will modify the file, call `manage_conventions action:get`. Treat what comes back the way you treat user instructions:\n\n- `content` \u2014 the conventions doc (statutes).\n- `precedents` \u2014 recent adjudications (case law), newest first, with `precedentCount` for the total.\n\nA precedent outranks your own judgment about what "looks wrong". If a precedent says a deviation is intent, do not "fix" it \u2014 and do not re-ask about it.\n\n## When to record\n\nRecord a precedent (`action:record_precedent`) when the designer **rules** on something durable. The signal is adjudication language:\n\n- "This is intentional, not drift" \u2192 `kind: "intent"`\n- "Approved \u2014 but never restructure this table again" \u2192 `kind: "decision"`\n- "Don\'t do it that way; do X instead" \u2192 `kind: "correction"`\n\nOne precedent = one sentence (\u2264280 chars), written so a future session with zero context understands the ruling. Attach `refs` \u2014 the variable IDs, node IDs, or page IDs the ruling concerns \u2014 because refs power in-band surfacing: any future write that touches them gets the precedent attached to its result automatically.\n\n**Always say in your reply that you recorded it** ("\u5DF2\u8BB0\u5165\u6587\u4EF6\u8BB0\u5FC6: \u2026"). Recording silently breaks the designer\'s trust in what the file knows.\n\n## When NOT to record\n\n- Transient, one-off choices ("make this one red for the screenshot").\n- Anything already in the conventions doc (point to it instead; propose a conventions edit if it deserves statute status).\n- Secrets, personal information, or anything about people rather than the file.\n- Your own inferences the designer has not confirmed. Precedents are the DESIGNER\'s rulings, not your observations. When unsure, ask: "\u8BB0\u4E3A\u5224\u4F8B\u5417?"\n\n## Promote rulings into enforcement\n\nWhen conventions or repeated precedents describe something ENFORCEABLE, propose the promotion \u2014 never enact it silently:\n\n- "Never touch page X" (in conventions or a repeated ruling) \u2192 send a panel proposal: `propose_rule` (via batch_execute) with `kind: "page_guard"`, the `pageIds`, and a `reason` that QUOTES the sentence it comes from. The designer gets a card and decides there; do not wait for the verdict and do not guard on your own. Mention in your reply that you proposed it.\n- Repeated identical adjudications ("this is intent" three times on the same token family) \u2192 offer to consolidate them into one conventions line, then remove the superseded precedents.\n\nGuards belong to the designer: never guard/unguard on your own judgment, and mention any guard change in your reply.\n\n## Kits \u2014 the law\'s common ancestor\n\nA designer who runs one product keeps its shared law as a **kit** (`manage_rulesets`; the panel\'s KIT row) \u2014 named, stored by the plugin on their machine, spanning every file. The file\'s carried law stays the working copy; the kit is the ancestor it descends from. When speaking to the designer, say kit.\n\nThis matters because **Figma discards file-carried law on branch\u2192main merges**. At session start, if `manage_rulesets action:status` says `file-empty`, the law was wiped: say so, and restore it (`action:restore`) with the designer\'s consent \u2014 or note it happened automatically when the set\'s autoRestore is on. Other states: `drifted` means file and ancestor differ \u2014 the designer chooses pull (`restore`) or promote the file\'s version (`push`); never resolve drift on your own.\n\nWhen the designer rules something clearly product-wide (not one-file-local), offer to promote that precedent into the linked kit (`action:promote` with the precedent id) so every linked file inherits it on restore.\n\n## Reporting style as trust matures\n\nEarly sessions: report every change. Once the designer clearly stops reading full receipts (they say "\u7B80\u62A5\u5C31\u597D" or approve digests), switch to digest mode: lead with **the two changes you are least sure about** and anything a precedent or guard touched, then one line for the rest ("\u5176\u4F59 N \u5904\u6309\u89C4\u7EA6"). Full detail must always remain one question away \u2014 digest is a summary, never a omission. Never digest away errors, guard rejections, or precedent hits.\n\n## Maintenance\n\n- The designer sees and can delete every entry in the plugin panel (Memory section). Their record, their rules.\n- Memory holds 200 entries / 64KB. When recording fails with a capacity error, offer a consolidation pass: `list_precedents`, merge superseded rulings into fewer summary entries (`record_precedent` the summaries, `remove_precedent` the merged ones), preserving every still-active ruling. Show the merge plan before applying it.\n- If a precedent contradicts current reality (the token it references is gone, the ruling was reversed), ask the designer whether to update or remove it \u2014 never silently drop case law.\n';
3714
3777
 
3715
3778
  // ../../docs/skills/qa-gate.md
3716
3779
  var qa_gate_default = '# QA gate \u2014 the full physical, on demand\n\nHuman-triggered, review-moment audit of a file (or a set of pages). Run it when the designer says "\u4F53\u68C0" / "is this ready for review?" / before a library merge. It composes the gauges into one report with receipts.\n\n## The pass\n\nRun in this order (each is read-only):\n\n1. `manage_conventions action:get` \u2014 the law you\'ll be judging against (conventions + precedents). If precedents mark deviations as intent, they are NOT findings \u2014 the file already ruled. List them as "waived by precedent", never as problems.\n2. `analyze_design aspect:readiness` \u2014 agent-readiness score + top gaps.\n3. `analyze_design aspect:ghosts` (pass `pageIds` for a multi-page pass) \u2014 stale references to soft-deleted variables. Any ghost count > 0 is a red flag for publish.\n4. `analyze_design aspect:voice` \u2014 the file\'s signature (radius/spacing/type, tokenized-paint rate, instance rate).\n5. `analyze_design aspect:overall` on the key screens the designer names (health score 0-100).\n6. `analyze_design aspect:tokens` \u2014 hardcoded values that match existing variables (each is one `tokenize` away from bound).\n\n## The report\n\nProduce a compact markdown report, structured exactly like this:\n\n```\n# QA gate \u2014 <file name> \u2014 <date>\nverdict: PASS | PASS WITH NOTES | BLOCK\n\nreadiness NN/100 (top gap: \u2026)\nghosts N refs (0 required for publish)\nhealth NN/100 (screens: \u2026)\ntokens N unbound-but-matchable\nvoice radius \u2026, spacing \u2026, \u2026% tokenized\n\n## findings (ranked)\n1. [BLOCK|NOTE] \u2026 \u2014 evidence: <numbers/nodeIds> \u2014 fix: <one tool call>\n\u2026\n\n## precedent-protected (not findings)\n- \u2026 (per precedent "\u2026", YYYY-MM-DD)\n```\n\nVerdict rules: any ghosts or health < 60 on a named key screen \u2192 BLOCK; readiness < 50 \u2192 note it but readiness alone never blocks; everything protected by a precedent is listed separately, never as a finding.\n\n## Rules\n\n- Numbers unedited: report what the tools returned, never round a failing number into a passing one.\n- Findings must carry evidence (counts, nodeIds) and a one-call fix each.\n- Offer \u2014 do not perform \u2014 fixes. The QA gate is a read-only pass; repairs are a separate, approved session.\n- If the designer wants the report in-file: create a page named `\xB7 QA Report <date>` and write the report as text there (ask first \u2014 it adds a page to their file).\n- Big files: run ghosts page-set by page-set (`pageIds`) instead of one giant pass, and say which pages were covered. Silence about coverage reads as "covered everything" \u2014 never let it.\n';
@@ -3807,4 +3870,4 @@ export {
3807
3870
  listToolCatalog,
3808
3871
  registerPrompts
3809
3872
  };
3810
- //# sourceMappingURL=chunk-BH6KSGCN.js.map
3873
+ //# sourceMappingURL=chunk-2MSOOX4G.js.map