diff --git a/src/specify_cli/presets.py b/src/specify_cli/presets.py index 041c832e45..fd9d4745f1 100644 --- a/src/specify_cli/presets.py +++ b/src/specify_cli/presets.py @@ -1048,9 +1048,9 @@ def _reconcile_skills(self, command_names: List[str]) -> None: short_name = cmd_name if short_name.startswith("speckit."): short_name = short_name[len("speckit."):] - desc = SKILL_DESCRIPTIONS.get( + desc = fm.get("description", "") or SKILL_DESCRIPTIONS.get( short_name.replace(".", "-"), - fm.get("description", f"Command: {short_name}"), + f"Command: {short_name}", ) init_opts = load_init_options(self.project_root) selected_ai = init_opts.get("ai") if isinstance(init_opts, dict) else "" @@ -1314,9 +1314,9 @@ def _register_skills( frontmatter[key] = core_frontmatter[key] original_desc = frontmatter.get("description", "") - enhanced_desc = SKILL_DESCRIPTIONS.get( + enhanced_desc = original_desc or SKILL_DESCRIPTIONS.get( short_name, - original_desc or f"Spec-kit workflow command: {short_name}", + f"Spec-kit workflow command: {short_name}", ) frontmatter = dict(frontmatter) frontmatter["description"] = enhanced_desc @@ -1417,9 +1417,9 @@ def _unregister_skills(self, skill_names: List[str], preset_dir: Path) -> None: ) original_desc = frontmatter.get("description", "") - enhanced_desc = SKILL_DESCRIPTIONS.get( + enhanced_desc = original_desc or SKILL_DESCRIPTIONS.get( short_name, - original_desc or f"Spec-kit workflow command: {short_name}", + f"Spec-kit workflow command: {short_name}", ) frontmatter_data = registrar.build_skill_frontmatter( diff --git a/tests/test_presets.py b/tests/test_presets.py index 52566cbedc..8fa700fa77 100644 --- a/tests/test_presets.py +++ b/tests/test_presets.py @@ -2266,6 +2266,37 @@ def _create_skill(self, skills_dir, skill_name, body="original body"): ) return skill_dir + def _create_command_preset(self, temp_dir, preset_id, command_name, description, body): + preset_dir = temp_dir / preset_id + preset_dir.mkdir() + (preset_dir / "commands").mkdir() + command_file = f"{command_name}.md" + (preset_dir / "commands" / command_file).write_text( + f"---\ndescription: {description}\n---\n\n{body}\n" + ) + manifest_data = { + "schema_version": "1.0", + "preset": { + "id": preset_id, + "name": preset_id, + "version": "1.0.0", + "description": "Test", + }, + "requires": {"speckit_version": ">=0.1.0"}, + "provides": { + "templates": [ + { + "type": "command", + "name": command_name, + "file": f"commands/{command_file}", + } + ] + }, + } + with open(preset_dir / "preset.yml", "w") as f: + yaml.dump(manifest_data, f) + return preset_dir + def test_skill_overridden_on_preset_install(self, project_dir, temp_dir): """When --ai-skills was used, a preset command override should update the skill.""" # Simulate --ai-skills having been used: write init-options + create skill @@ -2290,6 +2321,120 @@ def test_skill_overridden_on_preset_install(self, project_dir, temp_dir): metadata = manager.registry.get("self-test") assert "speckit-specify" in metadata.get("registered_skills", []) + def test_core_command_override_skill_uses_preset_command_description(self, project_dir, temp_dir): + """Preset skill overrides for core commands should keep preset frontmatter descriptions.""" + self._write_init_options(project_dir, ai="claude") + skills_dir = project_dir / ".claude" / "skills" + self._create_skill(skills_dir, "speckit-taskstoissues") + + preset_dir = temp_dir / "taskstoissues-description" + preset_dir.mkdir() + (preset_dir / "commands").mkdir() + (preset_dir / "commands" / "speckit.repro.taskstoissues.md").write_text( + "---\n" + "description: COMMAND-FRONTMATTER-DESCRIPTION\n" + "---\n\n" + "# Repro command body\n" + ) + manifest_data = { + "schema_version": "1.0", + "preset": { + "id": "taskstoissues-description", + "name": "Taskstoissues Description", + "version": "1.0.0", + "description": "Test", + }, + "requires": {"speckit_version": ">=0.1.0"}, + "provides": { + "templates": [ + { + "type": "command", + "name": "speckit.taskstoissues", + "file": "commands/speckit.repro.taskstoissues.md", + "description": "MANIFEST-DESCRIPTION", + "replaces": "speckit.taskstoissues", + "strategy": "replace", + } + ] + }, + } + with open(preset_dir / "preset.yml", "w") as f: + yaml.dump(manifest_data, f) + + manager = PresetManager(project_dir) + manager.install_from_directory(preset_dir, "0.1.5") + + skill_file = skills_dir / "speckit-taskstoissues" / "SKILL.md" + content = skill_file.read_text() + assert "description: COMMAND-FRONTMATTER-DESCRIPTION" in content + assert "Convert tasks from tasks.md into GitHub issues." not in content + assert "source: preset:taskstoissues-description" in content + + def test_core_skill_restore_uses_core_command_description(self, project_dir, temp_dir): + """Core skill restore should keep core command frontmatter descriptions.""" + self._write_init_options(project_dir, ai="claude") + skills_dir = project_dir / ".claude" / "skills" + self._create_skill(skills_dir, "speckit-taskstoissues") + + core_cmds = project_dir / ".specify" / "templates" / "commands" + core_cmds.mkdir(parents=True, exist_ok=True) + (core_cmds / "taskstoissues.md").write_text( + "---\n" + "description: CORE-FRONTMATTER-DESCRIPTION\n" + "---\n\n" + "core taskstoissues body\n" + ) + preset_dir = self._create_command_preset( + temp_dir, + "taskstoissues-restore", + "speckit.taskstoissues", + "PRESET-FRONTMATTER-DESCRIPTION", + "preset taskstoissues body\n", + ) + + manager = PresetManager(project_dir) + manager.install_from_directory(preset_dir, "0.1.5") + manager.remove("taskstoissues-restore") + + skill_file = skills_dir / "speckit-taskstoissues" / "SKILL.md" + content = skill_file.read_text() + assert "description: CORE-FRONTMATTER-DESCRIPTION" in content + assert "Convert tasks from tasks.md into GitHub issues." not in content + assert "source: templates/commands/taskstoissues.md" in content + assert "core taskstoissues body" in content + + def test_override_skill_reconcile_uses_override_command_description(self, project_dir, temp_dir): + """Override skill reconciliation should keep override frontmatter descriptions.""" + self._write_init_options(project_dir, ai="claude") + skills_dir = project_dir / ".claude" / "skills" + self._create_skill(skills_dir, "speckit-taskstoissues") + + overrides_dir = project_dir / ".specify" / "templates" / "overrides" + overrides_dir.mkdir(parents=True) + (overrides_dir / "speckit.taskstoissues.md").write_text( + "---\n" + "description: OVERRIDE-FRONTMATTER-DESCRIPTION\n" + "---\n\n" + "override taskstoissues body\n" + ) + preset_dir = self._create_command_preset( + temp_dir, + "taskstoissues-reconcile", + "speckit.taskstoissues", + "PRESET-FRONTMATTER-DESCRIPTION", + "preset taskstoissues body\n", + ) + + manager = PresetManager(project_dir) + manager.install_from_directory(preset_dir, "0.1.5") + + skill_file = skills_dir / "speckit-taskstoissues" / "SKILL.md" + content = skill_file.read_text() + assert "description: OVERRIDE-FRONTMATTER-DESCRIPTION" in content + assert "Convert tasks from tasks.md into GitHub issues." not in content + assert "source: override:speckit.taskstoissues" in content + assert "override taskstoissues body" in content + def test_skill_not_updated_when_ai_skills_disabled(self, project_dir, temp_dir): """When --ai-skills was NOT used, preset install should not touch skills.""" self._write_init_options(project_dir, ai="qwen", ai_skills=False)