Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Scaffold new Starchild skills with correct frontmatter, directory structure, and progressive-disclosure design principles.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/validate_skill.py
1#!/usr/bin/env python32"""3Skill Validator - Validates a skill directory structure45Usage:6python validate_skill.py <path/to/skill-folder>78Example:9python validate_skill.py ./workspace/skills/my-skill10"""1112import re13import sys14from pathlib import Path1516# Try to import yaml, fallback to basic parsing if not available17try:18import yaml19HAS_YAML = True20except ImportError:21HAS_YAML = False222324def extract_frontmatter(content: str) -> tuple:25"""Extract YAML frontmatter from markdown content."""26if not content.startswith("---"):27return None, content2829# Find the closing ---30end_match = re.search(r"\n---\n", content[3:])31if not end_match:32return None, content3334frontmatter_text = content[3:end_match.start() + 3]35body = content[end_match.end() + 3:]3637if HAS_YAML:38try:39frontmatter = yaml.safe_load(frontmatter_text)40except yaml.YAMLError as e:41return {"_error": str(e)}, body42else:43# Basic parsing without yaml library44frontmatter = {}45for line in frontmatter_text.split("\n"):46if ":" in line:47key, value = line.split(":", 1)48frontmatter[key.strip()] = value.strip()4950return frontmatter, body515253def lint_warnings(frontmatter: dict, body: str) -> list:54"""55Check for non-fatal issues and return warnings.56These are suggestions, not errors — the skill is still valid.57"""58warnings = []5960# emoji at top level instead of inside metadata.starchild61if "emoji" in frontmatter:62meta = frontmatter.get("metadata")63has_starchild_emoji = (64isinstance(meta, dict)65and isinstance(meta.get("starchild"), dict)66and meta["starchild"].get("emoji")67)68if not has_starchild_emoji:69warnings.append(70"emoji is at top level — move it to metadata.starchild.emoji "71"so the loader picks it up correctly"72)7374# requires at top level instead of inside metadata.starchild75if "requires" in frontmatter:76meta = frontmatter.get("metadata")77has_starchild_requires = (78isinstance(meta, dict)79and isinstance(meta.get("starchild"), dict)80and meta["starchild"].get("requires")81)82if not has_starchild_requires:83warnings.append(84"requires is at top level — move it to metadata.starchild.requires "85"so the loader picks it up correctly"86)8788# any_bins used instead of anyBins (camelCase)89meta = frontmatter.get("metadata")90if isinstance(meta, dict):91sc = meta.get("starchild") or meta.get("openclaw")92if isinstance(sc, dict):93req = sc.get("requires")94if isinstance(req, dict) and "any_bins" in req:95warnings.append(96"requires.any_bins should be requires.anyBins (camelCase) "97"in metadata.starchild"98)99100# Also check top-level requires for any_bins101top_req = frontmatter.get("requires")102if isinstance(top_req, dict) and "any_bins" in top_req:103warnings.append(104"requires.any_bins should be requires.anyBins (camelCase)"105)106107# Description missing "Use when" trigger pattern108desc = frontmatter.get("description", "")109if desc and "[TODO" not in desc:110desc_lower = desc.lower()111if "use when" not in desc_lower and "use for" not in desc_lower:112warnings.append(113'description lacks a "Use when" trigger — consider adding '114'"Use when <specific scenarios>" to help the agent decide '115"when to activate this skill"116)117118return warnings119120121def validate_skill(skill_path: Path) -> tuple:122"""123Validate a skill directory.124125Returns:126(is_valid, message, warnings)127"""128skill_path = Path(skill_path).resolve()129130# Check directory exists131if not skill_path.exists():132return False, f"Directory not found: {skill_path}", []133134if not skill_path.is_dir():135return False, f"Not a directory: {skill_path}", []136137# Check SKILL.md exists138skill_md = skill_path / "SKILL.md"139if not skill_md.exists():140return False, "SKILL.md not found", []141142# Read and parse SKILL.md143try:144content = skill_md.read_text(encoding="utf-8")145except Exception as e:146return False, f"Error reading SKILL.md: {e}", []147148# Extract frontmatter149frontmatter, body = extract_frontmatter(content)150151if frontmatter is None:152return False, "SKILL.md must start with YAML frontmatter (---)", []153154if "_error" in frontmatter:155return False, f"Invalid YAML frontmatter: {frontmatter['_error']}", []156157# Check required fields158if not frontmatter.get("name"):159return False, "Frontmatter missing required field: name", []160161if not frontmatter.get("description"):162return False, "Frontmatter missing required field: description", []163164# Check description is not placeholder165desc = frontmatter.get("description", "")166if "[TODO" in desc or not desc.strip():167return False, "Description contains TODO or is empty - please complete it", []168169# Check skill name format170name = frontmatter.get("name", "")171normalized = re.sub(r"[^a-z0-9-]", "", name.lower())172if name != normalized:173return False, f"Skill name should be lowercase hyphen-case: '{name}' -> '{normalized}'", []174175# Check body has content176if len(body.strip()) < 50:177return False, "SKILL.md body is too short (< 50 chars)", []178179# Collect lint warnings180warns = lint_warnings(frontmatter, body)181182# Warn about TODOs in body183todo_count = body.count("[TODO")184if todo_count > 0:185return True, f"Valid (but has {todo_count} TODO items remaining)", warns186187# Check line count188line_count = len(content.split("\n"))189if line_count > 500:190return True, f"Valid (but {line_count} lines - consider splitting to references/)", warns191192return True, "Valid skill", warns193194195def main():196if len(sys.argv) < 2:197print("Usage: python validate_skill.py <path/to/skill-folder>")198print("\nExample:")199print(" python validate_skill.py ./workspace/skills/my-skill")200sys.exit(1)201202skill_path = sys.argv[1]203print(f"Validating skill: {skill_path}\n")204205is_valid, message, warnings = validate_skill(skill_path)206207if is_valid:208print(f"[OK] {message}")209else:210print(f"[ERROR] {message}")211212# Print warnings213for warn in warnings:214print(f"[WARN] {warn}")215216sys.exit(0 if is_valid else 1)217218219if __name__ == "__main__":220main()221