Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
63 changes: 63 additions & 0 deletions tests/test_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import subprocess
import sys
import tempfile
from pathlib import Path

SCRIPT_DIR = os.path.join(os.path.dirname(__file__), "..", "src")

Expand Down Expand Up @@ -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()
Expand All @@ -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!")
Loading