Skip to content

Commit ccc44dd

Browse files
authored
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
1 parent 2c2fea8 commit ccc44dd

File tree

14 files changed

+1356
-249
lines changed

14 files changed

+1356
-249
lines changed

.github/workflows/scripts/create-release-packages.ps1

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,7 @@ agent: $basename
202202
}
203203

204204
# Create skills in <skills_dir>\<name>\SKILL.md format.
205-
# Most agents use hyphenated names (e.g. speckit-plan); Kimi is the
206-
# current dotted-name exception (e.g. speckit.plan).
205+
# Skills use hyphenated names (e.g. speckit-plan).
207206
#
208207
# Technical debt note:
209208
# Keep SKILL.md frontmatter aligned with `install_ai_skills()` and extension
@@ -463,7 +462,7 @@ function Build-Variant {
463462
'kimi' {
464463
$skillsDir = Join-Path $baseDir ".kimi/skills"
465464
New-Item -ItemType Directory -Force -Path $skillsDir | Out-Null
466-
New-Skills -SkillsDir $skillsDir -ScriptVariant $Script -AgentName 'kimi' -Separator '.'
465+
New-Skills -SkillsDir $skillsDir -ScriptVariant $Script -AgentName 'kimi'
467466
}
468467
'trae' {
469468
$rulesDir = Join-Path $baseDir ".trae/rules"

.github/workflows/scripts/create-release-packages.sh

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,7 @@ EOF
140140
}
141141

142142
# Create skills in <skills_dir>/<name>/SKILL.md format.
143-
# Most agents use hyphenated names (e.g. speckit-plan); Kimi is the
144-
# current dotted-name exception (e.g. speckit.plan).
143+
# Skills use hyphenated names (e.g. speckit-plan).
145144
#
146145
# Technical debt note:
147146
# Keep SKILL.md frontmatter aligned with `install_ai_skills()` and extension
@@ -321,7 +320,7 @@ build_variant() {
321320
generate_commands vibe md "\$ARGUMENTS" "$base_dir/.vibe/prompts" "$script" ;;
322321
kimi)
323322
mkdir -p "$base_dir/.kimi/skills"
324-
create_skills "$base_dir/.kimi/skills" "$script" "kimi" "." ;;
323+
create_skills "$base_dir/.kimi/skills" "$script" "kimi" ;;
325324
trae)
326325
mkdir -p "$base_dir/.trae/rules"
327326
generate_commands trae md "\$ARGUMENTS" "$base_dir/.trae/rules" "$script" ;;

scripts/bash/create-new-feature.sh

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,9 @@ get_highest_from_specs() {
8989
for dir in "$specs_dir"/*; do
9090
[ -d "$dir" ] || continue
9191
dirname=$(basename "$dir")
92-
# Only match sequential prefixes (###-*), skip timestamp dirs
93-
if echo "$dirname" | grep -q '^[0-9]\{3\}-'; then
94-
number=$(echo "$dirname" | grep -o '^[0-9]\{3\}')
92+
# Match sequential prefixes (>=3 digits), but skip timestamp dirs.
93+
if echo "$dirname" | grep -Eq '^[0-9]{3,}-' && ! echo "$dirname" | grep -Eq '^[0-9]{8}-[0-9]{6}-'; then
94+
number=$(echo "$dirname" | grep -Eo '^[0-9]+')
9595
number=$((10#$number))
9696
if [ "$number" -gt "$highest" ]; then
9797
highest=$number
@@ -115,9 +115,9 @@ get_highest_from_branches() {
115115
# Clean branch name: remove leading markers and remote prefixes
116116
clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||')
117117

118-
# Extract feature number if branch matches pattern ###-*
119-
if echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then
120-
number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0")
118+
# Extract sequential feature number (>=3 digits), skip timestamp branches.
119+
if echo "$clean_branch" | grep -Eq '^[0-9]{3,}-' && ! echo "$clean_branch" | grep -Eq '^[0-9]{8}-[0-9]{6}-'; then
120+
number=$(echo "$clean_branch" | grep -Eo '^[0-9]+' || echo "0")
121121
number=$((10#$number))
122122
if [ "$number" -gt "$highest" ]; then
123123
highest=$number

scripts/powershell/create-new-feature.ps1

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ param(
55
[switch]$Json,
66
[string]$ShortName,
77
[Parameter()]
8-
[int]$Number = 0,
8+
[long]$Number = 0,
99
[switch]$Timestamp,
1010
[switch]$Help,
1111
[Parameter(Position = 0, ValueFromRemainingArguments = $true)]
@@ -48,12 +48,15 @@ if ([string]::IsNullOrWhiteSpace($featureDesc)) {
4848
function Get-HighestNumberFromSpecs {
4949
param([string]$SpecsDir)
5050

51-
$highest = 0
51+
[long]$highest = 0
5252
if (Test-Path $SpecsDir) {
5353
Get-ChildItem -Path $SpecsDir -Directory | ForEach-Object {
54-
if ($_.Name -match '^(\d{3})-') {
55-
$num = [int]$matches[1]
56-
if ($num -gt $highest) { $highest = $num }
54+
# Match sequential prefixes (>=3 digits), but skip timestamp dirs.
55+
if ($_.Name -match '^(\d{3,})-' -and $_.Name -notmatch '^\d{8}-\d{6}-') {
56+
[long]$num = 0
57+
if ([long]::TryParse($matches[1], [ref]$num) -and $num -gt $highest) {
58+
$highest = $num
59+
}
5760
}
5861
}
5962
}
@@ -63,18 +66,20 @@ function Get-HighestNumberFromSpecs {
6366
function Get-HighestNumberFromBranches {
6467
param()
6568

66-
$highest = 0
69+
[long]$highest = 0
6770
try {
6871
$branches = git branch -a 2>$null
6972
if ($LASTEXITCODE -eq 0) {
7073
foreach ($branch in $branches) {
7174
# Clean branch name: remove leading markers and remote prefixes
7275
$cleanBranch = $branch.Trim() -replace '^\*?\s+', '' -replace '^remotes/[^/]+/', ''
7376

74-
# Extract feature number if branch matches pattern ###-*
75-
if ($cleanBranch -match '^(\d{3})-') {
76-
$num = [int]$matches[1]
77-
if ($num -gt $highest) { $highest = $num }
77+
# Extract sequential feature number (>=3 digits), skip timestamp branches.
78+
if ($cleanBranch -match '^(\d{3,})-' -and $cleanBranch -notmatch '^\d{8}-\d{6}-') {
79+
[long]$num = 0
80+
if ([long]::TryParse($matches[1], [ref]$num) -and $num -gt $highest) {
81+
$highest = $num
82+
}
7883
}
7984
}
8085
}
@@ -290,4 +295,3 @@ if ($Json) {
290295
Write-Output "HAS_GIT: $hasGit"
291296
Write-Output "SPECIFY_FEATURE environment variable set to: $branchName"
292297
}
293-

src/specify_cli/__init__.py

Lines changed: 82 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1490,12 +1490,6 @@ def load_init_options(project_path: Path) -> dict[str, Any]:
14901490
return {}
14911491

14921492

1493-
# Agent-specific skill directory overrides for agents whose skills directory
1494-
# doesn't follow the standard <agent_folder>/skills/ pattern
1495-
AGENT_SKILLS_DIR_OVERRIDES = {
1496-
"codex": ".agents/skills", # Codex agent layout override
1497-
}
1498-
14991493
# Default skills directory for agents not in AGENT_CONFIG
15001494
DEFAULT_SKILLS_DIR = ".agents/skills"
15011495

@@ -1528,13 +1522,9 @@ def load_init_options(project_path: Path) -> dict[str, Any]:
15281522
def _get_skills_dir(project_path: Path, selected_ai: str) -> Path:
15291523
"""Resolve the agent-specific skills directory for the given AI assistant.
15301524
1531-
Uses ``AGENT_SKILLS_DIR_OVERRIDES`` first, then falls back to
1532-
``AGENT_CONFIG[agent]["folder"] + "skills"``, and finally to
1533-
``DEFAULT_SKILLS_DIR``.
1525+
Uses ``AGENT_CONFIG[agent]["folder"] + "skills"`` and falls back to
1526+
``DEFAULT_SKILLS_DIR`` for unknown agents.
15341527
"""
1535-
if selected_ai in AGENT_SKILLS_DIR_OVERRIDES:
1536-
return project_path / AGENT_SKILLS_DIR_OVERRIDES[selected_ai]
1537-
15381528
agent_config = AGENT_CONFIG.get(selected_ai, {})
15391529
agent_folder = agent_config.get("folder", "")
15401530
if agent_folder:
@@ -1648,10 +1638,7 @@ def install_ai_skills(
16481638
command_name = command_name[len("speckit."):]
16491639
if command_name.endswith(".agent"):
16501640
command_name = command_name[:-len(".agent")]
1651-
if selected_ai == "kimi":
1652-
skill_name = f"speckit.{command_name}"
1653-
else:
1654-
skill_name = f"speckit-{command_name}"
1641+
skill_name = f"speckit-{command_name.replace('.', '-')}"
16551642

16561643
# Create skill directory (additive — never removes existing content)
16571644
skill_dir = skills_dir / skill_name
@@ -1730,8 +1717,64 @@ def _has_bundled_skills(project_path: Path, selected_ai: str) -> bool:
17301717
if not skills_dir.is_dir():
17311718
return False
17321719

1733-
pattern = "speckit.*/SKILL.md" if selected_ai == "kimi" else "speckit-*/SKILL.md"
1734-
return any(skills_dir.glob(pattern))
1720+
return any(skills_dir.glob("speckit-*/SKILL.md"))
1721+
1722+
1723+
def _migrate_legacy_kimi_dotted_skills(skills_dir: Path) -> tuple[int, int]:
1724+
"""Migrate legacy Kimi dotted skill dirs (speckit.xxx) to hyphenated format.
1725+
1726+
Temporary migration helper:
1727+
- Intended removal window: after 2026-06-25.
1728+
- Purpose: one-time cleanup for projects initialized before Kimi moved to
1729+
hyphenated skills (speckit-xxx).
1730+
1731+
Returns:
1732+
Tuple[migrated_count, removed_count]
1733+
- migrated_count: old dotted dir renamed to hyphenated dir
1734+
- removed_count: old dotted dir deleted when equivalent hyphenated dir existed
1735+
"""
1736+
if not skills_dir.is_dir():
1737+
return (0, 0)
1738+
1739+
migrated_count = 0
1740+
removed_count = 0
1741+
1742+
for legacy_dir in sorted(skills_dir.glob("speckit.*")):
1743+
if not legacy_dir.is_dir():
1744+
continue
1745+
if not (legacy_dir / "SKILL.md").exists():
1746+
continue
1747+
1748+
suffix = legacy_dir.name[len("speckit."):]
1749+
if not suffix:
1750+
continue
1751+
1752+
target_dir = skills_dir / f"speckit-{suffix.replace('.', '-')}"
1753+
1754+
if not target_dir.exists():
1755+
shutil.move(str(legacy_dir), str(target_dir))
1756+
migrated_count += 1
1757+
continue
1758+
1759+
# If the new target already exists, avoid destructive cleanup unless
1760+
# both SKILL.md files are byte-identical.
1761+
target_skill = target_dir / "SKILL.md"
1762+
legacy_skill = legacy_dir / "SKILL.md"
1763+
if target_skill.is_file():
1764+
try:
1765+
if target_skill.read_bytes() == legacy_skill.read_bytes():
1766+
# Preserve legacy directory when it contains extra user files.
1767+
has_extra_entries = any(
1768+
child.name != "SKILL.md" for child in legacy_dir.iterdir()
1769+
)
1770+
if not has_extra_entries:
1771+
shutil.rmtree(legacy_dir)
1772+
removed_count += 1
1773+
except OSError:
1774+
# Best-effort migration: preserve legacy dir on read failures.
1775+
pass
1776+
1777+
return (migrated_count, removed_count)
17351778

17361779

17371780
AGENT_SKILLS_MIGRATIONS = {
@@ -2094,16 +2137,33 @@ def init(
20942137

20952138
ensure_constitution_from_template(project_path, tracker=tracker)
20962139

2140+
# Determine skills directory and migrate any legacy Kimi dotted skills.
2141+
migrated_legacy_kimi_skills = 0
2142+
removed_legacy_kimi_skills = 0
2143+
skills_dir: Optional[Path] = None
2144+
if selected_ai in NATIVE_SKILLS_AGENTS:
2145+
skills_dir = _get_skills_dir(project_path, selected_ai)
2146+
if selected_ai == "kimi" and skills_dir.is_dir():
2147+
(
2148+
migrated_legacy_kimi_skills,
2149+
removed_legacy_kimi_skills,
2150+
) = _migrate_legacy_kimi_dotted_skills(skills_dir)
2151+
20972152
if ai_skills:
20982153
if selected_ai in NATIVE_SKILLS_AGENTS:
2099-
skills_dir = _get_skills_dir(project_path, selected_ai)
21002154
bundled_found = _has_bundled_skills(project_path, selected_ai)
21012155
if bundled_found:
2156+
detail = f"bundled skills → {skills_dir.relative_to(project_path)}"
2157+
if migrated_legacy_kimi_skills or removed_legacy_kimi_skills:
2158+
detail += (
2159+
f" (migrated {migrated_legacy_kimi_skills}, "
2160+
f"removed {removed_legacy_kimi_skills} legacy Kimi dotted skills)"
2161+
)
21022162
if tracker:
21032163
tracker.start("ai-skills")
2104-
tracker.complete("ai-skills", f"bundled skills → {skills_dir.relative_to(project_path)}")
2164+
tracker.complete("ai-skills", detail)
21052165
else:
2106-
console.print(f"[green]✓[/green] Using bundled agent skills in {skills_dir.relative_to(project_path)}/")
2166+
console.print(f"[green]✓[/green] Using {detail}")
21072167
else:
21082168
# Compatibility fallback: convert command templates to skills
21092169
# when an older template archive does not include native skills.
@@ -2288,7 +2348,7 @@ def _display_cmd(name: str) -> str:
22882348
if codex_skill_mode:
22892349
return f"$speckit-{name}"
22902350
if kimi_skill_mode:
2291-
return f"/skill:speckit.{name}"
2351+
return f"/skill:speckit-{name}"
22922352
return f"/speckit.{name}"
22932353

22942354
steps_lines.append(f"{step_num}. Start using {usage_label} with your AI agent:")

0 commit comments

Comments
 (0)