Skip to content

Commit 6e9af2b

Browse files
committed
fix: address second Copilot review round — 6 remaining observations
- Move to module scope (was inside per-template loop) - Add safety checks in setup() matching standard - Fix docstrings: global skills always removed on uninstall (standard) - Fix removal tracking: only report after successful rmtree - Override shared test_modified_file_survives_uninstall with Hermes-appropriate behaviour (global skills always removed, no hash tracking) - Update PR description to match implementation (global-only skills + marker)
1 parent a5fd163 commit 6e9af2b

2 files changed

Lines changed: 62 additions & 28 deletions

File tree

src/specify_cli/integrations/hermes/__init__.py

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
from shutil import rmtree
1717
from typing import Any
1818

19+
import yaml
20+
1921
from ..base import IntegrationOption, SkillsIntegration
2022
from ..manifest import IntegrationManifest
2123

@@ -29,7 +31,8 @@ class HermesIntegration(SkillsIntegration):
2931
Hermes discovers them globally. A project-local marker directory
3032
(``.hermes/skills/`` empty) is created so extension commands (e.g.
3133
git) can detect Hermes as an active integration. Uninstall removes
32-
both the marker and global skills.
34+
both the marker and all global ``speckit-*`` skills, matching the
35+
standard integration teardown behaviour.
3336
"""
3437

3538
key = "hermes"
@@ -91,6 +94,14 @@ def setup(
9194
if not templates:
9295
return []
9396

97+
# Safety check: verify manifest project_root matches (standard pattern)
98+
project_root_resolved = project_root.resolve()
99+
if manifest.project_root != project_root_resolved:
100+
raise ValueError(
101+
f"manifest.project_root ({manifest.project_root}) does not match "
102+
f"project_root ({project_root_resolved})"
103+
)
104+
94105
script_type = opts.get("script_type", "sh")
95106
arg_placeholder = (
96107
self.registrar_config.get("args", "$ARGUMENTS")
@@ -115,8 +126,6 @@ def setup(
115126
if raw.startswith("---"):
116127
parts = raw.split("---", 2)
117128
if len(parts) >= 3:
118-
import yaml
119-
120129
try:
121130
fm = yaml.safe_load(parts[1])
122131
if isinstance(fm, dict):
@@ -189,15 +198,16 @@ def teardown(
189198
*,
190199
force: bool = False,
191200
) -> tuple[list[Path], list[Path]]:
192-
"""Uninstall integration files and optionally clean up global skills.
201+
"""Uninstall integration files including global Hermes skills.
193202
194203
Removes the managed context section from AGENTS.md, removes the
195-
project-local marker directory, and delegates to
196-
``manifest.uninstall()`` for project-local tracked files.
204+
project-local marker directory, delegates to
205+
``manifest.uninstall()`` for project-local tracked files, and
206+
removes all ``speckit-*`` skills under ``~/.hermes/skills/``.
197207
198-
Global ``speckit-*`` skills under ``~/.hermes/skills/`` are only
199-
removed when ``force=True`` to avoid destroying skills shared with
200-
other Spec Kit projects.
208+
Global skills are always removed on teardown — this matches the
209+
standard integration behaviour where all files created by the
210+
integration are removed on ``specify integration uninstall``.
201211
"""
202212
# Remove managed context section from AGENTS.md
203213
self.remove_context_section(project_root)
@@ -214,17 +224,18 @@ def teardown(
214224
if hermes_dir.is_dir() and not any(hermes_dir.iterdir()):
215225
hermes_dir.rmdir()
216226

217-
# Remove global Hermes skills for speckit — only when force=True
218-
# to avoid destroying skills shared with other Spec Kit projects.
219-
if force:
220-
global_skills_dir = self._hermes_home_skills_dir()
221-
if global_skills_dir.is_dir():
222-
for skill_dir in sorted(global_skills_dir.iterdir()):
223-
if skill_dir.is_dir() and skill_dir.name.startswith("speckit-"):
224-
skill_file = skill_dir / "SKILL.md"
225-
if skill_file.exists():
226-
removed.append(skill_file)
227-
rmtree(skill_dir, ignore_errors=True)
227+
# Remove all global Hermes skills for speckit — these are always
228+
# removed on uninstall regardless of the force flag, matching the
229+
# standard behaviour where all integration files are cleaned up.
230+
global_skills_dir = self._hermes_home_skills_dir()
231+
if global_skills_dir.is_dir():
232+
for skill_dir in sorted(global_skills_dir.iterdir()):
233+
if skill_dir.is_dir() and skill_dir.name.startswith("speckit-"):
234+
try:
235+
rmtree(skill_dir)
236+
removed.append(skill_dir)
237+
except OSError:
238+
skipped.append(skill_dir)
228239

229240
return removed, skipped
230241

tests/integrations/test_integration_hermes.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -124,16 +124,19 @@ def test_install_uninstall_roundtrip(self, tmp_path, monkeypatch):
124124
for f in created:
125125
if "SKILL.md" in str(f):
126126
assert f.exists(), f"{f} does not exist"
127-
removed, skipped = i.teardown(tmp_path, m, force=True)
127+
# Global skills are removed on teardown without needing force
128+
removed, skipped = i.teardown(tmp_path, m, force=False)
128129
for f in created:
129130
if "SKILL.md" in str(f):
130131
assert not f.exists(), f"{f} should have been removed"
131132
# Local marker should be gone
132133
assert not (tmp_path / ".hermes" / "skills").exists()
133134

134135
def test_modified_file_survives_uninstall(self, tmp_path, monkeypatch):
135-
"""Override: Hermes global skills are removed on uninstall only
136-
when force=True (no hash-based preservation since they're not in manifest)."""
136+
"""Override: Hermes global skills are ALWAYS removed on uninstall
137+
(they live outside the project root and aren't hash-tracked in the
138+
manifest), so a modified global skill is still removed — matching
139+
the standard behaviour where all integration files are cleaned up."""
137140
home = _fake_home(tmp_path)
138141
monkeypatch.setattr(Path, "home", lambda: home)
139142

@@ -146,10 +149,30 @@ def test_modified_file_survives_uninstall(self, tmp_path, monkeypatch):
146149
assert len(skill_files) > 0
147150
modified_file = skill_files[0]
148151
modified_file.write_text("user modified this", encoding="utf-8")
149-
# Global skills are only removed with force=True
150-
removed, skipped = i.teardown(tmp_path, m, force=True)
152+
removed, skipped = i.uninstall(tmp_path, m)
151153
assert not modified_file.exists(), (
152-
"Modified global skill should be removed on teardown with force=True"
154+
"Modified global skill should be removed on teardown (standard behaviour)"
155+
)
156+
157+
def test_modified_global_skill_removed_on_teardown(self, tmp_path, monkeypatch):
158+
"""Override: Hermes global skills are removed on uninstall regardless
159+
of the force flag, matching standard integration behaviour."""
160+
home = _fake_home(tmp_path)
161+
monkeypatch.setattr(Path, "home", lambda: home)
162+
163+
i = get_integration(self.KEY)
164+
m = IntegrationManifest(self.KEY, tmp_path)
165+
created = i.install(tmp_path, m)
166+
m.save()
167+
# Pick a global skill file
168+
skill_files = [f for f in created if "SKILL.md" in str(f)]
169+
assert len(skill_files) > 0
170+
modified_file = skill_files[0]
171+
modified_file.write_text("user modified this", encoding="utf-8")
172+
# Global skills are removed on teardown regardless of force flag
173+
removed, skipped = i.teardown(tmp_path, m, force=False)
174+
assert not modified_file.exists(), (
175+
"Modified global skill should be removed on teardown (standard behaviour)"
153176
)
154177

155178
def test_pre_existing_skills_not_removed(self, tmp_path, monkeypatch):
@@ -257,8 +280,8 @@ def test_install_uninstall_cleanup(self, tmp_path, monkeypatch):
257280
# Verify local marker exists
258281
assert (tmp_path / ".hermes" / "skills").is_dir()
259282

260-
# Teardown with force=True to clean global skills
261-
removed, skipped = i.teardown(tmp_path, m, force=True)
283+
# Teardown — global skills removed without needing force=True
284+
removed, skipped = i.teardown(tmp_path, m, force=False)
262285

263286
# Global skills removed
264287
for f in global_skills:

0 commit comments

Comments
 (0)