From f38fabf01d55af6ec92034beb8aa198552a94883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Lange?= Date: Thu, 30 Apr 2026 20:18:29 +0200 Subject: [PATCH] feat: add framework compatibility hints to validate action - Warn when opencode-command/claude-code-command declared but only SKILL.md exists - Warn when cursor framework declared with legacy .mdc files - Tests for both new checks --- src/validate.py | 22 +++++++++++++++ tests/test_validate.py | 63 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/src/validate.py b/src/validate.py index 7a7d2a1..70f2124 100644 --- a/src/validate.py +++ b/src/validate.py @@ -134,6 +134,28 @@ def lint_package(manifest_path: Path) -> dict: if not manifests: findings["errors"].append("No capability manifest file found (*.yaml or *.yml)") + # Framework compatibility hints + manifest = load_manifest(str(manifest_path)) if manifest_path.exists() else {} + frameworks = manifest.get("frameworks", []) + if isinstance(frameworks, list): + has_cmd = "opencode-command" in frameworks or "claude-code-command" in frameworks + has_skill = any(f in frameworks for f in ("opencode", "claude-code", "gemini-cli", "cursor")) + skill_md = (pkg_dir / "SKILL.md") + if has_cmd and skill_md.exists() and any(f.endswith(".md") for f in os.listdir(pkg_dir) if f == "SKILL.md"): + findings["warnings"].append( + "Framework conflict: 'opencode-command' or 'claude-code-command' declared but only SKILL.md found. " + "Command adapters create command symlinks that can't parse SKILL.md YAML frontmatter. " + "Provide a separate .md file for commands or remove command frameworks." + ) + # Cursor: .mdc is legacy, should use .cursor/skills/ + if "cursor" in frameworks: + mdc_files = list(pkg_dir.glob("*.mdc")) + if mdc_files: + findings["warnings"].append( + "Cursor legacy format: .mdc files found. Cursor now expects SKILL.md in .cursor/skills/. " + "Convert .mdc files to SKILL.md format." + ) + return findings diff --git a/tests/test_validate.py b/tests/test_validate.py index ff5c204..334a36e 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -4,6 +4,7 @@ import subprocess import sys import tempfile +from pathlib import Path SCRIPT_DIR = os.path.join(os.path.dirname(__file__), "..", "src") @@ -184,6 +185,66 @@ def test_exchange_metadata_output(): shutil.rmtree(tmpdir) +def test_framework_command_conflict(): + """Warn when command frameworks declared but only SKILL.md exists.""" + with tempfile.TemporaryDirectory() as tmpdir: + manifest = tmpdir + "/capability.yaml" + import yaml + with open(manifest, "w") as f: + yaml.dump({ + "name": "test-skill", + "version": "1.0.0", + "kind": "skill", + "frameworks": ["opencode", "opencode-command"], + }, f) + Path(tmpdir + "/SKILL.md").write_text("# Test Skill\n\nSkill content.") + env = { + **os.environ, + "MANIFEST_PATH": manifest, + "STRICT_MODE": "false", + "GITHUB_OUTPUT": os.devnull, + } + result = subprocess.run( + [sys.executable, os.path.join(SCRIPT_DIR, "validate.py")], + capture_output=True, text=True, env=env, cwd=tmpdir, + ) + stdout = result.stdout.split("::set-output")[0].strip() + output = json.loads(stdout) + warnings = output["findings"]["warnings"] + assert any("opencode-command" in w and "SKILL.md" in w for w in warnings), \ + f"Expected command/SKILL.md conflict warning, got: {warnings}" + + +def test_cursor_mdc_legacy_warning(): + """Warn when cursor framework declared with .mdc files.""" + with tempfile.TemporaryDirectory() as tmpdir: + manifest = tmpdir + "/capability.yaml" + import yaml + with open(manifest, "w") as f: + yaml.dump({ + "name": "test-skill", + "version": "1.0.0", + "kind": "skill", + "frameworks": ["cursor"], + }, f) + Path(tmpdir + "/rules.mdc").write_text("# Old Cursor rules") + env = { + **os.environ, + "MANIFEST_PATH": manifest, + "STRICT_MODE": "false", + "GITHUB_OUTPUT": os.devnull, + } + result = subprocess.run( + [sys.executable, os.path.join(SCRIPT_DIR, "validate.py")], + capture_output=True, text=True, env=env, cwd=tmpdir, + ) + stdout = result.stdout.split("::set-output")[0].strip() + output = json.loads(stdout) + warnings = output["findings"]["warnings"] + assert any(".mdc" in w and "cursor" in w.lower() for w in warnings), \ + f"Expected cursor/.mdc legacy warning, got: {warnings}" + + if __name__ == "__main__": test_valid_manifest() test_invalid_kind() @@ -193,4 +254,6 @@ def test_exchange_metadata_output(): test_strict_mode_dot_file() test_no_dot_file_passes() test_exchange_metadata_output() + test_framework_command_conflict() + test_cursor_mdc_legacy_warning() print("All validate tests passed!")