Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions .github/workflows/shell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ jobs:
- name: Run tests/opencode-wrapper-removal.sh
run: ./tests/opencode-wrapper-removal.sh

managed-skill-targets:
name: managed skill target deduplication (#306)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests/managed-skill-targets.sh
run: ./tests/managed-skill-targets.sh

cli-transport:
name: CLI dispatch transport runtime
runs-on: ubuntu-latest
Expand Down
130 changes: 80 additions & 50 deletions lib/skills.sh
Original file line number Diff line number Diff line change
Expand Up @@ -131,52 +131,94 @@ _resolve_skills_dir_for_runtime() {
)
}

install_skills() {
# Primary skills dir — set from the currently sourced runtime (drives the
# summary output and the kimaki mirror source). Multi-runtime installs
# populate every detected runtime's skills dir below, but the primary
# stays the canonical one the rest of the script refers to.
SKILLS_DIR="$(runtime_skills_dir)"
# Resolve both the runtime roots that may contain managed skills and the
# canonical targets to populate. OpenCode discovers Claude's skill root, so a
# dual Claude Code + OpenCode install needs only the Claude copy.
_resolve_managed_skill_dirs() {
local -a runtimes=("${DETECTED_RUNTIMES[@]:-$RUNTIME}")
local has_claude=false has_opencode=false
local rt dir seen_dir already

if [ "$INSTALL_SKILLS" != true ]; then
log "Phase 8.5: Skipping upgrade skill (--no-skills)"
return
fi
WP_CODING_AGENTS_SKILL_ROOTS=()
WP_CODING_AGENTS_SKILL_TARGETS=()

log "Phase 8.5: Installing upgrade skill..."
for rt in "${runtimes[@]}"; do
[ "$rt" = "claude-code" ] && has_claude=true
[ "$rt" = "opencode" ] && has_opencode=true
done

# Build the unique list of skills dirs to populate. claude-code and
# Claude-compatible runtimes can resolve to $SITE_PATH/.claude/skills, so de-dupe.
local -a runtimes=("${DETECTED_RUNTIMES[@]:-$RUNTIME}")
local -a skills_dirs=()
local seen_dir rt dir
for rt in "${runtimes[@]}"; do
dir="$(_resolve_skills_dir_for_runtime "$rt")"
[ -n "$dir" ] || continue
local already=false
for seen_dir in "${skills_dirs[@]}"; do

already=false
for seen_dir in "${WP_CODING_AGENTS_SKILL_ROOTS[@]}"; do
[ "$seen_dir" = "$dir" ] && { already=true; break; }
done
[ "$already" = true ] || skills_dirs+=("$dir")
[ "$already" = true ] || WP_CODING_AGENTS_SKILL_ROOTS+=("$dir")

if [ "$rt" = "opencode" ] && [ "$has_claude" = true ] && [ "$has_opencode" = true ]; then
continue
fi

already=false
for seen_dir in "${WP_CODING_AGENTS_SKILL_TARGETS[@]}"; do
[ "$seen_dir" = "$dir" ] && { already=true; break; }
done
[ "$already" = true ] || WP_CODING_AGENTS_SKILL_TARGETS+=("$dir")
done

# Always guarantee the primary is in the list (for belt-and-braces when
# RUNTIME was set explicitly but somehow isn't in DETECTED_RUNTIMES).
local already=false
for seen_dir in "${skills_dirs[@]}"; do
[ "$seen_dir" = "$SKILLS_DIR" ] && { already=true; break; }
[ ${#WP_CODING_AGENTS_SKILL_TARGETS[@]} -gt 0 ] \
|| WP_CODING_AGENTS_SKILL_TARGETS=("$(runtime_skills_dir)")
SKILLS_DIR="${WP_CODING_AGENTS_SKILL_TARGETS[0]}"
}

_cleanup_managed_skill_duplicates() {
local root target is_target

for root in "${WP_CODING_AGENTS_SKILL_ROOTS[@]}"; do
if [ -d "$root/wp-coding-agents-setup" ]; then
if [ "$DRY_RUN" = true ]; then
echo -e "${BLUE}[dry-run]${NC} Would remove retired managed skill: $root/wp-coding-agents-setup"
else
rm -rf "$root/wp-coding-agents-setup"
log " Removed retired managed skill: $root/wp-coding-agents-setup"
fi
fi

is_target=false
for target in "${WP_CODING_AGENTS_SKILL_TARGETS[@]}"; do
[ "$root" = "$target" ] && { is_target=true; break; }
done
if [ "$is_target" = false ] && [ -d "$root/upgrade-wp-coding-agents" ]; then
if [ "$DRY_RUN" = true ]; then
echo -e "${BLUE}[dry-run]${NC} Would remove noncanonical managed skill: $root/upgrade-wp-coding-agents"
else
rm -rf "$root/upgrade-wp-coding-agents"
log " Removed noncanonical managed skill: $root/upgrade-wp-coding-agents"
fi
fi
done
[ "$already" = true ] || skills_dirs+=("$SKILLS_DIR")
}

install_skills() {
_resolve_managed_skill_dirs

if [ ${#skills_dirs[@]} -gt 1 ]; then
log " Detected ${#runtimes[@]} runtime(s): ${runtimes[*]}"
log " Populating ${#skills_dirs[@]} unique skills dir(s)"
if [ "$INSTALL_SKILLS" != true ]; then
log "Phase 8.5: Skipping upgrade skill (--no-skills)"
return
fi

# Install the managed upgrade skill into every detected runtime's skills dir.
log "Phase 8.5: Installing upgrade skill..."

if [ ${#WP_CODING_AGENTS_SKILL_TARGETS[@]} -gt 1 ]; then
log " Populating ${#WP_CODING_AGENTS_SKILL_TARGETS[@]} unique skills dir(s)"
fi

# Install the managed upgrade skill into each canonical runtime target.
local target_dir
for target_dir in "${skills_dirs[@]}"; do
if [ ${#skills_dirs[@]} -gt 1 ]; then
for target_dir in "${WP_CODING_AGENTS_SKILL_TARGETS[@]}"; do
if [ ${#WP_CODING_AGENTS_SKILL_TARGETS[@]} -gt 1 ]; then
log "→ Installing skills into $target_dir"
fi
SKILLS_DIR="$target_dir"
Expand All @@ -185,9 +227,10 @@ install_skills() {
install_skills_from_local_repo
done

# Reset SKILLS_DIR back to the primary for downstream consumers
# (kimaki mirror source, print_skills_summary, summary.sh).
SKILLS_DIR="$(runtime_skills_dir)"
# Keep existing fallback copies until the canonical install succeeds.
_cleanup_managed_skill_duplicates

SKILLS_DIR="${WP_CODING_AGENTS_SKILL_TARGETS[0]}"

if [ "$CHAT_BRIDGE" = "kimaki" ]; then
if [ "$DRY_RUN" = true ]; then
Expand All @@ -207,23 +250,10 @@ install_skills() {
print_skills_summary() {
echo ""

# Collect unique skills dirs across detected runtimes, same logic as
# install_skills. Falls back to SKILLS_DIR if DETECTED_RUNTIMES is empty.
local -a runtimes=("${DETECTED_RUNTIMES[@]:-$RUNTIME}")
local -a skills_dirs=()
local seen_dir rt dir
for rt in "${runtimes[@]}"; do
dir="$(_resolve_skills_dir_for_runtime "$rt")"
[ -n "$dir" ] || continue
local already=false
for seen_dir in "${skills_dirs[@]}"; do
[ "$seen_dir" = "$dir" ] && { already=true; break; }
done
[ "$already" = true ] || skills_dirs+=("$dir")
done
[ ${#skills_dirs[@]} -gt 0 ] || skills_dirs=("$SKILLS_DIR")
_resolve_managed_skill_dirs

for dir in "${skills_dirs[@]}"; do
local dir
for dir in "${WP_CODING_AGENTS_SKILL_TARGETS[@]}"; do
log "Managed upgrade skill target: $dir/"
if [ "$DRY_RUN" = false ]; then
ls -1 "$dir" 2>/dev/null | while read -r skill; do
Expand Down
2 changes: 1 addition & 1 deletion lib/summary.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ print_summary() {
if [ "$INSTALL_CHAT" = true ]; then
echo " Bridge: $CHAT_BRIDGE"
fi
SKILLS_DIR="$(runtime_skills_dir)"
_resolve_managed_skill_dirs
if [ "$INSTALL_SKILLS" = true ]; then
echo " Upgrade skill: $SKILLS_DIR"
else
Expand Down
144 changes: 144 additions & 0 deletions tests/managed-skill-targets.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#!/bin/bash
# Regression coverage for canonical managed skill targets (#306).
set -eu

SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT

# shellcheck disable=SC1091
source "$SCRIPT_DIR/lib/common.sh"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/lib/skills.sh"

DRY_RUN=false
INSTALL_SKILLS=true
CHAT_BRIDGE=""
LOCAL_MODE=true
FAILED=0

log() { :; }

assert_present() {
if [ -e "$1" ]; then
echo " ok $2"
else
echo " FAIL $2 (missing: $1)"
FAILED=$((FAILED + 1))
fi
}

assert_missing() {
if [ ! -e "$1" ]; then
echo " ok $2"
else
echo " FAIL $2 (unexpected: $1)"
FAILED=$((FAILED + 1))
fi
}

assert_eq() {
if [ "$1" = "$2" ]; then
echo " ok $3"
else
echo " FAIL $3 (expected: $2, actual: $1)"
FAILED=$((FAILED + 1))
fi
}

assert_contains() {
case "$1" in
*"$2"*) echo " ok $3" ;;
*)
echo " FAIL $3 (missing text: $2)"
FAILED=$((FAILED + 1))
;;
esac
}

assert_not_contains() {
case "$1" in
*"$2"*)
echo " FAIL $3 (unexpected text: $2)"
FAILED=$((FAILED + 1))
;;
*) echo " ok $3" ;;
esac
}

seed_root() {
local root="$1"
mkdir -p "$root/upgrade-wp-coding-agents" "$root/wp-coding-agents-setup" "$root/user-skill"
printf 'old\n' > "$root/upgrade-wp-coding-agents/SKILL.md"
printf 'retired\n' > "$root/wp-coding-agents-setup/SKILL.md"
printf 'user\n' > "$root/user-skill/SKILL.md"
}

run_case() {
SITE_PATH="$TMP/$1"
RUNTIME="$2"
shift 2
DETECTED_RUNTIMES=("$@")
mkdir -p "$SITE_PATH"
install_skills
}

echo "==> OpenCode only"
run_case opencode opencode opencode
assert_present "$SITE_PATH/.opencode/skills/upgrade-wp-coding-agents/SKILL.md" "uses .opencode/skills"
assert_missing "$SITE_PATH/.claude/skills/upgrade-wp-coding-agents" "does not create Claude target"

echo "==> Claude Code only"
run_case claude claude-code claude-code
assert_present "$SITE_PATH/.claude/skills/upgrade-wp-coding-agents/SKILL.md" "uses .claude/skills"
assert_missing "$SITE_PATH/.opencode/skills/upgrade-wp-coding-agents" "does not create OpenCode target"

echo "==> dual runtime cleanup and preservation"
SITE_PATH="$TMP/dual"
seed_root "$SITE_PATH/.claude/skills"
seed_root "$SITE_PATH/.opencode/skills"
RUNTIME=claude-code
DETECTED_RUNTIMES=(claude-code opencode)
install_skills
assert_present "$SITE_PATH/.claude/skills/upgrade-wp-coding-agents/SKILL.md" "keeps canonical Claude copy"
assert_eq "$SKILLS_DIR" "$SITE_PATH/.claude/skills" "reports the canonical target"
assert_missing "$SITE_PATH/.opencode/skills/upgrade-wp-coding-agents" "removes noncanonical OpenCode copy"
assert_missing "$SITE_PATH/.claude/skills/wp-coding-agents-setup" "removes retired Claude copy"
assert_missing "$SITE_PATH/.opencode/skills/wp-coding-agents-setup" "removes retired OpenCode copy"
assert_present "$SITE_PATH/.claude/skills/user-skill/SKILL.md" "preserves Claude user skill"
assert_present "$SITE_PATH/.opencode/skills/user-skill/SKILL.md" "preserves OpenCode user skill"
SUMMARY_OUTPUT="$(log() { printf '%s\n' "$1"; }; print_skills_summary)"
assert_contains "$SUMMARY_OUTPUT" "$SITE_PATH/.claude/skills/" "summary reports canonical Claude target"
assert_not_contains "$SUMMARY_OUTPUT" "$SITE_PATH/.opencode/skills/" "summary omits noncanonical OpenCode target"

echo "==> dual runtime install is idempotent"
install_skills
assert_present "$SITE_PATH/.claude/skills/upgrade-wp-coding-agents/SKILL.md" "canonical copy remains after rerun"
assert_missing "$SITE_PATH/.opencode/skills/upgrade-wp-coding-agents" "duplicate remains absent after rerun"
assert_present "$SITE_PATH/.opencode/skills/user-skill/SKILL.md" "user skill remains after rerun"

echo "==> dry run preserves managed directories"
seed_root "$SITE_PATH/.opencode/skills"
DRY_RUN=true
install_skills >/dev/null
DRY_RUN=false
assert_present "$SITE_PATH/.opencode/skills/upgrade-wp-coding-agents/SKILL.md" "dry run preserves duplicate"
assert_present "$SITE_PATH/.opencode/skills/wp-coding-agents-setup/SKILL.md" "dry run preserves retired skill"

echo "==> explicit runtime narrowing"
SITE_PATH="$TMP/narrowed"
seed_root "$SITE_PATH/.claude/skills"
seed_root "$SITE_PATH/.opencode/skills"
RUNTIME=opencode
DETECTED_RUNTIMES=(opencode)
install_skills
assert_present "$SITE_PATH/.opencode/skills/upgrade-wp-coding-agents/SKILL.md" "explicit OpenCode uses OpenCode target"
assert_missing "$SITE_PATH/.opencode/skills/wp-coding-agents-setup" "cleans scoped retired skill"
assert_present "$SITE_PATH/.claude/skills/wp-coding-agents-setup/SKILL.md" "does not clean unscoped Claude root"
assert_present "$SITE_PATH/.claude/skills/upgrade-wp-coding-agents/SKILL.md" "does not alter unscoped Claude copy"

if [ "$FAILED" -gt 0 ]; then
echo "FAILED: $FAILED assertion(s)"
exit 1
fi
echo "OK: managed skill target regressions passed"
Loading