Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scripts/skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
_COPILOT_EVENTS,
_check_hook_event_names,
check_plugin_components,
check_skill_frontmatter,
_HOOK_PY_RE,
_check_hook_wiring,
check_cursor_plugin,
Expand Down
11 changes: 11 additions & 0 deletions scripts/skillsgen/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
check_cursor_plugin,
check_plugin_components,
check_routing_tables,
check_skill_frontmatter,
)


Expand Down Expand Up @@ -103,6 +104,16 @@ def main() -> None:
print(f" - {err}", file=sys.stderr)
ok = False

frontmatter_errors = check_skill_frontmatter(repo_root)
if frontmatter_errors:
print(
"ERROR: skill SKILL.md frontmatter is invalid:",
file=sys.stderr,
)
for err in frontmatter_errors:
print(f" - {err}", file=sys.stderr)
ok = False

if not validate_manifest(repo_root):
ok = False

Expand Down
29 changes: 29 additions & 0 deletions scripts/skillsgen/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pathlib import Path

from skillsgen.common import _norm_rel_path, _read_frontmatter
from skillsgen.discovery import iter_all_skill_dirs


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -156,6 +157,34 @@ def check_plugin_components(repo_root: Path) -> list[str]:
return errors


def check_skill_frontmatter(repo_root: Path) -> list[str]:
"""Flag SKILL.md frontmatter that a strict YAML parser would reject.

Checked with a regex, not yaml.safe_load, because this package is
stdlib-only (the protected CI runner has no pypi). The failure that bites in
practice: an unquoted ':' in `description`, which strict-YAML skill loaders
read as a mapping separator and silently drop. Skills counterpart to the
commands check in check_plugin_components.

Returns a list of error strings (empty means all good).
"""
errors: list[str] = []
for skill_dir in iter_all_skill_dirs(repo_root):
rel = (skill_dir / "SKILL.md").relative_to(repo_root)
frontmatter = _read_frontmatter(skill_dir / "SKILL.md")
if frontmatter is None:
errors.append(f"Skill '{rel}' is missing YAML frontmatter.")
elif not re.search(r"^description:\s*\S", frontmatter, re.MULTILINE):
errors.append(f"Skill '{rel}' frontmatter is missing a 'description'.")
elif re.search(r"^description:[ \t]*[^\s\"'>|].*:(?:\s|$)", frontmatter, re.MULTILINE):
errors.append(
f"Skill '{rel}' has an unquoted ':' in its description, which "
"strict YAML parsers reject (the skill is then silently dropped "
"at load time). Quote the whole description string."
)
return errors


# Any hooks/*.py mentioned in a hooks wiring file, regardless of how the
# platform prefixes the path (${CLAUDE_PLUGIN_ROOT}/, plugin-root-relative, …).
_HOOK_PY_RE = re.compile(r"hooks/[\w.-]+\.py")
Expand Down
2 changes: 1 addition & 1 deletion skills/databricks-app-design/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: databricks-app-design
parent: databricks-apps
description: Design the UX of Databricks data apps — dashboards, KPI pages, reports, charts, tables, and Genie/chat data assistants — mapped to concrete AppKit components. Use when BUILDING or reviewing any UI that displays data or answers data questions: choosing genre, layout, charts, KPIs, semantic color, required states (loading/empty/error), IBCS notation, and AI-result trust (showing generated SQL/sources for Genie/chat). NOT for authoring managed AI/BI (Lakeview) dashboards (→ databricks-aibi-dashboards), non-data frontend (forms, settings, auth, marketing), or scaffolding/build/deploy (→ databricks-apps). Complements databricks-apps; use it alongside whenever the app has a dashboard, chart, table, KPI, report, or Genie/chat/AI surface.
description: "Design the UX of Databricks data apps — dashboards, KPI pages, reports, charts, tables, and Genie/chat data assistants — mapped to concrete AppKit components. Use when BUILDING or reviewing any UI that displays data or answers data questions: choosing genre, layout, charts, KPIs, semantic color, required states (loading/empty/error), IBCS notation, and AI-result trust (showing generated SQL/sources for Genie/chat). NOT for authoring managed AI/BI (Lakeview) dashboards (→ databricks-aibi-dashboards), non-data frontend (forms, settings, auth, marketing), or scaffolding/build/deploy (→ databricks-apps). Complements databricks-apps; use it alongside whenever the app has a dashboard, chart, table, KPI, report, or Genie/chat/AI surface."
metadata:
version: 0.1.0
---
Expand Down
44 changes: 44 additions & 0 deletions tests/skills_generator_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,5 +167,49 @@ def test_missing_keyword(self):
self.assertTrue(any("keyword" in e for e in errors), errors)


class SkillFrontmatterTest(unittest.TestCase):
def _make_skill(self, root: Path, name: str, description: str) -> None:
skill_dir = root / "skills" / name
skill_dir.mkdir(parents=True)
(skill_dir / "SKILL.md").write_text(
f"---\nname: {name}\ndescription: {description}\n---\n"
)

def test_repo_skills_are_clean(self):
# Pins the databricks-app-design fix and guards every shipped skill: no
# SKILL.md may carry a description a strict YAML parser would reject.
self.assertEqual(skills.check_skill_frontmatter(_REPO), [])

def test_unquoted_colon_flagged(self):
with tempfile.TemporaryDirectory() as d:
root = Path(d)
self._make_skill(
root, "databricks-bad", "Use when answering questions: pick a chart."
)
errors = skills.check_skill_frontmatter(root)
self.assertTrue(
any("databricks-bad" in e and "unquoted ':'" in e for e in errors),
errors,
)

def test_quoted_colon_ok(self):
with tempfile.TemporaryDirectory() as d:
root = Path(d)
self._make_skill(
root, "databricks-good", '"Use when answering questions: pick a chart."'
)
self.assertEqual(skills.check_skill_frontmatter(root), [])

def test_unquoted_no_colon_ok(self):
# The common, valid shape: a plain bare scalar with no colon. Guards
# against the regex over-matching and flagging legitimate descriptions.
with tempfile.TemporaryDirectory() as d:
root = Path(d)
self._make_skill(
root, "databricks-plain", "Use when building dashboards and charts."
)
self.assertEqual(skills.check_skill_frontmatter(root), [])


if __name__ == "__main__":
unittest.main()
Loading