Skip to content

Commit f40bfe8

Browse files
committed
Fix native skill preset cleanup
1 parent cf6d5d6 commit f40bfe8

3 files changed

Lines changed: 119 additions & 8 deletions

File tree

src/specify_cli/integrations/claude/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def setup(
7171
f"project_root ({project_root_resolved})"
7272
)
7373

74-
dest = self.commands_dest(project_root).resolve()
74+
dest = self.skills_dest(project_root).resolve()
7575
try:
7676
dest.relative_to(project_root_resolved)
7777
except ValueError as exc:

src/specify_cli/presets.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,10 @@ def _register_skills(
717717
ai_skills_enabled = bool(init_opts.get("ai_skills"))
718718
registrar = CommandRegistrar()
719719
agent_config = registrar.AGENT_CONFIGS.get(selected_ai, {})
720+
# Native skill agents (e.g. codex/kimi/agy) materialize brand-new
721+
# preset skills in _register_commands() because their detected agent
722+
# directory is already the skills directory. This flag is only for
723+
# command-backed agents that also mirror commands into skills.
720724
create_missing_skills = ai_skills_enabled and agent_config.get("extension") != "/SKILL.md"
721725

722726
written: List[str] = []
@@ -1042,14 +1046,15 @@ def remove(self, pack_id: str) -> bool:
10421046
if registered_skills:
10431047
self._unregister_skills(registered_skills, pack_dir)
10441048
try:
1045-
from . import NATIVE_SKILLS_AGENTS
1049+
from .agents import CommandRegistrar
10461050
except ImportError:
1047-
NATIVE_SKILLS_AGENTS = set()
1048-
registered_commands = {
1049-
agent_name: cmd_names
1050-
for agent_name, cmd_names in registered_commands.items()
1051-
if agent_name not in NATIVE_SKILLS_AGENTS
1052-
}
1051+
CommandRegistrar = None
1052+
if CommandRegistrar is not None:
1053+
registered_commands = {
1054+
agent_name: cmd_names
1055+
for agent_name, cmd_names in registered_commands.items()
1056+
if CommandRegistrar.AGENT_CONFIGS.get(agent_name, {}).get("extension") != "/SKILL.md"
1057+
}
10531058

10541059
# Unregister non-skill command files from AI agents.
10551060
if registered_commands:

tests/test_presets.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2352,6 +2352,55 @@ def test_kimi_skill_updated_even_when_ai_skills_disabled(self, project_dir, temp
23522352
metadata = manager.registry.get("self-test")
23532353
assert "speckit-specify" in metadata.get("registered_skills", [])
23542354

2355+
def test_kimi_new_skill_created_even_when_ai_skills_disabled(self, project_dir, temp_dir):
2356+
"""Kimi native skills should still receive brand-new preset commands."""
2357+
self._write_init_options(project_dir, ai="kimi", ai_skills=False)
2358+
skills_dir = project_dir / ".kimi" / "skills"
2359+
skills_dir.mkdir(parents=True, exist_ok=True)
2360+
2361+
preset_dir = temp_dir / "kimi-new-skill"
2362+
preset_dir.mkdir()
2363+
(preset_dir / "commands").mkdir()
2364+
(preset_dir / "commands" / "speckit.research.md").write_text(
2365+
"---\n"
2366+
"description: Kimi research workflow\n"
2367+
"---\n\n"
2368+
"preset:kimi-new-skill\n"
2369+
)
2370+
manifest_data = {
2371+
"schema_version": "1.0",
2372+
"preset": {
2373+
"id": "kimi-new-skill",
2374+
"name": "Kimi New Skill",
2375+
"version": "1.0.0",
2376+
"description": "Test",
2377+
},
2378+
"requires": {"speckit_version": ">=0.1.0"},
2379+
"provides": {
2380+
"templates": [
2381+
{
2382+
"type": "command",
2383+
"name": "speckit.research",
2384+
"file": "commands/speckit.research.md",
2385+
}
2386+
]
2387+
},
2388+
}
2389+
with open(preset_dir / "preset.yml", "w") as f:
2390+
yaml.dump(manifest_data, f)
2391+
2392+
manager = PresetManager(project_dir)
2393+
manager.install_from_directory(preset_dir, "0.1.5")
2394+
2395+
skill_file = skills_dir / "speckit-research" / "SKILL.md"
2396+
assert skill_file.exists()
2397+
content = skill_file.read_text()
2398+
assert "preset:kimi-new-skill" in content
2399+
assert "name: speckit-research" in content
2400+
2401+
metadata = manager.registry.get("kimi-new-skill")
2402+
assert "speckit-research" in metadata.get("registered_skills", [])
2403+
23552404
def test_kimi_preset_skill_override_resolves_script_placeholders(self, project_dir, temp_dir):
23562405
"""Kimi preset skill overrides should resolve placeholders and rewrite project paths."""
23572406
self._write_init_options(project_dir, ai="kimi", ai_skills=False, script="sh")
@@ -2404,6 +2453,63 @@ def test_kimi_preset_skill_override_resolves_script_placeholders(self, project_d
24042453
assert ".specify/memory/constitution.md" in content
24052454
assert "for kimi" in content
24062455

2456+
def test_agy_skill_restored_on_preset_remove(self, project_dir, temp_dir):
2457+
"""Agy preset removal should restore native skills instead of deleting them."""
2458+
self._write_init_options(project_dir, ai="agy", ai_skills=True)
2459+
skills_dir = project_dir / ".agent" / "skills"
2460+
self._create_skill(skills_dir, "speckit-specify", body="before override")
2461+
2462+
core_command = project_dir / ".specify" / "templates" / "commands" / "specify.md"
2463+
core_command.write_text(
2464+
"---\n"
2465+
"description: Restored core specify workflow\n"
2466+
"---\n\n"
2467+
"restored core body\n"
2468+
)
2469+
2470+
preset_dir = temp_dir / "agy-override"
2471+
preset_dir.mkdir()
2472+
(preset_dir / "commands").mkdir()
2473+
(preset_dir / "commands" / "speckit.specify.md").write_text(
2474+
"---\n"
2475+
"description: Agy override\n"
2476+
"---\n\n"
2477+
"preset agy body\n"
2478+
)
2479+
manifest_data = {
2480+
"schema_version": "1.0",
2481+
"preset": {
2482+
"id": "agy-override",
2483+
"name": "Agy Override",
2484+
"version": "1.0.0",
2485+
"description": "Test",
2486+
},
2487+
"requires": {"speckit_version": ">=0.1.0"},
2488+
"provides": {
2489+
"templates": [
2490+
{
2491+
"type": "command",
2492+
"name": "speckit.specify",
2493+
"file": "commands/speckit.specify.md",
2494+
}
2495+
]
2496+
},
2497+
}
2498+
with open(preset_dir / "preset.yml", "w") as f:
2499+
yaml.dump(manifest_data, f)
2500+
2501+
manager = PresetManager(project_dir)
2502+
manager.install_from_directory(preset_dir, "0.1.5")
2503+
2504+
skill_file = skills_dir / "speckit-specify" / "SKILL.md"
2505+
assert "preset agy body" in skill_file.read_text()
2506+
2507+
assert manager.remove("agy-override") is True
2508+
assert skill_file.exists()
2509+
restored = skill_file.read_text()
2510+
assert "restored core body" in restored
2511+
assert "name: speckit-specify" in restored
2512+
24072513
def test_preset_skill_registration_handles_non_dict_init_options(self, project_dir, temp_dir):
24082514
"""Non-dict init-options payloads should not crash preset install/remove flows."""
24092515
init_options = project_dir / ".specify" / "init-options.json"

0 commit comments

Comments
 (0)