Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
A comprehensive collection of Agent Skills for context engineering, multi-agent architectures, and production agent systems.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
researcher/scripts/validate_platform_compat.py
1#!/usr/bin/env python32"""Validate platform compatibility for the published Agent Skills corpus.34This script checks the parts of Cursor, Claude Code, Codex, and Open Plugins5compatibility that are deterministic from the repository contents:67- Published skills are strict-YAML parseable and match Agent Skills naming rules.8- Open Plugins and Claude marketplace manifests discover the same skill set.9- Local/manual install layouts preserve `skill-name/SKILL.md` under the platform10skill roots documented by Cursor, Claude Code, and Codex.11- If the upstream `agentskills` CLI from `skills-ref` is installed, each skill is12validated with the reference Agent Skills validator as well.13"""1415from __future__ import annotations1617import argparse18import json19import re20import shutil21import subprocess22import sys23import tempfile24from pathlib import Path25from typing import Any2627from skill_frontmatter import parse_frontmatter282930ROOT = Path(__file__).resolve().parents[2]31PLATFORM_SKILL_ROOTS = [32".cursor/skills",33".claude/skills",34".codex/skills",35".agents/skills",36]37SKILL_NAME_PATTERN = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")383940def error(message: str, errors: list[str]) -> None:41errors.append(message)424344def load_json(path: Path, errors: list[str]) -> dict[str, Any]:45try:46data = json.loads(path.read_text(encoding="utf-8"))47except (OSError, json.JSONDecodeError) as exc:48error(f"{path.relative_to(ROOT)}: {exc}", errors)49return {}50if not isinstance(data, dict):51error(f"{path.relative_to(ROOT)}: expected JSON object", errors)52return {}53return data545556def published_skill_dirs() -> list[Path]:57return sorted(path for path in (ROOT / "skills").iterdir() if (path / "SKILL.md").exists())585960def validate_skill_dir(skill_dir: Path, errors: list[str]) -> None:61skill_file = skill_dir / "SKILL.md"62if not skill_file.exists():63error(f"{skill_dir}: missing SKILL.md", errors)64return65data, issues = parse_frontmatter(skill_file.read_text(encoding="utf-8"))66for issue in issues:67error(f"{skill_file.relative_to(ROOT)}: {issue}", errors)68name = str(data.get("name", ""))69description = str(data.get("description", ""))70if name != skill_dir.name:71error(f"{skill_file.relative_to(ROOT)}: name {name!r} does not match directory {skill_dir.name!r}", errors)72if not SKILL_NAME_PATTERN.fullmatch(name):73error(f"{skill_file.relative_to(ROOT)}: name must be lowercase kebab-case without repeated hyphens", errors)74if len(description) > 1024:75error(f"{skill_file.relative_to(ROOT)}: description exceeds 1024 characters", errors)767778def validate_manifests(skill_names: list[str], errors: list[str]) -> None:79plugin_dir = ROOT / ".plugin"80plugin_path = plugin_dir / "plugin.json"81plugin = load_json(plugin_path, errors)82unexpected = sorted(path.name for path in plugin_dir.iterdir() if path.name != "plugin.json")83if unexpected:84error(f".plugin/ must contain only plugin.json, found: {unexpected}", errors)8586raw_skills = plugin.get("skills")87if isinstance(raw_skills, str):88skill_paths = [raw_skills]89elif isinstance(raw_skills, list) and all(isinstance(item, str) for item in raw_skills):90skill_paths = list(raw_skills)91else:92error(".plugin/plugin.json: skills must be a string or list of strings", errors)93skill_paths = []9495discovered: set[str] = set()96for raw_path in skill_paths:97path = Path(raw_path)98if not raw_path.startswith("./"):99error(f".plugin/plugin.json: skill path must start with './': {raw_path}", errors)100continue101if path.is_absolute() or ".." in path.parts:102error(f".plugin/plugin.json: skill path escapes plugin root: {raw_path}", errors)103continue104full_path = ROOT / raw_path105if not full_path.exists():106error(f".plugin/plugin.json: skill path does not exist: {raw_path}", errors)107continue108if (full_path / "SKILL.md").exists():109discovered.add(full_path.name)110elif full_path.is_dir():111discovered.update(path.name for path in full_path.iterdir() if (path / "SKILL.md").exists())112else:113error(f".plugin/plugin.json: skill path is not a directory: {raw_path}", errors)114if sorted(discovered) != skill_names:115error(f".plugin/plugin.json: discovered skills {sorted(discovered)} != corpus {skill_names}", errors)116117marketplace = load_json(ROOT / ".claude-plugin" / "marketplace.json", errors)118plugins = marketplace.get("plugins")119if not isinstance(plugins, list) or len(plugins) != 1:120error(".claude-plugin/marketplace.json: expected exactly one bundled plugin", errors)121return122entry = plugins[0]123if not isinstance(entry, dict):124error(".claude-plugin/marketplace.json: plugin entry must be an object", errors)125return126if entry.get("name") != plugin.get("name"):127error(".claude-plugin/marketplace.json: plugin name differs from .plugin/plugin.json", errors)128if entry.get("source") != "./":129error(".claude-plugin/marketplace.json: source must be './'", errors)130claude_skill_paths = entry.get("skills")131if not isinstance(claude_skill_paths, list) or not all(isinstance(item, str) for item in claude_skill_paths):132error(".claude-plugin/marketplace.json: skills must be a list of strings", errors)133return134claude_names = []135for raw_path in claude_skill_paths:136if not raw_path.startswith("./"):137error(f".claude-plugin/marketplace.json: skill path must start with './': {raw_path}", errors)138continue139path = ROOT / raw_path140if not (path / "SKILL.md").exists():141error(f".claude-plugin/marketplace.json: skill path missing SKILL.md: {raw_path}", errors)142continue143claude_names.append(path.name)144if sorted(claude_names) != skill_names:145error(f".claude-plugin/marketplace.json: discovered skills {sorted(claude_names)} != corpus {skill_names}", errors)146147148def validate_platform_install_layouts(skill_dirs: list[Path], errors: list[str]) -> None:149with tempfile.TemporaryDirectory(prefix="skill-platform-compat-") as tmp:150root = Path(tmp)151for platform_root in PLATFORM_SKILL_ROOTS:152target_root = root / platform_root153target_root.mkdir(parents=True, exist_ok=True)154for skill_dir in skill_dirs:155target = target_root / skill_dir.name156shutil.copytree(skill_dir, target)157validate_skill_dir(target, errors)158159160def run_reference_validator(skill_dirs: list[Path], require: bool, errors: list[str]) -> None:161agentskills = shutil.which("agentskills")162if agentskills is None:163if require:164error("agentskills CLI not found; install with `python -m pip install skills-ref`", errors)165return166167for skill_dir in skill_dirs:168completed = subprocess.run(169[agentskills, "validate", str(skill_dir)],170cwd=ROOT,171text=True,172capture_output=True,173check=False,174)175if completed.returncode != 0:176message = completed.stderr.strip() or completed.stdout.strip() or f"exit {completed.returncode}"177error(f"agentskills validate {skill_dir.relative_to(ROOT)} failed: {message}", errors)178179180def main() -> int:181parser = argparse.ArgumentParser(description="Validate cross-platform Agent Skills compatibility")182parser.add_argument(183"--require-reference-validator",184action="store_true",185help="Fail if the official agentskills CLI from skills-ref is unavailable.",186)187args = parser.parse_args()188189errors: list[str] = []190skill_dirs = published_skill_dirs()191skill_names = sorted(path.name for path in skill_dirs)192if not skill_dirs:193error("no published skills found under skills/", errors)194195for skill_dir in skill_dirs:196validate_skill_dir(skill_dir, errors)197validate_manifests(skill_names, errors)198validate_platform_install_layouts(skill_dirs, errors)199run_reference_validator(skill_dirs, args.require_reference_validator, errors)200201if errors:202for item in errors:203print(f"ERROR: {item}", file=sys.stderr)204return 1205206print(207"Platform compatibility passed: "208f"{len(skill_dirs)} skills, {len(PLATFORM_SKILL_ROOTS)} local install layouts"209)210return 0211212213if __name__ == "__main__":214raise SystemExit(main())215