-
Notifications
You must be signed in to change notification settings - Fork 6
Fixes skills generation #16
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
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
48a9ca8
fix: fix doc-sync hook automation and make it default after doc init
kzhivotov b6247bf
feat: tests
kzhivotov 726f8a3
feat: skill auto activation hooks
mvoutov 30c40f3
claude aspens hooks + small fix/safe for hooks dulication name
mvoutov 8a80120
Merge pull request #15 from aspenkit/fix/doc-sync-hook-automation
mvoutov 9335158
feat: skill auto activation hooks
mvoutov c51fe0d
claude aspens hooks + small fix/safe for hooks dulication name
mvoutov adc781b
Merge branch 'fixes-skills-generation' of https://github.com/aspenkit…
mvoutov c3ce9cd
fixes to hook activation and clean up
mvoutov ce443b6
fix: defensive guards for hooks and validation
mvoutov 0af7815
release notes
mvoutov File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,293 @@ | ||
| #!/bin/bash | ||
| # Note: Removed set -e to prevent hook failures from blocking edits | ||
|
|
||
| # IMPORTANT: All output MUST go to stderr (>&2), not stdout. | ||
| # stdout from PostToolUse hooks is injected into Claude's context. | ||
|
|
||
| # Post-tool-use hook that tracks edited files and their repos | ||
| # This runs after Edit, MultiEdit, or Write tools complete successfully | ||
|
|
||
| # Require jq for JSON parsing | ||
| if ! command -v jq &> /dev/null; then | ||
| exit 0 | ||
| fi | ||
|
|
||
| # Exit early if CLAUDE_PROJECT_DIR is not set | ||
| if [[ -z "$CLAUDE_PROJECT_DIR" ]]; then | ||
| exit 0 | ||
| fi | ||
|
|
||
| # Read tool information from stdin | ||
| tool_info=$(cat) | ||
|
|
||
|
|
||
| # Extract relevant data | ||
| tool_name=$(echo "$tool_info" | jq -r '.tool_name // empty') | ||
| file_path=$(echo "$tool_info" | jq -r '.tool_input.file_path // empty') | ||
| session_id=$(echo "$tool_info" | jq -r '.session_id // empty') | ||
|
|
||
|
|
||
| # Skip if not an edit tool or no file path | ||
| if [[ ! "$tool_name" =~ ^(Edit|MultiEdit|Write)$ ]] || [[ -z "$file_path" ]]; then | ||
| exit 0 # Exit 0 for skip conditions | ||
| fi | ||
|
|
||
| # Skip markdown files | ||
| if [[ "$file_path" =~ \.(md|markdown)$ ]]; then | ||
| exit 0 # Exit 0 for skip conditions | ||
| fi | ||
|
|
||
| # Create cache directory in project | ||
| cache_dir="$CLAUDE_PROJECT_DIR/.claude/tsc-cache/${session_id:-default}" | ||
| mkdir -p "$cache_dir" | ||
|
|
||
| # Function to detect repo from file path | ||
| detect_repo() { | ||
| local file="$1" | ||
| local project_root="$CLAUDE_PROJECT_DIR" | ||
|
|
||
| # Remove project root from path | ||
| local relative_path="${file#$project_root/}" | ||
|
|
||
| # Extract first directory component | ||
| local repo | ||
| repo=$(echo "$relative_path" | cut -d'/' -f1) | ||
|
|
||
| # Common project directory patterns | ||
| case "$repo" in | ||
| # Frontend variations | ||
| frontend|client|web|app|ui) | ||
| echo "$repo" | ||
| ;; | ||
| # Backend variations | ||
| backend|server|api|src|services) | ||
| echo "$repo" | ||
| ;; | ||
| # Database | ||
| database|prisma|migrations) | ||
| echo "$repo" | ||
| ;; | ||
| # Package/monorepo structure | ||
| packages) | ||
| # For monorepos, get the package name | ||
| local package=$(echo "$relative_path" | cut -d'/' -f2) | ||
| if [[ -n "$package" ]]; then | ||
| echo "packages/$package" | ||
| else | ||
| echo "$repo" | ||
| fi | ||
| ;; | ||
| # Examples directory | ||
| examples) | ||
| local example=$(echo "$relative_path" | cut -d'/' -f2) | ||
| if [[ -n "$example" ]]; then | ||
| echo "examples/$example" | ||
| else | ||
| echo "$repo" | ||
| fi | ||
| ;; | ||
| *) | ||
| # Check if it's a source file in root | ||
| if [[ ! "$relative_path" =~ / ]]; then | ||
| echo "root" | ||
| else | ||
| echo "unknown" | ||
| fi | ||
| ;; | ||
| esac | ||
| } | ||
|
|
||
| # Function to get build command for repo | ||
| get_build_command() { | ||
| local repo="$1" | ||
| local project_root="$CLAUDE_PROJECT_DIR" | ||
|
|
||
| # Map special repo names to actual paths | ||
| local repo_path | ||
| if [[ "$repo" == "root" ]] || [[ "$repo" == "src" ]] || [[ "$repo" == "unknown" ]]; then | ||
| repo_path="$project_root" | ||
| else | ||
| repo_path="$project_root/$repo" | ||
| fi | ||
|
|
||
| # Check if package.json exists and has a build script | ||
| if [[ -f "$repo_path/package.json" ]]; then | ||
| if grep -q '"build"' "$repo_path/package.json" 2>/dev/null; then | ||
| # Detect package manager (prefer pnpm, then npm, then yarn) | ||
| if [[ -f "$repo_path/pnpm-lock.yaml" ]]; then | ||
| echo "cd $repo_path && pnpm build" | ||
| elif [[ -f "$repo_path/package-lock.json" ]]; then | ||
| echo "cd $repo_path && npm run build" | ||
| elif [[ -f "$repo_path/yarn.lock" ]]; then | ||
| echo "cd $repo_path && yarn build" | ||
| else | ||
| echo "cd $repo_path && npm run build" | ||
| fi | ||
| return | ||
| fi | ||
| fi | ||
|
|
||
| # Special case for database with Prisma | ||
| if [[ "$repo" == "database" ]] || [[ "$repo" =~ prisma ]]; then | ||
| if [[ -f "$repo_path/schema.prisma" ]] || [[ -f "$repo_path/prisma/schema.prisma" ]]; then | ||
| echo "cd $repo_path && npx prisma generate" | ||
| return | ||
| fi | ||
| fi | ||
|
|
||
| # No build command found | ||
| echo "" | ||
| } | ||
|
|
||
| # Function to get TSC command for repo | ||
| get_tsc_command() { | ||
| local repo="$1" | ||
| local project_root="$CLAUDE_PROJECT_DIR" | ||
|
|
||
| # Map special repo names to actual paths | ||
| local repo_path | ||
| if [[ "$repo" == "root" ]] || [[ "$repo" == "src" ]] || [[ "$repo" == "unknown" ]]; then | ||
| repo_path="$project_root" | ||
| else | ||
| repo_path="$project_root/$repo" | ||
| fi | ||
|
|
||
| # Check if tsconfig.json exists | ||
| if [[ -f "$repo_path/tsconfig.json" ]]; then | ||
| # Check for Vite/React-specific tsconfig | ||
| if [[ -f "$repo_path/tsconfig.app.json" ]]; then | ||
| echo "cd $repo_path && npx tsc --project tsconfig.app.json --noEmit" | ||
| else | ||
| echo "cd $repo_path && npx tsc --noEmit" | ||
| fi | ||
| return | ||
| fi | ||
|
|
||
| # No TypeScript config found | ||
| echo "" | ||
| } | ||
|
|
||
| # Detect repo | ||
| repo=$(detect_repo "$file_path") | ||
|
|
||
| # Skip if unknown repo | ||
| if [[ "$repo" == "unknown" ]] || [[ -z "$repo" ]]; then | ||
| exit 0 # Exit 0 for skip conditions | ||
| fi | ||
|
|
||
| # Log edited file | ||
| echo "$(date +%s):$file_path:$repo" >> "$cache_dir/edited-files.log" | ||
|
|
||
| # Update affected repos list | ||
| if ! grep -q "^$repo$" "$cache_dir/affected-repos.txt" 2>/dev/null; then | ||
| echo "$repo" >> "$cache_dir/affected-repos.txt" | ||
| fi | ||
|
|
||
| # Store build commands | ||
| build_cmd=$(get_build_command "$repo") | ||
| tsc_cmd=$(get_tsc_command "$repo") | ||
|
|
||
| if [[ -n "$build_cmd" ]]; then | ||
| echo "$repo:build:$build_cmd" >> "$cache_dir/commands.txt.tmp" | ||
| fi | ||
|
|
||
| if [[ -n "$tsc_cmd" ]]; then | ||
| echo "$repo:tsc:$tsc_cmd" >> "$cache_dir/commands.txt.tmp" | ||
| fi | ||
|
|
||
| # Remove duplicates from commands | ||
| if [[ -f "$cache_dir/commands.txt.tmp" ]]; then | ||
| sort -u "$cache_dir/commands.txt.tmp" > "$cache_dir/commands.txt" | ||
| rm -f "$cache_dir/commands.txt.tmp" | ||
| fi | ||
|
|
||
| # ============================================ | ||
| # SESSION-STICKY SKILLS TRACKING | ||
| # ============================================ | ||
| # Detect which domain skill should be activated based on file path | ||
| # and persist it in session state for sticky behavior | ||
|
|
||
| detect_skill_domain() { | ||
| local file="$1" | ||
| local detected_skills="" | ||
|
|
||
| # Generated by aspens from skill-rules.json filePatterns | ||
| if [[ "$file" =~ /customize ]] || [[ "$file" =~ /customize-agents ]]; then | ||
| detected_skills="agent-customization" | ||
| elif [[ "$file" =~ /runner ]] || [[ "$file" =~ /skill-writer ]] || [[ "$file" =~ /prompts/ ]] || [[ "$file" =~ /tests/ ]]; then | ||
| detected_skills="claude-runner" | ||
| elif [[ "$file" =~ /doc-sync ]]; then | ||
| detected_skills="doc-sync" | ||
| elif [[ "$file" =~ /graph-builder ]] || [[ "$file" =~ /graph-builder.test ]]; then | ||
| detected_skills="import-graph" | ||
| elif [[ "$file" =~ /scanner ]] || [[ "$file" =~ /scan ]] || [[ "$file" =~ /scanner.test ]]; then | ||
| detected_skills="repo-scanning" | ||
| elif [[ "$file" =~ /doc-init ]] || [[ "$file" =~ /doc-sync ]] || [[ "$file" =~ /customize ]] || [[ "$file" =~ /context-builder ]] || [[ "$file" =~ /runner ]] || [[ "$file" =~ /skill-writer ]] || [[ "$file" =~ /prompts/ ]]; then | ||
| detected_skills="skill-generation" | ||
| elif [[ "$file" =~ /add ]] || [[ "$file" =~ /customize ]] || [[ "$file" =~ /templates/ ]]; then | ||
| detected_skills="template-library" | ||
| fi | ||
|
|
||
| echo "$detected_skills" | ||
| } | ||
|
|
||
| # Create session file path based on project directory hash | ||
| get_session_file() { | ||
| local project_dir="$1" | ||
| local hash=$(echo -n "$project_dir" | md5 2>/dev/null || echo -n "$project_dir" | md5sum | cut -d' ' -f1) | ||
| echo "${TMPDIR:-/tmp}/claude-skills-${hash}.json" | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| # Add skill to session state | ||
| add_skill_to_session() { | ||
| local skill="$1" | ||
| local session_file="$2" | ||
| local repo="$3" | ||
|
|
||
| if [[ -z "$skill" ]]; then | ||
| return | ||
| fi | ||
|
|
||
| # Create or update session file | ||
| if [[ -f "$session_file" ]]; then | ||
| # Check if jq is available | ||
| if command -v jq &> /dev/null; then | ||
| # Add skill to array, keeping unique values | ||
| jq --arg skill "$skill" --arg time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ | ||
| '.active_skills = ((.active_skills + [$skill]) | unique) | .last_updated = $time' \ | ||
| "$session_file" > "${session_file}.tmp" 2>/dev/null && \ | ||
| mv "${session_file}.tmp" "$session_file" | ||
| else | ||
| # Fallback: simple append check without jq | ||
| if ! grep -q "\"$skill\"" "$session_file" 2>/dev/null; then | ||
| # Read existing skills from file, append new one, rewrite | ||
| local existing_skills="" | ||
| if [[ -f "$session_file" ]]; then | ||
| # Extract skills array content: strip brackets, quotes, whitespace | ||
| existing_skills=$(grep -o '"active_skills":\[[^]]*\]' "$session_file" 2>/dev/null | sed 's/"active_skills":\[//;s/\]//;s/"//g;s/ //g') | ||
| fi | ||
| # Build new skills list | ||
| local new_skills="" | ||
| if [[ -n "$existing_skills" ]]; then | ||
| new_skills="\"$(echo "$existing_skills" | sed 's/,/","/g')\",\"$skill\"" | ||
| else | ||
| new_skills="\"$skill\"" | ||
| fi | ||
| echo "{\"repo\":\"$repo\",\"active_skills\":[$new_skills],\"last_updated\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" > "$session_file" | ||
| fi | ||
| fi | ||
| else | ||
| # Create new session file | ||
| echo "{\"repo\":\"$repo\",\"active_skills\":[\"$skill\"],\"last_updated\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" > "$session_file" | ||
| fi | ||
| } | ||
|
|
||
| # Track skill domain for session-sticky behavior | ||
| skill_domain=$(detect_skill_domain "$file_path") | ||
| if [[ -n "$skill_domain" ]]; then | ||
| session_file=$(get_session_file "$CLAUDE_PROJECT_DIR") | ||
| add_skill_to_session "$skill_domain" "$session_file" "$repo" | ||
| fi | ||
|
|
||
| # Exit cleanly | ||
| exit 0 | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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:
Repository: aspenkit/aspens
Length of output: 1236
🏁 Script executed:
Repository: aspenkit/aspens
Length of output: 964
🏁 Script executed:
Repository: aspenkit/aspens
Length of output: 1668
🏁 Script executed:
Repository: aspenkit/aspens
Length of output: 112
🏁 Script executed:
Repository: aspenkit/aspens
Length of output: 92
🏁 Script executed:
Repository: aspenkit/aspens
Length of output: 1489
🏁 Script executed:
Repository: aspenkit/aspens
Length of output: 2747
🏁 Script executed:
Repository: aspenkit/aspens
Length of output: 41
🏁 Script executed:
Repository: aspenkit/aspens
Length of output: 4581
🏁 Script executed:
Repository: aspenkit/aspens
Length of output: 1305
🏁 Script executed:
Repository: aspenkit/aspens
Length of output: 1519
🏁 Script executed:
Repository: aspenkit/aspens
Length of output: 1349
🏁 Script executed:
Repository: aspenkit/aspens
Length of output: 494
The broad
/tests/match masks specific test-domain rules.Detection is first-match-wins, so
tests/graph-builder.test.jsandtests/scanner.test.jswill incorrectly activateclaude-runnerinstead ofimport-graphandrepo-scanning. Thetests/*extract*pattern inclaude-runner's filePatterns converts to/tests/, which is too broad. Fix this inskill-rules.jsonby making the claude-runner test pattern more specific (e.g.,tests/*extract*should not match test files for other domains), or refactor the pattern ordering in the generator to prioritize specific filename matches over generic directory patterns.🤖 Prompt for AI Agents