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
2 changes: 2 additions & 0 deletions src/specify_cli/integrations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def _register_builtins() -> None:
from .gemini import GeminiIntegration
from .generic import GenericIntegration
from .goose import GooseIntegration
from .hermes import HermesIntegration
from .iflow import IflowIntegration
from .junie import JunieIntegration
from .kilocode import KilocodeIntegration
Expand Down Expand Up @@ -93,6 +94,7 @@ def _register_builtins() -> None:
_register(GeminiIntegration())
_register(GenericIntegration())
_register(GooseIntegration())
_register(HermesIntegration())
_register(IflowIntegration())
_register(JunieIntegration())
_register(KilocodeIntegration())
Expand Down
78 changes: 78 additions & 0 deletions src/specify_cli/integrations/hermes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Hermes Agent integration — skills-based agent.

Hermes Agent (https://github.com/NousResearch/hermes-agent) is an open-source
AI agent framework by Nous Research. It uses the ``.hermes/skills/`` directory
for agent skills, following the same ``speckit-<name>/SKILL.md`` layout as
Claude Code and Codex.

Usage::

specify init my-project --integration hermes
specify init --here --ai hermes
"""

from __future__ import annotations

from ..base import IntegrationOption, SkillsIntegration


class HermesIntegration(SkillsIntegration):
"""Integration for Hermes Agent skills."""

key = "hermes"
config = {
"name": "Hermes Agent",
"folder": ".hermes/",
"commands_subdir": "skills",
"install_url": "https://github.com/NousResearch/hermes-agent",
"requires_cli": True,
}
registrar_config = {
"dir": ".hermes/skills",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": "/SKILL.md",
}
context_file = "AGENTS.md"

@classmethod
def options(cls) -> list[IntegrationOption]:
return [
IntegrationOption(
"--skills",
is_flag=True,
default=True,
help="Install as agent skills (default for Hermes Agent)",
),
]

def build_exec_args(
self,
prompt: str,
*,
model: str | None = None,
output_json: bool = True,
) -> list[str] | None:
"""Build Hermes CLI invocation for programmatic dispatch.

Uses ``hermes chat -q`` for one-shot queries, mapping slash-command
invocations to the appropriate skill-based dispatch.
Comment on lines +58 to +59
"""
args = [self.key, "chat", "-Q"]

if model:
args.extend(["-m", model])
if output_json:
args.append("--json")

# If prompt starts with a slash command, pass it directly
# so Hermes can dispatch to the appropriate skill.
if prompt.startswith("/"):
command, _, remainder = prompt[1:].partition(" ")
args.extend(["-s", command])
if remainder:
args.extend(["-q", remainder])
Comment on lines +72 to +74
else:
args.extend(["-q", prompt])

return args
33 changes: 33 additions & 0 deletions tests/integrations/test_integration_hermes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Tests for HermesIntegration."""

from .test_integration_base_skills import SkillsIntegrationTests


class TestHermesIntegration(SkillsIntegrationTests):
KEY = "hermes"
FOLDER = ".hermes/"
COMMANDS_SUBDIR = "skills"
REGISTRAR_DIR = ".hermes/skills"
CONTEXT_FILE = "AGENTS.md"


class TestHermesAutoPromote:
"""--ai hermes auto-promotes to integration path."""

def test_ai_hermes_without_ai_skills_auto_promotes(self, tmp_path):
"""--ai hermes should work the same as --integration hermes."""
from typer.testing import CliRunner
from specify_cli import app

runner = CliRunner()
target = tmp_path / "test-proj"
result = runner.invoke(app, [
"init", str(target),
"--ai", "hermes",
"--no-git",
"--ignore-agent-tools",
"--script", "sh",
])

assert result.exit_code == 0, f"init --ai hermes failed: {result.output}"
assert (target / ".hermes" / "skills" / "speckit-plan" / "SKILL.md").exists()
Loading