From 42373b6742377d57cdc36cb219f45eccea609567 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 23 Feb 2026 18:48:07 +0000 Subject: [PATCH] fix(skills): support multiline frontmatter fallback without PyYAML --- .../skill-creator/scripts/quick_validate.py | 21 +++++++++--- .../scripts/test_quick_validate.py | 32 +++++++++++++++++-- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/skills/skill-creator/scripts/quick_validate.py b/skills/skill-creator/scripts/quick_validate.py index 3e0130bee18..e8737b4f156 100644 --- a/skills/skill-creator/scripts/quick_validate.py +++ b/skills/skill-creator/scripts/quick_validate.py @@ -32,13 +32,25 @@ def _parse_simple_frontmatter(frontmatter_text: str) -> Optional[dict[str, str]] Supports simple `key: value` mappings used by SKILL.md frontmatter. """ parsed: dict[str, str] = {} + current_key: Optional[str] = None for raw_line in frontmatter_text.splitlines(): - line = raw_line.strip() - if not line or line.startswith("#"): + stripped = raw_line.strip() + if not stripped or stripped.startswith("#"): continue - if ":" not in line: + + is_indented = raw_line[:1].isspace() + if is_indented: + if current_key is None: + return None + current_value = parsed[current_key] + parsed[current_key] = ( + f"{current_value}\n{stripped}" if current_value else stripped + ) + continue + + if ":" not in stripped: return None - key, value = line.split(":", 1) + key, value = stripped.split(":", 1) key = key.strip() value = value.strip() if not key: @@ -48,6 +60,7 @@ def _parse_simple_frontmatter(frontmatter_text: str) -> Optional[dict[str, str]] ): value = value[1:-1] parsed[key] = value + current_key = key return parsed diff --git a/skills/skill-creator/scripts/test_quick_validate.py b/skills/skill-creator/scripts/test_quick_validate.py index 717fbb8a8c2..199fcb633ad 100644 --- a/skills/skill-creator/scripts/test_quick_validate.py +++ b/skills/skill-creator/scripts/test_quick_validate.py @@ -7,7 +7,7 @@ import tempfile from pathlib import Path from unittest import TestCase, main -from quick_validate import validate_skill +import quick_validate class TestQuickValidate(TestCase): @@ -26,7 +26,7 @@ class TestQuickValidate(TestCase): content = "---\r\nname: crlf-skill\r\ndescription: ok\r\n---\r\n# Skill\r\n" (skill_dir / "SKILL.md").write_text(content, encoding="utf-8") - valid, message = validate_skill(skill_dir) + valid, message = quick_validate.validate_skill(skill_dir) self.assertTrue(valid, message) @@ -36,11 +36,37 @@ class TestQuickValidate(TestCase): content = "---\nname: bad-skill\ndescription: missing end\n# no closing fence\n" (skill_dir / "SKILL.md").write_text(content, encoding="utf-8") - valid, message = validate_skill(skill_dir) + valid, message = quick_validate.validate_skill(skill_dir) self.assertFalse(valid) self.assertEqual(message, "Invalid frontmatter format") + def test_fallback_parser_handles_multiline_frontmatter_without_pyyaml(self): + skill_dir = self.temp_dir / "multiline-skill" + skill_dir.mkdir(parents=True, exist_ok=True) + content = """--- +name: multiline-skill +description: Works without pyyaml +allowed-tools: + - gh +metadata: | + { + "owners": ["team-openclaw"] + } +--- +# Skill +""" + (skill_dir / "SKILL.md").write_text(content, encoding="utf-8") + + previous_yaml = quick_validate.yaml + quick_validate.yaml = None + try: + valid, message = quick_validate.validate_skill(skill_dir) + finally: + quick_validate.yaml = previous_yaml + + self.assertTrue(valid, message) + if __name__ == "__main__": main()