Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
efe6010
fix: unify hyphenated skills and migrate legacy kimi dotted dirs
RbBtSn0w Mar 25, 2026
17cea06
fix: preserve legacy kimi dotted preset skill overrides
RbBtSn0w Mar 25, 2026
3c8c42d
fix: migrate kimi legacy dotted skills without ai-skills flag
RbBtSn0w Mar 25, 2026
316a221
fix: harden kimi migration and cache hook init options
RbBtSn0w Mar 25, 2026
f858a0d
fix: apply kimi preset skill overrides without ai-skills flag
RbBtSn0w Mar 25, 2026
83323e3
fix: keep sequential branch numbering beyond 999
RbBtSn0w Mar 25, 2026
65e922a
test: align kimi scaffold skill path with hyphen naming
RbBtSn0w Mar 25, 2026
c161c61
chore: align hook typing and preset skill comment
RbBtSn0w Mar 25, 2026
4892a21
fix: restore AGENT_SKILLS_DIR_OVERRIDES compatibility export
RbBtSn0w Mar 26, 2026
aca3226
refactor: remove AGENT_SKILLS_DIR_OVERRIDES and update callers
RbBtSn0w Mar 26, 2026
1a8374b
fix(ps1): support sequential branch numbers above 999
RbBtSn0w Mar 26, 2026
9609fc3
fix: resolve preset skill placeholders for skills agents
RbBtSn0w Mar 26, 2026
e4e5503
Fix legacy kimi migration safety and preset skill dir checks
RbBtSn0w Mar 26, 2026
dd0de3f
Harden TOML rendering and consolidate preset skill restore parsing
RbBtSn0w Mar 26, 2026
70c521d
Fix PowerShell overflow and hook message fallback for empty invocations
RbBtSn0w Mar 26, 2026
691b4dd
Restore preset skills from extensions
RbBtSn0w Mar 26, 2026
3a63863
Refine preset skill restore helpers
RbBtSn0w Mar 26, 2026
ac580bd
Harden skill path and preset checks
RbBtSn0w Mar 26, 2026
734efdf
Guard non-dict init options
RbBtSn0w Mar 26, 2026
4aeaf5b
Avoid deleting unmanaged preset skill dirs
RbBtSn0w Mar 26, 2026
bd7f6a1
Unify extension skill naming with hooks
RbBtSn0w Mar 26, 2026
0d0382f
Harden extension native skill registration
RbBtSn0w Mar 26, 2026
0dbbda1
Normalize preset skill titles
RbBtSn0w Mar 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .github/workflows/scripts/create-release-packages.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,7 @@ agent: $basename
}

# Create skills in <skills_dir>\<name>\SKILL.md format.
# Most agents use hyphenated names (e.g. speckit-plan); Kimi is the
# current dotted-name exception (e.g. speckit.plan).
# Skills use hyphenated names (e.g. speckit-plan).
#
# Technical debt note:
# Keep SKILL.md frontmatter aligned with `install_ai_skills()` and extension
Expand Down Expand Up @@ -463,7 +462,7 @@ function Build-Variant {
'kimi' {
$skillsDir = Join-Path $baseDir ".kimi/skills"
New-Item -ItemType Directory -Force -Path $skillsDir | Out-Null
New-Skills -SkillsDir $skillsDir -ScriptVariant $Script -AgentName 'kimi' -Separator '.'
New-Skills -SkillsDir $skillsDir -ScriptVariant $Script -AgentName 'kimi'
}
'trae' {
$rulesDir = Join-Path $baseDir ".trae/rules"
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/scripts/create-release-packages.sh
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,7 @@ EOF
}

# Create skills in <skills_dir>/<name>/SKILL.md format.
# Most agents use hyphenated names (e.g. speckit-plan); Kimi is the
# current dotted-name exception (e.g. speckit.plan).
# Skills use hyphenated names (e.g. speckit-plan).
#
# Technical debt note:
# Keep SKILL.md frontmatter aligned with `install_ai_skills()` and extension
Expand Down Expand Up @@ -321,7 +320,7 @@ build_variant() {
generate_commands vibe md "\$ARGUMENTS" "$base_dir/.vibe/prompts" "$script" ;;
kimi)
mkdir -p "$base_dir/.kimi/skills"
create_skills "$base_dir/.kimi/skills" "$script" "kimi" "." ;;
create_skills "$base_dir/.kimi/skills" "$script" "kimi" ;;
trae)
mkdir -p "$base_dir/.trae/rules"
generate_commands trae md "\$ARGUMENTS" "$base_dir/.trae/rules" "$script" ;;
Expand Down
12 changes: 6 additions & 6 deletions scripts/bash/create-new-feature.sh
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ get_highest_from_specs() {
for dir in "$specs_dir"/*; do
[ -d "$dir" ] || continue
dirname=$(basename "$dir")
# Only match sequential prefixes (###-*), skip timestamp dirs
if echo "$dirname" | grep -q '^[0-9]\{3\}-'; then
number=$(echo "$dirname" | grep -o '^[0-9]\{3\}')
# Match sequential prefixes (>=3 digits), but skip timestamp dirs.
if echo "$dirname" | grep -Eq '^[0-9]{3,}-' && ! echo "$dirname" | grep -Eq '^[0-9]{8}-[0-9]{6}-'; then
number=$(echo "$dirname" | grep -Eo '^[0-9]+')
number=$((10#$number))
if [ "$number" -gt "$highest" ]; then
highest=$number
Expand All @@ -115,9 +115,9 @@ get_highest_from_branches() {
# Clean branch name: remove leading markers and remote prefixes
clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||')

# Extract feature number if branch matches pattern ###-*
if echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then
number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0")
# Extract sequential feature number (>=3 digits), skip timestamp branches.
if echo "$clean_branch" | grep -Eq '^[0-9]{3,}-' && ! echo "$clean_branch" | grep -Eq '^[0-9]{8}-[0-9]{6}-'; then
number=$(echo "$clean_branch" | grep -Eo '^[0-9]+' || echo "0")
number=$((10#$number))
if [ "$number" -gt "$highest" ]; then
highest=$number
Expand Down
26 changes: 15 additions & 11 deletions scripts/powershell/create-new-feature.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ param(
[switch]$Json,
[string]$ShortName,
[Parameter()]
[int]$Number = 0,
[long]$Number = 0,
[switch]$Timestamp,
[switch]$Help,
[Parameter(Position = 0, ValueFromRemainingArguments = $true)]
Expand Down Expand Up @@ -48,12 +48,15 @@ if ([string]::IsNullOrWhiteSpace($featureDesc)) {
function Get-HighestNumberFromSpecs {
param([string]$SpecsDir)

$highest = 0
[long]$highest = 0
if (Test-Path $SpecsDir) {
Get-ChildItem -Path $SpecsDir -Directory | ForEach-Object {
if ($_.Name -match '^(\d{3})-') {
$num = [int]$matches[1]
if ($num -gt $highest) { $highest = $num }
# Match sequential prefixes (>=3 digits), but skip timestamp dirs.
if ($_.Name -match '^(\d{3,})-' -and $_.Name -notmatch '^\d{8}-\d{6}-') {
[long]$num = 0
if ([long]::TryParse($matches[1], [ref]$num) -and $num -gt $highest) {
$highest = $num
}
}
}
}
Expand All @@ -63,18 +66,20 @@ function Get-HighestNumberFromSpecs {
function Get-HighestNumberFromBranches {
param()

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

# Extract feature number if branch matches pattern ###-*
if ($cleanBranch -match '^(\d{3})-') {
$num = [int]$matches[1]
if ($num -gt $highest) { $highest = $num }
# Extract sequential feature number (>=3 digits), skip timestamp branches.
if ($cleanBranch -match '^(\d{3,})-' -and $cleanBranch -notmatch '^\d{8}-\d{6}-') {
[long]$num = 0
if ([long]::TryParse($matches[1], [ref]$num) -and $num -gt $highest) {
$highest = $num
}
}
}
}
Expand Down Expand Up @@ -290,4 +295,3 @@ if ($Json) {
Write-Output "HAS_GIT: $hasGit"
Write-Output "SPECIFY_FEATURE environment variable set to: $branchName"
}

104 changes: 82 additions & 22 deletions src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -1528,13 +1522,9 @@ def load_init_options(project_path: Path) -> dict[str, Any]:
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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -2288,7 +2348,7 @@ def _display_cmd(name: str) -> str:
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:")
Expand Down
Loading
Loading