feat: add prerequisites field to skill spec — hide skills with unmet dependencies

Skills can now declare runtime prerequisites (env vars, CLI binaries) via
YAML frontmatter. Skills with unmet prerequisites are excluded from the
system prompt so the agent never claims capabilities it can't deliver, and
skill_view() warns the agent about what's missing.

Three layers of defense:
- build_skills_system_prompt() filters out unavailable skills
- _find_all_skills() flags unmet prerequisites in metadata
- skill_view() returns prerequisites_warning with actionable details

Tagged 12 bundled skills that have hard runtime dependencies:
gif-search (TENOR_API_KEY), notion (NOTION_API_KEY), himalaya, imessage,
apple-notes, apple-reminders, openhue, duckduckgo-search, codebase-inspection,
blogwatcher, songsee, mcporter.

Closes #658
Fixes #630
This commit is contained in:
kshitij 2026-03-08 12:55:09 +05:30
parent 76545ab365
commit f210510276
17 changed files with 336 additions and 11 deletions

View file

@ -170,6 +170,22 @@ def _skill_is_platform_compatible(skill_file: Path) -> bool:
return True # Err on the side of showing the skill
def _skill_prerequisites_met(skill_file: Path) -> bool:
"""Check if a SKILL.md's declared prerequisites are satisfied.
Returns True (show the skill) when prerequisites are met or not declared.
Returns False when the skill explicitly declares prerequisites that are missing.
"""
try:
from tools.skills_tool import _parse_frontmatter, check_skill_prerequisites
raw = skill_file.read_text(encoding="utf-8")[:2000]
frontmatter, _ = _parse_frontmatter(raw)
met, _ = check_skill_prerequisites(frontmatter)
return met
except Exception:
return True
def build_skills_system_prompt() -> str:
"""Build a compact skill index for the system prompt.
@ -191,6 +207,9 @@ def build_skills_system_prompt() -> str:
# Skip skills incompatible with the current OS platform
if not _skill_is_platform_compatible(skill_file):
continue
# Skip skills whose prerequisites (env vars, commands) are unmet
if not _skill_prerequisites_met(skill_file):
continue
rel_path = skill_file.relative_to(skills_dir)
parts = rel_path.parts
if len(parts) >= 2: