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:
Hamilton Snow
2026-03-26 23:53:30 +08:00
committed by GitHub
parent 2c2fea8783
commit ccc44dd00a
14 changed files with 1355 additions and 248 deletions

View File

@@ -1490,12 +1490,6 @@ def load_init_options(project_path: Path) -> dict[str, Any]:
return {}
# Agent-specific skill directory overrides for agents whose skills directory
# doesn't follow the standard <agent_folder>/skills/ pattern
AGENT_SKILLS_DIR_OVERRIDES = {
"codex": ".agents/skills", # Codex agent layout override
}
# Default skills directory for agents not in AGENT_CONFIG
DEFAULT_SKILLS_DIR = ".agents/skills"
@@ -1528,13 +1522,9 @@ SKILL_DESCRIPTIONS = {
def _get_skills_dir(project_path: Path, selected_ai: str) -> Path:
"""Resolve the agent-specific skills directory for the given AI assistant.
Uses ``AGENT_SKILLS_DIR_OVERRIDES`` first, then falls back to
``AGENT_CONFIG[agent]["folder"] + "skills"``, and finally to
``DEFAULT_SKILLS_DIR``.
Uses ``AGENT_CONFIG[agent]["folder"] + "skills"`` and falls back to
``DEFAULT_SKILLS_DIR`` for unknown agents.
"""
if selected_ai in AGENT_SKILLS_DIR_OVERRIDES:
return project_path / AGENT_SKILLS_DIR_OVERRIDES[selected_ai]
agent_config = AGENT_CONFIG.get(selected_ai, {})
agent_folder = agent_config.get("folder", "")
if agent_folder:
@@ -1648,10 +1638,7 @@ def install_ai_skills(
command_name = command_name[len("speckit."):]
if command_name.endswith(".agent"):
command_name = command_name[:-len(".agent")]
if selected_ai == "kimi":
skill_name = f"speckit.{command_name}"
else:
skill_name = f"speckit-{command_name}"
skill_name = f"speckit-{command_name.replace('.', '-')}"
# Create skill directory (additive — never removes existing content)
skill_dir = skills_dir / skill_name
@@ -1730,8 +1717,64 @@ def _has_bundled_skills(project_path: Path, selected_ai: str) -> bool:
if not skills_dir.is_dir():
return False
pattern = "speckit.*/SKILL.md" if selected_ai == "kimi" else "speckit-*/SKILL.md"
return any(skills_dir.glob(pattern))
return any(skills_dir.glob("speckit-*/SKILL.md"))
def _migrate_legacy_kimi_dotted_skills(skills_dir: Path) -> tuple[int, int]:
"""Migrate legacy Kimi dotted skill dirs (speckit.xxx) to hyphenated format.
Temporary migration helper:
- Intended removal window: after 2026-06-25.
- Purpose: one-time cleanup for projects initialized before Kimi moved to
hyphenated skills (speckit-xxx).
Returns:
Tuple[migrated_count, removed_count]
- migrated_count: old dotted dir renamed to hyphenated dir
- removed_count: old dotted dir deleted when equivalent hyphenated dir existed
"""
if not skills_dir.is_dir():
return (0, 0)
migrated_count = 0
removed_count = 0
for legacy_dir in sorted(skills_dir.glob("speckit.*")):
if not legacy_dir.is_dir():
continue
if not (legacy_dir / "SKILL.md").exists():
continue
suffix = legacy_dir.name[len("speckit."):]
if not suffix:
continue
target_dir = skills_dir / f"speckit-{suffix.replace('.', '-')}"
if not target_dir.exists():
shutil.move(str(legacy_dir), str(target_dir))
migrated_count += 1
continue
# If the new target already exists, avoid destructive cleanup unless
# both SKILL.md files are byte-identical.
target_skill = target_dir / "SKILL.md"
legacy_skill = legacy_dir / "SKILL.md"
if target_skill.is_file():
try:
if target_skill.read_bytes() == legacy_skill.read_bytes():
# Preserve legacy directory when it contains extra user files.
has_extra_entries = any(
child.name != "SKILL.md" for child in legacy_dir.iterdir()
)
if not has_extra_entries:
shutil.rmtree(legacy_dir)
removed_count += 1
except OSError:
# Best-effort migration: preserve legacy dir on read failures.
pass
return (migrated_count, removed_count)
AGENT_SKILLS_MIGRATIONS = {
@@ -2094,16 +2137,33 @@ def init(
ensure_constitution_from_template(project_path, tracker=tracker)
# Determine skills directory and migrate any legacy Kimi dotted skills.
migrated_legacy_kimi_skills = 0
removed_legacy_kimi_skills = 0
skills_dir: Optional[Path] = None
if selected_ai in NATIVE_SKILLS_AGENTS:
skills_dir = _get_skills_dir(project_path, selected_ai)
if selected_ai == "kimi" and skills_dir.is_dir():
(
migrated_legacy_kimi_skills,
removed_legacy_kimi_skills,
) = _migrate_legacy_kimi_dotted_skills(skills_dir)
if ai_skills:
if selected_ai in NATIVE_SKILLS_AGENTS:
skills_dir = _get_skills_dir(project_path, selected_ai)
bundled_found = _has_bundled_skills(project_path, selected_ai)
if bundled_found:
detail = f"bundled skills → {skills_dir.relative_to(project_path)}"
if migrated_legacy_kimi_skills or removed_legacy_kimi_skills:
detail += (
f" (migrated {migrated_legacy_kimi_skills}, "
f"removed {removed_legacy_kimi_skills} legacy Kimi dotted skills)"
)
if tracker:
tracker.start("ai-skills")
tracker.complete("ai-skills", f"bundled skills → {skills_dir.relative_to(project_path)}")
tracker.complete("ai-skills", detail)
else:
console.print(f"[green]✓[/green] Using bundled agent skills in {skills_dir.relative_to(project_path)}/")
console.print(f"[green]✓[/green] Using {detail}")
else:
# Compatibility fallback: convert command templates to skills
# when an older template archive does not include native skills.
@@ -2288,7 +2348,7 @@ def init(
if codex_skill_mode:
return f"$speckit-{name}"
if kimi_skill_mode:
return f"/skill:speckit.{name}"
return f"/skill:speckit-{name}"
return f"/speckit.{name}"
steps_lines.append(f"{step_num}. Start using {usage_label} with your AI agent:")