fix: PR #118 round 2 — null-safe parser, docs, consistency

- Null-safe extraction for description, license, and compatibility in
  skill_parser.py — YAML bare keys (e.g. `description:`) no longer
  produce the literal string "None"
- Log warning on skill catalog storage failure instead of silent swallow
- Use `enabled == 1` in list_skills_by_activation for consistency with
  other prompt_templates queries in both storage backends
- Add parser tests for YAML null description, license, and compatibility
- Update governance.md: document runtime config editing on installed
  skills, two-column modal layout, SPDX license dropdown, origin badge
This commit is contained in:
Patrick Buckley
2026-03-17 16:05:37 -07:00
committed by Patrick Buckley
parent 341d2f604f
commit c76a61841e
6 changed files with 52 additions and 5 deletions
+14
View File
@@ -104,6 +104,20 @@ etc.) since workstream templates were merged into the skills system in v0.8.0.
Discovery view has search bar, result cards, and "Import from GitHub" modal.
- SDK: `discover_skills(q)` and `install_skill(source, skill_id=..., url=...)`
on both Python and TypeScript console clients.
- **Runtime config on installed skills**: Installed (readonly) skills can have
their runtime configuration edited — model, temperature, reasoning effort,
token budget, max tokens, agent max turns, auto-approve, allowed tools,
and enabled flag. The server restricts updates to these fields only via
`_SKILL_RUNTIME_CONFIG_FIELDS` filtering; spec/content fields (name,
description, tags, license, compatibility, content, activation) remain
immutable. The admin UI shows "Save Config" instead of "Save" for these
skills. Audit action: `skill.update.config`.
- **Admin UI**: Create/Edit skill modals use a two-column spec manifest layout
(left: Identity / Manifest / Deployment; right: Skill Content editor with
monospace font). Runtime Config is a collapsible 3-column grid below.
License uses an SPDX identifier dropdown (MIT, Apache-2.0, GPL-3.0, etc.).
Installed skills show a cyan origin badge with source URL, spec fields are
disabled, and all collapsible sections auto-expand in view mode.
### Usage Tracking
+30
View File
@@ -394,6 +394,36 @@ Content.
result = parse_skill_md(raw)
assert result.version == "1.0.0"
def test_null_description_falls_back_to_body(self) -> None:
"""YAML null description must not produce 'None' string."""
raw = """\
---
name: null-desc
description:
---
First paragraph here.
"""
result = parse_skill_md(raw)
assert result.description == "First paragraph here."
assert "None" not in result.description
def test_null_license_and_compatibility(self) -> None:
"""YAML null license/compatibility must not produce 'None' string."""
raw = """\
---
name: null-fields
description: Test
license:
compatibility:
---
Content.
"""
result = parse_skill_md(raw)
assert result.license == ""
assert result.compatibility == ""
class TestStandardFieldLengths:
"""Spec caps: description <= 1024, compatibility <= 500."""
+1
View File
@@ -862,6 +862,7 @@ class ChatSession:
try:
search_skills = list_skills_by_activation("search", enabled_only=True, limit=30)
except Exception:
log.warning("session.skill_catalog_failed", exc_info=True)
search_skills = []
if search_skills:
catalog_lines = ["<available-skills>"]
+5 -3
View File
@@ -199,7 +199,8 @@ def parse_skill_md(raw: str, *, lenient: bool = False) -> ParsedSkill | None:
raise ValueError(name_err)
# Description — frontmatter or first paragraph of body
description = str(meta.get("description", "")).strip()
raw_desc = meta.get("description")
description = str(raw_desc).strip() if raw_desc is not None else ""
if not description and body:
first_line = body.split("\n")[0].strip()
# Skip markdown headings
@@ -220,7 +221,8 @@ def parse_skill_md(raw: str, *, lenient: bool = False) -> ParsedSkill | None:
)
description = description[:_MAX_DESCRIPTION_LEN]
compatibility = str(meta.get("compatibility", "")).strip()
raw_compat = meta.get("compatibility")
compatibility = str(raw_compat).strip() if raw_compat is not None else ""
if len(compatibility) > _MAX_COMPATIBILITY_LEN:
log.warning(
"skill_parser.compatibility_truncated",
@@ -238,7 +240,7 @@ def parse_skill_md(raw: str, *, lenient: bool = False) -> ParsedSkill | None:
version=_extract_str(meta, "version", default="1.0.0"),
# Standard uses "allowed-tools" (hyphenated); stored internally as allowed_tools
allowed_tools=_extract_list(meta, "allowed-tools"),
license=str(meta.get("license", "")).strip(),
license=_extract_str(meta, "license"),
compatibility=compatibility,
raw_frontmatter=meta,
)
+1 -1
View File
@@ -1697,7 +1697,7 @@ class PostgreSQLBackend:
.order_by(prompt_templates.c.name)
)
if enabled_only:
q = q.where(prompt_templates.c.enabled == True) # noqa: E712
q = q.where(prompt_templates.c.enabled == 1)
if limit > 0:
q = q.limit(limit)
rows = conn.execute(q).fetchall()
+1 -1
View File
@@ -1721,7 +1721,7 @@ class SQLiteBackend:
.order_by(prompt_templates.c.name)
)
if enabled_only:
q = q.where(prompt_templates.c.enabled == True) # noqa: E712
q = q.where(prompt_templates.c.enabled == 1)
if limit > 0:
q = q.limit(limit)
rows = conn.execute(q).fetchall()