Skip to content
Open
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
25 changes: 25 additions & 0 deletions agent/skill_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,31 @@ def parse_config_string_list(value) -> List[str]:


def _normalize_string_set(values) -> Set[str]:
"""Parse a disabled-skill config value into a set of skill names.

Handles the forms produced by different config paths:

* A YAML list (e.g. ``feishu: [airtable, architecture]``) — iterated
directly.
* A JSON-array string from ``hermes config set`` / JSON-mode editor
saves (``'["a","b"]'``) — parsed via ``parse_config_string_list``
(#86661).
* A **multi-line YAML block scalar** (e.g. ``"- airtable\\n- architecture"``)
— split on newlines, ``- `` prefix stripped from each line.
* A single skill name written as a scalar — treated as one element.
"""
if isinstance(values, str) and "\n" in values:
# Multi-line block scalar from `hermes config set` → split by line
items: list[str] = []
for line in values.split("\n"):
stripped = line.strip()
if stripped.startswith("- "):
items.append(stripped[2:])
elif stripped.startswith("-"):
items.append(stripped[1:])
elif stripped:
items.append(stripped)
values = items
return {name.strip() for name in parse_config_string_list(values) if name.strip()}


Expand Down
19 changes: 18 additions & 1 deletion hermes_cli/skills_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,28 @@ def _normalize_skill_names(values) -> Set[str]:
Mirrors ``agent.skill_utils._normalize_string_set``: ``None`` (YAML null)
means empty, a bare scalar (``disabled: my-skill``) means a single-item
list — NOT a set of its characters (#13026).

Also handles multi-line YAML block scalars produced by ``hermes config
set`` for list values (e.g. ``"- airtable\\n- architecture\\n..."``),
splitting on newlines and stripping ``- `` prefixes.
"""
if values is None:
return set()
if isinstance(values, str):
values = [values]
# Multi-line block scalar from `hermes config set` → split by line
if "\n" in values:
items: list[str] = []
for line in values.split("\n"):
stripped = line.strip()
if stripped.startswith("- "):
items.append(stripped[2:])
elif stripped.startswith("-"):
items.append(stripped[1:])
elif stripped:
items.append(stripped)
values = items
else:
values = [values]
try:
return {str(v).strip() for v in values if str(v).strip()}
except TypeError:
Expand Down
43 changes: 43 additions & 0 deletions tests/agent/test_skill_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,49 @@ def test_real_file_read_path(self, tmp_path):
assert fm["platforms"] == ["macos"]


# ── _normalize_string_set — multiline block scalar (#48333) ────────────────


class TestNormalizeStringSetMultiline:
"""_normalize_string_set must handle YAML block scalars produced by
``hermes config set`` that embed the whole list as a multi-line string
with ``\\n`` separators and ``- `` prefixes (e.g.
``"- airtable\\n- architecture\\n- ascii-art"``)."""

def test_multiline_block_scalar(self):
from agent.skill_utils import _normalize_string_set
result = _normalize_string_set(
"- airtable\n- architecture\n- ascii-art"
)
assert result == {"airtable", "architecture", "ascii-art"}

def test_single_line_scalar_unchanged(self):
from agent.skill_utils import _normalize_string_set
assert _normalize_string_set("my-skill") == {"my-skill"}

def test_yaml_list_unchanged(self):
from agent.skill_utils import _normalize_string_set
assert _normalize_string_set(["a", "b"]) == {"a", "b"}

def test_none_is_empty(self):
from agent.skill_utils import _normalize_string_set
assert _normalize_string_set(None) == set()

def test_multiline_no_dash_prefix(self):
"""Lines without ``- `` prefix are treated as bare names."""
from agent.skill_utils import _normalize_string_set
assert _normalize_string_set("airtable\narchitecture") == {
"airtable", "architecture"
}

def test_multiline_with_blank_lines(self):
from agent.skill_utils import _normalize_string_set
result = _normalize_string_set(
"- airtable\n\n- architecture\n\n\n"
)
assert result == {"airtable", "architecture"}


class TestBOMToleranceSiblingSites:
"""The BOM fix must cover every independent frontmatter parser, not just
the canonical ``parse_frontmatter`` — several modules reimplement the
Expand Down
44 changes: 44 additions & 0 deletions tests/hermes_cli/test_skills_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,47 @@ def test_none_becomes_uncategorized(self):
from hermes_cli.skills_config import _get_categories
skills = [{"name": "a", "category": None, "description": ""}]
assert "uncategorized" in _get_categories(skills)


# ---------------------------------------------------------------------------
# _normalize_skill_names — multiline block scalar (#48333)
# ---------------------------------------------------------------------------

class TestNormalizeSkillNamesMultiline:
"""_normalize_skill_names must handle YAML block scalars produced by
``hermes config set`` that embed the whole list as a multi-line string
with ``\\n`` separators and ``- `` prefixes (e.g.
``"- airtable\\n- architecture\\n- ascii-art"``)."""

def test_multiline_block_scalar(self):
from hermes_cli.skills_config import _normalize_skill_names
result = _normalize_skill_names(
"- airtable\n- architecture\n- ascii-art"
)
assert result == {"airtable", "architecture", "ascii-art"}

def test_single_line_scalar_unchanged(self):
from hermes_cli.skills_config import _normalize_skill_names
assert _normalize_skill_names("my-skill") == {"my-skill"}

def test_yaml_list_unchanged(self):
from hermes_cli.skills_config import _normalize_skill_names
assert _normalize_skill_names(["a", "b"]) == {"a", "b"}

def test_none_is_empty(self):
from hermes_cli.skills_config import _normalize_skill_names
assert _normalize_skill_names(None) == set()

def test_multiline_no_dash_prefix(self):
"""Lines without ``- `` prefix are treated as bare names."""
from hermes_cli.skills_config import _normalize_skill_names
assert _normalize_skill_names("airtable\narchitecture") == {
"airtable", "architecture"
}

def test_multiline_with_blank_lines(self):
from hermes_cli.skills_config import _normalize_skill_names
result = _normalize_skill_names(
"- airtable\n\n- architecture\n\n\n"
)
assert result == {"airtable", "architecture"}