Skip to content
Closed
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
29 changes: 19 additions & 10 deletions hermes_cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -768,7 +768,7 @@ def _clamp_command_names(

def _collect_gateway_skill_entries(
platform: str,
max_slots: int,
max_slots: int | None,
reserved_names: set[str],
desc_limit: int = 100,
sanitize_name: "Callable[[str], str] | None" = None,
Expand All @@ -787,7 +787,8 @@ def _collect_gateway_skill_entries(
platform: Platform identifier for per-platform skill filtering
(``"telegram"``, ``"discord"``, etc.).
max_slots: Maximum number of entries to return (remaining slots after
built-in/core commands).
built-in/core commands). Pass ``None`` to collect every eligible
plugin/skill candidate and apply the platform cap later.
reserved_names: Names already taken by built-in commands. Mutated
in-place as new names are added.
desc_limit: Max description length (40 for Telegram, 100 for Discord).
Expand Down Expand Up @@ -878,12 +879,20 @@ def _collect_gateway_skill_entries(
# any clamp-induced renames.
skill_triples = _clamp_command_names(skill_triples, reserved_names)

# Skills fill remaining slots — only tier that gets trimmed
remaining = max(0, max_slots - len(all_entries))
hidden_count = max(0, len(skill_triples) - remaining)
# Skills fill remaining slots — only tier that gets trimmed at this layer.
# Telegram uses max_slots=None so it can promote configured priority skills
# before applying the Bot API cap in telegram_menu_commands().
if max_slots is None:
remaining = len(skill_triples)
hidden_count = 0
else:
remaining = max(0, max_slots - len(all_entries))
hidden_count = max(0, len(skill_triples) - remaining)
for n, d, k in skill_triples[:remaining]:
all_entries.append((n, d, k))

if max_slots is None:
return all_entries, hidden_count
return all_entries[:max_slots], hidden_count


Expand Down Expand Up @@ -911,19 +920,19 @@ def telegram_menu_commands(max_commands: int = 100) -> tuple[list[tuple[str, str
core_commands = _prioritize_telegram_menu_commands(list(telegram_bot_commands()))
reserved_names = {n for n, _ in core_commands}
all_commands = list(core_commands)
hidden_core_count = max(0, len(all_commands) - max_commands)

remaining_slots = max(0, max_commands - len(all_commands))
entries, hidden_count = _collect_gateway_skill_entries(
entries, _hidden_at_collection = _collect_gateway_skill_entries(
platform="telegram",
max_slots=remaining_slots,
max_slots=None,
reserved_names=reserved_names,
desc_limit=40,
sanitize_name=_sanitize_telegram_name,
)
# Drop the cmd_key — Telegram only needs (name, desc) pairs.
all_commands.extend((n, d) for n, d, _k in entries)
return all_commands[:max_commands], hidden_count + hidden_core_count
all_commands = _prioritize_telegram_menu_commands(all_commands)
hidden_count = max(0, len(all_commands) - max_commands)
return all_commands[:max_commands], hidden_count


def discord_skill_commands(
Expand Down
44 changes: 44 additions & 0 deletions tests/hermes_cli/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -1242,6 +1242,50 @@ def test_configured_priority_replace_ignores_builtin_priority_order(self, tmp_pa

assert names[:2] == ["status", "help"]

def test_configured_priority_skill_survives_menu_cap(self, tmp_path, monkeypatch):
"""Configured Telegram priorities promote skill commands before trimming."""
from unittest.mock import patch

(tmp_path / "config.yaml").write_text(
"platforms:\n"
" telegram:\n"
" extra:\n"
" command_menu:\n"
" priority_mode: prepend\n"
" priority:\n"
" - startsession\n"
)
monkeypatch.setenv("HERMES_HOME", str(tmp_path))

fake_skills_dir = tmp_path / "skills"
fake_skills_dir.mkdir(exist_ok=True)
fake_cmds = {
f"/aaa-{i:02d}": {
"name": f"aaa-{i:02d}",
"description": f"Filler skill {i:02d}",
"skill_md_path": str(fake_skills_dir / f"aaa-{i:02d}" / "SKILL.md"),
"skill_dir": str(fake_skills_dir / f"aaa-{i:02d}"),
}
for i in range(50)
}
fake_cmds["/startsession"] = {
"name": "startsession",
"description": "Run Samus Start Session protocol",
"skill_md_path": str(fake_skills_dir / "startsession" / "SKILL.md"),
"skill_dir": str(fake_skills_dir / "startsession"),
}

with (
patch("agent.skill_commands.get_skill_commands", return_value=fake_cmds),
patch("tools.skills_tool.SKILLS_DIR", fake_skills_dir),
patch("agent.skill_utils.get_external_skills_dirs", return_value=[]),
):
menu, hidden = telegram_menu_commands(max_commands=30)

names = [name for name, _desc in menu]
assert names[0] == "startsession"
assert hidden > 0

def test_telegram_menu_max_commands_uses_config_with_safe_bounds(self, tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_HOME", str(tmp_path))

Expand Down