feat: preload CLI skills on launch (#1359)

* feat: preload CLI skills on launch

* test: cover continue with worktree and skills flags

* feat: show activated skills before CLI banner
This commit is contained in:
Teknium 2026-03-14 19:33:59 -07:00 committed by GitHub
parent 81cd367aec
commit 24f61d006a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 484 additions and 68 deletions

View file

@ -4,7 +4,11 @@ import os
from unittest.mock import patch
import tools.skills_tool as skills_tool_module
from agent.skill_commands import scan_skill_commands, build_skill_invocation_message
from agent.skill_commands import (
scan_skill_commands,
build_skill_invocation_message,
build_preloaded_skills_prompt,
)
def _make_skill(
@ -79,6 +83,33 @@ class TestScanSkillCommands:
assert "/generic-tool" in result
class TestBuildPreloadedSkillsPrompt:
def test_builds_prompt_for_multiple_named_skills(self, tmp_path):
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
_make_skill(tmp_path, "first-skill")
_make_skill(tmp_path, "second-skill")
prompt, loaded, missing = build_preloaded_skills_prompt(
["first-skill", "second-skill"]
)
assert missing == []
assert loaded == ["first-skill", "second-skill"]
assert "first-skill" in prompt
assert "second-skill" in prompt
assert "preloaded" in prompt.lower()
def test_reports_missing_named_skills(self, tmp_path):
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
_make_skill(tmp_path, "present-skill")
prompt, loaded, missing = build_preloaded_skills_prompt(
["present-skill", "missing-skill"]
)
assert "present-skill" in prompt
assert loaded == ["present-skill"]
assert missing == ["missing-skill"]
class TestBuildSkillInvocationMessage:
def test_loads_skill_by_stored_path_when_frontmatter_name_differs(self, tmp_path):
skill_dir = tmp_path / "mlops" / "audiocraft"

View file

@ -0,0 +1,77 @@
import sys
def test_top_level_skills_flag_defaults_to_chat(monkeypatch):
import hermes_cli.main as main_mod
captured = {}
def fake_cmd_chat(args):
captured["skills"] = args.skills
captured["command"] = args.command
monkeypatch.setattr(main_mod, "cmd_chat", fake_cmd_chat)
monkeypatch.setattr(
sys,
"argv",
["hermes", "-s", "hermes-agent-dev,github-auth"],
)
main_mod.main()
assert captured == {
"skills": ["hermes-agent-dev,github-auth"],
"command": None,
}
def test_chat_subcommand_accepts_skills_flag(monkeypatch):
import hermes_cli.main as main_mod
captured = {}
def fake_cmd_chat(args):
captured["skills"] = args.skills
captured["query"] = args.query
monkeypatch.setattr(main_mod, "cmd_chat", fake_cmd_chat)
monkeypatch.setattr(
sys,
"argv",
["hermes", "chat", "-s", "github-auth", "-q", "hello"],
)
main_mod.main()
assert captured == {
"skills": ["github-auth"],
"query": "hello",
}
def test_continue_worktree_and_skills_flags_work_together(monkeypatch):
import hermes_cli.main as main_mod
captured = {}
def fake_cmd_chat(args):
captured["continue_last"] = args.continue_last
captured["worktree"] = args.worktree
captured["skills"] = args.skills
captured["command"] = args.command
monkeypatch.setattr(main_mod, "cmd_chat", fake_cmd_chat)
monkeypatch.setattr(
sys,
"argv",
["hermes", "-c", "-w", "-s", "hermes-agent-dev"],
)
main_mod.main()
assert captured == {
"continue_last": True,
"worktree": True,
"skills": ["hermes-agent-dev"],
"command": "chat",
}

View file

@ -0,0 +1,130 @@
from __future__ import annotations
import importlib
import os
import sys
from unittest.mock import MagicMock, patch
import pytest
def _make_real_cli(**kwargs):
clean_config = {
"model": {
"default": "anthropic/claude-opus-4.6",
"base_url": "https://openrouter.ai/api/v1",
"provider": "auto",
},
"display": {"compact": False, "tool_progress": "all"},
"agent": {},
"terminal": {"env_type": "local"},
}
clean_env = {"LLM_MODEL": "", "HERMES_MAX_ITERATIONS": ""}
prompt_toolkit_stubs = {
"prompt_toolkit": MagicMock(),
"prompt_toolkit.history": MagicMock(),
"prompt_toolkit.styles": MagicMock(),
"prompt_toolkit.patch_stdout": MagicMock(),
"prompt_toolkit.application": MagicMock(),
"prompt_toolkit.layout": MagicMock(),
"prompt_toolkit.layout.processors": MagicMock(),
"prompt_toolkit.filters": MagicMock(),
"prompt_toolkit.layout.dimension": MagicMock(),
"prompt_toolkit.layout.menus": MagicMock(),
"prompt_toolkit.widgets": MagicMock(),
"prompt_toolkit.key_binding": MagicMock(),
"prompt_toolkit.completion": MagicMock(),
"prompt_toolkit.formatted_text": MagicMock(),
}
with patch.dict(sys.modules, prompt_toolkit_stubs), patch.dict(
"os.environ", clean_env, clear=False
):
import cli as cli_mod
cli_mod = importlib.reload(cli_mod)
with patch.object(cli_mod, "get_tool_definitions", return_value=[]), patch.dict(
cli_mod.__dict__, {"CLI_CONFIG": clean_config}
):
return cli_mod.HermesCLI(**kwargs)
class _DummyCLI:
def __init__(self, **kwargs):
self.kwargs = kwargs
self.session_id = "session-123"
self.system_prompt = "base prompt"
self.preloaded_skills = []
def show_banner(self):
return None
def show_tools(self):
return None
def show_toolsets(self):
return None
def run(self):
return None
def test_main_applies_preloaded_skills_to_system_prompt(monkeypatch):
import cli as cli_mod
created = {}
def fake_cli(**kwargs):
created["cli"] = _DummyCLI(**kwargs)
return created["cli"]
monkeypatch.setattr(cli_mod, "HermesCLI", fake_cli)
monkeypatch.setattr(
cli_mod,
"build_preloaded_skills_prompt",
lambda skills, task_id=None: ("skill prompt", ["hermes-agent-dev", "github-auth"], []),
)
with pytest.raises(SystemExit):
cli_mod.main(skills="hermes-agent-dev,github-auth", list_tools=True)
cli_obj = created["cli"]
assert cli_obj.system_prompt == "base prompt\n\nskill prompt"
assert cli_obj.preloaded_skills == ["hermes-agent-dev", "github-auth"]
def test_main_raises_for_unknown_preloaded_skill(monkeypatch):
import cli as cli_mod
monkeypatch.setattr(cli_mod, "HermesCLI", lambda **kwargs: _DummyCLI(**kwargs))
monkeypatch.setattr(
cli_mod,
"build_preloaded_skills_prompt",
lambda skills, task_id=None: ("", [], ["missing-skill"]),
)
with pytest.raises(ValueError, match=r"Unknown skill\(s\): missing-skill"):
cli_mod.main(skills="missing-skill", list_tools=True)
def test_show_banner_prints_preloaded_skills_once_before_banner():
cli_obj = _make_real_cli(compact=False)
cli_obj.preloaded_skills = ["hermes-agent-dev", "github-auth"]
cli_obj.console = MagicMock()
with patch("cli.build_welcome_banner") as mock_banner, patch(
"shutil.get_terminal_size", return_value=os.terminal_size((120, 40))
):
cli_obj.show_banner()
cli_obj.show_banner()
print_calls = [
call.args[0]
for call in cli_obj.console.print.call_args_list
if call.args and isinstance(call.args[0], str)
]
startup_lines = [line for line in print_calls if "Activated skills:" in line]
assert len(startup_lines) == 1
assert "Activated skills:" in startup_lines[0]
assert "hermes-agent-dev, github-auth" in startup_lines[0]
assert mock_banner.call_count == 2