Skip to content

Commit b82e39e

Browse files
iamaeroplaneclaude
andcommitted
fix(agents): remove orphaned SKILL.md parent dirs on unregister
For SKILL.md-based agents (codex, kimi), each command lives in its own subdirectory (e.g. .agents/skills/speckit-ext-cmd/SKILL.md). The previous unregister_commands() only unlinked the file, leaving an empty parent dir. Now attempts rmdir() on the parent when it differs from the agent commands dir. OSError is silenced so non-empty dirs (e.g. user files) are safely left. Adds test_unregister_skill_removes_parent_directory to cover this. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent dfb88bb commit b82e39e

2 files changed

Lines changed: 57 additions & 0 deletions

File tree

src/specify_cli/agents.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,15 @@ def unregister_commands(
563563
cmd_file = commands_dir / f"{output_name}{agent_config['extension']}"
564564
if cmd_file.exists():
565565
cmd_file.unlink()
566+
# For SKILL.md agents each command lives in its own subdirectory
567+
# (e.g. .agents/skills/speckit-ext-cmd/SKILL.md). Remove the
568+
# parent dir when it becomes empty to avoid orphaned directories.
569+
parent = cmd_file.parent
570+
if parent != commands_dir and parent.exists():
571+
try:
572+
parent.rmdir() # no-op if dir still has other files
573+
except OSError:
574+
pass
566575

567576
if agent_name == "copilot":
568577
prompt_file = project_root / ".github" / "prompts" / f"{cmd_name}.prompt.md"

tests/test_extensions.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1715,6 +1715,54 @@ def test_non_copilot_agent_no_companion_file(self, extension_dir, project_dir):
17151715
prompts_dir = project_dir / ".github" / "prompts"
17161716
assert not prompts_dir.exists()
17171717

1718+
def test_unregister_skill_removes_parent_directory(self, project_dir, temp_dir):
1719+
"""Unregistering a SKILL.md command should remove the empty parent subdirectory."""
1720+
import yaml
1721+
1722+
ext_dir = temp_dir / "cleanup-ext"
1723+
ext_dir.mkdir()
1724+
(ext_dir / "commands").mkdir()
1725+
1726+
manifest_data = {
1727+
"schema_version": "1.0",
1728+
"extension": {
1729+
"id": "cleanup-ext",
1730+
"name": "Cleanup Extension",
1731+
"version": "1.0.0",
1732+
"description": "Test",
1733+
},
1734+
"requires": {"speckit_version": ">=0.1.0"},
1735+
"provides": {
1736+
"commands": [
1737+
{
1738+
"name": "speckit.cleanup-ext.run",
1739+
"file": "commands/run.md",
1740+
"description": "Run",
1741+
}
1742+
]
1743+
},
1744+
}
1745+
with open(ext_dir / "extension.yml", "w") as f:
1746+
yaml.dump(manifest_data, f)
1747+
(ext_dir / "commands" / "run.md").write_text("---\ndescription: Run\n---\n\nBody")
1748+
1749+
skills_dir = project_dir / ".agents" / "skills"
1750+
skills_dir.mkdir(parents=True)
1751+
1752+
registrar = CommandRegistrar()
1753+
from specify_cli.extensions import ExtensionManifest
1754+
manifest = ExtensionManifest(ext_dir / "extension.yml")
1755+
registered = registrar.register_commands_for_agent("codex", manifest, ext_dir, project_dir)
1756+
1757+
skill_subdir = skills_dir / "speckit-cleanup-ext-run"
1758+
assert skill_subdir.exists(), "Skill subdirectory should exist after registration"
1759+
assert (skill_subdir / "SKILL.md").exists()
1760+
1761+
registrar.unregister_commands({"codex": ["speckit.cleanup-ext.run"]}, project_dir)
1762+
1763+
assert not (skill_subdir / "SKILL.md").exists(), "SKILL.md should be removed"
1764+
assert not skill_subdir.exists(), "Empty parent subdirectory should be removed"
1765+
17181766

17191767
# ===== Utility Function Tests =====
17201768

0 commit comments

Comments
 (0)