Add and ship OpenCode support for one Claude Code plugin at a time. Includes core migration to a reviewable `opencode-plugin/` adapter, maintainer-facing docs, and follow-up CI/versioning setup for package publishing or skill-copy drift checks.
92
92%
Does it follow best practices?
Impact
97%
1.25xAverage score across 2 eval scenarios
Passed
No known issues
#!/usr/bin/env python3
"""Validate Agent Skill files for OpenCode-compatible sharing."""
from __future__ import annotations
import argparse
from pathlib import Path
from _common import SKILL_NAME_RE, iter_skill_files, parse_frontmatter, print_result, rel
DISCOVERY_PREFIXES = (
"skills/",
".claude/skills/",
".agents/skills/",
".opencode/skills/",
"opencode-plugin/skills/",
)
def is_discoverable_or_packaged(relative_path: str) -> bool:
return any(relative_path.startswith(prefix) for prefix in DISCOVERY_PREFIXES)
def validate_skill_file(path: Path, root: Path) -> tuple[list[dict[str, str]], list[str], list[str]]:
checks: list[dict[str, str]] = []
warnings: list[str] = []
errors: list[str] = []
relative = rel(path, root)
frontmatter, parse_errors = parse_frontmatter(path)
for error in parse_errors:
errors.append(f"{relative}: {error}")
name = frontmatter.get("name", "")
description = frontmatter.get("description", "")
expected_name = path.parent.name
def add_check(text: str, passed: bool, evidence: str) -> None:
checks.append({"path": relative, "text": text, "passed": str(passed).lower(), "evidence": evidence})
add_check("frontmatter has name", bool(name), name or "missing")
add_check("frontmatter has description", bool(description), f"{len(description)} chars" if description else "missing")
add_check("name matches OpenCode skill-name format", bool(name and SKILL_NAME_RE.match(name)), name or "missing")
add_check("name matches containing directory", name == expected_name, f"name={name or '<missing>'}, directory={expected_name}")
add_check("description length is 1-1024 characters", 1 <= len(description) <= 1024, f"{len(description)} chars")
add_check("path is discoverable or package-local", is_discoverable_or_packaged(relative), relative)
if description and len(description) > 1024:
errors.append(f"{relative}: description exceeds 1024 characters")
if name and not SKILL_NAME_RE.match(name):
errors.append(f"{relative}: name does not match ^[a-z0-9]+(-[a-z0-9]+)*$")
if name and name != expected_name:
errors.append(f"{relative}: name does not match containing directory {expected_name}")
if not is_discoverable_or_packaged(relative):
warnings.append(f"{relative}: path is not a known OpenCode/Claude/agent discovery path or opencode-plugin packaged skill path")
return checks, warnings, errors
def validate_skills(root: Path) -> dict:
root = root.resolve()
checks: list[dict[str, str]] = []
warnings: list[str] = []
errors: list[str] = []
skill_files = iter_skill_files(root)
if not skill_files:
warnings.append("No SKILL.md files found")
for skill_file in skill_files:
file_checks, file_warnings, file_errors = validate_skill_file(skill_file, root)
checks.extend(file_checks)
warnings.extend(file_warnings)
errors.extend(file_errors)
passed = sum(1 for check in checks if check["passed"] == "true")
return {
"title": "Agent Skill Validation",
"summary": {
"target": str(root),
"skill_count": len(skill_files),
"checks_passed": passed,
"checks_total": len(checks),
"status": "fail" if errors else "pass",
},
"checks": checks,
"warnings": warnings,
"errors": errors,
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("target", nargs="?", default=".", help="Repo to validate")
parser.add_argument("--format", choices=("json", "markdown"), default="json")
args = parser.parse_args()
result = validate_skills(Path(args.target))
print_result(result, args.format)
return 1 if result["errors"] else 0
if __name__ == "__main__":
raise SystemExit(main())