Skip to content

Commit 3da5e0b

Browse files
committed
fix: add first-class global/home-based agent dir support in CommandRegistrar
Resolves Copilot HIGH concern (discussion_r3312194525): HermesIntegration.registrar_config.dir was '.hermes/skills' (project- relative), but skills live in ~/.hermes/skills/ (global). Extensions and presets registering commands for the 'hermes' agent via CommandRegistrar would write to the project-local marker directory instead of the real global skills directory, making those commands invisible to Hermes. Fix consists of three parts: 1. CommandRegistrar._resolve_agent_dir now supports '~/'-prefixed and absolute paths in agent_config['dir']. Relative paths still resolve against project_root as before — zero change for existing agents (Claude, Codex, Gemini, etc.). 2. HermesIntegration.registrar_config.dir changed from '.hermes/skills' to '~/.hermes/skills', so extensions/presets write directly to the global directory Hermes searches at runtime. 3. Two inline project_root / agent_config['dir'] calls in the extension update backup/restore paths (src/specify_cli/__init__.py) now delegate to _resolve_agent_dir, giving them the same global-dir support plus the legacy_dir fallback they were missing (improvement for all agents). Test side-effect: test_update_failure_rolls_back_registry_hooks_and_commands was constructing verification paths with project_dir / '~/.hermes/skills' (literal tilde) — fixed to use _resolve_agent_dir and monkeypatch Path.home() so Hermes' global dir doesn't leak into the real filesystem.
1 parent 6e9af2b commit 3da5e0b

5 files changed

Lines changed: 34 additions & 11 deletions

File tree

src/specify_cli/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4328,7 +4328,9 @@ def extension_update(
43284328
if agent_name not in registrar.AGENT_CONFIGS:
43294329
continue
43304330
agent_config = registrar.AGENT_CONFIGS[agent_name]
4331-
commands_dir = project_root / agent_config["dir"]
4331+
commands_dir = _AgentReg._resolve_agent_dir(
4332+
agent_name, agent_config, project_root
4333+
)
43324334

43334335
for cmd_name in cmd_names:
43344336
output_name = _AgentReg._compute_output_name(agent_name, cmd_name, agent_config)
@@ -4489,7 +4491,9 @@ def extension_update(
44894491
if agent_name not in registrar.AGENT_CONFIGS:
44904492
continue
44914493
agent_config = registrar.AGENT_CONFIGS[agent_name]
4492-
commands_dir = project_root / agent_config["dir"]
4494+
commands_dir = _AgentReg._resolve_agent_dir(
4495+
agent_name, agent_config, project_root
4496+
)
44934497

44944498
for cmd_name in cmd_names:
44954499
output_name = _AgentReg._compute_output_name(agent_name, cmd_name, agent_config)

src/specify_cli/agents.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -654,15 +654,25 @@ def _resolve_agent_dir(
654654
) -> Path:
655655
"""Return the agent command directory, falling back to legacy_dir.
656656
657-
When the canonical directory (``agent_config["dir"]``) does not
658-
exist but a ``legacy_dir`` is configured and present on disk,
659-
returns the legacy path and emits a deprecation warning advising
660-
the user to upgrade.
657+
Supports project-relative paths (e.g. ``.claude/skills/``),
658+
home-relative paths (e.g. ``~/.hermes/skills``), and absolute
659+
paths — the ``agent_config["dir"]`` value is resolved verbatim
660+
when absolute or starting with ``~/``, or joined with
661+
``project_root`` when relative.
662+
663+
When the canonical directory does not exist but a ``legacy_dir``
664+
is configured and present on disk, returns the legacy path and
665+
emits a deprecation warning advising the user to upgrade.
661666
662667
Integrations that do not declare ``legacy_dir`` get the canonical
663668
path unconditionally — no fallback, no warning.
664669
"""
665-
agent_dir = project_root / agent_config["dir"]
670+
dir_str = agent_config["dir"]
671+
if dir_str.startswith("~"):
672+
agent_dir = Path(dir_str).expanduser()
673+
else:
674+
p = Path(dir_str)
675+
agent_dir = p if p.is_absolute() else project_root / p
666676
if not agent_dir.exists():
667677
legacy = agent_config.get("legacy_dir")
668678
if legacy:

src/specify_cli/integrations/hermes/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class HermesIntegration(SkillsIntegration):
4444
"requires_cli": True,
4545
}
4646
registrar_config = {
47-
"dir": ".hermes/skills",
47+
"dir": "~/.hermes/skills",
4848
"format": "markdown",
4949
"args": "$ARGUMENTS",
5050
"extension": "/SKILL.md",

tests/integrations/test_integration_hermes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class TestHermesIntegration(SkillsIntegrationTests):
2929
KEY = "hermes"
3030
FOLDER = ".hermes/"
3131
COMMANDS_SUBDIR = "skills"
32-
REGISTRAR_DIR = ".hermes/skills"
32+
REGISTRAR_DIR = "~/.hermes/skills"
3333
CONTEXT_FILE = "AGENTS.md"
3434

3535
# -- Hermes-specific setup: skills go to ~/.hermes/skills/ -------------

tests/test_extensions.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3744,13 +3744,20 @@ def fake_install_from_zip(self_obj, _zip_path, speckit_version):
37443744
).read_text()
37453745
assert restored_config_content == original_config_content
37463746

3747-
def test_update_failure_rolls_back_registry_hooks_and_commands(self, tmp_path):
3747+
def test_update_failure_rolls_back_registry_hooks_and_commands(self, tmp_path, monkeypatch):
37483748
"""Failed update should restore original registry, hooks, and command files."""
37493749
from typer.testing import CliRunner
37503750
from unittest.mock import patch
37513751
from specify_cli import app
37523752
import yaml
37533753

3754+
# Isolate home directory so Hermes' global ~/.hermes/skills/ doesn't
3755+
# interfere — without a real skills dir, Hermes is skipped during
3756+
# command registration, keeping the test focused on Claude/Codex/etc.
3757+
fake_home = tmp_path / "home"
3758+
fake_home.mkdir()
3759+
monkeypatch.setattr(Path, "home", lambda: fake_home)
3760+
37543761
runner = CliRunner()
37553762
project_dir = tmp_path / "project"
37563763
project_dir.mkdir()
@@ -3772,7 +3779,9 @@ def test_update_failure_rolls_back_registry_hooks_and_commands(self, tmp_path):
37723779
if agent_name not in agent_registrar.AGENT_CONFIGS:
37733780
continue
37743781
agent_cfg = agent_registrar.AGENT_CONFIGS[agent_name]
3775-
commands_dir = project_dir / agent_cfg["dir"]
3782+
commands_dir = AgentRegistrar._resolve_agent_dir(
3783+
agent_name, agent_cfg, project_dir
3784+
)
37763785
for cmd_name in cmd_names:
37773786
output_name = AgentRegistrar._compute_output_name(agent_name, cmd_name, agent_cfg)
37783787
cmd_path = commands_dir / f"{output_name}{agent_cfg['extension']}"

0 commit comments

Comments
 (0)