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
9 changes: 9 additions & 0 deletions integrations/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,15 @@
"author": "spec-kit-core",
"repository": "https://github.com/github/spec-kit",
"tags": ["cli"]
},
"hermes": {
"id": "hermes",
"name": "Hermes Agent",
"version": "1.0.0",
"description": "Hermes Agent skills-based integration by Nous Research",
"author": "spec-kit-core",
"repository": "https://github.com/github/spec-kit",
"tags": ["cli", "skills"]
}
}
}
26 changes: 17 additions & 9 deletions src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1944,11 +1944,14 @@ def integration_uninstall(
console.print(f"[dim]Details:[/dim] {exc}")
raise typer.Exit(1)

removed, skipped = manifest.uninstall(project_root, force=force)

# Remove managed context section from the agent context file
if integration:
integration.remove_context_section(project_root)
if not integration:
console.print(
f"[yellow]Warning:[/yellow] Integration '{key}' not found "
"in registry. Falling back to manifest-based cleanup."
)
removed, skipped = manifest.uninstall(project_root, force=force)
else:
removed, skipped = integration.teardown(project_root, manifest, force=force)

Comment on lines +1947 to 1955
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in b938619 — integration_switch Phase 1 now calls current_integration.teardown(project_root, old_manifest, force=force) instead of old_manifest.uninstall() + remove_context_section(), matching the pattern used in integration_uninstall. Custom teardown logic (Hermes global skills, etc.) now runs during switches.

remaining = [installed for installed in installed_keys if installed != key]
new_default = default_key if default_key != key else (remaining[0] if remaining else None)
Expand Down Expand Up @@ -2090,8 +2093,9 @@ def integration_switch(
f"run [cyan]specify integration uninstall {installed_key}[/cyan], then retry."
)
raise typer.Exit(1)
removed, skipped = old_manifest.uninstall(project_root, force=force)
current_integration.remove_context_section(project_root)
removed, skipped = current_integration.teardown(
project_root, old_manifest, force=force,
)
if removed:
console.print(f" Removed {len(removed)} file(s)")
if skipped:
Expand Down Expand Up @@ -4325,7 +4329,9 @@ def extension_update(
if agent_name not in registrar.AGENT_CONFIGS:
continue
agent_config = registrar.AGENT_CONFIGS[agent_name]
commands_dir = project_root / agent_config["dir"]
commands_dir = _AgentReg._resolve_agent_dir(
agent_name, agent_config, project_root
)

for cmd_name in cmd_names:
output_name = _AgentReg._compute_output_name(agent_name, cmd_name, agent_config)
Expand Down Expand Up @@ -4486,7 +4492,9 @@ def extension_update(
if agent_name not in registrar.AGENT_CONFIGS:
continue
agent_config = registrar.AGENT_CONFIGS[agent_name]
commands_dir = project_root / agent_config["dir"]
commands_dir = _AgentReg._resolve_agent_dir(
agent_name, agent_config, project_root
)

for cmd_name in cmd_names:
output_name = _AgentReg._compute_output_name(agent_name, cmd_name, agent_config)
Expand Down
37 changes: 32 additions & 5 deletions src/specify_cli/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -654,15 +654,28 @@ def _resolve_agent_dir(
) -> Path:
"""Return the agent command directory, falling back to legacy_dir.

When the canonical directory (``agent_config["dir"]``) does not
exist but a ``legacy_dir`` is configured and present on disk,
returns the legacy path and emits a deprecation warning advising
the user to upgrade.
Supports project-relative paths (e.g. ``.claude/skills/``),
home-relative paths (e.g. ``~/.hermes/skills``), and absolute
paths — the ``agent_config["dir"]`` value is resolved verbatim
when absolute or starting with ``~/``, or joined with
``project_root`` when relative.

When the canonical directory does not exist but a ``legacy_dir``
is configured and present on disk, returns the legacy path and
emits a deprecation warning advising the user to upgrade.

Integrations that do not declare ``legacy_dir`` get the canonical
path unconditionally — no fallback, no warning.
"""
agent_dir = project_root / agent_config["dir"]
dir_str = agent_config["dir"]
if dir_str.startswith("~"):
# Use Path.home() + remainder instead of expanduser() so tests
# that monkeypatch Path.home() can properly isolate the home dir.
# expanduser() uses OS env/user lookup and ignores monkeypatches.
agent_dir = Path.home() / dir_str[1:].lstrip("/")
else:
p = Path(dir_str)
agent_dir = p if p.is_absolute() else project_root / p
Comment on lines +670 to +678
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in a90862b_resolve_agent_dir() now expands ~/... via Path.home() / dir_str[1:].lstrip('/') instead of expanduser(). This makes monkeypatched Path.home() effective for test isolation. expanduser() is reserved for ~user/... patterns only.

if not agent_dir.exists():
legacy = agent_config.get("legacy_dir")
if legacy:
Expand Down Expand Up @@ -704,6 +717,15 @@ def register_commands_for_all_agents(

self._ensure_configs()
for agent_name, agent_config in self.AGENT_CONFIGS.items():
# Check detect_dir first (project-local marker) if configured,
# falling back to the resolved dir for output. This prevents
# global dirs (e.g. ~/.hermes/skills) from causing false
# detection in every project.
detect_dir_str = agent_config.get("detect_dir")
if detect_dir_str:
detect_path = project_root / detect_dir_str
if not detect_path.exists():
continue
agent_dir = self._resolve_agent_dir(
agent_name, agent_config, project_root,
)
Expand Down Expand Up @@ -755,6 +777,11 @@ def register_commands_for_non_skill_agents(
for agent_name, agent_config in self.AGENT_CONFIGS.items():
if agent_config.get("extension") == "/SKILL.md":
continue
detect_dir_str = agent_config.get("detect_dir")
if detect_dir_str:
detect_path = project_root / detect_dir_str
if not detect_path.exists():
continue
agent_dir = self._resolve_agent_dir(
agent_name, agent_config, project_root,
)
Expand Down
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
Loading