Commit 6765f9d06ae7
Changed files (5)
dots
.config
agent-skills
dots/.config/agent-skills/agent-skill-manager
@@ -0,0 +1,461 @@
+#!/usr/bin/env bash
+# agent-skill-manager - Manage project-specific agent skills
+#
+# Usage:
+# agent-skill-manager init [dir] Initialize skills for project
+# agent-skill-manager sync [dir] Sync skills from config
+# agent-skill-manager clean [dir] Remove project skills
+# agent-skill-manager status [dir] Show active skills
+# agent-skill-manager detect [dir] Detect project config
+
+set -euo pipefail
+
+GLOBAL_SKILLS_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/claude/skills"
+PROJECTS_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/agent-skills/projects.toml"
+SKILLS_DIR=".claude/skills"
+
+# Colors
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m'
+
+log_info() { echo -e "${BLUE}ℹ${NC} $*"; }
+log_success() { echo -e "${GREEN}✓${NC} $*"; }
+log_warning() { echo -e "${YELLOW}⚠${NC} $*"; }
+log_error() { echo -e "${RED}✗${NC} $*" >&2; }
+
+# Detect project configuration by git remote or path
+detect_project() {
+ local project_dir="${1:-.}"
+
+ cd "$project_dir" || exit 1
+
+ if [[ ! -f "$PROJECTS_CONFIG" ]]; then
+ log_error "Projects config not found: $PROJECTS_CONFIG"
+ return 1
+ fi
+
+ # Try git remote first
+ if git rev-parse --git-dir >/dev/null 2>&1; then
+ local remote_url
+ remote_url=$(git remote get-url origin 2>/dev/null || echo "")
+
+ if [[ -n "$remote_url" ]]; then
+ # Normalize URL
+ remote_url=$(echo "$remote_url" | sed 's/\.git$//' | sed 's|git@github.com:|github.com/|' | sed 's|https://||')
+
+ # Check patterns
+ local project_key
+ project_key=$(awk -F'"' '/^\[project\."/ {print $2}' "$PROJECTS_CONFIG" | while read -r pattern; do
+ local regex="${pattern//\*/.*}"
+
+ if [[ "$remote_url" =~ $regex ]]; then
+ echo "$pattern"
+ break
+ fi
+ done | head -1)
+
+ if [[ -n "$project_key" ]]; then
+ echo "$project_key"
+ return 0
+ fi
+ fi
+ fi
+
+ # Try path-based matching
+ local current_path
+ current_path=$(pwd)
+
+ local project_key
+ project_key=$(awk -F'"' '/^\[project\."/ {print $2}' "$PROJECTS_CONFIG" | while read -r pattern; do
+ if [[ "$pattern" == *"*"* ]]; then
+ local regex="${pattern//\*/.*}"
+
+ if [[ "$current_path" =~ $regex ]]; then
+ echo "$pattern"
+ break
+ fi
+ fi
+ done | head -1)
+
+ if [[ -n "$project_key" ]]; then
+ echo "$project_key"
+ return 0
+ fi
+
+ return 1
+}
+
+# Parse required skills for a project from central config
+parse_project_skills() {
+ local project_key="$1"
+ local in_project=0
+ local in_array=0
+ local skills=""
+
+ while IFS= read -r line; do
+ # Remove comments and trim
+ line=$(echo "$line" | sed 's/#.*//' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
+
+ [[ -z "$line" ]] && continue
+
+ # Check for project section
+ if [[ "$line" == "[project.\"$project_key\"]" ]]; then
+ in_project=1
+ continue
+ fi
+
+ # Exit project section on next section header
+ if [[ $in_project -eq 1 ]] && [[ "$line" =~ ^\[.*\]$ ]]; then
+ break
+ fi
+
+ # Start of required_skills array
+ if [[ $in_project -eq 1 ]] && [[ "$line" =~ ^required_skills[[:space:]]*=[[:space:]]*\[(.*) ]]; then
+ skills="${BASH_REMATCH[1]}"
+ # Check if array closes on same line
+ if [[ "$skills" =~ (.*)\][[:space:]]*$ ]]; then
+ # Single line array
+ skills="${BASH_REMATCH[1]}"
+ echo "$skills" | tr ',' '\n' | sed 's/^[[:space:]]*"//' | sed 's/"[[:space:]]*$//'
+ return 0
+ else
+ # Multi-line array - continue collecting
+ in_array=1
+ continue
+ fi
+ fi
+
+ # Continue collecting array elements
+ if [[ $in_array -eq 1 ]]; then
+ if [[ "$line" =~ (.*)\][[:space:]]*$ ]]; then
+ # End of array
+ skills="$skills,${BASH_REMATCH[1]}"
+ echo "$skills" | tr ',' '\n' | sed 's/^[[:space:]]*"//' | sed 's/"[[:space:]]*$//' | sed '/^$/d'
+ return 0
+ else
+ # More array elements
+ skills="$skills,$line"
+ fi
+ fi
+ done < "$PROJECTS_CONFIG"
+}
+
+# Get project name from config
+get_project_name() {
+ local project_key="$1"
+ local in_project=0
+
+ while IFS= read -r line; do
+ line=$(echo "$line" | sed 's/#.*//' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
+ [[ -z "$line" ]] && continue
+
+ if [[ "$line" == "[project.\"$project_key\"]" ]]; then
+ in_project=1
+ continue
+ fi
+
+ if [[ $in_project -eq 1 ]] && [[ "$line" =~ ^\[.*\]$ ]]; then
+ break
+ fi
+
+ if [[ $in_project -eq 1 ]] && [[ "$line" =~ ^name[[:space:]]*=[[:space:]]*\"(.*)\" ]]; then
+ echo "${BASH_REMATCH[1]}"
+ return 0
+ fi
+ done < "$PROJECTS_CONFIG"
+
+ echo "$project_key"
+}
+
+# Initialize project skills
+init_project_skills() {
+ local project_dir="${1:-.}"
+
+ cd "$project_dir" || exit 1
+
+ local project_key
+ project_key=$(detect_project "$project_dir") || {
+ log_error "No project configuration found for: $project_dir"
+ log_info "Add project to $PROJECTS_CONFIG"
+ return 1
+ }
+
+ local project_name
+ project_name=$(get_project_name "$project_key")
+
+ log_info "Initializing skills for: $project_name"
+
+ mkdir -p "$SKILLS_DIR"
+
+ local skills
+ mapfile -t skills < <(parse_project_skills "$project_key")
+
+ if [[ ${#skills[@]} -eq 0 ]]; then
+ log_warning "No required skills found for: $project_name"
+ return 0
+ fi
+
+ local copied=0
+ local skipped=0
+
+ for skill in "${skills[@]}"; do
+ [[ -z "$skill" ]] && continue
+
+ local source="$GLOBAL_SKILLS_DIR/$skill"
+ local target="$SKILLS_DIR/$skill"
+
+ if [[ ! -d "$source" ]]; then
+ log_warning "Skill not found: $skill"
+ skipped=$((skipped + 1))
+ continue
+ fi
+
+ if [[ -d "$target" ]]; then
+ log_info "Skill already exists: $skill"
+ skipped=$((skipped + 1))
+ continue
+ fi
+
+ cp -r "$source" "$target"
+ log_success "Copied: $skill"
+ copied=$((copied + 1))
+ done
+
+ log_success "Initialized $copied skills, skipped $skipped"
+ ensure_gitignore
+}
+
+# Sync skills
+sync_project_skills() {
+ local project_dir="${1:-.}"
+
+ cd "$project_dir" || exit 1
+
+ local project_key
+ project_key=$(detect_project "$project_dir") || {
+ return 0
+ }
+
+ local project_name
+ project_name=$(get_project_name "$project_key")
+
+ log_info "Syncing skills for: $project_name"
+
+ local required_skills
+ mapfile -t required_skills < <(parse_project_skills "$project_key")
+
+ declare -A required_map
+ for skill in "${required_skills[@]}"; do
+ [[ -z "$skill" ]] && continue
+ required_map["$skill"]=1
+ done
+
+ for skill in "${required_skills[@]}"; do
+ [[ -z "$skill" ]] && continue
+
+ local source="$GLOBAL_SKILLS_DIR/$skill"
+ local target="$SKILLS_DIR/$skill"
+
+ if [[ ! -d "$source" ]]; then
+ log_warning "Skill not found: $skill"
+ continue
+ fi
+
+ if [[ -d "$target" ]]; then
+ rsync -a --delete "$source/" "$target/"
+ log_success "Updated: $skill"
+ else
+ mkdir -p "$SKILLS_DIR"
+ cp -r "$source" "$target"
+ log_success "Added: $skill"
+ fi
+ done
+
+ if [[ -d "$SKILLS_DIR" ]]; then
+ for existing in "$SKILLS_DIR"/*; do
+ [[ ! -d "$existing" ]] && continue
+
+ local skill_name
+ skill_name=$(basename "$existing")
+
+ if [[ ! -v required_map["$skill_name"] ]]; then
+ rm -rf "$existing"
+ log_info "Removed: $skill_name"
+ fi
+ done
+ fi
+
+ ensure_gitignore
+}
+
+# Clean project skills
+clean_project_skills() {
+ local project_dir="${1:-.}"
+
+ cd "$project_dir" || exit 1
+
+ if [[ ! -d "$SKILLS_DIR" ]]; then
+ log_info "No skills directory found"
+ return 0
+ fi
+
+ log_info "Cleaning skills"
+ rm -rf "$SKILLS_DIR"
+ log_success "Removed all project skills"
+}
+
+# Show status
+show_status() {
+ local project_dir="${1:-.}"
+
+ cd "$project_dir" || exit 1
+
+ echo "Project: $project_dir"
+ echo
+
+ local project_key
+ project_key=$(detect_project "$project_dir") || {
+ echo "No project configuration found"
+ echo
+ if [[ -d "$SKILLS_DIR" ]]; then
+ echo "Project-local skills (manually managed):"
+ for skill in "$SKILLS_DIR"/*; do
+ [[ ! -d "$skill" ]] && continue
+ echo " • $(basename "$skill")"
+ done
+ fi
+ return 0
+ }
+
+ local project_name
+ project_name=$(get_project_name "$project_key")
+
+ echo "Project: $project_name"
+ echo "Config: $project_key"
+ echo
+
+ echo "Required skills:"
+ local skills
+ mapfile -t skills < <(parse_project_skills "$project_key")
+
+ for skill in "${skills[@]}"; do
+ [[ -z "$skill" ]] && continue
+
+ local source="$GLOBAL_SKILLS_DIR/$skill"
+ local target="$SKILLS_DIR/$skill"
+
+ if [[ -d "$target" ]]; then
+ echo -e " ${GREEN}✓${NC} $skill (active)"
+ else
+ if [[ -d "$source" ]]; then
+ echo -e " ${YELLOW}○${NC} $skill (available)"
+ else
+ echo -e " ${RED}✗${NC} $skill (not found)"
+ fi
+ fi
+ done
+}
+
+# Show detected project
+show_detect() {
+ local project_dir="${1:-.}"
+
+ cd "$project_dir" || exit 1
+
+ local project_key
+ project_key=$(detect_project "$project_dir") || {
+ echo "No project configuration detected"
+ return 1
+ }
+
+ local project_name
+ project_name=$(get_project_name "$project_key")
+
+ echo "Detected project: $project_name"
+ echo "Config key: $project_key"
+ echo "Required skills:"
+ parse_project_skills "$project_key" | while read -r skill; do
+ echo " - $skill"
+ done
+}
+
+# Ensure gitignore
+ensure_gitignore() {
+ local gitignore=".gitignore"
+
+ if [[ ! -f "$gitignore" ]]; then
+ echo ".claude/skills/" > "$gitignore"
+ log_success "Created .gitignore"
+ elif ! grep -q "^.claude/skills/" "$gitignore" 2>/dev/null; then
+ echo ".claude/skills/" >> "$gitignore"
+ log_success "Added .claude/skills/ to .gitignore"
+ fi
+}
+
+# Main
+main() {
+ local command="${1:-}"
+ shift || true
+
+ case "$command" in
+ init)
+ init_project_skills "$@"
+ ;;
+ sync)
+ sync_project_skills "$@"
+ ;;
+ clean)
+ clean_project_skills "$@"
+ ;;
+ status)
+ show_status "$@"
+ ;;
+ detect)
+ show_detect "$@"
+ ;;
+ ""|--help|-h)
+ cat <<EOF
+agent-skill-manager - Manage project-specific Claude skills
+
+Usage:
+ agent-skill-manager init [dir] Initialize skills for project
+ agent-skill-manager sync [dir] Sync skills (update/add/remove)
+ agent-skill-manager clean [dir] Remove all project skills
+ agent-skill-manager status [dir] Show skill status
+ agent-skill-manager detect [dir] Detect project configuration
+
+Configuration:
+ Central config: $PROJECTS_CONFIG
+ Global skills: $GLOBAL_SKILLS_DIR
+ Project skills: .claude/skills/ (gitignored)
+
+The central config maps git remotes and paths to required skills.
+Skills are automatically detected and synced based on your location.
+
+Examples:
+ # Detect what project you're in
+ agent-skill-manager detect
+
+ # Initialize skills for current project
+ agent-skill-manager init
+
+ # Sync skills after config changes
+ agent-skill-manager sync
+
+Git Hook Integration:
+ Add to .git/hooks/post-checkout:
+ #!/bin/bash
+ agent-skill-manager sync 2>/dev/null || true
+EOF
+ ;;
+ *)
+ log_error "Unknown command: $command"
+ exit 1
+ ;;
+ esac
+}
+
+main "$@"
dots/.config/agent-skills/projects.toml
@@ -0,0 +1,172 @@
+# Agent Skills Project Configuration
+# Master configuration for project-specific AI agent skills
+#
+# Location: ~/.config/agent-skills/projects.toml
+# Managed by: ~/src/home/dots/Makefile
+#
+# This configuration works with:
+# - Claude Code (via agent-skill-manager)
+# - Future: GitHub Copilot, OpenCode, pi, etc.
+
+# Projects are keyed by their git remote URL patterns or directory paths
+# When you enter a project directory, agent-skill-manager detects which
+# project config applies and activates the corresponding skills.
+
+# ============================================================================
+# Home Repository - Personal Infrastructure
+# ============================================================================
+[project."*/home"]
+name = "home"
+description = "Personal NixOS infrastructure and homelab"
+required_skills = [
+ "Homelab",
+ "Nix",
+ "Git",
+ "SystematicDebugging",
+ "CORE",
+]
+
+# ============================================================================
+# Nixpkgs - NixOS Package Repository
+# ============================================================================
+[project."github.com/NixOS/nixpkgs"]
+name = "nixpkgs"
+description = "Contributing to NixOS/nixpkgs"
+required_skills = [
+ "Nixpkgs",
+ "Nix",
+ "Git",
+ "SystematicDebugging",
+ "CORE",
+]
+
+# Alternative path-based matcher for local nixpkgs clones
+[project."*/nixpkgs"]
+name = "nixpkgs-local"
+description = "Local nixpkgs repository"
+required_skills = [
+ "Nixpkgs",
+ "Nix",
+ "Git",
+ "SystematicDebugging",
+ "CORE",
+]
+
+# ============================================================================
+# Tekton Projects - Upstream Contribution
+# ============================================================================
+[project."github.com/tektoncd/*"]
+name = "tekton"
+description = "Tekton upstream development"
+required_skills = [
+ "Tekton",
+ "Kubernetes",
+ "golang",
+ "Git",
+ "GitHub",
+ "TestDrivenDevelopment",
+ "SystematicDebugging",
+ "CORE",
+]
+
+# ============================================================================
+# Work Projects - Red Hat
+# ============================================================================
+[project."gitlab.com/redhat/*"]
+name = "redhat"
+description = "Red Hat internal projects"
+required_skills = [
+ "Jira",
+ "Tekton",
+ "Kubernetes",
+ "golang",
+ "Git",
+ "SystematicDebugging",
+ "CORE",
+]
+
+[project."github.com/openshift/*"]
+name = "openshift"
+description = "OpenShift projects"
+required_skills = [
+ "Jira",
+ "Tekton",
+ "Kubernetes",
+ "golang",
+ "Git",
+ "GitHub",
+ "SystematicDebugging",
+ "CORE",
+]
+
+[project."github.com/konflux-ci/*"]
+name = "konflux"
+description = "Konflux CI projects"
+required_skills = [
+ "Jira",
+ "Tekton",
+ "Kubernetes",
+ "golang",
+ "Git",
+ "GitHub",
+ "SystematicDebugging",
+ "CORE",
+]
+
+# ============================================================================
+# Emacs Configuration
+# ============================================================================
+[project."*/emacs.d"]
+name = "emacs"
+description = "Emacs configuration"
+required_skills = [
+ "EmacsLisp",
+ "Git",
+ "SystematicDebugging",
+ "CORE",
+]
+
+[project."*/tools/emacs"]
+name = "emacs-tools"
+description = "Emacs tools in home repository"
+required_skills = [
+ "EmacsLisp",
+ "Nix",
+ "Git",
+ "SystematicDebugging",
+ "CORE",
+]
+
+# ============================================================================
+# Go Projects - Personal
+# ============================================================================
+[project."github.com/vdemeester/*"]
+name = "personal-go"
+description = "Personal Go projects"
+required_skills = [
+ "golang",
+ "Git",
+ "GitHub",
+ "TestDrivenDevelopment",
+ "SystematicDebugging",
+ "CORE",
+]
+
+# ============================================================================
+# Default Fallback
+# ============================================================================
+# If no project matches, these skills are always available from:
+# - Claude: ~/.config/claude/skills/
+# - Future agents: their respective skill directories
+#
+# Global skills that are ALWAYS loaded:
+# - CORE (personal AI infrastructure)
+# - Git (universal)
+# - GitHub (universal)
+# - Org (personal notes/TODOs)
+# - Email (personal)
+# - Journal (personal)
+# - TODOs (personal task management)
+# - Python, golang, Rust (languages)
+# - Nix (used across multiple projects)
+# - All others in global skills directory
dots/.config/agent-skills/README.md
@@ -0,0 +1,132 @@
+# Agent Skills Project Configuration
+
+Central configuration for managing project-specific AI agent skills across different tools (Claude Code, GitHub Copilot, OpenCode, pi, etc.).
+
+## Purpose
+
+This directory contains a single source of truth (`projects.toml`) that maps git repositories and directory paths to required skills. When you work in a project, the appropriate skills are automatically activated based on this configuration.
+
+## Configuration File
+
+**`projects.toml`**: Maps projects to required skills using git remote URLs or path patterns.
+
+Example:
+```toml
+[project."github.com/vdemeester/home"]
+name = "home"
+description = "Personal NixOS infrastructure"
+required_skills = ["Homelab", "Nix", "Git", "CORE"]
+```
+
+## How It Works
+
+### For Claude Code
+
+1. **Global skills**: `~/.config/claude/skills/` (always loaded)
+2. **Project skills**: `.claude/skills/` (project-specific, gitignored)
+3. **Tool**: `agent-skill-manager` syncs skills from global to project based on `projects.toml`
+
+When you work in a project:
+- Git hooks automatically sync the required skills to `.claude/skills/`
+- Both global and project skills are available
+- Project `.gitignore` prevents committing skill content
+- Configuration is centralized in this directory
+
+### For Future Tools
+
+The same `projects.toml` can be used by other AI coding tools:
+- GitHub Copilot: Could load project-specific instructions
+- OpenCode: Could activate project-specific plugins
+- pi: Could enable project-specific extensions
+
+## File Structure
+
+```
+~/.config/agent-skills/
+├── projects.toml # Central configuration (THIS FILE)
+└── README.md # This documentation
+
+~/src/home/
+├── .claude/
+│ └── skills/ # Auto-synced based on projects.toml
+│ ├── Homelab/ # (gitignored, copied from global)
+│ └── Nix/ # (gitignored, copied from global)
+└── .gitignore # Contains: .claude/skills/
+
+~/.config/claude/skills/ # Global skills library
+├── CORE/
+├── Git/
+├── Homelab/
+├── Nix/
+└── ...
+```
+
+## Usage
+
+### agent-skill-manager
+
+```bash
+# Detect what project you're in
+agent-skill-manager detect
+
+# Initialize skills for current project
+agent-skill-manager init
+
+# Sync skills (after config changes or git operations)
+agent-skill-manager sync
+
+# Check skill status
+agent-skill-manager status
+
+# Remove all project skills
+agent-skill-manager clean
+```
+
+### Adding a New Project
+
+1. Edit `~/.config/agent-skills/projects.toml`
+2. Add a `[project."pattern"]` section
+3. List required skills in `required_skills = [...]`
+4. Run `agent-skill-manager sync` in the project
+
+Example:
+```toml
+[project."github.com/myorg/myrepo"]
+name = "myrepo"
+description = "My awesome project"
+required_skills = ["Python", "Git", "CORE"]
+```
+
+### Git Hook Integration
+
+Add to `.git/hooks/post-checkout`:
+```bash
+#!/bin/bash
+agent-skill-manager sync 2>/dev/null || true
+```
+
+Or use the provided hook templates in `~/src/home/dots/.config/agent-skills/hooks/`.
+
+## Pattern Matching
+
+Projects are matched by:
+1. **Git remote URL** (exact or wildcard): `github.com/user/repo` or `github.com/tektoncd/*`
+2. **Directory path** (wildcard): `*/nixpkgs` or `*/emacs.d`
+
+Wildcards (`*`) match any characters in that segment.
+
+## Benefits
+
+- **Single source of truth**: One config for all projects
+- **Tool-agnostic**: Works with Claude, Copilot, OpenCode, pi, etc.
+- **No committed skills**: Skills are gitignored, only config is versioned
+- **Automatic sync**: Git hooks keep skills up to date
+- **Context-aware**: Different skills for different projects
+- **Centrally managed**: Update once in dotfiles, applies everywhere
+
+## Related Files
+
+- `~/src/home/dots/Makefile`: Manages dotfile installation
+- `~/.config/copilot-hooks/`: Similar pattern for Copilot-specific hooks
+- `~/.config/claude/`: Claude-specific configuration
+- `~/.config/opencode/`: OpenCode-specific configuration
dots/Makefile
@@ -53,13 +53,19 @@ lazypr : ~/.config/lazypr/config.toml
all += gh-news
gh-news : ~/.config/gh-news/config.toml
-all += git-template copilot-hooks opencode-plugin pi-agent agent-skills agent-skill-manager
+all += git-template copilot-hooks opencode-plugin pi-agent agent-skills agent-skill-manager-bin
git-template : ~/.config/git/template
copilot-hooks : ~/.config/copilot-hooks
opencode-plugin : ~/.config/opencode/plugin
pi-agent : ~/.pi/agent/extensions ~/.pi/agent/AGENTS.md ~/.pi/agent/README.md
agent-skills : ~/.config/agent-skills
-agent-skill-manager : ~/bin/agent-skill-manager
+agent-skill-manager-bin : ~/bin/agent-skill-manager
+
+# Agent skill manager tool
+~/bin/agent-skill-manager : $(dotfiles)/.config/agent-skills/agent-skill-manager force
+ @echo "📋 Linking $(dotfiles)/.config/agent-skills/agent-skill-manager → ~/bin/agent-skill-manager"
+ @mkdir -p ~/bin
+ @ln -snf $(dotfiles)/.config/agent-skills/agent-skill-manager ~/bin/agent-skill-manager
# Backward compatibility: symlink ~/.claude to ~/.config/claude
~/.claude : force
.gitignore
@@ -28,3 +28,4 @@ hardware-configuration.nix
.claude/settings.local.json
/tools/gcal-to-org/gcal-to-org
.playwright-mcp/
+.claude/skills/