-
Notifications
You must be signed in to change notification settings - Fork 1
Skills sync revamp #124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Skills sync revamp #124
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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,92 @@ 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); \ | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
| tmp_spec=$$(mktemp); \ | |
| tmp_spec=$$(mktemp "$${TMPDIR:-/tmp}/skills-spec.XXXXXX"); \ |
Copilot
AI
Apr 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mktemp is invoked without a template for before_file/after_file. This is not portable on macOS/BSD (often errors with “too few X's in template”). Use a portable mktemp pattern for these temp files as well.
| before_file=$$(mktemp); \ | |
| after_file=$$(mktemp); \ | |
| before_file=$$(mktemp "$${TMPDIR:-/tmp}/before_file.XXXXXX"); \ | |
| after_file=$$(mktemp "$${TMPDIR:-/tmp}/after_file.XXXXXX"); \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check for potential manifest filename collisions in SKILLS.txt
if [ -f "SKILLS.txt" ]; then
echo "Repos in SKILLS.txt:"
grep -v '^#' SKILLS.txt | grep -v '^$' | awk '{print $1}' | while read repo; do
sanitized=$(echo "$repo" | sed 's#[^A-Za-z0-9_.-]#_#g')
echo " $repo -> $sanitized.skills"
done | sort | uniq -d -f1
else
echo "SKILLS.txt not found"
fiRepository: shunkakinoki/dotagents
Length of output: 86
🏁 Script executed:
# Check if SKILLS.txt exists and examine its content
echo "=== Checking SKILLS.txt ==="
if [ -f "SKILLS.txt" ]; then
echo "SKILLS.txt exists. First 30 lines (non-comment):"
head -30 SKILLS.txt | grep -v '^#' | grep -v '^$'
echo ""
echo "Total non-empty, non-comment lines:"
grep -v '^#' SKILLS.txt | grep -v '^$' | wc -l
else
echo "SKILLS.txt not found"
fiRepository: shunkakinoki/dotagents
Length of output: 1075
🏁 Script executed:
# Examine the Makefile context around line 212
echo "=== Makefile context (lines 200-220) ==="
sed -n '200,220p' MakefileRepository: shunkakinoki/dotagents
Length of output: 1165
🏁 Script executed:
# Check for any actual collision cases by analyzing all repos
echo "=== Analyzing repos for collisions ==="
if [ -f "SKILLS.txt" ]; then
grep -v '^#' SKILLS.txt | grep -v '^$' | awk '{print $1}' | while read repo; do
sanitized=$(echo "$repo" | sed 's#[^A-Za-z0-9_.-]#_#g')
echo "$sanitized"
done | sort > /tmp/sanitized_names.txt
# Find duplicates
echo "Duplicate sanitized names:"
uniq -d /tmp/sanitized_names.txt
# Show what repos map to each duplicate
if [ -s /tmp/sanitized_names.txt ]; then
echo ""
echo "Full mapping:"
grep -v '^#' SKILLS.txt | grep -v '^$' | awk '{print $1}' | while read repo; do
sanitized=$(echo "$repo" | sed 's#[^A-Za-z0-9_.-]#_#g')
echo "$sanitized <- $repo"
done | sort
fi
fiRepository: shunkakinoki/dotagents
Length of output: 1951
Potential manifest filename collision for different repos is theoretically possible but not present in current SKILLS.txt.
The sanitization sed 's#[^A-Za-z0-9_.-]#_#g' replaces / with _, so repos like owner/foo-bar and owner_foo-bar would produce the same manifest filename. Analysis of the 39 repos currently in SKILLS.txt confirms no collisions exist today; however, this remains a latent collision risk as the file grows.
Consider updating to a more collision-resistant scheme (e.g., double-underscore for slash: s#/#__#g) as a defensive measure.
🔧 Suggested fix
- manifest_file="$$manifest_dir/$$(printf '%s' "$$repo" | sed 's#[^A-Za-z0-9_.-]#_#g').skills"; \
+ manifest_file="$$manifest_dir/$$(printf '%s' "$$repo" | sed 's#/#__#g; s#[^A-Za-z0-9_.-]#_#g').skills"; \📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| manifest_file="$$manifest_dir/$$(printf '%s' "$$repo" | sed 's#[^A-Za-z0-9_.-]#_#g').skills"; \ | |
| manifest_file="$$manifest_dir/$$(printf '%s' "$$repo" | sed 's#/#__#g; s#[^A-Za-z0-9_.-]#_#g').skills"; \ |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Makefile` at line 212, The manifest filename generation using manifest_file
and sed 's#[^A-Za-z0-9_.-]#_#g' can produce collisions (e.g., owner/foo ->
owner_foo); update the sanitization used when computing manifest_file to make
slashes distinct or otherwise collision-resistant — for example, first map '/'
to a unique token like '__' (apply s#/#__#g before the generic sanitizer) or
append a short hash of $$repo (e.g., use a checksum of $$repo) to the sanitized
name; change the expression that builds manifest_file so it either replaces '/'
with '__' prior to the broad character replacement or includes the hash to
ensure uniqueness.
Copilot
AI
Apr 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Manifest filenames are derived from a sanitized repo string (sed 's#[^A-Za-z0-9_.-]#_#g'). Different repos can collide to the same manifest name (e.g., a/b vs a_b), causing managed-clean/install state to mix. Consider using a collision-resistant encoding (e.g., include a hash of the repo) for manifest filenames.
Copilot
AI
Apr 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
skills-install performs critical filesystem state updates (cp to the spec state file) but the recipe doesn’t use set -e or check return codes, so a failure can still print “Managed external skills are in sync.” and leave the cache inconsistent. Consider failing the target on unexpected errors (while still allowing bunx skills add to be non-fatal if that’s intended).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The target exits successfully even when installations fail.
When failed=1, the message is printed but the target exits with status 0. This masks failures in CI/CD pipelines and makes make sync report success even when some skills failed to install.
🐛 Proposed fix to propagate failure status
if [ "$$failed" = "1" ]; then \
echo "Some skills failed to install (see above)."; \
+ exit 1; \
else \
echo "Managed external skills are in sync."; \
fi📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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 | |
| if [ "$$failed" = "1" ]; then \ | |
| echo "Some skills failed to install (see above)."; \ | |
| exit 1; \ | |
| else \ | |
| echo "Managed external skills are in sync."; \ | |
| fi |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Makefile` around lines 243 - 247, The Makefile currently prints "Some skills
failed to install" when the variable failed is set but still exits with status
0, masking CI failures; modify the recipe that checks the failed variable (the
if [ "$$failed" = "1" ]; then ... fi block) to explicitly exit with a non‑zero
status in the failure branch (e.g., add an "exit 1" or "false" immediately after
the failure echo) so the make target (e.g., sync/managed-skills target that sets
failed) propagates failure to the caller; ensure the change remains inside the
same shell invocation (preserve the existing backslash line-continuations or use
.ONESHELL).
Copilot
AI
Apr 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
skills-refresh runs skills-managed-clean, then forces skills-install with DOTAGENTS_FORCE_SKILLS_INSTALL=1. But skills-install will call skills-managed-clean again when the force flag is set, doing the cleanup twice. Consider removing the first clean or adjusting skills-install to skip the clean when invoked from refresh.
| @$(MAKE) skills-managed-clean |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The updated
skills-cleandescription says it removes “all” skills, but the implementation usesrm -rf "$target"/*, which won’t remove dotfiles/directories (e.g.,.foo). Either adjust the wording or update removal to include hidden entries so behavior matches the description.