From 2322d6cc961d5ba36ec08d65d0f9c0ee2ed9d9e0 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 10 Apr 2026 20:04:34 +0800 Subject: [PATCH 1/2] feat: skills-sync-revamp --- Makefile | 174 +++++++++++++++++++++++++++++++++++++++++++++++++----- README.md | 8 ++- 2 files changed, 165 insertions(+), 17 deletions(-) diff --git a/Makefile b/Makefile index 0453e84..82a929d 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,10 @@ RULES_TARGET_DIR := $(dir $(lastword $(MAKEFILE_LIST))).ruler SKILLS_SRC_DIR := $(dir $(lastword $(MAKEFILE_LIST)))skills SKILLS_RULER_DIR := $(dir $(lastword $(MAKEFILE_LIST))).ruler/skills SKILLS_TARGET_DIRS := $(HOME)/.claude/skills $(HOME)/.cursor/skills $(HOME)/.codex/skills $(HOME)/.roo/skills $(HOME)/.gemini/skills $(HOME)/.agents/skills $(HOME)/.vibe/skills $(HOME)/.config/opencode/skills +SKILLS_STATE_DIR := $(HOME)/.cache/dotagents/skills +SKILLS_MANIFEST_DIR := $(SKILLS_STATE_DIR)/manifests +SKILLS_SPEC_STATE_FILE := $(SKILLS_STATE_DIR)/skills.txt.normalized +SKILLS_EXTERNAL_SOURCE_DIR := $(HOME)/.agents/skills MCP_SRC := $(dir $(lastword $(MAKEFILE_LIST))).ruler/mcp.json MCP_TARGET_DIRS := $(HOME)/.cursor $(HOME)/.claude $(HOME)/.codex @@ -32,13 +36,12 @@ SKILLS_FILE := $(dir $(lastword $(MAKEFILE_LIST)))SKILLS.txt ifeq ($(DOTAGENTS_SKIP_SYNC),) .PHONY: sync sync: ruler-prepare ## Sync project commands, skills, and MCP configuration to assistant-specific directories. - @make ruler-apply-global - @make commands-sync - @make skills-clean - @make skills-install - @make skills-sync - @make mcp-sync - @make ruler-dotdirs-sync + @$(MAKE) ruler-apply-global + @$(MAKE) commands-sync + @$(MAKE) skills-install + @$(MAKE) skills-sync + @$(MAKE) mcp-sync + @$(MAKE) ruler-dotdirs-sync endif .PHONY: ruler-prepare @@ -81,23 +84,91 @@ ruler-rules-copy: ## Copy rules to .ruler directory. # ==================================================================================== .PHONY: skills-clean -skills-clean: ## Remove all globally installed skills for a clean reinstall. +skills-clean: ## Remove all globally installed skills and cached install state. @for target in $(SKILLS_TARGET_DIRS); do \ if [ -d "$$target" ]; then \ rm -rf "$$target"/*; \ echo "Cleaned $$target"; \ fi; \ done + @if [ -d "$(SKILLS_STATE_DIR)" ]; then \ + rm -rf "$(SKILLS_STATE_DIR)"; \ + echo "Cleared $(SKILLS_STATE_DIR)"; \ + fi + +.PHONY: skills-managed-clean +skills-managed-clean: ## Remove managed external skills recorded from SKILLS.txt. + @manifest_dir="$(SKILLS_MANIFEST_DIR)"; \ + if [ -d "$$manifest_dir" ]; then \ + for manifest in "$$manifest_dir"/*.skills; do \ + if [ ! -f "$$manifest" ]; then \ + continue; \ + fi; \ + while IFS= read -r skill || [ -n "$$skill" ]; do \ + if [ -z "$$skill" ]; then \ + continue; \ + fi; \ + for target in $(SKILLS_TARGET_DIRS); do \ + if [ -e "$$target/$$skill" ] || [ -L "$$target/$$skill" ]; then \ + rm -rf "$$target/$$skill"; \ + echo "Removed $$target/$$skill"; \ + fi; \ + done; \ + done < "$$manifest"; \ + done; \ + fi + @if [ -d "$(SKILLS_STATE_DIR)" ]; then \ + rm -rf "$(SKILLS_STATE_DIR)"; \ + echo "Cleared $(SKILLS_STATE_DIR)"; \ + fi .PHONY: skills-install -skills-install: ## Install skills from SKILLS.txt (supports per-repo skill selection). - @failed=0; \ - grep -v '^\s*#' $(SKILLS_FILE) | grep -v '^\s*$$' | while IFS= read -r line; do \ - repo=$$(echo "$$line" | awk '{print $$1}'); \ - skill_args=$$(echo "$$line" | awk '{print $$2}' | tr ',' '\n' | sed '/^$$/d' | while read -r s; do printf " --skill $$s"; done); \ - if [ -n "$$skill_args" ]; then \ +skills-install: ## Ensure skills from SKILLS.txt are installed and reconcile managed removals. + @state_dir="$(SKILLS_STATE_DIR)"; \ + manifest_dir="$(SKILLS_MANIFEST_DIR)"; \ + spec_state="$(SKILLS_SPEC_STATE_FILE)"; \ + external_source="$(SKILLS_EXTERNAL_SOURCE_DIR)"; \ + tmp_spec=$$(mktemp); \ + spec_changed=0; \ + failed=0; \ + mkdir -p "$$state_dir" "$$manifest_dir" "$$external_source"; \ + list_external_skills() { \ + if [ -d "$$external_source" ]; then \ + find "$$external_source" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) -exec basename {} \; | LC_ALL=C sort -u; \ + fi; \ + }; \ + remove_repo_skills() { \ + manifest_file="$$1"; \ + if [ ! -f "$$manifest_file" ]; then \ + return 0; \ + fi; \ + while IFS= read -r skill || [ -n "$$skill" ]; do \ + if [ -z "$$skill" ]; then \ + continue; \ + fi; \ + for target in $(SKILLS_TARGET_DIRS); do \ + if [ -e "$$target/$$skill" ] || [ -L "$$target/$$skill" ]; then \ + rm -rf "$$target/$$skill"; \ + fi; \ + done; \ + done < "$$manifest_file"; \ + }; \ + install_repo() { \ + repo="$$1"; \ + normalized_skills="$$2"; \ + manifest_file="$$3"; \ + cleanup_old="$$4"; \ + before_file=$$(mktemp); \ + after_file=$$(mktemp); \ + if [ "$$cleanup_old" = "1" ]; then \ + remove_repo_skills "$$manifest_file"; \ + fi; \ + list_external_skills > "$$before_file"; \ + if [ -n "$$normalized_skills" ]; then \ + skill_args=$$(printf '%s\n' "$$normalized_skills" | tr ',' '\n' | sed '/^$$/d' | while IFS= read -r s; do printf " --skill %s" "$$s"; done); \ echo "Installing selected skills from $$repo..."; \ if bunx skills add $$repo --global --yes $$skill_args "$$manifest_file"; \ echo "✓ Installed $$repo (selective)"; \ else \ echo "✗ Failed to install $$repo (continuing...)"; \ @@ -106,19 +177,90 @@ skills-install: ## Install skills from SKILLS.txt (supports per-repo skill selec else \ echo "Installing all skills from $$repo..."; \ if bunx skills add $$repo --global --yes "$$after_file"; \ + comm -13 "$$before_file" "$$after_file" > "$$manifest_file"; \ echo "✓ Installed $$repo (all)"; \ else \ echo "✗ Failed to install $$repo (continuing...)"; \ failed=1; \ fi; \ fi; \ - done; \ + rm -f "$$before_file" "$$after_file"; \ + }; \ + while IFS= read -r raw_line || [ -n "$$raw_line" ]; do \ + line=$$(printf '%s' "$$raw_line" | sed 's/^[[:space:]]*//; s/[[:space:]]*$$//'); \ + case "$$line" in \ + ''|\#*) continue ;; \ + esac; \ + repo=$$(printf '%s\n' "$$line" | awk '{print $$1}'); \ + skills_csv=$$(printf '%s\n' "$$line" | awk '{print $$2}'); \ + normalized_skills=$$(printf '%s\n' "$$skills_csv" | tr ',' '\n' | sed '/^$$/d' | LC_ALL=C sort | paste -sd, -); \ + printf '%s|%s\n' "$$repo" "$$normalized_skills" >> "$$tmp_spec"; \ + done < "$(SKILLS_FILE)"; \ + if [ "$${DOTAGENTS_FORCE_SKILLS_INSTALL:-0}" = "1" ]; then \ + spec_changed=1; \ + echo "Forcing managed external skill reinstall..."; \ + elif [ ! -f "$$spec_state" ] || ! cmp -s "$$tmp_spec" "$$spec_state"; then \ + spec_changed=1; \ + echo "Detected SKILLS.txt changes; refreshing managed external skills..."; \ + fi; \ + if [ "$$spec_changed" = "1" ]; then \ + $(MAKE) skills-managed-clean; \ + mkdir -p "$$state_dir" "$$manifest_dir" "$$external_source"; \ + while IFS='|' read -r repo normalized_skills || [ -n "$$repo$$normalized_skills" ]; do \ + manifest_file="$$manifest_dir/$$(printf '%s' "$$repo" | sed 's#[^A-Za-z0-9_.-]#_#g').skills"; \ + install_repo "$$repo" "$$normalized_skills" "$$manifest_file" 0; \ + done < "$$tmp_spec"; \ + else \ + while IFS='|' read -r repo normalized_skills || [ -n "$$repo$$normalized_skills" ]; do \ + manifest_file="$$manifest_dir/$$(printf '%s' "$$repo" | sed 's#[^A-Za-z0-9_.-]#_#g').skills"; \ + reinstall_repo=0; \ + if [ ! -f "$$manifest_file" ]; then \ + reinstall_repo=1; \ + echo "Reinstalling $$repo (missing manifest)"; \ + else \ + while IFS= read -r skill || [ -n "$$skill" ]; do \ + if [ -z "$$skill" ]; then \ + continue; \ + fi; \ + if [ ! -e "$$external_source/$$skill" ] && [ ! -L "$$external_source/$$skill" ]; then \ + reinstall_repo=1; \ + echo "Reinstalling $$repo (missing $$external_source/$$skill)"; \ + break; \ + fi; \ + for target in $(SKILLS_TARGET_DIRS); do \ + if [ ! -e "$$target/$$skill" ] && [ ! -L "$$target/$$skill" ]; then \ + reinstall_repo=1; \ + echo "Reinstalling $$repo (missing $$target/$$skill)"; \ + break; \ + fi; \ + done; \ + if [ "$$reinstall_repo" = "1" ]; then \ + break; \ + fi; \ + done < "$$manifest_file"; \ + fi; \ + if [ "$$reinstall_repo" = "1" ]; then \ + install_repo "$$repo" "$$normalized_skills" "$$manifest_file" 1; \ + else \ + echo "Skipping $$repo (installed state matches SKILLS.txt)"; \ + fi; \ + done < "$$tmp_spec"; \ + fi; \ + cp "$$tmp_spec" "$$spec_state"; \ + rm -f "$$tmp_spec"; \ if [ "$$failed" = "1" ]; then \ echo "Some skills failed to install (see above)."; \ else \ - echo "All external skills installed successfully."; \ + echo "Managed external skills are in sync."; \ fi +.PHONY: skills-refresh +skills-refresh: ## Force a clean reinstall of external skills and re-sync local repo skills. + @$(MAKE) skills-managed-clean + @DOTAGENTS_FORCE_SKILLS_INSTALL=1 $(MAKE) skills-install + @$(MAKE) skills-sync + .PHONY: skills-install-repo skills-install-repo: ## Install a single skill repo. Usage: make skills-install-repo REPO=owner/repo [SKILLS=a,b,c] @if [ -z "$(REPO)" ]; then \ diff --git a/README.md b/README.md index 4bee0b8..ad634d7 100644 --- a/README.md +++ b/README.md @@ -7,4 +7,10 @@ Rules for Agents make sync ``` -This runs the full pipeline: prepares `.ruler/`, generates agent instruction files (`~/.claude/CLAUDE.md`, etc.) via Ruler, then syncs commands, skills, MCP config, and dot directories to `$HOME`. +This runs the full pipeline: prepares `.ruler/`, generates agent instruction files (`~/.claude/CLAUDE.md`, etc.) via Ruler, reconciles managed external skills from `SKILLS.txt` against the actual installed skill directories, then syncs commands, local repo skills, MCP config, and dot directories to `$HOME`. + +To force a clean reinstall of external skills, run: + +```bash +make skills-refresh +``` From aee2627bb26c443e6bc3747603c37ee307492433 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Fri, 10 Apr 2026 20:35:46 +0800 Subject: [PATCH 2/2] fix: skills-install cache validation and manifest recording - Cache check: only validate ~/.agents/skills/ (source of truth), not all target dirs which get modified by IDEs - Manifest recording: use before/after diff for selective installs so phantom skills (requested but nonexistent) don't cause perpetual reinstalls --- Makefile | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index 82a929d..58448e5 100644 --- a/Makefile +++ b/Makefile @@ -168,7 +168,8 @@ skills-install: ## Ensure skills from SKILLS.txt are installed and reconcile man skill_args=$$(printf '%s\n' "$$normalized_skills" | tr ',' '\n' | sed '/^$$/d' | while IFS= read -r s; do printf " --skill %s" "$$s"; done); \ echo "Installing selected skills from $$repo..."; \ if bunx skills add $$repo --global --yes $$skill_args "$$manifest_file"; \ + list_external_skills > "$$after_file"; \ + comm -13 "$$before_file" "$$after_file" > "$$manifest_file"; \ echo "✓ Installed $$repo (selective)"; \ else \ echo "✗ Failed to install $$repo (continuing...)"; \ @@ -228,16 +229,6 @@ skills-install: ## Ensure skills from SKILLS.txt are installed and reconcile man echo "Reinstalling $$repo (missing $$external_source/$$skill)"; \ break; \ fi; \ - for target in $(SKILLS_TARGET_DIRS); do \ - if [ ! -e "$$target/$$skill" ] && [ ! -L "$$target/$$skill" ]; then \ - reinstall_repo=1; \ - echo "Reinstalling $$repo (missing $$target/$$skill)"; \ - break; \ - fi; \ - done; \ - if [ "$$reinstall_repo" = "1" ]; then \ - break; \ - fi; \ done < "$$manifest_file"; \ fi; \ if [ "$$reinstall_repo" = "1" ]; then \