#!/usr/bin/env python3 """ SKILL.md Validator for Hermes Agent Validates SKILL.md files against the hermes-agent-skill-authoring standards. """ import re import sys from pathlib import Path try: from ruamel.yaml import YAML YAML_AVAILABLE = True except ImportError: YAML_AVAILABLE = False def validate_skill_md(file_path): """Validate a SKILL.md file against the hermes authoring standards.""" errors = [] warnings = [] path = Path(file_path) if not path.exists(): return {'valid': False, 'errors': [f"File not found: {path}"], 'warnings': []} content = path.read_text() # Check 1: Starts with --- as first bytes if not content.startswith("---\n") and not (len(content) > 4 and content[:4] == "---\r"): errors.append("File must start with '---' as the first bytes (no leading blank line)") # Check 2: Find closing --- body = "" if content.startswith("---"): # Find the closing delimiter m = re.search(r'\n---\s*\n', content[3:]) if not m: errors.append("Missing closing '---\\n' after frontmatter") return {'valid': False, 'errors': errors, 'warnings': warnings} frontmatter_end = 3 + m.end() - 1 # Position after closing --- frontmatter_text = content[3:frontmatter_end - 1] body = content[frontmatter_end:] # Rest is body else: return {'valid': False, 'errors': errors + ["File must start with '---'"], 'warnings': warnings} # Check 3: Parse YAML frontmatter fm = {} if YAML_AVAILABLE: yaml = YAML(typ='safe') try: fm = yaml.load(frontmatter_text) or {} if not isinstance(fm, dict): errors.append(f"Frontmatter must be a YAML mapping, got {type(fm).__name__}") return {'valid': False, 'errors': errors, 'warnings': warnings} except Exception as e: errors.append(f"YAML parse error: {e}") return {'valid': False, 'errors': errors, 'warnings': warnings} else: warnings.append("ruamel.yaml not installed, skipping YAML validation") fm = parse_frontmatter_simple(frontmatter_text) # Validate required fields # Check name if 'name' not in fm: errors.append("Missing required field: 'name'") elif not isinstance(fm['name'], str): errors.append("'name' must be a string") elif len(fm['name']) > 64: warnings.append(f"'name' exceeds 64 chars: {len(fm['name'])}") elif not re.match(r'^[a-z0-9](?:-[a-z0-9]+)*$', fm['name']): warnings.append(f"'name' should be lowercase-hyphenated: {fm['name']}") # Check description if 'description' not in fm: errors.append("Missing required field: 'description'") elif not isinstance(fm['description'], str): errors.append("'description' must be a string") elif len(fm['description']) > 60: errors.append(f"'description' is {len(fm['description'])} chars (max 60)") elif not fm['description'].endswith('.'): errors.append("description must end with a period") elif has_marketing_words(fm['description']): warnings.append("description contains marketing words") # Check version if 'version' not in fm: errors.append("Missing required field: 'version'") elif not isinstance(fm['version'], str): errors.append("'version' must be a string") elif not re.match(r'^\d+\.\d+\.\d+$', fm['version']): warnings.append(f"version should be semver: {fm['version']}") # Check author if 'author' not in fm: errors.append("Missing required field: 'author'") elif not isinstance(fm['author'], str): errors.append("'author' must be a string") # Check license if 'license' not in fm: errors.append("Missing required field: 'license'") # Check platforms if 'platforms' not in fm: errors.append("Missing required field: 'platforms'") elif not isinstance(fm['platforms'], list): errors.append("'platforms' must be a list") elif not all(isinstance(p, str) for p in fm['platforms']): errors.append("'platforms' must contain only strings") # Check metadata.hermes if 'metadata' not in fm: errors.append("Missing required field: 'metadata'") else: if 'hermes' not in fm['metadata']: warnings.append("metadata.hermes section missing") else: if 'tags' not in fm['metadata']['hermes']: warnings.append("metadata.hermes.tags missing") if 'related_skills' not in fm['metadata']['hermes']: warnings.append("metadata.hermes.related_skills missing") # Validate description chars for trigger window if 'description' in fm and len(fm['description']) > 57: warnings.append("description may be truncated in system prompt (57 chars + '...')") # Check 4: Non-empty body if body and not body.strip(): errors.append("Body is empty after frontmatter") # Check 5: Body has reasonable content if body and len(body.strip()) < 50: warnings.append("Body content seems unusually short") return { 'valid': len(errors) == 0, 'errors': errors, 'warnings': warnings } def has_marketing_words(text): """Check if text contains marketing words that should be avoided.""" marketing = ['powerful', 'comprehensive', 'seamless', 'advanced', 'robust', 'intuitive', 'elegant', 'cutting-edge', 'state-of-the-art'] text_lower = text.lower() return any(word in text_lower for word in marketing) def parse_frontmatter_simple(text): """Simple YAML-like parser for basic validation when ruamel.yaml unavailable.""" result = {} for line in text.split('\n'): if ':' in line: key, _, value = line.partition(':') key = key.strip().strip('\'"') value = value.strip().strip('\'"') if value: # Try to parse as list if value.startswith('[') and value.endswith(']'): items = value[1:-1].split(',') result[key] = [i.strip().strip('\'"') for i in items if i.strip()] else: result[key] = value return result def main(): if len(sys.argv) < 2: print("Usage: skill_md_validator.py ") sys.exit(1) result = validate_skill_md(sys.argv[1]) print(f"Validating: {sys.argv[1]}") print("-" * 40) if result['warnings']: print("Warnings:") for w in result['warnings']: print(f" [WARNING] {w}") if result['errors']: print("Errors:") for e in result['errors']: print(f" [ERROR] {e}") print("-" * 40) if result['valid']: print("VALID") sys.exit(0) else: print("INVALID") sys.exit(1) if __name__ == "__main__": main()