mirror of
https://github.com/github/spec-kit.git
synced 2026-03-27 15:53:07 +00:00
Unify Kimi/Codex skill naming and migrate legacy dotted Kimi dirs (#1971)
* fix: unify hyphenated skills and migrate legacy kimi dotted dirs * fix: preserve legacy kimi dotted preset skill overrides * fix: migrate kimi legacy dotted skills without ai-skills flag * fix: harden kimi migration and cache hook init options * fix: apply kimi preset skill overrides without ai-skills flag * fix: keep sequential branch numbering beyond 999 * test: align kimi scaffold skill path with hyphen naming * chore: align hook typing and preset skill comment * fix: restore AGENT_SKILLS_DIR_OVERRIDES compatibility export * refactor: remove AGENT_SKILLS_DIR_OVERRIDES and update callers * fix(ps1): support sequential branch numbers above 999 * fix: resolve preset skill placeholders for skills agents * Fix legacy kimi migration safety and preset skill dir checks * Harden TOML rendering and consolidate preset skill restore parsing * Fix PowerShell overflow and hook message fallback for empty invocations * Restore preset skills from extensions * Refine preset skill restore helpers * Harden skill path and preset checks * Guard non-dict init options * Avoid deleting unmanaged preset skill dirs * Unify extension skill naming with hooks * Harden extension native skill registration * Normalize preset skill titles
This commit is contained in:
@@ -24,8 +24,8 @@ import specify_cli
|
||||
|
||||
from specify_cli import (
|
||||
_get_skills_dir,
|
||||
_migrate_legacy_kimi_dotted_skills,
|
||||
install_ai_skills,
|
||||
AGENT_SKILLS_DIR_OVERRIDES,
|
||||
DEFAULT_SKILLS_DIR,
|
||||
SKILL_DESCRIPTIONS,
|
||||
AGENT_CONFIG,
|
||||
@@ -169,8 +169,8 @@ class TestGetSkillsDir:
|
||||
result = _get_skills_dir(project_dir, "copilot")
|
||||
assert result == project_dir / ".github" / "skills"
|
||||
|
||||
def test_codex_uses_override(self, project_dir):
|
||||
"""Codex should use the AGENT_SKILLS_DIR_OVERRIDES value."""
|
||||
def test_codex_skills_dir_from_agent_config(self, project_dir):
|
||||
"""Codex should resolve skills directory from AGENT_CONFIG folder."""
|
||||
result = _get_skills_dir(project_dir, "codex")
|
||||
assert result == project_dir / ".agents" / "skills"
|
||||
|
||||
@@ -203,12 +203,71 @@ class TestGetSkillsDir:
|
||||
# Should always end with "skills"
|
||||
assert result.name == "skills"
|
||||
|
||||
def test_override_takes_precedence_over_config(self, project_dir):
|
||||
"""AGENT_SKILLS_DIR_OVERRIDES should take precedence over AGENT_CONFIG."""
|
||||
for agent_key in AGENT_SKILLS_DIR_OVERRIDES:
|
||||
result = _get_skills_dir(project_dir, agent_key)
|
||||
expected = project_dir / AGENT_SKILLS_DIR_OVERRIDES[agent_key]
|
||||
assert result == expected
|
||||
class TestKimiLegacySkillMigration:
|
||||
"""Test temporary migration from Kimi dotted skill names to hyphenated names."""
|
||||
|
||||
def test_migrates_legacy_dotted_skill_directory(self, project_dir):
|
||||
skills_dir = project_dir / ".kimi" / "skills"
|
||||
legacy_dir = skills_dir / "speckit.plan"
|
||||
legacy_dir.mkdir(parents=True)
|
||||
(legacy_dir / "SKILL.md").write_text("legacy")
|
||||
|
||||
migrated, removed = _migrate_legacy_kimi_dotted_skills(skills_dir)
|
||||
|
||||
assert migrated == 1
|
||||
assert removed == 0
|
||||
assert not legacy_dir.exists()
|
||||
assert (skills_dir / "speckit-plan" / "SKILL.md").exists()
|
||||
|
||||
def test_removes_legacy_dir_when_hyphenated_target_exists_with_same_content(self, project_dir):
|
||||
skills_dir = project_dir / ".kimi" / "skills"
|
||||
legacy_dir = skills_dir / "speckit.plan"
|
||||
legacy_dir.mkdir(parents=True)
|
||||
(legacy_dir / "SKILL.md").write_text("legacy")
|
||||
target_dir = skills_dir / "speckit-plan"
|
||||
target_dir.mkdir(parents=True)
|
||||
(target_dir / "SKILL.md").write_text("legacy")
|
||||
|
||||
migrated, removed = _migrate_legacy_kimi_dotted_skills(skills_dir)
|
||||
|
||||
assert migrated == 0
|
||||
assert removed == 1
|
||||
assert not legacy_dir.exists()
|
||||
assert (target_dir / "SKILL.md").read_text() == "legacy"
|
||||
|
||||
def test_keeps_legacy_dir_when_hyphenated_target_differs(self, project_dir):
|
||||
skills_dir = project_dir / ".kimi" / "skills"
|
||||
legacy_dir = skills_dir / "speckit.plan"
|
||||
legacy_dir.mkdir(parents=True)
|
||||
(legacy_dir / "SKILL.md").write_text("legacy")
|
||||
target_dir = skills_dir / "speckit-plan"
|
||||
target_dir.mkdir(parents=True)
|
||||
(target_dir / "SKILL.md").write_text("new")
|
||||
|
||||
migrated, removed = _migrate_legacy_kimi_dotted_skills(skills_dir)
|
||||
|
||||
assert migrated == 0
|
||||
assert removed == 0
|
||||
assert legacy_dir.exists()
|
||||
assert (legacy_dir / "SKILL.md").read_text() == "legacy"
|
||||
assert (target_dir / "SKILL.md").read_text() == "new"
|
||||
|
||||
def test_keeps_legacy_dir_when_matching_target_but_extra_files_exist(self, project_dir):
|
||||
skills_dir = project_dir / ".kimi" / "skills"
|
||||
legacy_dir = skills_dir / "speckit.plan"
|
||||
legacy_dir.mkdir(parents=True)
|
||||
(legacy_dir / "SKILL.md").write_text("legacy")
|
||||
(legacy_dir / "notes.txt").write_text("custom")
|
||||
target_dir = skills_dir / "speckit-plan"
|
||||
target_dir.mkdir(parents=True)
|
||||
(target_dir / "SKILL.md").write_text("legacy")
|
||||
|
||||
migrated, removed = _migrate_legacy_kimi_dotted_skills(skills_dir)
|
||||
|
||||
assert migrated == 0
|
||||
assert removed == 0
|
||||
assert legacy_dir.exists()
|
||||
assert (legacy_dir / "notes.txt").read_text() == "custom"
|
||||
|
||||
|
||||
# ===== install_ai_skills Tests =====
|
||||
@@ -473,8 +532,7 @@ class TestInstallAiSkills:
|
||||
skills_dir = _get_skills_dir(proj, agent_key)
|
||||
assert skills_dir.exists()
|
||||
skill_dirs = [d.name for d in skills_dir.iterdir() if d.is_dir()]
|
||||
# Kimi uses dotted skill names; other agents use hyphen-separated names.
|
||||
expected_skill_name = "speckit.specify" if agent_key == "kimi" else "speckit-specify"
|
||||
expected_skill_name = "speckit-specify"
|
||||
assert expected_skill_name in skill_dirs
|
||||
assert (skills_dir / expected_skill_name / "SKILL.md").exists()
|
||||
|
||||
@@ -773,6 +831,32 @@ class TestNewProjectCommandSkip:
|
||||
mock_skills.assert_called_once()
|
||||
assert mock_skills.call_args.kwargs.get("overwrite_existing") is True
|
||||
|
||||
def test_kimi_legacy_migration_runs_without_ai_skills_flag(self, tmp_path):
|
||||
"""Kimi init should migrate dotted legacy skills even when --ai-skills is not set."""
|
||||
from typer.testing import CliRunner
|
||||
|
||||
runner = CliRunner()
|
||||
target = tmp_path / "kimi-legacy-no-ai-skills"
|
||||
|
||||
def fake_download(project_path, *args, **kwargs):
|
||||
legacy_dir = project_path / ".kimi" / "skills" / "speckit.plan"
|
||||
legacy_dir.mkdir(parents=True, exist_ok=True)
|
||||
(legacy_dir / "SKILL.md").write_text("---\nname: speckit.plan\n---\n\nlegacy\n")
|
||||
|
||||
with patch("specify_cli.download_and_extract_template", side_effect=fake_download), \
|
||||
patch("specify_cli.ensure_executable_scripts"), \
|
||||
patch("specify_cli.ensure_constitution_from_template"), \
|
||||
patch("specify_cli.is_git_repo", return_value=False), \
|
||||
patch("specify_cli.shutil.which", return_value="/usr/bin/kimi"):
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["init", str(target), "--ai", "kimi", "--script", "sh", "--no-git"],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert not (target / ".kimi" / "skills" / "speckit.plan").exists()
|
||||
assert (target / ".kimi" / "skills" / "speckit-plan" / "SKILL.md").exists()
|
||||
|
||||
def test_codex_ai_skills_here_mode_preserves_existing_codex_dir(self, tmp_path, monkeypatch):
|
||||
"""Codex --here skills init should not delete a pre-existing .codex directory."""
|
||||
from typer.testing import CliRunner
|
||||
@@ -1118,12 +1202,12 @@ class TestCliValidation:
|
||||
assert "Optional skills that you can use for your specs" in result.output
|
||||
|
||||
def test_kimi_next_steps_show_skill_invocation(self, monkeypatch):
|
||||
"""Kimi next-steps guidance should display /skill:speckit.* usage."""
|
||||
"""Kimi next-steps guidance should display /skill:speckit-* usage."""
|
||||
from typer.testing import CliRunner
|
||||
|
||||
def _fake_download(*args, **kwargs):
|
||||
project_path = Path(args[0])
|
||||
skill_dir = project_path / ".kimi" / "skills" / "speckit.specify"
|
||||
skill_dir = project_path / ".kimi" / "skills" / "speckit-specify"
|
||||
skill_dir.mkdir(parents=True, exist_ok=True)
|
||||
(skill_dir / "SKILL.md").write_text("---\ndescription: Test skill\n---\n\nBody.\n")
|
||||
|
||||
@@ -1137,7 +1221,7 @@ class TestCliValidation:
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "/skill:speckit.constitution" in result.output
|
||||
assert "/skill:speckit-constitution" in result.output
|
||||
assert "/speckit.constitution" not in result.output
|
||||
assert "Optional skills that you can use for your specs" in result.output
|
||||
|
||||
|
||||
Reference in New Issue
Block a user