"""Markdown skill registry. Skills are .md files in the skills/ directory with YAML frontmatter: --- name: press-release-writer description: Write professional press releases tools: [write_press_releases, submit_press_release] agents: [writer, default] --- # Press Release Workflow ... Files without frontmatter (like companies.md, headlines.md) are data files and get skipped. """ from __future__ import annotations import logging from dataclasses import dataclass, field from pathlib import Path import yaml log = logging.getLogger(__name__) @dataclass class SkillDef: name: str description: str content: str file_path: Path tools: list[str] = field(default_factory=list) agents: list[str] = field(default_factory=list) def _parse_frontmatter(text: str) -> tuple[dict, str]: """Split YAML frontmatter from markdown content. Returns (frontmatter_dict, body) or ({}, full_text) if no frontmatter. """ if not text.startswith("---"): return {}, text end = text.find("---", 3) if end == -1: return {}, text yaml_block = text[3:end].strip() body = text[end + 3 :].strip() try: meta = yaml.safe_load(yaml_block) or {} except yaml.YAMLError as e: log.warning("Failed to parse YAML frontmatter: %s", e) return {}, text if not isinstance(meta, dict): return {}, text return meta, body class SkillRegistry: """Discovers and holds markdown skill definitions.""" def __init__(self, skills_dir: Path): self._skills: dict[str, SkillDef] = {} self._skills_dir = skills_dir self._discover() def _discover(self): """Load all .md files with valid frontmatter from the skills directory.""" if not self._skills_dir.exists(): log.warning("Skills directory not found: %s", self._skills_dir) return for path in sorted(self._skills_dir.glob("*.md")): try: text = path.read_text(encoding="utf-8") except Exception as e: log.warning("Failed to read skill file %s: %s", path.name, e) continue meta, body = _parse_frontmatter(text) if not meta.get("name"): # No frontmatter or no name — skip (data file) continue skill = SkillDef( name=meta["name"], description=meta.get("description", ""), content=body, file_path=path, tools=meta.get("tools", []), agents=meta.get("agents", []), ) self._skills[skill.name] = skill log.info("Loaded skill: %s (%s)", skill.name, path.name) def get(self, name: str) -> SkillDef | None: return self._skills.get(name) def list_skills(self) -> list[SkillDef]: return list(self._skills.values()) def get_prompt_section(self, agent_name: str = "default") -> str: """Build a prompt section with skills relevant to an agent. If a skill's agents list is empty, it's available to all agents. Otherwise, only agents listed in the skill's agents list get it. """ parts = [] for skill in self._skills.values(): if skill.agents and agent_name not in skill.agents: continue parts.append(f"### Skill: {skill.name}\n{skill.description}\n") if not parts: return "" return "# Available Skills\n" + "\n".join(parts) def get_body(self, name: str) -> str: """Get the body content of a skill (without frontmatter).""" skill = self._skills.get(name) return skill.content if skill else ""