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..3e5ab70c0 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,81 @@ for (const entry of marketplace.plugins ?? []) { } } -// 3. Report results +// 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) { + 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}"` + ); + } +} + +// 5. 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,