From d8ed1d2677a5d19958b60010fb7c3f2e35448724 Mon Sep 17 00:00:00 2001 From: Mosquito1123 Date: Sun, 27 Sep 2026 16:59:17 +0800 Subject: [PATCH 1/2] Validate SKILL.md frontmatter in CI scripts/validate-plugins.mjs only validated marketplace.json and each plugin.json, and the workflow's paths filter excluded **/SKILL.md, so skill frontmatter was never checked and could drift silently. - Add schemas/skill.schema.json describing SKILL.md frontmatter (required `name` and `description`; name in kebab-case; unknown keys allowed so plugins can keep harness-specific options). - Parse and validate every SKILL.md frontmatter with Ajv, and require the frontmatter `name` to match the skill's folder name. - Add **/SKILL.md and scripts/** to the workflow paths filter and install the `yaml` parser. Fix the violations this exposes: - agent-compatibility/skills/check-agent-compatibility: quote a description containing ": " so the frontmatter parses (closes #381). - third_party/x/skills/x-api-mcp-guide: name "X MCP guide" -> "x-api-mcp-guide" (closes #269). - pstack/skills/poteto-mode: name "Poteto Mode" -> "poteto-mode" (closes #237). - pstack/skills/make-bot-ui: name "Make Bot UI" -> "make-bot-ui". - third_party/x/skills/x-chat: name "X Chat" -> "x-chat". - third_party/x-money/skills/x-money-guide: name "X Money guide" -> "x-money-guide". The validator is green on the updated tree and reports each of these when reintroduced. --- .github/workflows/validate-plugins.yml | 4 +- .../skills/check-agent-compatibility/SKILL.md | 2 +- pstack/skills/make-bot-ui/SKILL.md | 2 +- pstack/skills/poteto-mode/SKILL.md | 2 +- schemas/skill.schema.json | 22 ++++++++ scripts/validate-plugins.mjs | 55 ++++++++++++++++++- .../x-money/skills/x-money-guide/SKILL.md | 2 +- third_party/x/skills/x-api-mcp-guide/SKILL.md | 2 +- third_party/x/skills/x-chat/SKILL.md | 2 +- 9 files changed, 83 insertions(+), 10 deletions(-) create mode 100644 schemas/skill.schema.json diff --git a/.github/workflows/validate-plugins.yml b/.github/workflows/validate-plugins.yml index 03e8e1928..89cc7b5dc 100644 --- a/.github/workflows/validate-plugins.yml +++ b/.github/workflows/validate-plugins.yml @@ -5,7 +5,9 @@ on: paths: - ".cursor-plugin/marketplace.json" - "**/plugin.json" + - "**/SKILL.md" - "schemas/**" + - "scripts/**" jobs: validate: @@ -18,7 +20,7 @@ jobs: node-version: 20 - name: Install dependencies - run: npm install --no-save ajv ajv-formats + run: npm install --no-save ajv ajv-formats yaml - name: Validate plugin definitions run: node scripts/validate-plugins.mjs diff --git a/agent-compatibility/skills/check-agent-compatibility/SKILL.md b/agent-compatibility/skills/check-agent-compatibility/SKILL.md index 6d88d4a5f..40e4a0446 100644 --- a/agent-compatibility/skills/check-agent-compatibility/SKILL.md +++ b/agent-compatibility/skills/check-agent-compatibility/SKILL.md @@ -1,6 +1,6 @@ --- name: check-agent-compatibility -description: Run the full repository compatibility pass: scanner score, startup path, validation loop, and docs reliability. +description: "Run the full repository compatibility pass: scanner score, startup path, validation loop, and docs reliability." --- # Check agent compatibility diff --git a/pstack/skills/make-bot-ui/SKILL.md b/pstack/skills/make-bot-ui/SKILL.md index 285df7569..446342ca3 100644 --- a/pstack/skills/make-bot-ui/SKILL.md +++ b/pstack/skills/make-bot-ui/SKILL.md @@ -1,5 +1,5 @@ --- -name: Make Bot UI +name: make-bot-ui description: >- Use when building a custom UI (page, dashboard, buttons) that should wake a Grok Bot over a webhook, when the user must provide a webhook sender key, or diff --git a/pstack/skills/poteto-mode/SKILL.md b/pstack/skills/poteto-mode/SKILL.md index f8ddcae37..3b4dcbedf 100644 --- a/pstack/skills/poteto-mode/SKILL.md +++ b/pstack/skills/poteto-mode/SKILL.md @@ -1,5 +1,5 @@ --- -name: Poteto Mode +name: poteto-mode description: poteto's agent style for concise, detailed responses, deliberate subagents, unslopped prose, simple code, and verified work. Use for poteto, /poteto-mode, or requests to work in this style. disable-model-invocation: true mode: true diff --git a/schemas/skill.schema.json b/schemas/skill.schema.json new file mode 100644 index 000000000..e4eb1eb1d --- /dev/null +++ b/schemas/skill.schema.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://cursor.com/schemas/cursor-plugin/skill.json", + "title": "Cursor Skill Frontmatter", + "description": "Schema for the YAML frontmatter of a SKILL.md — defines a single skill's metadata. Unknown keys are permitted so plugins can add harness-specific options.", + "type": "object", + "required": ["name", "description"], + "additionalProperties": true, + "properties": { + "name": { + "type": "string", + "minLength": 1, + "pattern": "^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$", + "description": "Unique skill identifier in kebab-case. Must match the skill's folder name." + }, + "description": { + "type": "string", + "minLength": 1, + "description": "Short description of what the skill does and when to use it." + } + } +} diff --git a/scripts/validate-plugins.mjs b/scripts/validate-plugins.mjs index 6a7870854..aa78aa84b 100644 --- a/scripts/validate-plugins.mjs +++ b/scripts/validate-plugins.mjs @@ -1,10 +1,11 @@ #!/usr/bin/env node -import { readFileSync, existsSync } from "fs"; -import { resolve, dirname } from "path"; +import { readFileSync, existsSync, readdirSync } from "fs"; +import { resolve, dirname, basename, relative } from "path"; import { fileURLToPath } from "url"; import Ajv from "ajv"; import addFormats from "ajv-formats"; +import { parse as parseYaml } from "yaml"; const __dirname = dirname(fileURLToPath(import.meta.url)); const root = resolve(__dirname, ".."); @@ -17,12 +18,14 @@ const marketplaceSchema = loadJSON( resolve(root, "schemas/marketplace.schema.json") ); const pluginSchema = loadJSON(resolve(root, "schemas/plugin.schema.json")); +const skillSchema = loadJSON(resolve(root, "schemas/skill.schema.json")); const ajv = new Ajv({ allErrors: true }); addFormats(ajv); const validateMarketplace = ajv.compile(marketplaceSchema); const validatePlugin = ajv.compile(pluginSchema); +const validateSkill = ajv.compile(skillSchema); let errors = 0; @@ -92,7 +95,53 @@ for (const entry of marketplace.plugins ?? []) { } } -// 3. Report results +// 3. Validate skill frontmatter +const skillFiles = []; + +(function collectSkillFiles(dir) { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + if (entry.name === ".git" || entry.name === "node_modules") continue; + const full = resolve(dir, entry.name); + if (entry.isDirectory()) collectSkillFiles(full); + else if (entry.name === "SKILL.md") skillFiles.push(full); + } +})(root); + +for (const skillPath of skillFiles.sort()) { + const skillRel = relative(root, skillPath); + const text = readFileSync(skillPath, "utf-8"); + const frontmatterMatch = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text); + + if (!frontmatterMatch) { + fail(`${skillRel}: missing YAML frontmatter (file must start with "---")`); + continue; + } + + let frontmatter; + try { + frontmatter = parseYaml(frontmatterMatch[1]) ?? {}; + } catch (err) { + fail(`${skillRel}: frontmatter is not valid YAML (${err.message.split("\n")[0]})`); + continue; + } + + if (!validateSkill(frontmatter)) { + fail(`${skillRel}: frontmatter does not match schemas/skill.schema.json:`); + for (const err of validateSkill.errors) { + console.error(` ${err.instancePath || "/"}: ${err.message}`); + } + continue; + } + + const folder = basename(dirname(skillPath)); + if (frontmatter.name !== folder) { + fail( + `${skillRel}: frontmatter name "${frontmatter.name}" does not match folder name "${folder}"` + ); + } +} + +// 4. Report results if (errors > 0) { console.error(`\nValidation failed with ${errors} error(s).`); process.exit(1); diff --git a/third_party/x-money/skills/x-money-guide/SKILL.md b/third_party/x-money/skills/x-money-guide/SKILL.md index a40a45126..ee8738379 100644 --- a/third_party/x-money/skills/x-money-guide/SKILL.md +++ b/third_party/x-money/skills/x-money-guide/SKILL.md @@ -1,5 +1,5 @@ --- -name: X Money guide +name: x-money-guide description: >- Read this before the first X Money action in a session and again on any X Money error, refusal, or missing capability. Covers the approval rule for diff --git a/third_party/x/skills/x-api-mcp-guide/SKILL.md b/third_party/x/skills/x-api-mcp-guide/SKILL.md index 5bf111d80..43ed1d3de 100644 --- a/third_party/x/skills/x-api-mcp-guide/SKILL.md +++ b/third_party/x/skills/x-api-mcp-guide/SKILL.md @@ -1,5 +1,5 @@ --- -name: X MCP guide +name: x-api-mcp-guide description: >- ALWAYS read this when a user connects the X plugin or any X MCP, before using any X connection, and again on any X error. Do not call an X tool until this diff --git a/third_party/x/skills/x-chat/SKILL.md b/third_party/x/skills/x-chat/SKILL.md index 0d2b97a92..3ae4fb4cd 100644 --- a/third_party/x/skills/x-chat/SKILL.md +++ b/third_party/x/skills/x-chat/SKILL.md @@ -1,5 +1,5 @@ --- -name: X Chat +name: x-chat description: >- Read, summarize, or send encrypted X Chat (XChat) DMs via the X plugin MCP plus local chatxdk / xchat_lite.py. Use when the user mentions X Chat, xchat, From 3ea2c187c93edfeda2f123d5dadfa08357a9552a Mon Sep 17 00:00:00 2001 From: Mosquito1123 Date: Sun, 27 Sep 2026 17:52:53 +0800 Subject: [PATCH 2/2] Validate that every plugin is registered in the marketplace The validator only iterated the marketplace entries, so a plugin directory added without a marketplace entry would pass CI and never be listed. Walk the repository for `.cursor-plugin/plugin.json` files and fail when one is missing from `.cursor-plugin/marketplace.json`. --- scripts/validate-plugins.mjs | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/scripts/validate-plugins.mjs b/scripts/validate-plugins.mjs index aa78aa84b..3e5ab70c0 100644 --- a/scripts/validate-plugins.mjs +++ b/scripts/validate-plugins.mjs @@ -95,7 +95,35 @@ for (const entry of marketplace.plugins ?? []) { } } -// 3. Validate skill frontmatter +// 3. Check every plugin in the repository is registered in the marketplace +const marketplaceSources = new Set( + (marketplace.plugins ?? []).map((entry) => resolve(root, entry.source)) +); + +const pluginDirs = []; + +(function collectPluginDirs(dir) { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + if (entry.name === ".git" || entry.name === "node_modules") continue; + const full = resolve(dir, entry.name); + if (!entry.isDirectory()) continue; + if (existsSync(resolve(full, ".cursor-plugin/plugin.json"))) { + pluginDirs.push(full); + } else { + collectPluginDirs(full); + } + } +})(root); + +for (const pluginDir of pluginDirs) { + if (!marketplaceSources.has(pluginDir)) { + fail( + `${relative(root, pluginDir)}: has a .cursor-plugin/plugin.json but is not listed in .cursor-plugin/marketplace.json` + ); + } +} + +// 4. Validate skill frontmatter const skillFiles = []; (function collectSkillFiles(dir) { @@ -141,7 +169,7 @@ for (const skillPath of skillFiles.sort()) { } } -// 4. Report results +// 5. Report results if (errors > 0) { console.error(`\nValidation failed with ${errors} error(s).`); process.exit(1);