fix(build): generate flat Claude skill directories
Claude discovers only skills/<name>/SKILL.md, so 84 nested skills, including all rule skills, were invisible. Flatten the categorized catalog for Claude, keep references, and fail the build on duplicate names or a name that differs from its directory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5c1bee043d
commit
3203e4ed67
5 files changed
+43
-13
No files matched your search
@@ -1 +1 @@
|
|||||||
Read skills/rules/rules-orchestration/SKILL.md from your installed configuration before acting as a subagent.
|
Read skills/rules-orchestration/SKILL.md from your installed configuration before acting as a subagent.
|
||||||
@@ -94,7 +94,18 @@ foreach ($shell in @('powershell','bash')) {
|
|||||||
New-Item (Join-Path $output "codex-$shell") -ItemType Directory -Force | Out-Null
|
New-Item (Join-Path $output "codex-$shell") -ItemType Directory -Force | Out-Null
|
||||||
New-Item (Join-Path $output "claude-$shell") -ItemType Directory -Force | Out-Null
|
New-Item (Join-Path $output "claude-$shell") -ItemType Directory -Force | Out-Null
|
||||||
Copy-Directory (Join-Path $shared 'skills') (Join-Path $output "codex-$shell/skills")
|
Copy-Directory (Join-Path $shared 'skills') (Join-Path $output "codex-$shell/skills")
|
||||||
Copy-Directory (Join-Path $shared 'skills') (Join-Path $output "claude-$shell/skills")
|
# Claude discovers only skills/<name>/SKILL.md, so flatten the categorized source tree.
|
||||||
|
$claudeSkills = Join-Path $output "claude-$shell/skills"
|
||||||
|
New-Item $claudeSkills -ItemType Directory -Force | Out-Null
|
||||||
|
Get-ChildItem (Join-Path $shared 'skills') -File | ForEach-Object { Copy-Item $_.FullName (Join-Path $claudeSkills $_.Name) -Force }
|
||||||
|
$claudeSkillNames = @{}
|
||||||
|
foreach ($skillFile in Get-ChildItem (Join-Path $shared 'skills') -File -Filter 'SKILL.md' -Recurse) {
|
||||||
|
$skillName = $skillFile.Directory.Name
|
||||||
|
if (@(Get-Content -LiteralPath $skillFile.FullName -TotalCount 6) -notcontains "name: $skillName") { throw "Skill name must match its directory: $($skillFile.FullName)" }
|
||||||
|
if ($claudeSkillNames.ContainsKey($skillName)) { throw "Duplicate Claude skill name: $skillName" }
|
||||||
|
$claudeSkillNames[$skillName] = $true
|
||||||
|
Copy-Directory $skillFile.DirectoryName (Join-Path $claudeSkills $skillName)
|
||||||
|
}
|
||||||
Copy-Directory (Join-Path $shared 'rules') (Join-Path $output "codex-$shell/rules")
|
Copy-Directory (Join-Path $shared 'rules') (Join-Path $output "codex-$shell/rules")
|
||||||
Copy-Directory (Join-Path $shared 'hooks') (Join-Path $output "codex-$shell/hooks")
|
Copy-Directory (Join-Path $shared 'hooks') (Join-Path $output "codex-$shell/hooks")
|
||||||
Copy-Directory (Join-Path $shared 'hooks') (Join-Path $output "claude-$shell/hooks")
|
Copy-Directory (Join-Path $shared 'hooks') (Join-Path $output "claude-$shell/hooks")
|
||||||
@@ -117,12 +128,12 @@ foreach ($shell in @('powershell','bash')) {
|
|||||||
$ruleName = "rules-$($ruleDirectory.Name)-$($ruleParts -join '-')"
|
$ruleName = "rules-$($ruleDirectory.Name)-$($ruleParts -join '-')"
|
||||||
$references = $null
|
$references = $null
|
||||||
}
|
}
|
||||||
if ($ruleSkills.Name -contains $ruleName) { throw "Duplicate generated rule skill name: $ruleName" }
|
if ($ruleSkills.Name -contains $ruleName -or $claudeSkillNames.ContainsKey($ruleName)) { throw "Duplicate generated rule skill name: $ruleName" }
|
||||||
$ruleSkills += [pscustomobject]@{ Name = $ruleName; Body = $ruleFile.FullName; References = $references }
|
$ruleSkills += [pscustomobject]@{ Name = $ruleName; Body = $ruleFile.FullName; References = $references }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
foreach ($ruleSkill in $ruleSkills | Sort-Object Name) {
|
foreach ($ruleSkill in $ruleSkills | Sort-Object Name) {
|
||||||
$skillDir = Join-Path $output "claude-$shell/skills/rules/$($ruleSkill.Name)"
|
$skillDir = Join-Path $claudeSkills $ruleSkill.Name
|
||||||
New-Item $skillDir -ItemType Directory -Force | Out-Null
|
New-Item $skillDir -ItemType Directory -Force | Out-Null
|
||||||
$ruleContent = Get-Content $ruleSkill.Body -Raw
|
$ruleContent = Get-Content $ruleSkill.Body -Raw
|
||||||
$ruleTitle = (Get-Content $ruleSkill.Body | Where-Object { $_ -match '^#+\s+' } | Select-Object -First 1) -replace '^#+\s+', ''
|
$ruleTitle = (Get-Content $ruleSkill.Body | Where-Object { $_ -match '^#+\s+' } | Select-Object -First 1) -replace '^#+\s+', ''
|
||||||
|
|||||||
@@ -79,7 +79,19 @@ done
|
|||||||
for shell in powershell bash; do
|
for shell in powershell bash; do
|
||||||
mkdir -p "$output/codex-$shell" "$output/claude-$shell"
|
mkdir -p "$output/codex-$shell" "$output/claude-$shell"
|
||||||
copy_directory "$shared/skills" "$output/codex-$shell/skills"
|
copy_directory "$shared/skills" "$output/codex-$shell/skills"
|
||||||
copy_directory "$shared/skills" "$output/claude-$shell/skills"
|
# Claude discovers only skills/<name>/SKILL.md, so flatten the categorized source tree.
|
||||||
|
claude_skills="$output/claude-$shell/skills"
|
||||||
|
mkdir -p "$claude_skills"
|
||||||
|
find "$shared/skills" -maxdepth 1 -type f -exec cp {} "$claude_skills"/ \;
|
||||||
|
declare -A claude_skill_names=()
|
||||||
|
while IFS= read -r skill_file; do
|
||||||
|
skill_source=$(dirname -- "$skill_file")
|
||||||
|
skill_name=$(basename -- "$skill_source")
|
||||||
|
head -n 6 "$skill_file" | grep -qx "name: $skill_name" || { printf 'Skill name must match its directory: %s\n' "$skill_file" >&2; exit 1; }
|
||||||
|
[ -z "${claude_skill_names[$skill_name]+x}" ] || { printf 'Duplicate Claude skill name: %s\n' "$skill_name" >&2; exit 1; }
|
||||||
|
claude_skill_names[$skill_name]=1
|
||||||
|
copy_directory "$skill_source" "$claude_skills/$skill_name"
|
||||||
|
done < <(find "$shared/skills" -type f -name SKILL.md | sort)
|
||||||
copy_directory "$shared/rules" "$output/codex-$shell/rules"
|
copy_directory "$shared/rules" "$output/codex-$shell/rules"
|
||||||
copy_directory "$shared/hooks" "$output/codex-$shell/hooks"
|
copy_directory "$shared/hooks" "$output/codex-$shell/hooks"
|
||||||
copy_directory "$shared/hooks" "$output/claude-$shell/hooks"
|
copy_directory "$shared/hooks" "$output/claude-$shell/hooks"
|
||||||
@@ -103,12 +115,12 @@ while IFS= read -r rule_path; do
|
|||||||
*.md) skill_name="rules-${rule_path%.md}"; rule_body="$shared/rules/$rule_path"; references_dir='' ;;
|
*.md) skill_name="rules-${rule_path%.md}"; rule_body="$shared/rules/$rule_path"; references_dir='' ;;
|
||||||
*) continue ;;
|
*) continue ;;
|
||||||
esac
|
esac
|
||||||
if [[ -n "${rule_skill_names[$skill_name]:-}" ]]; then
|
if [[ -n "${rule_skill_names[$skill_name]:-}" || -n "${claude_skill_names[$skill_name]:-}" ]]; then
|
||||||
printf 'Duplicate generated rule skill name: %s\n' "$skill_name" >&2
|
printf 'Duplicate generated rule skill name: %s\n' "$skill_name" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
rule_skill_names[$skill_name]=1
|
rule_skill_names[$skill_name]=1
|
||||||
skill_dir="$output/claude-$shell/skills/rules/$skill_name"
|
skill_dir="$claude_skills/$skill_name"
|
||||||
mkdir -p "$skill_dir"
|
mkdir -p "$skill_dir"
|
||||||
rule_title=$(sed -n 's/^#\{1,6\} *//p' "$rule_body" | head -n 1)
|
rule_title=$(sed -n 's/^#\{1,6\} *//p' "$rule_body" | head -n 1)
|
||||||
if [ -z "$rule_title" ]; then printf 'Missing rule title in %s\n' "$rule_body" >&2; exit 1; fi
|
if [ -z "$rule_title" ]; then printf 'Missing rule title in %s\n' "$rule_body" >&2; exit 1; fi
|
||||||
|
|||||||
@@ -48,7 +48,9 @@ foreach ($shell in @('powershell','bash')) {
|
|||||||
if ($client -eq 'claude' -and (Get-Content (Join-Path $package 'agents/reviewer.md') -Raw) -notmatch "(?m)^tools: Read,Grep,Glob,$expectedReviewerShellTool\r?`$") { throw "Reviewer capability profile is missing in $package" }
|
if ($client -eq 'claude' -and (Get-Content (Join-Path $package 'agents/reviewer.md') -Raw) -notmatch "(?m)^tools: Read,Grep,Glob,$expectedReviewerShellTool\r?`$") { throw "Reviewer capability profile is missing in $package" }
|
||||||
$expectedSkillCount = if ($client -eq 'claude') { $sourceSkillCount + $ruleSkillCount } else { $sourceSkillCount }
|
$expectedSkillCount = if ($client -eq 'claude') { $sourceSkillCount + $ruleSkillCount } else { $sourceSkillCount }
|
||||||
if (@(Get-ChildItem "$package/skills" -Filter SKILL.md -Recurse).Count -ne $expectedSkillCount) { throw "Missing skills in $package" }
|
if (@(Get-ChildItem "$package/skills" -Filter SKILL.md -Recurse).Count -ne $expectedSkillCount) { throw "Missing skills in $package" }
|
||||||
if ((Test-Path "$package/skills/debugging/SKILL.md") -or -not (Test-Path "$package/skills/reviews/security-review/SKILL.md") -or -not (Test-Path "$package/skills/copywriting/SKILL.md") -or -not (Test-Path "$package/skills/marketing-psychology/SKILL.md") -or -not (Test-Path "$package/skills/image/SKILL.md") -or (Test-Path "$package/skills/marketing-plan/SKILL.md") -or (Test-Path "$package/skills/social/SKILL.md")) { throw "Incorrect skill catalog in $package" }
|
$securityReview = if ($client -eq 'claude') { 'skills/security-review/SKILL.md' } else { 'skills/reviews/security-review/SKILL.md' }
|
||||||
|
if ($client -eq 'claude' -and @(Get-ChildItem "$package/skills" -Filter SKILL.md -Recurse | Where-Object { $_.Directory.Parent.FullName -ne (Resolve-Path "$package/skills").Path }).Count) { throw "Nested Claude skills in $package" }
|
||||||
|
if ((Test-Path "$package/skills/debugging/SKILL.md") -or -not (Test-Path "$package/$securityReview") -or -not (Test-Path "$package/skills/copywriting/SKILL.md") -or -not (Test-Path "$package/skills/marketing-psychology/SKILL.md") -or -not (Test-Path "$package/skills/image/SKILL.md") -or (Test-Path "$package/skills/marketing-plan/SKILL.md") -or (Test-Path "$package/skills/social/SKILL.md")) { throw "Incorrect skill catalog in $package" }
|
||||||
$expected = if ($shell -eq 'powershell') { '__POWERSHELL_COMMAND__ .*flashbang.ps1' } else { 'bash .*flashbang.sh' }
|
$expected = if ($shell -eq 'powershell') { '__POWERSHELL_COMMAND__ .*flashbang.ps1' } else { 'bash .*flashbang.sh' }
|
||||||
if ($content -notmatch $expected -or $content -match '__HOOK_|__POWERSHELL_HOOK_') { throw "Incorrect shell command in $package" }
|
if ($content -notmatch $expected -or $content -match '__HOOK_|__POWERSHELL_HOOK_') { throw "Incorrect shell command in $package" }
|
||||||
if ($shell -eq 'powershell' -and $content -match 'WindowStyle\s+Hidden') { throw "PowerShell hook hides the terminal in $package" }
|
if ($shell -eq 'powershell' -and $content -match 'WindowStyle\s+Hidden') { throw "PowerShell hook hides the terminal in $package" }
|
||||||
@@ -76,7 +78,7 @@ foreach ($shell in @('powershell','bash')) {
|
|||||||
if ($docContent -notmatch [regex]::Escape('rules/security/index.md')) { throw "$package is missing common rule loading instructions." }
|
if ($docContent -notmatch [regex]::Escape('rules/security/index.md')) { throw "$package is missing common rule loading instructions." }
|
||||||
if ($client -eq 'claude') {
|
if ($client -eq 'claude') {
|
||||||
foreach ($ruleSource in $ruleSources) {
|
foreach ($ruleSource in $ruleSources) {
|
||||||
$skillPath = Join-Path $package "skills/rules/$($ruleSource.Skill)/SKILL.md"
|
$skillPath = Join-Path $package "skills/$($ruleSource.Skill)/SKILL.md"
|
||||||
if (-not (Test-Path -LiteralPath $skillPath)) { throw "$package is missing generated skill $($ruleSource.Skill)." }
|
if (-not (Test-Path -LiteralPath $skillPath)) { throw "$package is missing generated skill $($ruleSource.Skill)." }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +89,7 @@ foreach ($shell in @('powershell','bash')) {
|
|||||||
if ($client -eq 'codex' -and $docContent -match 'Always load and apply `rules/general\.md`') { throw "$package must not still instruct loading general.md by path" }
|
if ($client -eq 'codex' -and $docContent -match 'Always load and apply `rules/general\.md`') { throw "$package must not still instruct loading general.md by path" }
|
||||||
if ($client -eq 'claude' -and (Test-Path (Join-Path $package 'rules/general.md'))) { throw 'Claude must not automatically load a second copy of general rules.' }
|
if ($client -eq 'claude' -and (Test-Path (Join-Path $package 'rules/general.md'))) { throw 'Claude must not automatically load a second copy of general rules.' }
|
||||||
foreach ($splitRule in @('angular','wpf','ui-ux')) {
|
foreach ($splitRule in @('angular','wpf','ui-ux')) {
|
||||||
$referencesDir = if ($client -eq 'codex') { Join-Path $package "rules/$splitRule/references" } else { Join-Path $package "skills/rules/rules-$splitRule/references" }
|
$referencesDir = if ($client -eq 'codex') { Join-Path $package "rules/$splitRule/references" } else { Join-Path $package "skills/rules-$splitRule/references" }
|
||||||
if (-not (Test-Path $referencesDir) -or (Get-ChildItem $referencesDir -Filter '*.md').Count -eq 0) { throw "$package is missing reference files for $splitRule" }
|
if (-not (Test-Path $referencesDir) -or (Get-ChildItem $referencesDir -Filter '*.md').Count -eq 0) { throw "$package is missing reference files for $splitRule" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,12 @@ for shell in powershell bash; do
|
|||||||
[ "$client" != claude ] || expected_skill_count=$((source_skill_count + rule_skill_count))
|
[ "$client" != claude ] || expected_skill_count=$((source_skill_count + rule_skill_count))
|
||||||
test "$(find "$package/skills" -name SKILL.md | wc -l)" -eq "$expected_skill_count"
|
test "$(find "$package/skills" -name SKILL.md | wc -l)" -eq "$expected_skill_count"
|
||||||
test ! -e "$package/skills/debugging/SKILL.md"
|
test ! -e "$package/skills/debugging/SKILL.md"
|
||||||
test -f "$package/skills/reviews/security-review/SKILL.md"
|
if [ "$client" = claude ]; then
|
||||||
|
test -f "$package/skills/security-review/SKILL.md"
|
||||||
|
test "$(find "$package/skills" -mindepth 3 -name SKILL.md | wc -l)" -eq 0
|
||||||
|
else
|
||||||
|
test -f "$package/skills/reviews/security-review/SKILL.md"
|
||||||
|
fi
|
||||||
test -f "$package/skills/copywriting/SKILL.md"
|
test -f "$package/skills/copywriting/SKILL.md"
|
||||||
test -f "$package/skills/marketing-psychology/SKILL.md"
|
test -f "$package/skills/marketing-psychology/SKILL.md"
|
||||||
test -f "$package/skills/image/SKILL.md"
|
test -f "$package/skills/image/SKILL.md"
|
||||||
@@ -96,7 +101,7 @@ for shell in powershell bash; do
|
|||||||
if [ "$client" = claude ]; then
|
if [ "$client" = claude ]; then
|
||||||
for rule_path in "${rule_paths[@]}"; do
|
for rule_path in "${rule_paths[@]}"; do
|
||||||
skill_name=$(rule_skill_name "$rule_path")
|
skill_name=$(rule_skill_name "$rule_path")
|
||||||
test -f "$package/skills/rules/$skill_name/SKILL.md"
|
test -f "$package/skills/$skill_name/SKILL.md"
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
printf '%s' "$doc_content" | grep -Fq 'Use the `mcporter` CLI for MCP servers this configuration registers there'
|
printf '%s' "$doc_content" | grep -Fq 'Use the `mcporter` CLI for MCP servers this configuration registers there'
|
||||||
@@ -116,7 +121,7 @@ for shell in powershell bash; do
|
|||||||
if [ "$client" = codex ]; then
|
if [ "$client" = codex ]; then
|
||||||
references_dir="$package/rules/$split_rule/references"
|
references_dir="$package/rules/$split_rule/references"
|
||||||
else
|
else
|
||||||
references_dir="$package/skills/rules/rules-$split_rule/references"
|
references_dir="$package/skills/rules-$split_rule/references"
|
||||||
fi
|
fi
|
||||||
if [ ! -d "$references_dir" ] || [ -z "$(find "$references_dir" -maxdepth 1 -name '*.md' -print -quit)" ]; then
|
if [ ! -d "$references_dir" ] || [ -z "$(find "$references_dir" -maxdepth 1 -name '*.md' -print -quit)" ]; then
|
||||||
printf '%s is missing reference files for %s\n' "$package" "$split_rule" >&2
|
printf '%s is missing reference files for %s\n' "$package" "$split_rule" >&2
|
||||||
|
|||||||
Reference in new issue
Block a user