Run all structural checks on a single compiled skill directory. Args: skill_dir: `` /output/skills/ `` strict: if True, warnings are surfaced; otherwise only errors Returns a ValidationResult. Use ``result.passed`` for the default semantics (errors block, warnin
(skill_dir: Path, *, strict: bool = False)
| 74 | |
| 75 | |
| 76 | def validate_skill(skill_dir: Path, *, strict: bool = False) -> ValidationResult: |
| 77 | """Run all structural checks on a single compiled skill directory. |
| 78 | |
| 79 | Args: |
| 80 | skill_dir: ``<kb>/output/skills/<name>`` |
| 81 | strict: if True, warnings are surfaced; otherwise only errors |
| 82 | |
| 83 | Returns a ValidationResult. Use ``result.passed`` for the default |
| 84 | semantics (errors block, warnings don't); use ``result.passed_strict`` |
| 85 | when running ``--strict``. |
| 86 | """ |
| 87 | result = ValidationResult() |
| 88 | skill_dir = Path(skill_dir) |
| 89 | |
| 90 | if not skill_dir.is_dir(): |
| 91 | result.errors.append(f"Skill directory does not exist: {skill_dir}") |
| 92 | return result |
| 93 | |
| 94 | skill_md = skill_dir / "SKILL.md" |
| 95 | if not skill_md.exists(): |
| 96 | result.errors.append("Missing required file: SKILL.md") |
| 97 | return result |
| 98 | |
| 99 | # File size |
| 100 | skill_size = skill_md.stat().st_size |
| 101 | if skill_size > SKILL_MD_MAX_BYTES: |
| 102 | result.errors.append(f"SKILL.md is {skill_size} bytes; max is {SKILL_MD_MAX_BYTES} bytes.") |
| 103 | |
| 104 | text = skill_md.read_text(encoding="utf-8") |
| 105 | |
| 106 | # Frontmatter |
| 107 | fm = _extract_frontmatter(text) |
| 108 | if fm is None: |
| 109 | result.errors.append("SKILL.md has no YAML frontmatter (must start with `---` ... `---`).") |
| 110 | return result |
| 111 | |
| 112 | try: |
| 113 | meta = yaml.safe_load(fm) or {} |
| 114 | except yaml.YAMLError as exc: |
| 115 | result.errors.append(f"Frontmatter is not valid YAML: {exc}") |
| 116 | return result |
| 117 | |
| 118 | if not isinstance(meta, dict): |
| 119 | result.errors.append("Frontmatter must be a YAML mapping.") |
| 120 | return result |
| 121 | |
| 122 | extras = set(meta.keys()) - ALLOWED_FRONTMATTER_KEYS |
| 123 | if extras: |
| 124 | # Treat as warning, not error — keeps strict mode user-controllable |
| 125 | result.warnings.append( |
| 126 | f"Frontmatter contains unknown keys: {sorted(extras)}. " |
| 127 | f"Anthropic Skills spec only allows: " |
| 128 | f"{sorted(ALLOWED_FRONTMATTER_KEYS)}." |
| 129 | ) |
| 130 | |
| 131 | # name field |
| 132 | name = meta.get("name") |
| 133 | if not name: |