fix(skill_engine): support YAML block scalars in SKILL.md frontmatter (#1)

# reviewed
Diff reviewed locally: 211 insertions, 17 deletions across 4 files. 13/13 regression tests pass. No new deps. Production code is skill_utils.py only; rest is tests.
This commit is contained in:
SA_653 2026-05-24 16:17:02 -07:00 committed by GitHub
parent 25b98602f2
commit 6d348254a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 211 additions and 17 deletions

View file

@ -58,6 +58,18 @@ _YAML_NEEDS_QUOTE_RE = re.compile(r"[:\#\[\]{}&*!|>'\"%@`]")
def _yaml_quote(value: str) -> str:
"""Quote a YAML scalar value if it contains special characters."""
if "\n" in value:
trailing_newlines = len(value) - len(value.rstrip("\n"))
if trailing_newlines == 0:
chomping = "|-"
elif trailing_newlines == 1:
chomping = "|"
else:
chomping = "|+"
lines = value.split("\n")
if trailing_newlines and lines and lines[-1] == "":
lines = lines[:-1]
return chomping + "\n" + "\n".join(f" {line}" for line in lines)
if not value or not _YAML_NEEDS_QUOTE_RE.search(value):
return value
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
@ -72,15 +84,108 @@ def _yaml_unquote(value: str) -> str:
inner = value[1:-1]
if value[0] == '"':
inner = inner.replace('\\"', '"').replace("\\\\", "\\")
else:
inner = inner.replace("''", "'")
return inner
return value
_BLOCK_SCALAR_HEADER_RE = re.compile(r"^([>|])([+-]?)([1-9]?)$")
def _parse_yaml_lines(lines: list[str]) -> dict[str, Any]:
"""Parse a flat YAML mapping.
Supports inline scalars (quoted or bare) and block scalars
(>, |, >-, |-, >+, |+, with optional indent indicator).
"""
fm: dict[str, Any] = {}
i = 0
while i < len(lines):
line = lines[i]
if ":" not in line:
i += 1
continue
key, value = line.split(":", 1)
key = key.strip()
if not key:
i += 1
continue
parent_indent = len(line) - len(line.lstrip(" "))
value = value.strip()
block_header = _BLOCK_SCALAR_HEADER_RE.match(value)
if not block_header:
fm[key] = _yaml_unquote(value)
i += 1
continue
style, chomping, indent_indicator = block_header.groups()
explicit_indent = int(indent_indicator) if indent_indicator else None
block_indent: int | None = explicit_indent
block_lines: list[str] = []
i += 1
while i < len(lines):
continuation = lines[i]
if continuation.strip():
indent = len(continuation) - len(continuation.lstrip(" "))
if block_indent is None:
if indent <= parent_indent:
break
block_indent = indent
if indent < block_indent:
break
block_lines.append(continuation[block_indent:])
else:
block_lines.append("")
i += 1
parsed = _render_block_scalar(block_lines, style, chomping)
fm[key] = parsed
return fm
def _render_block_scalar(lines: list[str], style: str, chomping: str) -> str:
"""Render collected block scalar lines according to the supported subset."""
if style == "|":
value = "\n".join(lines)
else:
paragraphs: list[str] = []
current: list[str] = []
for line in lines:
if line == "":
if current:
paragraphs.append(" ".join(current))
current = []
else:
current.append(line)
if current:
paragraphs.append(" ".join(current))
value = "\n".join(paragraphs)
trailing_empty_count = 0
for line in reversed(lines):
if line == "":
trailing_empty_count += 1
else:
break
value = value.rstrip("\n")
if chomping == "-":
return value
if chomping == "+":
return value + ("\n" * (trailing_empty_count + 1))
return value + "\n"
def parse_frontmatter(content: str) -> Dict[str, Any]:
"""Parse YAML frontmatter into a flat dict.
Simple line-by-line parser (no PyYAML dependency).
Handles both quoted and unquoted values.
Dependency-free parser for flat mappings.
Handles quoted/unquoted values and YAML block scalars.
Returns ``{}`` if no valid frontmatter is found.
"""
if not content.startswith("---"):
@ -88,14 +193,7 @@ def parse_frontmatter(content: str) -> Dict[str, Any]:
match = _FRONTMATTER_RE.match(content)
if not match:
return {}
fm: Dict[str, Any] = {}
for line in match.group(1).split("\n"):
if ":" in line:
key, value = line.split(":", 1)
key = key.strip()
if key:
fm[key] = _yaml_unquote(value.strip())
return fm
return _parse_yaml_lines(match.group(1).split("\n"))
def get_frontmatter_field(content: str, field_name: str) -> Optional[str]:
@ -108,12 +206,9 @@ def get_frontmatter_field(content: str, field_name: str) -> Optional[str]:
match = _FRONTMATTER_RE.match(content)
if not match:
return None
for line in match.group(1).split("\n"):
if ":" in line:
key, value = line.split(":", 1)
if key.strip() == field_name:
return _yaml_unquote(value.strip())
return None
fm = _parse_yaml_lines(match.group(1).split("\n"))
value = fm.get(field_name)
return value if isinstance(value, str) else None
def set_frontmatter_field(content: str, field_name: str, value: str) -> str:
@ -306,4 +401,3 @@ def truncate(text: str, max_chars: int) -> str:
if len(text) <= max_chars:
return text
return text[:max_chars] + f"\n\n... [truncated at {max_chars} chars]"

1
tests/__init__.py Normal file
View file

@ -0,0 +1 @@

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,98 @@
import sys
import types
sys.modules.setdefault("colorama", types.SimpleNamespace(init=lambda **_: None))
from openspace.skill_engine.skill_utils import (
get_frontmatter_field,
normalize_frontmatter,
parse_frontmatter,
)
def _content(frontmatter: str) -> str:
return f"---\n{frontmatter}\n---\nBody\n"
def test_inline_unquoted():
assert parse_frontmatter(_content("description: hello"))["description"] == "hello"
def test_inline_double_quoted():
assert (
parse_frontmatter(_content('description: "hello: world"'))["description"]
== "hello: world"
)
def test_inline_single_quoted():
assert parse_frontmatter(_content("description: 'don''t'"))["description"] == "don't"
def test_block_folded():
fm = parse_frontmatter(_content("description: >\n line1\n line2"))
assert fm["description"] == "line1 line2\n"
def test_block_literal():
fm = parse_frontmatter(_content("description: |\n line1\n line2"))
assert fm["description"] == "line1\nline2\n"
def test_block_folded_strip():
fm = parse_frontmatter(_content("description: >-\n line1\n line2"))
assert fm["description"] == "line1 line2"
def test_block_literal_keep_all():
fm = parse_frontmatter(_content("description: |+\n line1\n line2\n\n"))
assert fm["description"] == "line1\nline2\n\n\n"
def test_block_with_blank_line_in_folded():
fm = parse_frontmatter(_content("description: >\n line1\n\n line2"))
assert fm["description"] == "line1\nline2\n"
def test_block_followed_by_next_key():
fm = parse_frontmatter(_content("description: >-\n line1\n line2\nname: next"))
assert fm["description"] == "line1 line2"
assert fm["name"] == "next"
def test_get_frontmatter_field_block_scalar():
content = _content("description: >-\n line1\n line2")
assert get_frontmatter_field(content, "description") == "line1 line2"
def test_normalize_roundtrip_block_scalar():
content = _content("description: >-\n foo\n bar")
normalized = normalize_frontmatter(content)
assert parse_frontmatter(normalized)["description"] == "foo bar"
def test_normalize_preserves_existing_inline():
content = _content("description: hello\nname: skill")
assert normalize_frontmatter(content) == content
FIXTURE = """---
name: by-codex-delegation
description: >
Decide whether to delegate implementation to Codex vs implement inline,
write the SPARC brief, and hand off correctly.
triggers:
- 'delegate to codex'
---
# By Codex Delegation
Body content.
"""
def test_fixture_realworld():
fm = parse_frontmatter(FIXTURE)
assert fm["name"] == "by-codex-delegation"
assert "Decide whether to delegate" in fm["description"]
assert "SPARC brief" in fm["description"]
assert fm["description"] != ">"