figma-relai 0.2.8 → 0.4.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
@@ -59,7 +59,7 @@ Understanding a design. "How is this screen put together?" gets you structure, c
59
59
 
60
60
  Bulk edits. "Translate every button label to English" or "recolor this for dark mode" become one round-trip across dozens of nodes instead of an afternoon of clicking.
61
61
 
62
- Audits. `analyze_design` checks color-token coverage, auto-layout quality, component health, and accessibility (WCAG contrast, touch targets, text sizes) — or all four at once as a weighted 0–100 health score you can put in a review.
62
+ Audits. `analyze_design` checks color-token coverage, auto-layout quality, component health, and accessibility (WCAG contrast, touch targets, text sizes) — or all four at once as a weighted 0–100 health score you can put in a review. It also scores agent-readiness (how prepared the file is for AI work, with top gaps), fingerprints the file's voice (its radius/spacing/type signature), and runs a ghost census for references to soft-deleted variables.
63
63
 
64
64
  Design systems. Variable collections with modes, token binding, shared styles, components with proper variants, team-library imports. `get_design_system` inventories what the file — and the libraries it uses — already has, so the AI builds from your components instead of redrawing near-copies; `analyze_design`'s tokens aspect finds hardcoded values that visually match an existing variable, and one `tokenize` call binds them all. These run as declarative operations with precondition checks, so the same request behaves the same way every time, and a failure tells the AI what to do next ("call set_layout_mode first") instead of dumping a stack trace.
65
65
 
@@ -69,7 +69,7 @@ Everything else. `execute_figma` runs JavaScript against the Figma Plugin API di
69
69
 
70
70
  The plugin is the designer's side of the deal: a live activity feed of everything the AI does, an "AI connected" indicator that means an agent is actually paired (not just that a server is running), and a Stop button that cancels pending work. Selection and page changes you make flow back to the AI as events, so "now do the same to this one" works without re-explaining. The relay is local: file contents move only between Figma, your machine, and the AI client you already trust.
71
71
 
72
- Three dials go further when you want them. **Approvals** ("Ask before big edits") holds bulk writes and code execution until you press Approve in the panel. **Lock to selection** rejects edits outside whatever you've selected — the AI gets a clear error, not a silent pass. And **file conventions** are a little CLAUDE.md stored inside the Figma file itself: naming rules, spacing habits, do-not-touch pages — every future session, from any AI client, reads it before working. The UI speaks English, 日本語, and 中文.
72
+ Three dials go further when you want them. **Approvals** ("Ask before big edits") holds bulk writes and code execution until you press Approve in the panel. **Lock to selection** rejects edits outside whatever you've selected — the AI gets a clear error, not a silent pass. And **file conventions** are a little CLAUDE.md stored inside the Figma file itself: naming rules, spacing habits, do-not-touch pages — every future session, from any AI client, reads it before working. Since 0.3 the file also keeps **memory**: rulings you make ("this deviation is intent, not drift") are recorded as precedents inside the file, and any future edit that touches what they reference gets the precedent attached to its result — the AI is corrected by your own case law, in the moment. The panel's Memory row shows every entry; delete any of them any time. **AI no-go zones** guard whole pages: writes into a guarded page are rejected at dispatch, and only you edit the guard list. And confirmation is one four-stop dial — OPEN · RISK · BULK · ALL: at RISK (the default) only ghost-making and irreversible operations ask; dial to OPEN for branch workflows. The UI speaks English, 日本語, and 中文.
73
73
 
74
74
  ## How it works
75
75
 
@@ -103,7 +103,7 @@ Ports are fixed by Figma's plugin sandbox: the manifest allowlists `ws://localho
103
103
  | Comments | `manage_comments` (needs a token — see below) |
104
104
  | Advanced | `batch_execute` · `execute_figma` · `join_room` |
105
105
 
106
- Each tool is self-describing, so the AI sees full parameter docs. The same contract also exists as a file: `npx figma-relai manifest` prints a machine-readable JSON of every tool schema, plugin command, and known pitfall — generated from the running code on every build (committed as `docs/manifest.json`), so it cannot drift — and `npx figma-relai docs <tool>` renders it for humans. Nine skill documents ship alongside as MCP prompts: token strategy, component conventions, audit workflows, a Plugin API cheat sheet for `execute_figma`, and recipes for design-system-first building, bulk cleanup, and comment-driven collaboration.
106
+ Each tool is self-describing, so the AI sees full parameter docs. The same contract also exists as a file: `npx figma-relai manifest` prints a machine-readable JSON of every tool schema, plugin command, and known pitfall — generated from the running code on every build (committed as `docs/manifest.json`), so it cannot drift — and `npx figma-relai docs <tool>` renders it for humans. Eleven skill documents ship alongside as MCP prompts: token strategy, component conventions, audit workflows, the QA gate, a Plugin API cheat sheet for `execute_figma`, file memory & precedents, and recipes for design-system-first building, bulk cleanup, and comment-driven collaboration. Your own skills load too: drop markdown files with a name/description frontmatter into `~/.figma-relai/skills/` and they register as `user:` prompts.
107
107
 
108
108
  ## Relai and Figma's official MCP
109
109
 
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/user-skills.ts
4
+ import { readdirSync, readFileSync } from "fs";
5
+ import { homedir } from "os";
6
+ import { join } from "path";
7
+ var NAME_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
8
+ var LAYERS = /* @__PURE__ */ new Set(["physical", "craft", "voice"]);
9
+ function parseSkillFrontmatter(raw) {
10
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
11
+ if (!match) {
12
+ return { error: "missing frontmatter block (--- name/description ---)" };
13
+ }
14
+ const fields = {};
15
+ for (const line of match[1].split(/\r?\n/)) {
16
+ const kv = line.match(/^([a-zA-Z_]+)\s*:\s*(.+?)\s*$/);
17
+ if (kv) fields[kv[1]] = kv[2].replace(/^["']|["']$/g, "");
18
+ }
19
+ if (!fields.name || !NAME_RE.test(fields.name)) {
20
+ return { error: `invalid or missing name (kebab-case, got "${fields.name ?? ""}")` };
21
+ }
22
+ if (!fields.description) {
23
+ return { error: "missing description" };
24
+ }
25
+ if (fields.layer && !LAYERS.has(fields.layer)) {
26
+ return { error: `unknown layer "${fields.layer}" (physical | craft | voice)` };
27
+ }
28
+ const body = match[2].trim();
29
+ if (!body) return { error: "empty body" };
30
+ return {
31
+ skill: {
32
+ name: fields.name,
33
+ description: fields.description,
34
+ layer: fields.layer,
35
+ provenance: fields.provenance,
36
+ text: body
37
+ }
38
+ };
39
+ }
40
+ function userSkillsDir() {
41
+ return join(homedir(), ".figma-relai", "skills");
42
+ }
43
+ function loadUserSkills(dir = userSkillsDir()) {
44
+ const out = { skills: [], errors: [] };
45
+ let files;
46
+ try {
47
+ files = readdirSync(dir).filter((f) => f.endsWith(".md"));
48
+ } catch {
49
+ return out;
50
+ }
51
+ for (const file of files.sort()) {
52
+ try {
53
+ const { skill, error } = parseSkillFrontmatter(readFileSync(join(dir, file), "utf8"));
54
+ if (skill) out.skills.push(skill);
55
+ else out.errors.push({ file, error: error ?? "unparseable" });
56
+ } catch (e) {
57
+ out.errors.push({ file, error: e instanceof Error ? e.message : String(e) });
58
+ }
59
+ }
60
+ return out;
61
+ }
62
+
63
+ export {
64
+ loadUserSkills
65
+ };
66
+ //# sourceMappingURL=chunk-SDINVROO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/user-skills.ts"],"sourcesContent":["// User-authored skills — personal workflow documents loaded at runtime from\n// ~/.figma-relai/skills/*.md and exposed as MCP prompts with a \"user:\" prefix.\n// The frontmatter carries the future sharing format from day one: layer\n// declares what stratum of knowhow this is (physical API facts / craft\n// patterns / brand voice), provenance says where it was earned.\n\nimport { readdirSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport interface UserSkill {\n name: string;\n description: string;\n layer?: \"physical\" | \"craft\" | \"voice\";\n provenance?: string;\n text: string;\n}\n\nexport interface SkillParseResult {\n skill?: UserSkill;\n error?: string;\n}\n\nconst NAME_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;\nconst LAYERS = new Set([\"physical\", \"craft\", \"voice\"]);\n\n/** Pure frontmatter parser — `--- key: value ---` block, body follows. */\nexport function parseSkillFrontmatter(raw: string): SkillParseResult {\n const match = raw.match(/^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?([\\s\\S]*)$/);\n if (!match) {\n return { error: \"missing frontmatter block (--- name/description ---)\" };\n }\n const fields: Record<string, string> = {};\n for (const line of match[1].split(/\\r?\\n/)) {\n const kv = line.match(/^([a-zA-Z_]+)\\s*:\\s*(.+?)\\s*$/);\n if (kv) fields[kv[1]] = kv[2].replace(/^[\"']|[\"']$/g, \"\");\n }\n if (!fields.name || !NAME_RE.test(fields.name)) {\n return { error: `invalid or missing name (kebab-case, got \"${fields.name ?? \"\"}\")` };\n }\n if (!fields.description) {\n return { error: \"missing description\" };\n }\n if (fields.layer && !LAYERS.has(fields.layer)) {\n return { error: `unknown layer \"${fields.layer}\" (physical | craft | voice)` };\n }\n const body = match[2].trim();\n if (!body) return { error: \"empty body\" };\n return {\n skill: {\n name: fields.name,\n description: fields.description,\n layer: fields.layer as UserSkill[\"layer\"],\n provenance: fields.provenance,\n text: body,\n },\n };\n}\n\nexport function userSkillsDir(): string {\n return join(homedir(), \".figma-relai\", \"skills\");\n}\n\nexport interface LoadedUserSkills {\n skills: UserSkill[];\n errors: Array<{ file: string; error: string }>;\n}\n\nexport function loadUserSkills(dir: string = userSkillsDir()): LoadedUserSkills {\n const out: LoadedUserSkills = { skills: [], errors: [] };\n let files: string[];\n try {\n files = readdirSync(dir).filter((f) => f.endsWith(\".md\"));\n } catch {\n return out; // no directory — user skills are optional\n }\n for (const file of files.sort()) {\n try {\n const { skill, error } = parseSkillFrontmatter(readFileSync(join(dir, file), \"utf8\"));\n if (skill) out.skills.push(skill);\n else out.errors.push({ file, error: error ?? \"unparseable\" });\n } catch (e) {\n out.errors.push({ file, error: e instanceof Error ? e.message : String(e) });\n }\n }\n return out;\n}\n"],"mappings":";;;AAMA,SAAS,aAAa,oBAAoB;AAC1C,SAAS,eAAe;AACxB,SAAS,YAAY;AAerB,IAAM,UAAU;AAChB,IAAM,SAAS,oBAAI,IAAI,CAAC,YAAY,SAAS,OAAO,CAAC;AAG9C,SAAS,sBAAsB,KAA+B;AACnE,QAAM,QAAQ,IAAI,MAAM,6CAA6C;AACrE,MAAI,CAAC,OAAO;AACV,WAAO,EAAE,OAAO,uDAAuD;AAAA,EACzE;AACA,QAAM,SAAiC,CAAC;AACxC,aAAW,QAAQ,MAAM,CAAC,EAAE,MAAM,OAAO,GAAG;AAC1C,UAAM,KAAK,KAAK,MAAM,+BAA+B;AACrD,QAAI,GAAI,QAAO,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,EAAE,QAAQ,gBAAgB,EAAE;AAAA,EAC1D;AACA,MAAI,CAAC,OAAO,QAAQ,CAAC,QAAQ,KAAK,OAAO,IAAI,GAAG;AAC9C,WAAO,EAAE,OAAO,6CAA6C,OAAO,QAAQ,EAAE,KAAK;AAAA,EACrF;AACA,MAAI,CAAC,OAAO,aAAa;AACvB,WAAO,EAAE,OAAO,sBAAsB;AAAA,EACxC;AACA,MAAI,OAAO,SAAS,CAAC,OAAO,IAAI,OAAO,KAAK,GAAG;AAC7C,WAAO,EAAE,OAAO,kBAAkB,OAAO,KAAK,+BAA+B;AAAA,EAC/E;AACA,QAAM,OAAO,MAAM,CAAC,EAAE,KAAK;AAC3B,MAAI,CAAC,KAAM,QAAO,EAAE,OAAO,aAAa;AACxC,SAAO;AAAA,IACL,OAAO;AAAA,MACL,MAAM,OAAO;AAAA,MACb,aAAa,OAAO;AAAA,MACpB,OAAO,OAAO;AAAA,MACd,YAAY,OAAO;AAAA,MACnB,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAEO,SAAS,gBAAwB;AACtC,SAAO,KAAK,QAAQ,GAAG,gBAAgB,QAAQ;AACjD;AAOO,SAAS,eAAe,MAAc,cAAc,GAAqB;AAC9E,QAAM,MAAwB,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,EAAE;AACvD,MAAI;AACJ,MAAI;AACF,YAAQ,YAAY,GAAG,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,KAAK,CAAC;AAAA,EAC1D,QAAQ;AACN,WAAO;AAAA,EACT;AACA,aAAW,QAAQ,MAAM,KAAK,GAAG;AAC/B,QAAI;AACF,YAAM,EAAE,OAAO,MAAM,IAAI,sBAAsB,aAAa,KAAK,KAAK,IAAI,GAAG,MAAM,CAAC;AACpF,UAAI,MAAO,KAAI,OAAO,KAAK,KAAK;AAAA,UAC3B,KAAI,OAAO,KAAK,EAAE,MAAM,OAAO,SAAS,cAAc,CAAC;AAAA,IAC9D,SAAS,GAAG;AACV,UAAI,OAAO,KAAK,EAAE,MAAM,OAAO,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,EAAE,CAAC;AAAA,IAC7E;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ loadUserSkills
4
+ } from "./chunk-SDINVROO.js";
2
5
  import {
3
6
  __export
4
7
  } from "./chunk-2H7UOFLK.js";
@@ -9,7 +12,7 @@ function createServer() {
9
12
  return new McpServer(
10
13
  {
11
14
  name: "Relai",
12
- version: "0.2.8"
15
+ version: "0.4.0"
13
16
  },
14
17
  {
15
18
  instructions: `
@@ -146,6 +149,16 @@ var FIGMA_COMMANDS = [
146
149
  "scan_token_drift",
147
150
  "get_conventions",
148
151
  "set_conventions",
152
+ "record_precedent",
153
+ "list_precedents",
154
+ "update_precedent",
155
+ "remove_precedent",
156
+ "get_guards",
157
+ "set_guards",
158
+ "audit_voice",
159
+ "audit_voice_drift",
160
+ "audit_readiness",
161
+ "audit_ghosts",
149
162
  // Components
150
163
  "get_local_components",
151
164
  "get_instance_overrides",
@@ -1350,10 +1363,10 @@ import { z as z9 } from "zod";
1350
1363
  function register8(server, sendCommand) {
1351
1364
  server.tool(
1352
1365
  "manage_pages",
1353
- "Page operations: list all pages, create/rename/delete a page, or set a page's background color. To make a page current, use navigate with switch_page.",
1366
+ "Page operations: list all pages, create/rename/delete a page, or set a page's background color. To make a page current, use navigate with switch_page. Guards: list_guards shows the designer's AI no-go zones; guard/unguard change them \u2014 ONLY do that when the designer explicitly asks (guards are theirs; every change shows in their activity feed). Writes into a guarded page are rejected at dispatch.",
1354
1367
  {
1355
- action: z9.enum(["list", "create", "rename", "delete", "set_background"]),
1356
- pageId: z9.string().optional().describe("Target page (rename/delete/set_background)"),
1368
+ action: z9.enum(["list", "create", "rename", "delete", "set_background", "list_guards", "guard", "unguard"]),
1369
+ pageId: z9.string().optional().describe("Target page (rename/delete/set_background/guard/unguard)"),
1357
1370
  name: z9.string().optional().describe("Page name (create/rename)"),
1358
1371
  color: colorSchema.optional().describe("Background color (set_background)")
1359
1372
  },
@@ -1376,6 +1389,18 @@ function register8(server, sendCommand) {
1376
1389
  case "set_background":
1377
1390
  result = await sendCommand("set_page_background", { pageId, color });
1378
1391
  break;
1392
+ case "list_guards":
1393
+ result = await sendCommand("get_guards", {});
1394
+ break;
1395
+ case "guard":
1396
+ case "unguard": {
1397
+ if (!pageId) throw new Error(`${action} needs pageId`);
1398
+ const state = await sendCommand("get_guards", {});
1399
+ const current = state.pages.filter((p) => p.guarded).map((p) => p.id);
1400
+ const next = action === "guard" ? [.../* @__PURE__ */ new Set([...current, pageId])] : current.filter((id) => id !== pageId);
1401
+ result = await sendCommand("set_guards", { pages: next });
1402
+ break;
1403
+ }
1379
1404
  }
1380
1405
  return jsonResult(result);
1381
1406
  } catch (error) {
@@ -1811,17 +1836,45 @@ function register16(server, sendCommand) {
1811
1836
  );
1812
1837
  server.tool(
1813
1838
  "manage_conventions",
1814
- "File-level design conventions \u2014 a CLAUDE.md that lives INSIDE this Figma file (shared plugin data, travels with the file). action:get reads it, action:set overwrites it with markdown. get_document_overview auto-includes it, and whatever it says (naming rules, spacing habits, do-not-touch areas, library preferences) should be FOLLOWED like user instructions. When the designer states a durable preference ('always use our green', 'never touch the Archive page'), offer to record it here so every future session \u2014 from any AI client \u2014 inherits it.",
1839
+ "The file's law: conventions (statutes) + precedents (case law), stored INSIDE this Figma file so they travel with it to every future session from any AI client. action:get returns both \u2014 read them BEFORE working and follow them like user instructions. Conventions are a markdown doc (action:set overwrites). Precedents are single adjudications the designer made \u2014 record one (action:record_precedent, one sentence, with refs to the tokens/nodes/pages it concerns) whenever the designer rules on something durable ('this deviation is intent, not drift', 'never restructure this table'), and SAY in your reply that you recorded it. Write results automatically surface precedents whose refs they touch.",
1815
1840
  {
1816
- action: z17.enum(["get", "set"]),
1817
- content: z17.string().optional().describe("set: the full markdown doc (overwrites; max 20k chars)")
1841
+ action: z17.enum([
1842
+ "get",
1843
+ "set",
1844
+ "record_precedent",
1845
+ "list_precedents",
1846
+ "update_precedent",
1847
+ "remove_precedent"
1848
+ ]),
1849
+ content: z17.string().optional().describe("set: the full markdown doc (overwrites; max 20k chars)"),
1850
+ kind: z17.enum(["decision", "intent", "correction"]).optional().describe("record/update_precedent: decision (an approval/rejection), intent (a deviation that is deliberate), correction (a do-it-differently ruling). Default intent"),
1851
+ text: z17.string().optional().describe("record/update_precedent: the adjudication, one sentence, \u2264280 chars"),
1852
+ id: z17.string().optional().describe("update/remove_precedent: precedent id (from list_precedents)"),
1853
+ refs: z17.object({
1854
+ tokens: z17.array(z17.string()).optional().describe("Variable IDs this precedent concerns"),
1855
+ nodes: z17.array(z17.string()).optional().describe("Node IDs this precedent concerns"),
1856
+ pages: z17.array(z17.string()).optional().describe("Page IDs this precedent concerns")
1857
+ }).optional().describe("What the precedent is anchored to \u2014 enables in-band surfacing when writes touch these"),
1858
+ limit: z17.number().optional().describe("list_precedents: max entries returned (default 50)")
1818
1859
  },
1819
- async ({ action, content }) => {
1860
+ async ({ action, content, kind, text, id, refs, limit }) => {
1820
1861
  try {
1821
- if (action === "set") {
1822
- return jsonResult(await sendCommand("set_conventions", { content: content ?? "" }));
1862
+ switch (action) {
1863
+ case "set":
1864
+ return jsonResult(await sendCommand("set_conventions", { content: content ?? "" }));
1865
+ case "record_precedent":
1866
+ return jsonResult(
1867
+ await sendCommand("record_precedent", { kind, text, refs, source: "chat" })
1868
+ );
1869
+ case "list_precedents":
1870
+ return jsonResult(await sendCommand("list_precedents", { limit }));
1871
+ case "update_precedent":
1872
+ return jsonResult(await sendCommand("update_precedent", { id, kind, text, refs }));
1873
+ case "remove_precedent":
1874
+ return jsonResult(await sendCommand("remove_precedent", { id }));
1875
+ default:
1876
+ return jsonResult(await sendCommand("get_conventions", {}));
1823
1877
  }
1824
- return jsonResult(await sendCommand("get_conventions", {}));
1825
1878
  } catch (error) {
1826
1879
  return errorResult(error);
1827
1880
  }
@@ -2496,25 +2549,65 @@ function register19(server, sendCommand) {
2496
2549
  registerAnalysisTools(interceptor, sendCommand);
2497
2550
  server.tool(
2498
2551
  "analyze_design",
2499
- "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), or overall (runs color/layout/components/accessibility and returns a weighted 0-100 health score \u2014 good for audits and reports). Defaults to the current selection; tokens defaults to the current page.",
2552
+ `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.`,
2500
2553
  {
2501
- aspect: z20.enum(["color", "layout", "components", "accessibility", "tokens", "overall"]),
2502
- nodeId: z20.string().optional().describe("Root node to analyze (default: current selection)")
2554
+ aspect: z20.enum(["color", "layout", "components", "accessibility", "tokens", "overall", "voice", "readiness", "ghosts"]),
2555
+ nodeId: z20.string().optional().describe("Root node to analyze (default: current selection)"),
2556
+ pageIds: z20.array(z20.string()).optional().describe("voice/ghosts: pages to scan (default: current page)")
2503
2557
  },
2504
2558
  { readOnlyHint: true },
2505
- async ({ aspect, nodeId }) => {
2559
+ async ({ aspect, nodeId, pageIds }) => {
2506
2560
  if (aspect === "overall") {
2507
2561
  return runOverallAudit(aspectHandlers, nodeId);
2508
2562
  }
2509
2563
  if (aspect === "tokens") {
2510
2564
  return runTokenDrift(sendCommand, nodeId);
2511
2565
  }
2566
+ if (aspect === "voice") {
2567
+ return runVoice(sendCommand, pageIds);
2568
+ }
2569
+ if (aspect === "readiness") {
2570
+ return runReadiness(sendCommand);
2571
+ }
2572
+ if (aspect === "ghosts") {
2573
+ return runGhosts(sendCommand, pageIds);
2574
+ }
2512
2575
  const handler = aspectHandlers.get(ASPECT_TOOL[aspect]);
2513
2576
  if (!handler) throw new Error(`Unknown aspect: ${aspect}`);
2514
2577
  return handler({ nodeId });
2515
2578
  }
2516
2579
  );
2517
2580
  }
2581
+ async function runVoice(sendCommand, pageIds) {
2582
+ const data = await sendCommand("audit_voice", { pageIds }, 12e4);
2583
+ const sig = (k) => (data.signatures?.[k] ?? []).slice(0, 3).map((e) => `${e.value} (${Math.round(e.share * 100)}%)`).join(", ") || "\u2014";
2584
+ return standardResult({
2585
+ summary: `Voice of ${data.pages?.join(", ")}: radius ${sig("cornerRadius")} \xB7 spacing ${sig("spacing")} \xB7 type ${sig("fontSize")} \xB7 ${Math.round((data.tokenizedPaintRate ?? 0) * 100)}% tokenized paint \xB7 ${Math.round((data.instanceRate ?? 0) * 100)}% instance rate (${data.nodesScanned} nodes).`,
2586
+ data,
2587
+ recommended_next: [
2588
+ { tool: "validate_design_rules", reason: "Check any node against this voice (voice_drift rule, advisory)" }
2589
+ ]
2590
+ });
2591
+ }
2592
+ async function runReadiness(sendCommand) {
2593
+ const data = await sendCommand("audit_readiness", {}, 12e4);
2594
+ const gaps = (data.topGaps ?? []).map((g) => `${g.dimension} (\u2212${g.lost}): ${g.fix}`);
2595
+ return standardResult({
2596
+ summary: `Agent-readiness: ${data.score ?? 0}/100.${gaps.length ? ` Top gaps: ${gaps.join(" | ")}` : " Fully wired."}`,
2597
+ data,
2598
+ recommended_next: (data.score ?? 0) < 80 ? [{ tool: "manage_conventions", reason: "Close the cheapest gap first \u2014 conventions and precedents compound" }] : []
2599
+ });
2600
+ }
2601
+ async function runGhosts(sendCommand, pageIds) {
2602
+ const data = await sendCommand("audit_ghosts", { pageIds }, 3e5);
2603
+ const ghosts = data.ghostRefs ?? 0;
2604
+ return standardResult({
2605
+ summary: ghosts === 0 ? `No ghost references on ${data.pages?.join(", ")} (${data.refsScanned} refs across ${data.nodesScanned} nodes; dangling: ${data.danglingRefs ?? 0}).` : `${ghosts} ghost reference(s) to ${data.ghostCount} soft-deleted variable(s) on ${data.pages?.join(", ")} \u2014 they still render but are invisible in pickers and dead on publish.`,
2606
+ data,
2607
+ warnings: ghosts > 0 ? [{ category: "general", message: `${ghosts} bindings reference soft-deleted variables` }] : [],
2608
+ recommended_next: ghosts > 0 ? [{ tool: "manage_variables", reason: "Rebind to live twins (compare terminal values, collection-aware) \u2014 never delete-and-recreate" }] : []
2609
+ });
2610
+ }
2518
2611
  async function runTokenDrift(sendCommand, nodeId) {
2519
2612
  const data = await sendCommand("scan_token_drift", { nodeId, fix: false }, 12e4);
2520
2613
  const matched = data.stats?.matched ?? 0;
@@ -3257,6 +3350,19 @@ function register20(server, sendCommand) {
3257
3350
  nodeId: targetId,
3258
3351
  fix: hasDefaultName ? { tool: "update_node", reason: "Rename with a semantic name", args: { nodeId: targetId } } : void 0
3259
3352
  });
3353
+ try {
3354
+ const drift = await sendCommand("audit_voice_drift", { nodeId: targetId }, 6e4);
3355
+ const count = drift?.flagCount ?? 0;
3356
+ const sample = (drift?.flags ?? []).slice(0, 3).map((f) => `${f.name}: ${f.property}=${f.value}`).join("; ");
3357
+ results.push({
3358
+ rule: "voice_drift",
3359
+ passed: true,
3360
+ severity: "info",
3361
+ message: count === 0 ? "On voice: every sampled value sits in this file's signature." : `${count} value(s) off this file's voice (advisory \u2014 may be intent): ${sample}${count > 3 ? " \u2026" : ""}. If intentional, record a precedent; if drift, align to the file's signature.`,
3362
+ nodeId: targetId
3363
+ });
3364
+ } catch {
3365
+ }
3260
3366
  if (nodeInfo.type === "INSTANCE" || nodeInfo.name?.toLowerCase().includes("button")) {
3261
3367
  const w = nodeInfo.width ?? 0;
3262
3368
  const h = nodeInfo.height ?? 0;
@@ -3496,7 +3602,7 @@ var design_tokens_strategy_default = "# Design Tokens Strategy \u2014 4-Tier Tok
3496
3602
  var component_architecture_strategy_default = "# Component Architecture \u2014 Component Conventions\n\n## Token-First Principle\nEvery visual property must reference a token. No raw values in components.\n- Colors \u2192 Tier 2 semantic tokens or Tier 3 component tokens\n- Sizes (height, spacing) \u2192 Tier 2 or Tier 3 size tokens\n- Typography \u2192 Tier 2 typography tokens\n- Border radius/width \u2192 Tier 2 border tokens\n\n## Tier 3 Token Pattern\nWhen a component needs brand-differentiated values:\n\n`{component}/color/{role}/{state}`\n- Roles: background, border, content\n- States: default, hover, pressed, disabled\n- Selected states: selected, selected-hover, selected-pressed, selected-disabled\n- Variants: primary, ai, critical, ghost (prefix before role)\n\nExample \u2014 Button tokens:\n- `button/color/background/default` \u2192 neutral button bg\n- `button/primary/color/background/hover` \u2192 primary variant hover\n- `button/size/sm` / `button/size/md` / `button/size/lg`\n\n## State Management\nStandard interactive states:\n- default \u2192 hover \u2192 pressed \u2192 disabled (always present)\n- selected \u2192 selected-hover \u2192 selected-pressed \u2192 selected-disabled (for toggleable elements)\n\n## Size Variants\nUse sm / md / lg consistently. Define as Tier 3 FLOAT tokens when brand-differentiated.\n- `{component}/size/sm`, `{component}/size/md`, `{component}/size/lg`\n\n## Component Structure\n1. Use auto-layout (VERTICAL or HORIZONTAL) for all containers\n2. Use HUG for content-driven sizes, FILL for responsive elements\n3. Use component properties for configurable aspects:\n - BOOLEAN: toggle visibility (icons, labels)\n - INSTANCE_SWAP: swap nested components\n - VARIANT: select between variant sets\n - TEXT: configurable text content\n\n## Workflow\n1. `get_selection_context` \u2192 understand current component structure\n2. `manage_components` (list) \u2192 check existing components\n3. Design the component with auto-layout\n4. Create variants for states using `manage_components` (create_set)\n5. `manage_variables` (bind) \u2192 attach Tier 2/3 tokens to all visual properties\n6. `manage_components` (set_props) \u2192 configure instance properties\n\n## Verification\n- `analyze_design` (aspect: components) \u2192 find detached instances, unused components\n- `verify_changes` \u2192 confirm properties match expected values\n- `get_node_data` (detail: variables) \u2192 ensure all fills/strokes are token-bound\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";
3497
3603
 
3498
3604
  // ../../docs/skills/design-audit-strategy.md
3499
- var design_audit_strategy_default = "# Design Audit Strategy\n\n## Phase 1: Overview\n1. `get_document_overview` \u2192 pages, component/style/variable counts\n2. `get_design_tokens` \u2192 token system structure (collections, modes, coverage)\n\n## Phase 2: Color & Token Audit\n1. `analyze_design` (aspect: color) \u2192 find colors not backed by variables\n2. `get_node_data` (detail: variables) on key nodes \u2192 verify correct tier references\n3. Check: components should reference Tier 2/3 tokens, never Tier 1 or raw values\n\n## Phase 3: Layout Quality\n1. `analyze_design` (aspect: layout) \u2192 detect missing auto-layout, inconsistent spacing\n2. Check: all containers should use auto-layout (not absolute positioning)\n3. Check: spacing should use token values, not arbitrary px\n\n## Phase 4: Component Health\n1. `analyze_design` (aspect: components) \u2192 detached instances, unused components\n2. `manage_components` (list) \u2192 review component inventory\n3. Check: all instances should be connected to their main component\n\n## Phase 5: Accessibility\n1. `analyze_design` (aspect: accessibility) \u2192 contrast ratios, touch target sizes\n2. Check: minimum contrast 4.5:1 for text, 3:1 for large text\n3. Check: touch targets \u2265 44\xD744px\n\n## Phase 6: Comprehensive Validation\n1. `validate_design_rules` \u2192 runs all checks in one pass:\n - token_coverage: % of nodes with variable bindings\n - auto_layout: % of frames using auto-layout\n - naming_convention: layer naming quality\n - touch_target_size: minimum size compliance\n\n## Reporting\n- `screenshot` before and after fixes\n- `annotate` (set) / `annotate` (set_multiple) to flag issues directly in the design\n- Summarize findings by severity (critical \u2192 warning \u2192 info)\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";
3605
+ var design_audit_strategy_default = "# Design Audit Strategy\n\n## Phase 1: Overview\n1. `get_document_overview` \u2192 pages, component/style/variable counts\n2. `get_design_tokens` \u2192 token system structure (collections, modes, coverage)\n\n## Phase 2: Color & Token Audit\n1. `analyze_design` (aspect: color) \u2192 find colors not backed by variables\n2. `get_node_data` (detail: variables) on key nodes \u2192 verify correct tier references\n3. Check: components should reference Tier 2/3 tokens, never Tier 1 or raw values\n\n## Phase 3: Layout Quality\n1. `analyze_design` (aspect: layout) \u2192 detect missing auto-layout, inconsistent spacing\n2. Check: all containers should use auto-layout (not absolute positioning)\n3. Check: spacing should use token values, not arbitrary px\n\n## Phase 4: Component Health\n1. `analyze_design` (aspect: components) \u2192 detached instances, unused components\n2. `manage_components` (list) \u2192 review component inventory\n3. Check: all instances should be connected to their main component\n\n## Phase 5: Accessibility\n1. `analyze_design` (aspect: accessibility) \u2192 contrast ratios, touch target sizes\n2. Check: minimum contrast 4.5:1 for text, 3:1 for large text\n3. Check: touch targets \u2265 44\xD744px\n\n## Phase 6: Comprehensive Validation\n1. `validate_design_rules` \u2192 runs all checks in one pass:\n - token_coverage: % of nodes with variable bindings\n - auto_layout: % of frames using auto-layout\n - naming_convention: layer naming quality\n - touch_target_size: minimum size compliance\n\n## Reporting\n- `screenshot` before and after fixes\n- `annotate` (set) / `annotate` (set_multiple) to flag issues directly in the design\n- Summarize findings by severity (critical \u2192 warning \u2192 info)\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\n## Gauges (0.4+)\n\nFold the gauges into audits where they fit: `aspect:readiness` opens the report (one number + top gaps), `aspect:ghosts` is a publish blocker when nonzero (live-list criterion), `aspect:voice` gives the file's signature so findings can distinguish drift from intent \u2014 and anything a precedent marks as intent is not a finding. For the full review-moment pass, load the `qa-gate` skill instead.\n";
3500
3606
 
3501
3607
  // ../../docs/skills/token-audit.md
3502
3608
  var token_audit_default = "# Token Audit \u2014 Token Hierarchy Compliance Check\n\n## Purpose\nVerify that a design correctly uses the 4-tier token hierarchy:\n- No raw color/size values (everything should be token-bound)\n- Correct tier references (components \u2192 Tier 2/3, never Tier 1 directly)\n- Consistent scope usage\n\n## Audit Process\n\n### Step 1: Scope the Audit\n- `get_selection_context` \u2192 identify the area to audit\n- Or `get_document_overview` \u2192 audit the full document\n\n### Step 2: Color Token Coverage\n- `analyze_design` (aspect: color) \u2192 find all colors and whether they're token-bound\n- Flag any raw hex values not backed by variables\n\n### Step 3: Variable Binding Check\n- `get_node_data` (detail: variables) on key nodes \u2192 check what's bound\n- For each binding, verify the variable comes from the correct tier:\n - \u2705 Component referencing Tier 2 (e.g., `color/content/default`)\n - \u2705 Component referencing Tier 3 (e.g., `button/color/background/default`)\n - \u274C Component referencing Tier 1 (e.g., `color-static/slate/500`)\n - \u274C Component referencing Tier 1 dynamic (e.g., `color-dynamic/slate/500`)\n\n### Step 4: Scope Validation\n- `manage_variables` (list) \u2192 check that scopes are set correctly:\n - Color content tokens \u2192 TEXT_FILL\n - Color background tokens \u2192 FRAME_FILL, SHAPE_FILL\n - Color border tokens \u2192 STROKE_COLOR\n - Size tokens \u2192 WIDTH_HEIGHT\n - Border radius \u2192 CORNER_RADIUS\n - Border width \u2192 STROKE_FLOAT\n\n### Step 5: Comprehensive Check\n- `validate_design_rules` \u2192 run all design rules at once\n\n## Output Format\nReport findings as:\n- **Coverage**: X% of nodes have token bindings\n- **Tier violations**: list of nodes referencing wrong tier\n- **Raw values**: list of unbound colors/sizes with suggested token mappings\n- **Scope issues**: variables with missing or incorrect scopes\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";
@@ -3508,11 +3614,17 @@ var component_spec_default = "# Component Specification Generator\n\n## Purpose\
3508
3614
  var comment_driven_tasks_default = '# Comment-driven tasks\n\nDesigners leave requests as **Figma comments**; you pick them up, do the work, and reply on the thread. This turns Relai into an async collaborator without the designer ever leaving Figma \u2014 comments are their native input surface.\n\n## Honest model\n\nThis is **polling, not a live feed**. An MCP server only acts when called, so requests are noticed when you check \u2014 at the start of a session, when the user says "check the comments", or on a loop the user runs in their own client. Say so if the user expects real-time pickup.\n\nRequires `FIGMA_TOKEN` (personal access token with comment scopes). A free-plan token works on the user\'s own files.\n\n## The loop\n\n1. **Scan** \u2014 `manage_comments` `action:"list"` with `unresolved:true`, plus `since:<checkedAt from the previous scan>` after the first pass. Keep the returned `checkedAt` as the next cursor.\n2. **Select** \u2014 treat a comment as a task when it reads as an instruction ("make this...", "fix...", "@relai ..."). Skip chatter between humans and threads you already replied to (your replies carry your token account\'s handle).\n3. **Anchor** \u2014 pinned comments carry a `nodeId`. Start there: `get_node_details` / `screenshot` for context. Unpinned comments apply to the file broadly; ask before guessing a target.\n4. **Claim** \u2014 reply on the thread *before* working: `action:"reply"` with a one-liner like `\u{1F916} On it \u2014 <what you understood>`. This prevents double-work across sessions and tells the designer they were heard.\n5. **Execute** \u2014 normal Relai flow: design-system first, verify with `verify_visual` / `screenshot`.\n6. **Report back** \u2014 reply on the same thread with what changed and the node ids touched, e.g. `\u{1F916} Done: CTA restyled to primary/600, radius token applied (2 nodes). Reply here if it\'s off.` The REST API cannot mark threads resolved \u2014 the designer resolves it, which doubles as their sign-off.\n\n## One-shot phrasing for users\n\nUsers can drive the whole loop with a single ask: *"Check the file\'s comments and handle anything addressed to you, then report back on each thread."* For a standing loop, they re-issue that ask periodically (or wire it to their client\'s scheduler if it has one).\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';
3509
3615
 
3510
3616
  // ../../docs/skills/design-system-first.md
3511
- var design_system_first_default = '# Design-system-first building\n\nThe difference between "AI slop" and work a design team accepts is almost always this: **slop draws new shapes; good work reuses the file\'s own system**. Before building any UI in a file that has one, wire yourself to it.\n\n## The sequence\n\n1. **`get_design_system`** \u2014 one call, cached per session. Read what exists: components (sorted by usage \u2014 high-usage items are the file\'s vocabulary), variable collections, styles, and the remote items the file already consumes. If the team\'s library lives in another file and `FIGMA_TOKEN` is set, pass `libraryFileUrl` for the full catalog.\n2. **`manage_conventions` (get)** \u2014 if the file carries conventions, they outrank your defaults. (`get_document_overview` includes them automatically.)\n3. **Compose from instances** \u2014 `manage_components` `action:"instantiate"` with the component key (works for local AND enabled-library keys; remote keys import automatically). Set variants/text via `set_props`. Only draw raw shapes for genuinely new elements.\n4. **Bind, don\'t hardcode** \u2014 colors/spacing/radius come from the file\'s variables (`manage_variables` `action:"bind"`, or variable fields in `set_properties`). If you must hardcode during exploration, run `analyze_design aspect:"tokens"` afterward and fix with `manage_variables action:"tokenize" fix:true`.\n5. **Verify like a designer** \u2014 `screenshot` / `verify_visual` after each increment, not at the end.\n\n## When the file has no system\n\nDon\'t invent variables for a one-off request. But if the work IS system-shaped (a palette, a type scale, components), build variables first, then styles/components on top \u2014 and offer to record naming decisions in `manage_conventions`.\n\n## Signals you\'re drifting\n\n- You drew a rectangle that looks like an existing Button/Card/Badge \u2192 replace it with an instance.\n- A hex value appears in your code that `get_design_system` lists as a variable \u2192 bind instead.\n- You renamed or restructured library instances the designer didn\'t ask about \u2192 undo; instances belong to their component.\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';
3617
+ var design_system_first_default = '# Design-system-first building\n\nThe difference between "AI slop" and work a design team accepts is almost always this: **slop draws new shapes; good work reuses the file\'s own system**. Before building any UI in a file that has one, wire yourself to it.\n\n## The sequence\n\n1. **`get_design_system`** \u2014 one call, cached per session. Read what exists: components (sorted by usage \u2014 high-usage items are the file\'s vocabulary), variable collections, styles, and the remote items the file already consumes. If the team\'s library lives in another file and `FIGMA_TOKEN` is set, pass `libraryFileUrl` for the full catalog.\n2. **`manage_conventions` (get)** \u2014 if the file carries conventions or precedents, they outrank your defaults: conventions are the statutes, precedents are the designer\'s case law (a precedent saying a deviation is intent means you never "fix" it). (`get_document_overview` includes conventions automatically.)\n3. **Compose from instances** \u2014 `manage_components` `action:"instantiate"` with the component key (works for local AND enabled-library keys; remote keys import automatically). Set variants/text via `set_props`. Only draw raw shapes for genuinely new elements.\n4. **Bind, don\'t hardcode** \u2014 colors/spacing/radius come from the file\'s variables (`manage_variables` `action:"bind"`, or variable fields in `set_properties`). If you must hardcode during exploration, run `analyze_design aspect:"tokens"` afterward and fix with `manage_variables action:"tokenize" fix:true`.\n5. **Verify like a designer** \u2014 `screenshot` / `verify_visual` after each increment, not at the end.\n\n## When the file has no system\n\nDon\'t invent variables for a one-off request. But if the work IS system-shaped (a palette, a type scale, components), build variables first, then styles/components on top \u2014 and offer to record naming decisions in `manage_conventions`.\n\n## Signals you\'re drifting\n\n- You drew a rectangle that looks like an existing Button/Card/Badge \u2192 replace it with an instance.\n- A hex value appears in your code that `get_design_system` lists as a variable \u2192 bind instead.\n- You renamed or restructured library instances the designer didn\'t ask about \u2192 undo; instances belong to their component.\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';
3512
3618
 
3513
3619
  // ../../docs/skills/janitorial-cleanup.md
3514
3620
  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';
3515
3621
 
3622
+ // ../../docs/skills/memory-and-precedents.md
3623
+ 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\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';
3624
+
3625
+ // ../../docs/skills/qa-gate.md
3626
+ 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.\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';
3627
+
3516
3628
  // src/prompts.ts
3517
3629
  var SKILLS = [
3518
3630
  [
@@ -3559,6 +3671,16 @@ var SKILLS = [
3559
3671
  "janitorial-cleanup",
3560
3672
  "Bulk cleanup recipes: content-based layer renaming, tokenizing hardcoded values, spacing normalization, detached-instance sweeps \u2014 with preview-first discipline.",
3561
3673
  janitorial_cleanup_default
3674
+ ],
3675
+ [
3676
+ "memory-and-precedents",
3677
+ "The file's case law: when to record a designer adjudication as a precedent, when not to, and how memory is maintained. Load in any session that adjudicates or touches a file with existing precedents.",
3678
+ memory_and_precedents_default
3679
+ ],
3680
+ [
3681
+ "qa-gate",
3682
+ "Review-moment full physical: readiness + ghosts + voice + health + tokens composed into one verdicted report with evidence. Load when asked 'is this ready?' or before a library merge.",
3683
+ qa_gate_default
3562
3684
  ]
3563
3685
  ];
3564
3686
  function registerPrompts(server) {
@@ -3572,6 +3694,16 @@ function registerPrompts(server) {
3572
3694
  ]
3573
3695
  }));
3574
3696
  }
3697
+ for (const skill of loadUserSkills().skills) {
3698
+ server.prompt(`user:${skill.name}`, skill.description, () => ({
3699
+ messages: [
3700
+ {
3701
+ role: "user",
3702
+ content: { type: "text", text: skill.text }
3703
+ }
3704
+ ]
3705
+ }));
3706
+ }
3575
3707
  }
3576
3708
 
3577
3709
  export {
@@ -3585,4 +3717,4 @@ export {
3585
3717
  listToolCatalog,
3586
3718
  registerPrompts
3587
3719
  };
3588
- //# sourceMappingURL=chunk-RDSEEOKP.js.map
3720
+ //# sourceMappingURL=chunk-XFLMHHMX.js.map