├── .claude ├── hooks │ ├── .prettierrc.json │ ├── skill-activation-prompt.sh │ ├── .env.example │ ├── tsconfig.json │ ├── cleanup-stale-state.sh │ ├── .eslintrc.json │ ├── package.json │ ├── lib │ │ ├── __tests__ │ │ │ ├── constants.test.ts │ │ │ ├── schema-validator.test.ts │ │ │ ├── cache.test.ts │ │ │ ├── keyword-fallback.test.ts │ │ │ ├── dependency-resolution.test.ts │ │ │ ├── skill-state-manager.test.ts │ │ │ ├── output-formatter.test.ts │ │ │ ├── skill-filtering.test.ts │ │ │ └── affinity-injection.test.ts │ │ ├── debug-logger.ts │ │ ├── keyword-matcher.ts │ │ ├── types.ts │ │ ├── schema-validator.ts │ │ ├── constants.ts │ │ ├── skill-resolution.ts │ │ ├── skill-state-manager.ts │ │ ├── intent-analyzer.ts │ │ ├── cache-manager.ts │ │ ├── intent-scorer.ts │ │ ├── anthropic-client.ts │ │ ├── output-formatter.ts │ │ └── skill-filtration.ts │ ├── eslint.config.js │ ├── config │ │ └── intent-analysis-prompt.txt │ ├── skill-activation-prompt.ts │ └── README.md ├── settings.json ├── skills │ ├── skill-developer │ │ └── resources │ │ │ ├── patterns-library.md │ │ │ ├── trigger-types.md │ │ │ ├── hook-mechanisms.md │ │ │ ├── skill-rules-reference.md │ │ │ ├── skill-creation-guide.md │ │ │ └── two-tier-system.md │ ├── skill-rules.schema.json │ ├── skill-rules.json │ ├── README.md │ └── git-workflow │ │ └── SKILL.md └── commands │ └── wrap.md ├── .gitignore ├── LICENSE ├── .github ├── workflows │ ├── ci.yml │ └── auto-merge.yml └── dependabot.yml ├── .pre-commit-config.yaml └── docs └── GETTING-STARTED.md /.claude/hooks/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "es5", 8 | "bracketSpacing": true, 9 | "arrowParens": "always" 10 | } 11 | -------------------------------------------------------------------------------- /.claude/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "UserPromptSubmit": [ 4 | { 5 | "hooks": [ 6 | { 7 | "type": "command", 8 | "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/skill-activation-prompt.sh" 9 | } 10 | ] 11 | } 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.claude/hooks/skill-activation-prompt.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | cd "${CLAUDE_PROJECT_DIR}/.claude/hooks" 5 | 6 | # Load API key from .env if available 7 | if [ -f .env ]; then 8 | set -a # automatically export variables 9 | # shellcheck disable=SC1091 10 | source .env 11 | set +a 12 | fi 13 | 14 | cat | npx tsx skill-activation-prompt.ts 15 | -------------------------------------------------------------------------------- /.claude/skills/skill-developer/resources/patterns-library.md: -------------------------------------------------------------------------------- 1 | # Common Patterns Library 2 | 3 | Ready-to-use keyword examples for skill triggers. See trigger-types.md for implementation details. 4 | 5 | ______________________________________________________________________ 6 | 7 | **Related Files:** 8 | 9 | - [SKILL.md](../SKILL.md) - Main skill guide 10 | - [trigger-types.md](trigger-types.md) - Detailed trigger documentation 11 | - [skill-rules-reference.md](skill-rules-reference.md) - Complete schema 12 | -------------------------------------------------------------------------------- /.claude/hooks/.env.example: -------------------------------------------------------------------------------- 1 | # Anthropic API Key for AI-powered skill intent analysis 2 | # Get your key from: https://console.anthropic.com/ 3 | # Required - hook will fail with helpful error if not set 4 | ANTHROPIC_API_KEY=sk-ant-your-key-here 5 | 6 | # Claude model to use for intent analysis 7 | # Optional - defaults to claude-haiku-4-5 (fast and cost-effective) 8 | # Available models: 9 | # - claude-haiku-4-5 (recommended: fast, cheap, accurate) 10 | # - claude-sonnet-4-5 (more capable, higher cost) 11 | # - claude-opus-4 (highest cost, not recommended for this use case) 12 | # CLAUDE_SKILLS_MODEL=claude-haiku-4-5 13 | -------------------------------------------------------------------------------- /.claude/hooks/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "lib": ["ES2022"], 7 | "outDir": "./dist", 8 | "rootDir": ".", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "resolveJsonModule": true, 14 | "declaration": true, 15 | "declarationMap": true, 16 | "sourceMap": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true 21 | }, 22 | "include": ["**/*.ts"], 23 | "exclude": ["node_modules", "dist", "state"] 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # TypeScript 8 | *.tsbuildinfo 9 | dist/ 10 | build/ 11 | 12 | # Environment variables 13 | .env 14 | .env.local 15 | .env.*.local 16 | 17 | # IDE 18 | .vscode/ 19 | .idea/ 20 | *.swp 21 | *.swo 22 | *~ 23 | .DS_Store 24 | 25 | # Testing 26 | coverage/ 27 | .nyc_output/ 28 | 29 | # Cache directories 30 | .cache/ 31 | *.log 32 | 33 | # Skills system state and cache 34 | .claude/hooks/state/ 35 | .cache/ 36 | .claude/hooks/skill-injection-debug.log 37 | 38 | # Build artifacts 39 | *.tgz 40 | 41 | # Python (if users add Python tooling) 42 | __pycache__/ 43 | *.py[cod] 44 | *$py.class 45 | *.so 46 | .Python 47 | venv/ 48 | env/ 49 | ENV/ 50 | 51 | # Temporary files 52 | *.tmp 53 | *.temp 54 | .tmp/ 55 | -------------------------------------------------------------------------------- /.claude/hooks/cleanup-stale-state.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Cleanup stale skill state files 3 | # Run this periodically to prevent accumulation of orphaned state files 4 | 5 | set -euo pipefail 6 | 7 | # Get project directory 8 | PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}" 9 | STATE_DIR="$PROJECT_DIR/.claude/hooks/state" 10 | 11 | # Exit if state directory doesn't exist 12 | if [ ! -d "$STATE_DIR" ]; then 13 | echo "No state directory found at: $STATE_DIR" 14 | exit 0 15 | fi 16 | 17 | # Delete files older than 7 days 18 | echo "Cleaning up stale state files (>7 days old) in: $STATE_DIR" 19 | find "$STATE_DIR" -name "*-skills-suggested.json" -type f -mtime +7 -delete 20 | 21 | # Count remaining files 22 | REMAINING=$(find "$STATE_DIR" -name "*-skills-suggested.json" -type f | wc -l | tr -d ' ') 23 | echo "Cleanup complete. Remaining state files: $REMAINING" 24 | -------------------------------------------------------------------------------- /.claude/hooks/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 2022, 5 | "sourceType": "module", 6 | "project": "./tsconfig.json" 7 | }, 8 | "plugins": ["@typescript-eslint"], 9 | "extends": [ 10 | "eslint:recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "rules": { 14 | "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], 15 | "@typescript-eslint/explicit-function-return-type": "error", 16 | "@typescript-eslint/no-explicit-any": "warn", 17 | "@typescript-eslint/no-unsafe-member-access": "off", 18 | "@typescript-eslint/no-unsafe-assignment": "off", 19 | "@typescript-eslint/no-unsafe-argument": "off", 20 | "@typescript-eslint/no-unsafe-return": "off", 21 | "@typescript-eslint/no-unsafe-call": "off", 22 | "no-console": ["warn", { "allow": ["warn", "error", "log"] }], 23 | "prefer-const": "error" 24 | }, 25 | "ignorePatterns": ["node_modules/", "*.js", "dist/"] 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Jeff Lester 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | name: Test & Type Check 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v5 17 | 18 | - name: Setup Node.js 19 | uses: actions/setup-node@v6 20 | with: 21 | node-version: '20' 22 | cache: 'npm' 23 | cache-dependency-path: '.claude/hooks/package-lock.json' 24 | 25 | - name: Install dependencies 26 | working-directory: .claude/hooks 27 | run: npm ci 28 | 29 | - name: Run type checking 30 | working-directory: .claude/hooks 31 | run: npm run check 32 | 33 | - name: Run tests 34 | working-directory: .claude/hooks 35 | run: npm test 36 | 37 | - name: Report results 38 | if: always() 39 | run: | 40 | if [ ${{ job.status }} == 'success' ]; then 41 | echo "✅ All checks passed! (118 tests)" 42 | else 43 | echo "❌ Tests failed. Review logs above." 44 | exit 1 45 | fi 46 | -------------------------------------------------------------------------------- /.claude/hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "claude-skills-hooks", 3 | "version": "1.0.0", 4 | "description": "TypeScript hooks for Claude Code skill auto-activation", 5 | "private": true, 6 | "type": "module", 7 | "scripts": { 8 | "check": "tsc --noEmit", 9 | "lint": "eslint .", 10 | "lint:fix": "eslint . --fix", 11 | "format": "prettier --write '**/*.ts'", 12 | "format:check": "prettier --check '**/*.ts'", 13 | "test": "vitest run", 14 | "test:watch": "vitest watch", 15 | "test:coverage": "vitest run --coverage", 16 | "test:integration": "tsx skill-activation-prompt.ts < test-input.json" 17 | }, 18 | "dependencies": { 19 | "@anthropic-ai/sdk": "^0.71.2", 20 | "@types/node": "^25.0.3", 21 | "tsx": "^4.21.0", 22 | "typescript": "^5.3.3" 23 | }, 24 | "devDependencies": { 25 | "@eslint/js": "^9.39.2", 26 | "@typescript-eslint/eslint-plugin": "^8.50.0", 27 | "@typescript-eslint/parser": "^8.46.4", 28 | "eslint": "^9.39.2", 29 | "globals": "^16.2.0", 30 | "prettier": "^3.7.4", 31 | "vitest": "^4.0.16" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration for automated dependency updates 2 | # Docs: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 3 | 4 | version: 2 5 | updates: 6 | # Node.js dependencies in Claude hooks 7 | - package-ecosystem: "npm" 8 | directory: "/.claude/hooks" 9 | schedule: 10 | interval: "daily" 11 | time: "09:00" 12 | timezone: "America/Los_Angeles" 13 | open-pull-requests-limit: 5 14 | # Group related updates to reduce PR noise 15 | groups: 16 | # Group all TypeScript/ESLint updates together 17 | typescript-eslint: 18 | patterns: 19 | - "@typescript-eslint/*" 20 | - "typescript" 21 | - "eslint" 22 | # Group all dev dependencies (minor/patch only) 23 | dev-dependencies: 24 | dependency-type: "development" 25 | update-types: 26 | - "minor" 27 | - "patch" 28 | # Auto-approve and enable auto-merge for Dependabot PRs 29 | # (requires GitHub Actions workflow to complete) 30 | assignees: 31 | - "jefflester" 32 | labels: 33 | - "dependencies" 34 | - "automated" 35 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Auto-merge Dependabot PRs 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | 7 | jobs: 8 | auto-merge: 9 | name: Auto-merge passing Dependabot PRs 10 | runs-on: ubuntu-latest 11 | 12 | # Only run for Dependabot PRs 13 | if: github.actor == 'dependabot[bot]' 14 | 15 | permissions: 16 | contents: write 17 | pull-requests: write 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v5 22 | 23 | - name: Wait for CI checks 24 | uses: lewagon/wait-on-check-action@v1.4.1 25 | with: 26 | ref: ${{ github.event.pull_request.head.sha }} 27 | check-name: 'Test & Type Check' 28 | repo-token: ${{ secrets.GITHUB_TOKEN }} 29 | wait-interval: 10 30 | 31 | - name: Enable auto-merge 32 | run: gh pr merge --auto --squash "${{ github.event.pull_request.number }}" 33 | env: 34 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: Log success 37 | run: | 38 | echo "✅ Auto-merge enabled for Dependabot PR #${{ github.event.pull_request.number }}" 39 | echo "PR will merge automatically once all checks pass (no approval required)" 40 | -------------------------------------------------------------------------------- /.claude/hooks/lib/__tests__/constants.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import * as constants from '../constants'; 3 | 4 | describe('constants', () => { 5 | it('should have sensible confidence thresholds', () => { 6 | expect(constants.CONFIDENCE_THRESHOLD).toBeGreaterThan(constants.SUGGESTED_THRESHOLD); 7 | expect(constants.CONFIDENCE_THRESHOLD).toBeLessThanOrEqual(1.0); 8 | expect(constants.SUGGESTED_THRESHOLD).toBeGreaterThanOrEqual(0.0); 9 | }); 10 | 11 | it('should limit skill injection counts', () => { 12 | expect(constants.MAX_REQUIRED_SKILLS).toBeGreaterThan(0); 13 | expect(constants.MAX_SUGGESTED_SKILLS).toBeGreaterThan(0); 14 | }); 15 | 16 | it('should have reasonable cache TTL', () => { 17 | expect(constants.CACHE_TTL_MS).toBeGreaterThan(0); 18 | expect(constants.CACHE_CLEANUP_AGE_MS).toBeGreaterThan(constants.CACHE_TTL_MS); 19 | }); 20 | 21 | it('should have short prompt threshold for keyword fallback', () => { 22 | expect(constants.SHORT_PROMPT_WORD_THRESHOLD).toBeGreaterThan(0); 23 | expect(constants.SHORT_PROMPT_WORD_THRESHOLD).toBeLessThan(50); 24 | }); 25 | 26 | it('should have default injection order in valid range', () => { 27 | expect(constants.DEFAULT_INJECTION_ORDER).toBeGreaterThanOrEqual(0); 28 | expect(constants.DEFAULT_INJECTION_ORDER).toBeLessThanOrEqual(100); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /.claude/commands/wrap.md: -------------------------------------------------------------------------------- 1 | ______________________________________________________________________ 2 | 3 | ## description: Wrap up current work 4 | 5 | Wrap up the current work by following the checklist below. 6 | 7 | **NOTE: If you are low on context and/or if any of the wrap tasks are easily 8 | delegated, feel free to leverage subagents to complete the task(s) to stretch 9 | your remaining context. 10 | 11 | **Checklist:** 12 | 13 | For all changes/additions made, please: 14 | 15 | - Lint the code. It should pass pre-commit checks. 16 | - Check to see if there are any related tests. If there are, update them to 17 | ensure they reflect the changes/additions made. 18 | - Check to see if there are any related docs. If there are, modify them as 19 | necessary to account for the changed/added code. 20 | - Check if any skills need updates (NOTE: all skill/resource docs should be \<= 21 | 500 LOC): 22 | - Activate the "skill-developer" skill to assist with skill maintenance. 23 | - Check that proper code reuse was employed, no DRY violations were introduced, 24 | and existing codebase patterns were followed. Be wary of magic strings/numbers 25 | and use constants/enums where appropriate. 26 | - If there are ANY actionable next steps or current problems/failures, detail 27 | them in the form of an actionable summary for the next session. 28 | 29 | When finished, provide a commit message (without any single quotes) summarizing 30 | the changes made. 31 | -------------------------------------------------------------------------------- /.claude/hooks/eslint.config.js: -------------------------------------------------------------------------------- 1 | import tseslint from '@typescript-eslint/eslint-plugin'; 2 | import tsparser from '@typescript-eslint/parser'; 3 | import js from '@eslint/js'; 4 | import globals from 'globals'; 5 | 6 | export default [ 7 | js.configs.recommended, 8 | { 9 | files: ['**/*.ts'], 10 | languageOptions: { 11 | parser: tsparser, 12 | parserOptions: { 13 | ecmaVersion: 2022, 14 | sourceType: 'module', 15 | project: './tsconfig.json', 16 | }, 17 | globals: { 18 | ...globals.node, 19 | NodeJS: 'readonly', 20 | }, 21 | }, 22 | plugins: { 23 | '@typescript-eslint': tseslint, 24 | }, 25 | rules: { 26 | ...tseslint.configs.recommended.rules, 27 | '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], 28 | '@typescript-eslint/explicit-function-return-type': 'error', 29 | '@typescript-eslint/no-explicit-any': 'warn', 30 | '@typescript-eslint/no-unsafe-member-access': 'off', 31 | '@typescript-eslint/no-unsafe-assignment': 'off', 32 | '@typescript-eslint/no-unsafe-argument': 'off', 33 | '@typescript-eslint/no-unsafe-return': 'off', 34 | '@typescript-eslint/no-unsafe-call': 'off', 35 | 'no-console': ['warn', { allow: ['warn', 'error', 'log'] }], 36 | 'prefer-const': 'error', 37 | }, 38 | }, 39 | { 40 | ignores: ['node_modules/', '*.js', 'dist/', 'eslint.config.js'], 41 | }, 42 | ]; 43 | -------------------------------------------------------------------------------- /.claude/hooks/lib/debug-logger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Debug logging for skill injection system 3 | * 4 | * Conditional logging controlled by CLAUDE_SKILLS_DEBUG environment variable. 5 | * Provides detailed trace of skill injection pipeline for troubleshooting. 6 | */ 7 | 8 | import { appendFileSync, existsSync, statSync, renameSync } from 'fs'; 9 | import { join } from 'path'; 10 | 11 | const DEBUG_SKILLS = process.env.CLAUDE_SKILLS_DEBUG === '1'; 12 | const MAX_LOG_SIZE = 10 * 1024 * 1024; // 10MB 13 | 14 | /** 15 | * Log debug message to skill injection debug log 16 | * 17 | * Only logs when CLAUDE_SKILLS_DEBUG=1 environment variable is set. 18 | * Automatically rotates log file when it exceeds 10MB. 19 | * Never throws - logging failures are caught and logged to stderr. 20 | * 21 | * @param message - Message to log 22 | */ 23 | export function debugLog(message: string): void { 24 | if (!DEBUG_SKILLS) return; 25 | 26 | try { 27 | const logPath = join( 28 | process.env.CLAUDE_PROJECT_DIR || process.cwd(), 29 | '.claude', 30 | 'hooks', 31 | 'skill-injection-debug.log' 32 | ); 33 | 34 | // Rotate log if too large 35 | if (existsSync(logPath)) { 36 | const stats = statSync(logPath); 37 | if (stats.size > MAX_LOG_SIZE) { 38 | renameSync(logPath, `${logPath}.old`); 39 | } 40 | } 41 | 42 | const timestamp = new Date().toISOString(); 43 | appendFileSync(logPath, `[${timestamp}] ${message}\n`); 44 | } catch (err) { 45 | // Silently fail - logging must never break the hook 46 | console.error('⚠️ Debug logging failed:', err); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.claude/hooks/lib/keyword-matcher.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Keyword-based skill detection fallback 3 | * 4 | * Provides simple keyword matching when AI intent analysis is unavailable 5 | * (short prompts, API errors, no API key). Checks configured keywords 6 | * against the user prompt. 7 | */ 8 | 9 | import type { SkillRule } from './types.js'; 10 | 11 | /** 12 | * Detect skills using keyword matching 13 | * 14 | * Used as fallback when AI analysis is unavailable. Checks if any skill's 15 | * configured keywords appear in the prompt (case-insensitive). 16 | * 17 | * @param prompt - The user's input prompt 18 | * @param skills - Available skills configuration 19 | * @returns Detected skills (all marked as required, none as suggested) 20 | * 21 | * @example 22 | * ```typescript 23 | * const prompt = "Fix the authentication service"; 24 | * const skills = { 25 | * 'service-layer-development': { 26 | * promptTriggers: { keywords: ['service', 'authentication'] } 27 | * } 28 | * }; 29 | * const result = matchSkillsByKeywords(prompt, skills); 30 | * // Returns: { required: ['service-layer-development'], suggested: [] } 31 | * ``` 32 | */ 33 | export function matchSkillsByKeywords( 34 | prompt: string, 35 | skills: Record 36 | ): { required: string[]; suggested: string[] } { 37 | const promptLower = prompt.toLowerCase(); 38 | const detected: string[] = []; 39 | 40 | for (const [name, config] of Object.entries(skills)) { 41 | const keywords = config.promptTriggers?.keywords || []; 42 | if (keywords.some((kw: string) => promptLower.includes(kw.toLowerCase()))) { 43 | detected.push(name); 44 | } 45 | } 46 | 47 | return { required: detected, suggested: [] }; 48 | } 49 | -------------------------------------------------------------------------------- /.claude/hooks/lib/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Centralized type definitions for the skill activation system 3 | * 4 | * This module contains all interfaces and types used across the hooks system, 5 | * providing a single source of truth for type definitions. 6 | */ 7 | 8 | /** 9 | * Skill confidence score from AI intent analysis 10 | */ 11 | export interface SkillConfidence { 12 | name: string; 13 | confidence: number; 14 | reason: string; 15 | } 16 | 17 | /** 18 | * AI intent analysis result from Anthropic API 19 | */ 20 | export interface IntentAnalysis { 21 | primary_intent: string; 22 | skills: SkillConfidence[]; 23 | } 24 | 25 | /** 26 | * Categorized analysis result with required and suggested skills 27 | */ 28 | export interface AnalysisResult { 29 | required: string[]; 30 | suggested: string[]; 31 | fromCache?: boolean; 32 | scores?: Record; // skill name -> confidence score (debug mode) 33 | } 34 | 35 | /** 36 | * Prompt trigger configuration 37 | */ 38 | export interface PromptTriggers { 39 | keywords?: string[]; 40 | } 41 | 42 | /** 43 | * Skill rule configuration from skill-rules.json 44 | */ 45 | export interface SkillRule { 46 | type: 'guardrail' | 'domain'; 47 | description?: string; 48 | autoInject?: boolean; 49 | requiredSkills?: string[]; 50 | injectionOrder?: number; 51 | promptTriggers?: PromptTriggers; 52 | affinity?: string[]; // Bidirectional complementary skills (max 2) 53 | } 54 | 55 | /** 56 | * Skill rules configuration (skill-rules.json structure) 57 | */ 58 | export interface SkillRulesConfig { 59 | version: string; 60 | skills: Record; 61 | } 62 | 63 | /** 64 | * Session state tracking acknowledged skills 65 | */ 66 | export interface SessionState { 67 | acknowledgedSkills: string[]; 68 | } 69 | 70 | /** 71 | * Cache entry for intent analysis results 72 | */ 73 | export interface CacheEntry { 74 | timestamp: number; 75 | result: { 76 | required: string[]; 77 | suggested: string[]; 78 | }; 79 | } 80 | -------------------------------------------------------------------------------- /.claude/hooks/lib/schema-validator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Runtime validation for skill-rules.json structure 3 | * 4 | * Provides basic runtime validation to catch configuration errors early. 5 | * Full schema validation happens in pre-commit hooks via Python + jsonschema. 6 | */ 7 | 8 | export interface ValidationResult { 9 | valid: boolean; 10 | errors: string[]; 11 | } 12 | 13 | /** 14 | * Validate skill-rules.json structure at runtime 15 | * 16 | * @param skillRules - The parsed skill rules configuration object 17 | * @returns Validation result with any errors found 18 | * 19 | * @example 20 | * const rules = JSON.parse(readFileSync('skill-rules.json', 'utf-8')); 21 | * const result = validateSkillRules(rules); 22 | * if (!result.valid) { 23 | * console.error('Validation errors:', result.errors); 24 | * } 25 | */ 26 | export function validateSkillRules(skillRules: any): ValidationResult { 27 | const errors: string[] = []; 28 | 29 | if (!skillRules.version) { 30 | errors.push('Missing required field: version'); 31 | } 32 | 33 | if (!skillRules.skills || typeof skillRules.skills !== 'object') { 34 | errors.push('Missing or invalid field: skills (must be object)'); 35 | return { valid: false, errors }; 36 | } 37 | 38 | for (const [skillName, config] of Object.entries(skillRules.skills)) { 39 | const skill = config as any; 40 | 41 | // Validate required fields 42 | if (!skill.type || !['guardrail', 'domain'].includes(skill.type)) { 43 | errors.push(`${skillName}: Invalid or missing 'type' (must be 'guardrail' or 'domain')`); 44 | } 45 | 46 | if (typeof skill.autoInject !== 'boolean') { 47 | errors.push(`${skillName}: Missing or invalid 'autoInject' (must be boolean)`); 48 | } 49 | 50 | // Validate affinity if present 51 | if (skill.affinity) { 52 | if (!Array.isArray(skill.affinity)) { 53 | errors.push(`${skillName}: affinity must be array`); 54 | } else if (skill.affinity.length > 2) { 55 | errors.push(`${skillName}: affinity can have max 2 skills`); 56 | } 57 | } 58 | } 59 | 60 | return { valid: errors.length === 0, errors }; 61 | } 62 | -------------------------------------------------------------------------------- /.claude/skills/skill-developer/resources/trigger-types.md: -------------------------------------------------------------------------------- 1 | # Trigger Types - Complete Guide 2 | 3 | Complete reference for configuring skill triggers in Claude Code's skill auto-activation system. 4 | 5 | ______________________________________________________________________ 6 | 7 | ## Keyword Triggers (Explicit) 8 | 9 | ### How It Works 10 | 11 | Case-insensitive substring matching in user's prompt. 12 | 13 | ### Use For 14 | 15 | Topic-based activation where user explicitly mentions the subject. 16 | 17 | ### Configuration 18 | 19 | ```json 20 | "promptTriggers": { 21 | "keywords": ["layout", "grid", "toolbar", "submission"] 22 | } 23 | ``` 24 | 25 | ### Example 26 | 27 | - User prompt: "how does the **layout** system work?" 28 | - Matches: "layout" keyword 29 | - Activates: `project-catalog-developer` 30 | 31 | ### Best Practices 32 | 33 | - Use specific, unambiguous terms 34 | - Include common variations ("layout", "layout system", "grid layout") 35 | - Avoid overly generic words ("system", "work", "create") 36 | - Test with real prompts 37 | 38 | ______________________________________________________________________ 39 | 40 | ______________________________________________________________________ 41 | 42 | File path and content triggers are not yet implemented. 43 | 44 | ______________________________________________________________________ 45 | 46 | ## Best Practices Summary 47 | 48 | ### DO: 49 | 50 | ✅ Use specific, unambiguous keywords 51 | ✅ Test all patterns with real examples 52 | ✅ Include common variations 53 | 54 | ### DON'T: 55 | 56 | ❌ Use overly generic keywords ("system", "work") 57 | 58 | ### Testing Your Triggers 59 | 60 | **Test keyword triggers:** 61 | 62 | ```bash 63 | echo '{"session_id":"test","prompt":"your test prompt"}' | \ 64 | npx tsx .claude/hooks/skill-activation-prompt.ts 65 | ``` 66 | 67 | ______________________________________________________________________ 68 | 69 | **Related Files:** 70 | 71 | - [SKILL.md](../SKILL.md) - Main skill guide 72 | - [skill-rules-reference.md](skill-rules-reference.md) - Complete skill-rules.json schema 73 | - [patterns-library.md](patterns-library.md) - Ready-to-use pattern library 74 | -------------------------------------------------------------------------------- /.claude/hooks/lib/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration constants for skill activation system 3 | * 4 | * Most thresholds can be overridden via environment variables for tuning 5 | * without code changes. See inline comments for env var names. 6 | */ 7 | 8 | // Confidence thresholds for AI-powered skill detection 9 | // Higher threshold (0.65) ensures only truly critical skills are auto-injected 10 | // Lower threshold (0.50) allows for skill suggestions without forcing injection 11 | // Override: SKILL_CONFIDENCE_THRESHOLD, SKILL_SUGGESTED_THRESHOLD 12 | export const CONFIDENCE_THRESHOLD = parseFloat( 13 | process.env.SKILL_CONFIDENCE_THRESHOLD || '0.65' 14 | ); 15 | export const SUGGESTED_THRESHOLD = parseFloat(process.env.SKILL_SUGGESTED_THRESHOLD || '0.50'); 16 | 17 | // Skill injection limits to prevent context overload 18 | // Standard limit is 2 skills - prevents overwhelming Claude with too many guidelines 19 | // Affinity skills are auto-injected free of slot cost (don't count toward limit) 20 | export const MAX_REQUIRED_SKILLS = 2; // Maximum critical skills to auto-inject 21 | export const MAX_SUGGESTED_SKILLS = 2; // Maximum recommended skills to suggest 22 | 23 | // Short prompts use keyword matching instead of AI analysis 24 | // Saves API costs and latency for simple prompts where intent is unclear 25 | // Override: SKILL_SHORT_PROMPT_WORDS (default: 6 words) 26 | export const SHORT_PROMPT_WORD_THRESHOLD = parseInt( 27 | process.env.SKILL_SHORT_PROMPT_WORDS || '6', 28 | 10 29 | ); 30 | 31 | // Cache configuration for AI intent analysis 32 | // 1 hour TTL balances freshness vs API cost (~$0.0003 per analysis) 33 | // 24 hour cleanup prevents unbounded cache growth 34 | // Override: SKILL_CACHE_TTL_MS 35 | export const CACHE_TTL_MS = parseInt( 36 | process.env.SKILL_CACHE_TTL_MS || String(60 * 60 * 1000), 37 | 10 38 | ); 39 | export const CACHE_CLEANUP_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours 40 | 41 | // Dependency resolution defaults 42 | // Skills without explicit injectionOrder use this value (mid-range 0-100) 43 | export const DEFAULT_INJECTION_ORDER = 50; 44 | 45 | // Banner formatting 46 | // Character width for visual consistency in terminal output 47 | export const BANNER_WIDTH = 45; 48 | -------------------------------------------------------------------------------- /.claude/skills/skill-rules.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "Claude Code Skill Rules Configuration", 4 | "type": "object", 5 | "required": ["version", "skills"], 6 | "properties": { 7 | "version": { 8 | "type": "string", 9 | "pattern": "^\\d+\\.\\d+$", 10 | "description": "Schema version (e.g., '1.0')" 11 | }, 12 | "skills": { 13 | "type": "object", 14 | "patternProperties": { 15 | "^[a-z][a-z0-9-]*$": { 16 | "type": "object", 17 | "required": ["type"], 18 | "properties": { 19 | "type": { 20 | "type": "string", 21 | "enum": ["guardrail", "domain"], 22 | "description": "Skill type: guardrail (blocking) or domain (guidance)" 23 | }, 24 | "autoInject": { 25 | "type": "boolean", 26 | "description": "Auto-inject skill content when triggered" 27 | }, 28 | "requiredSkills": { 29 | "type": "array", 30 | "items": { "type": "string" }, 31 | "description": "Dependencies (other skills required)" 32 | }, 33 | "injectionOrder": { 34 | "type": "number", 35 | "minimum": 0, 36 | "maximum": 100, 37 | "description": "Order for dependency injection (0-100)" 38 | }, 39 | "description": { 40 | "type": "string", 41 | "description": "Human-readable skill description" 42 | }, 43 | "promptTriggers": { 44 | "type": "object", 45 | "properties": { 46 | "keywords": { 47 | "type": "array", 48 | "items": { "type": "string" }, 49 | "description": "Keywords to match in prompts" 50 | } 51 | } 52 | }, 53 | "affinity": { 54 | "type": "array", 55 | "items": { "type": "string" }, 56 | "maxItems": 2, 57 | "description": "Bidirectional complementary skills (max 2)" 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /.claude/hooks/lib/skill-resolution.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Skill dependency resolution with cycle detection 3 | * 4 | * Resolves skill dependencies recursively using depth-first search, 5 | * detects circular dependencies, and sorts by injection order. 6 | */ 7 | 8 | import type { SkillRule } from './types.js'; 9 | 10 | /** 11 | * Resolve skill dependencies recursively with cycle detection 12 | * 13 | * Performs a depth-first traversal of skill dependencies, building a complete 14 | * set of skills to inject including all transitive dependencies. Detects 15 | * circular dependencies and sorts final result by injection order. 16 | * 17 | * @param skills - Initial set of skills to resolve 18 | * @param skillRules - Skill configuration from skill-rules.json 19 | * @returns Sorted array of skill names (dependencies first, then ordered by injectionOrder) 20 | * 21 | * @example 22 | * ```typescript 23 | * const skills = ['service-layer-development']; 24 | * const resolved = resolveSkillDependencies(skills, skillRules); 25 | * // Returns: ['api-protocols', 'service-layer-development'] 26 | * ``` 27 | */ 28 | export function resolveSkillDependencies( 29 | skills: string[], 30 | skillRules: Record 31 | ): string[] { 32 | const resolved = new Set(); 33 | const visiting = new Set(); // For cycle detection 34 | const errors: string[] = []; // Collect all errors 35 | 36 | function visit(skillName: string, path: string[] = []): void { 37 | // Cycle detection 38 | if (visiting.has(skillName)) { 39 | errors.push(`Circular dependency: ${[...path, skillName].join(' → ')}`); 40 | return; 41 | } 42 | 43 | // Already resolved 44 | if (resolved.has(skillName)) return; 45 | 46 | const skill = skillRules[skillName]; 47 | if (!skill) { 48 | errors.push(`Skill not found: ${skillName}`); 49 | return; 50 | } 51 | 52 | // Mark as visiting 53 | visiting.add(skillName); 54 | path.push(skillName); 55 | 56 | // Visit dependencies first (DFS) 57 | const deps = skill.requiredSkills || []; 58 | deps.forEach((dep) => visit(dep, [...path])); 59 | 60 | // Add to resolved 61 | resolved.add(skillName); 62 | visiting.delete(skillName); 63 | } 64 | 65 | // Visit each root skill 66 | skills.forEach((skill) => visit(skill)); 67 | 68 | // Report all errors together 69 | if (errors.length > 0) { 70 | console.error('⚠️ Skill dependency resolution errors:'); 71 | errors.forEach((err) => console.error(` - ${err}`)); 72 | } 73 | 74 | // Sort by injection order 75 | return Array.from(resolved).sort((a, b) => { 76 | const orderA = skillRules[a]?.injectionOrder || 50; 77 | const orderB = skillRules[b]?.injectionOrder || 50; 78 | return orderA - orderB; 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /.claude/skills/skill-developer/resources/hook-mechanisms.md: -------------------------------------------------------------------------------- 1 | # Hook Mechanisms - Deep Dive 2 | 3 | Technical deep dive into how the UserPromptSubmit hook works. 4 | 5 | ______________________________________________________________________ 6 | 7 | ## UserPromptSubmit Hook Flow 8 | 9 | ### Execution Sequence 10 | 11 | ``` 12 | User submits prompt 13 | ↓ 14 | skill-activation-prompt.sh executes 15 | ↓ 16 | npx tsx skill-activation-prompt.ts 17 | ↓ 18 | Hook reads stdin (JSON with prompt) 19 | ↓ 20 | Loads skill-rules.json 21 | ↓ 22 | Matches keywords + intent patterns 23 | ↓ 24 | Groups matches by priority (critical → high → medium → low) 25 | ↓ 26 | Outputs formatted message to stdout 27 | ↓ 28 | stdout becomes context for Claude (injected before prompt) 29 | ↓ 30 | Claude sees: [skill suggestion] + user's prompt 31 | ``` 32 | 33 | ### Key Points 34 | 35 | - **Exit code**: Always 0 (allow) 36 | - **stdout**: → Claude's context (injected as system message) 37 | - **Timing**: Runs BEFORE Claude processes prompt 38 | - **Behavior**: Non-blocking, advisory only 39 | - **Purpose**: Make Claude aware of relevant skills 40 | 41 | ### Input Format 42 | 43 | ```json 44 | { 45 | "session_id": "abc-123", 46 | "transcript_path": "/path/to/transcript.json", 47 | "cwd": "/root/git/your-project", 48 | "permission_mode": "normal", 49 | "hook_event_name": "UserPromptSubmit", 50 | "prompt": "how does the layout system work?" 51 | } 52 | ``` 53 | 54 | ### Output Format (to stdout) 55 | 56 | ``` 57 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 58 | 🎯 SKILL ACTIVATION CHECK 59 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 60 | 61 | 📚 RECOMMENDED SKILLS: 62 | → project-catalog-developer 63 | 64 | ACTION: Use Skill tool BEFORE responding 65 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 66 | ``` 67 | 68 | Claude sees this output as additional context before processing the user's prompt. 69 | 70 | ______________________________________________________________________ 71 | 72 | ## Performance Considerations 73 | 74 | ### Target Metrics 75 | 76 | - **UserPromptSubmit**: < 100ms 77 | 78 | ### Performance Bottlenecks 79 | 80 | 1. **Loading skill-rules.json** (every execution) 81 | 82 | - Future: Cache in memory 83 | - Future: Watch for changes, reload only when needed 84 | 85 | 1. **Regex matching** (UserPromptSubmit) 86 | 87 | - Intent patterns matching 88 | - Future: Lazy compile, cache compiled regexes 89 | 90 | ### Optimization Strategies 91 | 92 | **Reduce patterns:** 93 | 94 | - Use more specific patterns (fewer to check) 95 | - Combine similar patterns where possible 96 | 97 | ______________________________________________________________________ 98 | 99 | **Related Files:** 100 | 101 | - [SKILL.md](../SKILL.md) - Main skill guide 102 | - [skill-rules-reference.md](skill-rules-reference.md) - Configuration reference 103 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # Pre-commit hooks for Claude Skills Supercharged 2 | # Install: pip install pre-commit && pre-commit install 3 | # Run manually: pre-commit run --all-files 4 | 5 | repos: 6 | # General file checks 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: v4.5.0 9 | hooks: 10 | - id: trailing-whitespace 11 | args: [--markdown-linebreak-ext=md] 12 | - id: end-of-file-fixer 13 | - id: check-yaml 14 | args: [--unsafe] # Allow custom tags in YAML 15 | - id: check-json 16 | - id: check-merge-conflict 17 | - id: check-case-conflict 18 | - id: mixed-line-ending 19 | args: [--fix=lf] 20 | - id: detect-private-key 21 | 22 | # TypeScript/JavaScript linting (uses project's eslint config) 23 | - repo: https://github.com/pre-commit/mirrors-eslint 24 | rev: v9.8.0 25 | hooks: 26 | - id: eslint 27 | files: \.tsx?$ 28 | types: [file] 29 | additional_dependencies: 30 | - eslint@^8.0.0 31 | - typescript@^5.0.0 32 | - '@typescript-eslint/parser@^6.0.0' 33 | - '@typescript-eslint/eslint-plugin@^6.0.0' 34 | args: [--config, .claude/hooks/.eslintrc.json] 35 | 36 | # Markdown linting 37 | - repo: https://github.com/igorshubovych/markdownlint-cli 38 | rev: v0.37.0 39 | hooks: 40 | - id: markdownlint 41 | args: [--disable, MD013, MD033, MD041] # Disable line-length, HTML, first-line-heading 42 | 43 | # TypeScript type checking and tests 44 | - repo: local 45 | hooks: 46 | - id: tsc 47 | name: TypeScript type check 48 | entry: bash -c 'cd .claude/hooks && npx tsc --noEmit' 49 | language: system 50 | files: \.tsx?$ 51 | pass_filenames: false 52 | 53 | - id: typescript-tests 54 | name: Run TypeScript unit tests 55 | entry: bash -c 'cd .claude/hooks && npm test' 56 | language: system 57 | files: '^\.claude/hooks/.*\.ts$' 58 | pass_filenames: false 59 | description: Run TypeScript unit tests (fast ~130ms) 60 | 61 | # Skill file length check (500-line rule) 62 | - repo: local 63 | hooks: 64 | - id: check-skill-length 65 | name: Check skill files under 500 lines 66 | entry: bash -c 'for f in .claude/skills/*/SKILL.md; do lines=$(wc -l < "$f"); if [ $lines -gt 500 ]; then echo "ERROR - $f has $lines lines (max 500)"; exit 1; fi; done' 67 | language: system 68 | files: \.claude/skills/.*/SKILL\.md$ 69 | pass_filenames: false 70 | 71 | # Validate skill-rules.json 72 | - repo: local 73 | hooks: 74 | - id: validate-skill-rules 75 | name: Validate skill-rules.json schema 76 | entry: bash -c 'cd .claude/hooks && npx tsx -e "import {validateSkillRules} from \"./lib/schema-validator\"; import fs from \"fs\"; const rules = JSON.parse(fs.readFileSync(\"../skills/skill-rules.json\", \"utf-8\")); validateSkillRules(rules);"' 77 | language: system 78 | files: \.claude/skills/skill-rules\.json$ 79 | pass_filenames: false 80 | -------------------------------------------------------------------------------- /.claude/hooks/lib/skill-state-manager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Session state management for skill acknowledgments 3 | * 4 | * Tracks which skills have been suggested/injected in each conversation 5 | * to avoid re-suggesting the same skills repeatedly. State is persisted 6 | * per-conversation using conversation_id or session_id. 7 | */ 8 | 9 | import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'fs'; 10 | import { join } from 'path'; 11 | import type { SessionState } from './types.js'; 12 | 13 | /** 14 | * Extended session state with metadata 15 | */ 16 | interface ExtendedSessionState extends SessionState { 17 | timestamp: number; 18 | injectedSkills: string[]; 19 | injectionTimestamp: number; 20 | } 21 | 22 | /** 23 | * Read acknowledged skills from session state file 24 | * 25 | * Returns list of skills that have been suggested/injected in previous 26 | * turns of the current conversation. 27 | * 28 | * @param stateDir - State directory path (.claude/hooks/state) 29 | * @param stateId - Conversation or session ID 30 | * @returns Array of acknowledged skill names 31 | */ 32 | export function readAcknowledgedSkills(stateDir: string, stateId: string): string[] { 33 | const stateFile = join(stateDir, `${stateId}-skills-suggested.json`); 34 | 35 | if (!existsSync(stateFile)) { 36 | return []; 37 | } 38 | 39 | try { 40 | const existing: ExtendedSessionState = JSON.parse(readFileSync(stateFile, 'utf-8')); 41 | return existing.acknowledgedSkills || []; 42 | } catch { 43 | // Invalid JSON, start fresh 44 | return []; 45 | } 46 | } 47 | 48 | /** 49 | * Write session state to track acknowledged skills 50 | * 51 | * Uses atomic write pattern (write to temp file, then rename) to prevent 52 | * corruption from concurrent hook invocations. 53 | * 54 | * @param stateDir - State directory path 55 | * @param stateId - Conversation or session ID 56 | * @param acknowledgedSkills - All skills acknowledged (existing + new) 57 | * @param injectedSkills - Skills injected this turn 58 | */ 59 | export function writeSessionState( 60 | stateDir: string, 61 | stateId: string, 62 | acknowledgedSkills: string[], 63 | injectedSkills: string[] 64 | ): void { 65 | try { 66 | // Ensure state directory exists 67 | mkdirSync(stateDir, { recursive: true }); 68 | 69 | const stateFile = join(stateDir, `${stateId}-skills-suggested.json`); 70 | const tempFile = `${stateFile}.tmp`; 71 | 72 | const stateData: ExtendedSessionState = { 73 | timestamp: Date.now(), 74 | acknowledgedSkills, 75 | injectedSkills, 76 | injectionTimestamp: Date.now(), 77 | }; 78 | 79 | // Atomic write: write to temp file, then rename 80 | // This prevents corruption if multiple hooks run concurrently 81 | writeFileSync(tempFile, JSON.stringify(stateData, null, 2)); 82 | 83 | // renameSync is atomic on POSIX systems - overwrites existing file atomically 84 | renameSync(tempFile, stateFile); 85 | } catch (err) { 86 | // Don't fail the hook if state writing fails 87 | console.error('Warning: Failed to write session state:', err); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /.claude/skills/skill-rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "skills": { 4 | "skill-developer": { 5 | "type": "domain", 6 | "autoInject": true, 7 | "requiredSkills": [], 8 | "description": "Skill for creating and managing Claude Code skills. Covers skill structure, YAML frontmatter, trigger patterns, hook mechanisms (UserPromptSubmit), session tracking, 500-line rule, progressive disclosure, and relationship to CLAUDE.md. Use when creating new skills, modifying skill-rules.json, understanding triggers, debugging activation, or understanding the skills system architecture.", 9 | "promptTriggers": { 10 | "keywords": [ 11 | "Claude Code skill", 12 | "skill system", 13 | "create skill", 14 | "add skill", 15 | "skill triggers", 16 | "skill rules", 17 | "skill hook", 18 | "skill activation", 19 | "skill development", 20 | "skill-rules.json", 21 | "intent-analysis-prompt", 22 | "UserPromptSubmit hook", 23 | "progressive disclosure", 24 | "500-line rule" 25 | ] 26 | } 27 | }, 28 | "python-best-practices": { 29 | "type": "domain", 30 | "autoInject": true, 31 | "requiredSkills": [], 32 | "description": "Python development best practices including PEP 8 style guidelines, type hints, docstring conventions (NumPy format), and common patterns. Use when writing or modifying Python code.", 33 | "promptTriggers": { 34 | "keywords": [ 35 | "python", 36 | "python best practices", 37 | "PEP 8", 38 | "type hints", 39 | "docstring", 40 | "python style", 41 | "python patterns" 42 | ] 43 | } 44 | }, 45 | "git-workflow": { 46 | "type": "domain", 47 | "autoInject": true, 48 | "requiredSkills": [], 49 | "description": "Git workflow best practices including commit messages (Conventional Commits), branching strategies, pull requests, and collaboration patterns. Use when working with Git version control.", 50 | "promptTriggers": { 51 | "keywords": [ 52 | "git", 53 | "commit", 54 | "commit message", 55 | "branch", 56 | "merge", 57 | "pull request", 58 | "PR", 59 | "rebase", 60 | "git flow" 61 | ] 62 | } 63 | }, 64 | "api-security": { 65 | "type": "guardrail", 66 | "autoInject": true, 67 | "requiredSkills": [], 68 | "description": "API security best practices and common vulnerability prevention. Enforces security checks for authentication, input validation, SQL injection, XSS, and OWASP Top 10 vulnerabilities. Use when building or modifying APIs.", 69 | "promptTriggers": { 70 | "keywords": [ 71 | "api", 72 | "endpoint", 73 | "route", 74 | "authentication", 75 | "authorization", 76 | "security", 77 | "SQL injection", 78 | "XSS", 79 | "CORS", 80 | "API security" 81 | ] 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /.claude/hooks/lib/intent-analyzer.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Intent analysis orchestrator 4 | * 5 | * Coordinates AI-powered intent analysis using modular components: 6 | * - Anthropic API client for skill scoring 7 | * - Cache manager for result persistence 8 | * - Keyword matcher for fallback 9 | * - Intent scorer for categorization 10 | */ 11 | 12 | import { createHash } from 'crypto'; 13 | import { SHORT_PROMPT_WORD_THRESHOLD } from './constants.js'; 14 | import { readCache, writeCache } from './cache-manager.js'; 15 | import { callAnthropicAPI } from './anthropic-client.js'; 16 | import { matchSkillsByKeywords } from './keyword-matcher.js'; 17 | import { categorizeSkills, formatDebugOutput, buildAnalysisResult } from './intent-scorer.js'; 18 | import type { AnalysisResult, SkillRule } from './types.js'; 19 | 20 | // Re-export types for backward compatibility 21 | export type { SkillConfidence, IntentAnalysis, AnalysisResult } from './types.js'; 22 | 23 | /** 24 | * Analyzes user intent using AI to determine relevant skills 25 | * 26 | * Uses Claude Haiku 4.5 to analyze the user's prompt and assign confidence 27 | * scores to each skill. Falls back to keyword matching for short prompts 28 | * (<10 words) or if AI analysis fails. Results are cached for 1 hour. 29 | * 30 | * @param prompt - The user's input prompt to analyze 31 | * @param availableSkills - Record of skill configurations from skill-rules.json 32 | * @returns Promise resolving to required/suggested skill lists with optional scores 33 | * 34 | * @example 35 | * const result = await analyzeIntent("Fix authentication service", skillRules); 36 | * // Returns: { required: ['service-layer-development'], suggested: [], fromCache: false } 37 | */ 38 | export async function analyzeIntent( 39 | prompt: string, 40 | availableSkills: Record 41 | ): Promise { 42 | // Skip AI analysis for short prompts (saves API calls) 43 | const wordCount = prompt.trim().split(/\s+/).length; 44 | if (wordCount <= SHORT_PROMPT_WORD_THRESHOLD) { 45 | return matchSkillsByKeywords(prompt, availableSkills); 46 | } 47 | 48 | // Check cache first - include skills hash to invalidate when definitions change 49 | const skillsHash = createHash('md5') 50 | .update(JSON.stringify(availableSkills)) 51 | .digest('hex') 52 | .substring(0, 8); 53 | const cacheKey = createHash('md5') 54 | .update(prompt + skillsHash) 55 | .digest('hex'); 56 | 57 | const cached = readCache(cacheKey); 58 | if (cached) { 59 | return { ...cached, fromCache: true }; 60 | } 61 | 62 | // Call Anthropic API 63 | try { 64 | const analysis = await callAnthropicAPI(prompt, availableSkills); 65 | 66 | // Debug logging 67 | if (process.env.CLAUDE_SKILL_DEBUG === 'true') { 68 | formatDebugOutput(analysis); 69 | } 70 | 71 | // Categorize by confidence thresholds 72 | const categorized = categorizeSkills(analysis); 73 | 74 | // Build result with optional debug scores 75 | const result = buildAnalysisResult( 76 | categorized, 77 | analysis, 78 | process.env.CLAUDE_SKILL_DEBUG === 'true' 79 | ); 80 | 81 | writeCache(cacheKey, { required: result.required, suggested: result.suggested }); 82 | return result; 83 | } catch (error) { 84 | console.warn('Intent analysis failed, falling back to keyword matching:', error); 85 | return matchSkillsByKeywords(prompt, availableSkills); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /.claude/hooks/lib/cache-manager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Cache management for intent analysis results 3 | * 4 | * Provides an LRU-style cache with automatic cleanup of stale entries. 5 | * Results are cached based on prompt + skills hash to invalidate when 6 | * skill definitions change. 7 | */ 8 | 9 | import { 10 | readFileSync, 11 | writeFileSync, 12 | existsSync, 13 | mkdirSync, 14 | readdirSync, 15 | unlinkSync, 16 | statSync, 17 | } from 'fs'; 18 | import { join } from 'path'; 19 | import { CACHE_TTL_MS, CACHE_CLEANUP_AGE_MS } from './constants.js'; 20 | import type { CacheEntry } from './types.js'; 21 | 22 | // Use project root for cache directory, not hooks cwd 23 | const CACHE_DIR = join( 24 | process.env.CLAUDE_PROJECT_DIR || process.cwd(), 25 | '.cache', 26 | 'intent-analysis' 27 | ); 28 | 29 | /** 30 | * Read cached intent analysis result 31 | * 32 | * @param key - MD5 hash of prompt + skills configuration 33 | * @returns Cached result if found and not expired, null otherwise 34 | */ 35 | export function readCache(key: string): { required: string[]; suggested: string[] } | null { 36 | const cachePath = join(CACHE_DIR, `${key}.json`); 37 | if (!existsSync(cachePath)) { 38 | return null; 39 | } 40 | 41 | try { 42 | const data: CacheEntry = JSON.parse(readFileSync(cachePath, 'utf-8')); 43 | const age = Date.now() - data.timestamp; 44 | 45 | if (age > CACHE_TTL_MS) { 46 | return null; // Expired 47 | } 48 | 49 | return data.result; 50 | } catch { 51 | return null; 52 | } 53 | } 54 | 55 | /** 56 | * Write intent analysis result to cache 57 | * 58 | * Automatically cleans up cache entries older than 24 hours to prevent unbounded growth. 59 | * 60 | * @param key - MD5 hash of prompt + skills configuration 61 | * @param result - Analysis result to cache 62 | */ 63 | export function writeCache(key: string, result: { required: string[]; suggested: string[] }): void { 64 | // Ensure cache directory exists 65 | if (!existsSync(CACHE_DIR)) { 66 | mkdirSync(CACHE_DIR, { recursive: true }); 67 | } 68 | 69 | // Cleanup old cache entries (>24 hours) 70 | cleanupOldCacheEntries(); 71 | 72 | const cachePath = join(CACHE_DIR, `${key}.json`); 73 | const entry: CacheEntry = { 74 | timestamp: Date.now(), 75 | result, 76 | }; 77 | 78 | writeFileSync(cachePath, JSON.stringify(entry)); 79 | } 80 | 81 | /** 82 | * Remove cache entries older than 24 hours 83 | * 84 | * Runs automatically during writeCache to prevent unbounded cache growth. 85 | * Failures are logged in debug mode but don't fail the operation. 86 | */ 87 | function cleanupOldCacheEntries(): void { 88 | try { 89 | if (!existsSync(CACHE_DIR)) { 90 | return; 91 | } 92 | 93 | const files = readdirSync(CACHE_DIR); 94 | const now = Date.now(); 95 | 96 | files.forEach((file) => { 97 | const filePath = join(CACHE_DIR, file); 98 | try { 99 | const stats = statSync(filePath); 100 | const age = now - stats.mtimeMs; 101 | 102 | if (age > CACHE_CLEANUP_AGE_MS) { 103 | unlinkSync(filePath); 104 | } 105 | } catch (err) { 106 | // Log in debug mode for troubleshooting 107 | if (process.env.CLAUDE_SKILL_DEBUG === 'true') { 108 | console.warn(`Cache cleanup: failed to process ${file}:`, err); 109 | } 110 | } 111 | }); 112 | } catch (err) { 113 | // Log directory-level errors in debug mode 114 | if (process.env.CLAUDE_SKILL_DEBUG === 'true') { 115 | console.warn('Cache cleanup failed:', err); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /.claude/hooks/lib/intent-scorer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Intent analysis result scoring and categorization 3 | * 4 | * Categorizes skills by confidence thresholds (required vs suggested) 5 | * and provides debug output formatting for AI analysis results. 6 | */ 7 | 8 | import { 9 | CONFIDENCE_THRESHOLD, 10 | SUGGESTED_THRESHOLD, 11 | MAX_REQUIRED_SKILLS, 12 | MAX_SUGGESTED_SKILLS, 13 | } from './constants.js'; 14 | import type { IntentAnalysis, AnalysisResult } from './types.js'; 15 | 16 | /** 17 | * Categorize skills by confidence thresholds 18 | * 19 | * Sorts skills into required (>0.65) and suggested (0.50-0.65) tiers, 20 | * limiting each to max counts. 21 | * 22 | * @param analysis - Raw intent analysis from AI 23 | * @returns Categorized result with required and suggested skills 24 | */ 25 | export function categorizeSkills(analysis: IntentAnalysis): AnalysisResult { 26 | // Validate input - guard against malformed API responses 27 | if (!Array.isArray(analysis.skills)) { 28 | return { required: [], suggested: [] }; 29 | } 30 | 31 | const required = analysis.skills 32 | .filter((s) => s.confidence > CONFIDENCE_THRESHOLD) 33 | .sort((a, b) => b.confidence - a.confidence) 34 | .slice(0, MAX_REQUIRED_SKILLS) 35 | .map((s) => s.name); 36 | 37 | const suggested = analysis.skills 38 | .filter((s) => s.confidence >= SUGGESTED_THRESHOLD && s.confidence <= CONFIDENCE_THRESHOLD) 39 | .sort((a, b) => b.confidence - a.confidence) 40 | .slice(0, MAX_SUGGESTED_SKILLS) 41 | .map((s) => s.name); 42 | 43 | return { required, suggested }; 44 | } 45 | 46 | /** 47 | * Format debug output for AI intent analysis 48 | * 49 | * Displays primary intent, all scored skills with tiers (REQUIRED/SUGGESTED/LOW), 50 | * and AI reasoning for each skill. 51 | * 52 | * @param analysis - Intent analysis result from AI 53 | */ 54 | export function formatDebugOutput(analysis: IntentAnalysis): void { 55 | console.error('\n━━━━━━ AI INTENT ANALYSIS DEBUG ━━━━━━'); 56 | console.error(`Primary Intent: ${analysis.primary_intent}`); 57 | console.error('\nAll Skills Scored:'); 58 | 59 | analysis.skills 60 | .sort((a, b) => b.confidence - a.confidence) 61 | .forEach((skill) => { 62 | const tier = 63 | skill.confidence > CONFIDENCE_THRESHOLD 64 | ? 'REQUIRED' 65 | : skill.confidence >= SUGGESTED_THRESHOLD 66 | ? 'SUGGESTED' 67 | : 'LOW'; 68 | console.error(` ${skill.name.padEnd(25)} ${skill.confidence.toFixed(2)} [${tier}]`); 69 | console.error(` → ${skill.reason}`); 70 | }); 71 | 72 | console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); 73 | } 74 | 75 | /** 76 | * Build analysis result with optional debug scores 77 | * 78 | * Creates final result object, optionally including confidence scores 79 | * when debug mode is enabled. 80 | * 81 | * @param categorized - Categorized skills (required/suggested) 82 | * @param analysis - Original analysis with confidence scores 83 | * @param includeScores - Whether to include debug scores 84 | * @returns Analysis result with optional scores 85 | */ 86 | export function buildAnalysisResult( 87 | categorized: AnalysisResult, 88 | analysis: IntentAnalysis, 89 | includeScores: boolean 90 | ): AnalysisResult { 91 | const result: AnalysisResult = { 92 | required: categorized.required, 93 | suggested: categorized.suggested, 94 | }; 95 | 96 | if (includeScores) { 97 | result.scores = {}; 98 | analysis.skills.forEach((skill) => { 99 | result.scores![skill.name] = skill.confidence; 100 | }); 101 | } 102 | 103 | return result; 104 | } 105 | -------------------------------------------------------------------------------- /.claude/hooks/lib/__tests__/schema-validator.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { validateSkillRules } from '../schema-validator'; 3 | 4 | describe('validateSkillRules', () => { 5 | it('should accept valid skill rules', () => { 6 | const validRules = { 7 | version: '1.0', 8 | skills: { 9 | 'test-skill': { 10 | type: 'domain', 11 | enforcement: 'suggest', 12 | priority: 'high', 13 | autoInject: true, 14 | requiredSkills: [], 15 | }, 16 | }, 17 | }; 18 | 19 | const result = validateSkillRules(validRules); 20 | expect(result.valid).toBe(true); 21 | expect(result.errors).toHaveLength(0); 22 | }); 23 | 24 | it('should reject missing version', () => { 25 | const invalid = { 26 | skills: {}, 27 | }; 28 | 29 | const result = validateSkillRules(invalid); 30 | expect(result.valid).toBe(false); 31 | expect(result.errors).toContain('Missing required field: version'); 32 | }); 33 | 34 | it('should reject missing skills object', () => { 35 | const invalid = { 36 | version: '1.0', 37 | }; 38 | 39 | const result = validateSkillRules(invalid); 40 | expect(result.valid).toBe(false); 41 | expect(result.errors.some((e) => e.includes('skills'))).toBe(true); 42 | }); 43 | 44 | it('should reject invalid skill type', () => { 45 | const invalid = { 46 | version: '1.0', 47 | skills: { 48 | 'bad-skill': { 49 | type: 'invalid', 50 | enforcement: 'suggest', 51 | priority: 'high', 52 | autoInject: true, 53 | }, 54 | }, 55 | }; 56 | 57 | const result = validateSkillRules(invalid); 58 | expect(result.valid).toBe(false); 59 | expect(result.errors.some((e) => e.includes('bad-skill'))).toBe(true); 60 | expect(result.errors.some((e) => e.includes('type'))).toBe(true); 61 | }); 62 | 63 | it('should reject missing autoInject field', () => { 64 | const invalid = { 65 | version: '1.0', 66 | skills: { 67 | 'test-skill': { 68 | type: 'domain', 69 | enforcement: 'suggest', 70 | priority: 'high', 71 | }, 72 | }, 73 | }; 74 | 75 | const result = validateSkillRules(invalid); 76 | expect(result.valid).toBe(false); 77 | expect(result.errors.some((e) => e.includes('autoInject'))).toBe(true); 78 | }); 79 | 80 | it('should validate affinity configuration', () => { 81 | const withAffinity = { 82 | version: '1.0', 83 | skills: { 84 | 'skill-with-affinity': { 85 | type: 'domain', 86 | enforcement: 'suggest', 87 | priority: 'high', 88 | autoInject: true, 89 | affinity: ['other-skill'], 90 | }, 91 | }, 92 | }; 93 | 94 | const result = validateSkillRules(withAffinity); 95 | expect(result.valid).toBe(true); 96 | }); 97 | 98 | it('should reject non-array affinity', () => { 99 | const invalid = { 100 | version: '1.0', 101 | skills: { 102 | 'test-skill': { 103 | type: 'domain', 104 | enforcement: 'suggest', 105 | priority: 'high', 106 | autoInject: true, 107 | affinity: 'not-an-array', 108 | }, 109 | }, 110 | }; 111 | 112 | const result = validateSkillRules(invalid); 113 | expect(result.valid).toBe(false); 114 | expect(result.errors.some((e) => e.includes('affinity must be array'))).toBe(true); 115 | }); 116 | 117 | it('should reject affinity with more than 2 skills', () => { 118 | const invalid = { 119 | version: '1.0', 120 | skills: { 121 | 'test-skill': { 122 | type: 'domain', 123 | enforcement: 'suggest', 124 | priority: 'high', 125 | autoInject: true, 126 | affinity: ['skill-1', 'skill-2', 'skill-3'], 127 | }, 128 | }, 129 | }; 130 | 131 | const result = validateSkillRules(invalid); 132 | expect(result.valid).toBe(false); 133 | expect(result.errors.some((e) => e.includes('affinity can have max 2 skills'))).toBe(true); 134 | }); 135 | }); 136 | -------------------------------------------------------------------------------- /.claude/hooks/config/intent-analysis-prompt.txt: -------------------------------------------------------------------------------- 1 | Analyze this user prompt and determine which domain skills are relevant for the PRIMARY task. 2 | 3 | User prompt: "{{USER_PROMPT}}" 4 | 5 | Available skills: 6 | {{SKILL_DESCRIPTIONS}} 7 | 8 | IMPORTANT SCORING GUIDANCE: 9 | Confidence Thresholds: 10 | - > 0.65: REQUIRED (auto-injected as critical skill) 11 | - 0.50 to 0.65: SUGGESTED (recommended but not auto-injected) 12 | - < 0.50: IGNORED (not considered relevant) 13 | 14 | Scoring examples (not separate tiers): 15 | - 0.85-0.95: User is DIRECTLY working on this domain (editing code, implementing features) 16 | - 0.72-0.75: User needs this domain knowledge to complete their primary task 17 | - 0.58-0.65: Domain is related and helpful but not strictly essential 18 | - 0.42-0.50: Domain mentioned in passing or as background context 19 | - 0.0-0.30: Not relevant to primary task 20 | 21 | EXAMPLES OF WHAT NOT TO SUGGEST: 22 | - "Investigate the code" → Do NOT suggest troubleshooting (researching code ≠ debugging runtime issues) 23 | - "Read the logs" → Do NOT suggest troubleshooting if examining config/code (not live debugging) 24 | - "Check the test file" → Do NOT suggest testing-strategy unless writing/running tests 25 | - "Look at component code" → Do NOT suggest code-patterns unless modifying components 26 | - "How does X work?" → Research/understanding ≠ implementing features 27 | 28 | CRITICAL SELECTION RULES: 29 | - You MUST select ONLY the TOP 2 most relevant skills for the primary task (confidence > 0.65) 30 | - Be extremely selective - prioritize the 2 skills most essential to completing the main action 31 | - Additional related skills should be marked 0.5-0.7 (suggested, not critical) 32 | - If no skills are truly essential, return 0 critical skills 33 | - Focus on MAIN ACTION, not tangential mentions 34 | 35 | MULTI-DOMAIN WORK DETECTION: 36 | - If the prompt involves ACTIVE work across multiple domains, score both high (0.75-0.90) 37 | Examples: 38 | * "Modify the frontend component's API integration" → frontend-framework (0.90) + api-protocols (0.85) 39 | * "Write tests for the integration layer" → testing-strategy (0.90) + system-architecture (0.85) 40 | * "Fix the CI workflow that builds components" → deployment-automation (0.90) + frontend-framework (0.75) 41 | 42 | - If the prompt is META-LEVEL (testing/checking/understanding the system itself), deprioritize: 43 | Examples: 44 | * "Skill system check: frontend, backend, testing" → Likely troubleshooting/research, not active work 45 | * "How does the component system work?" → Research, not implementation (score 0.50-0.65) 46 | * "Check if the integration tools are working" → troubleshooting (0.75), not integration-tools (0.55) 47 | 48 | - KEYWORD SOUP (listing many domains without clear action) should score lower (0.50-0.65): 49 | Examples: 50 | * "Review: frontend, backend, testing, deployment, database" → Ambiguous intent, likely research (all 0.50-0.60) 51 | * Exception: If context suggests active work across domains, score higher 52 | 53 | SKILL-DEVELOPER DETECTION (Skills System Itself): 54 | - The skill-developer skill should score REQUIRED (> 0.65) for prompts about the SKILLS SYSTEM ITSELF 55 | - Questions about HOW the skills system works, HOW skills are loaded, or MODIFYING skill configuration 56 | Examples of REQUIRED relevance (0.80-0.95): 57 | * "How does the skills activation hook work?" → skill-developer (0.92) 58 | * "Explain the intent analysis prompt" → skill-developer (0.88) 59 | * "Why isn't skill X loading?" → skill-developer (0.85) 60 | * "How are skills detected and injected?" → skill-developer (0.90) 61 | * "Modify skill-rules.json" → skill-developer (0.88) 62 | * "Add a new skill trigger pattern" → skill-developer (0.87) 63 | * "Debug the UserPromptSubmit hook" → skill-developer (0.82) 64 | * "Explain how intent patterns work" → skill-developer (0.84) 65 | 66 | - Do NOT score skill-developer high (> 0.65) for: 67 | * Using existing domain skills (frontend-framework, testing-strategy, etc.) → (0.15-0.35) 68 | * General questions about components, integration, testing, etc. → Use domain-specific skills instead 69 | * Working with actual code (not skill system code) → (0.10-0.30) 70 | 71 | Return JSON with confidence scores (0.0-1.0) for each skill's relevance to the PRIMARY task intent. 72 | Only include skills with confidence >= 0.50 (SUGGESTED threshold or higher). 73 | 74 | Response format: 75 | { 76 | "primary_intent": "brief description of main task", 77 | "skills": [ 78 | {"name": "skill-name", "confidence": 0.95, "reason": "why this skill is relevant"} 79 | ] 80 | } 81 | -------------------------------------------------------------------------------- /.claude/hooks/lib/__tests__/cache.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; 2 | import { createHash } from 'crypto'; 3 | import { CACHE_TTL_MS } from '../constants'; 4 | 5 | /** 6 | * Tests for intent analysis caching logic 7 | * 8 | * Mirrors the caching algorithm in intent-analyzer.ts 9 | */ 10 | 11 | interface CacheEntry { 12 | timestamp: number; 13 | result: { 14 | required: string[]; 15 | suggested: string[]; 16 | }; 17 | } 18 | 19 | /** 20 | * In-memory cache simulation for testing 21 | */ 22 | class TestCache { 23 | private cache: Map = new Map(); 24 | 25 | read(key: string): { required: string[]; suggested: string[] } | null { 26 | const entry = this.cache.get(key); 27 | if (!entry) return null; 28 | 29 | const age = Date.now() - entry.timestamp; 30 | if (age > CACHE_TTL_MS) { 31 | return null; // Expired 32 | } 33 | 34 | return entry.result; 35 | } 36 | 37 | write(key: string, result: { required: string[]; suggested: string[] }): void { 38 | this.cache.set(key, { 39 | timestamp: Date.now(), 40 | result, 41 | }); 42 | } 43 | 44 | clear(): void { 45 | this.cache.clear(); 46 | } 47 | } 48 | 49 | /** 50 | * Generate cache key with skills hash 51 | * (Mirrors intent-analyzer.ts lines 86-97) 52 | */ 53 | function generateCacheKey(prompt: string, skills: Record): string { 54 | const skillsHash = createHash('md5').update(JSON.stringify(skills)).digest('hex').substring(0, 8); 55 | 56 | return createHash('md5') 57 | .update(prompt + skillsHash) 58 | .digest('hex'); 59 | } 60 | 61 | describe('Cache Logic', () => { 62 | let testCache: TestCache; 63 | let originalDateNow: () => number; 64 | 65 | beforeEach(() => { 66 | testCache = new TestCache(); 67 | originalDateNow = Date.now; 68 | }); 69 | 70 | afterEach(() => { 71 | Date.now = originalDateNow; 72 | }); 73 | 74 | it('should return cached result if not expired', () => { 75 | const prompt = 'Fix component bug'; 76 | const skills = { 'component-development': { keywords: ['component'] } }; 77 | const cacheKey = generateCacheKey(prompt, skills); 78 | const mockResult = { required: ['component-development'], suggested: [] }; 79 | 80 | // Write to cache 81 | testCache.write(cacheKey, mockResult); 82 | 83 | // Read from cache (should hit) 84 | const cached = testCache.read(cacheKey); 85 | 86 | expect(cached).toEqual(mockResult); 87 | }); 88 | 89 | it('should return null for expired cache entries', () => { 90 | let fakeTime = Date.now(); 91 | Date.now = vi.fn(() => fakeTime); 92 | 93 | const prompt = 'Test prompt'; 94 | const skills = { 'test-skill': { keywords: ['test'] } }; 95 | const cacheKey = generateCacheKey(prompt, skills); 96 | const mockResult = { required: ['test-skill'], suggested: [] }; 97 | 98 | // Write to cache at time T 99 | testCache.write(cacheKey, mockResult); 100 | 101 | // Advance time beyond TTL 102 | fakeTime += CACHE_TTL_MS + 1000; 103 | 104 | // Read from cache (should miss - expired) 105 | const cached = testCache.read(cacheKey); 106 | 107 | expect(cached).toBeNull(); 108 | }); 109 | 110 | it('should invalidate cache when skill configuration changes', () => { 111 | const prompt = 'Fix component'; 112 | const skills1 = { 113 | 'component-development': { 114 | keywords: ['component'], 115 | description: 'Version 1', 116 | }, 117 | }; 118 | const skills2 = { 119 | 'component-development': { 120 | keywords: ['component'], 121 | description: 'Version 2', // Changed description 122 | }, 123 | }; 124 | 125 | const key1 = generateCacheKey(prompt, skills1); 126 | const key2 = generateCacheKey(prompt, skills2); 127 | 128 | // Different skills = different keys 129 | expect(key1).not.toEqual(key2); 130 | }); 131 | 132 | it('should return null for non-existent cache entries', () => { 133 | const nonExistentKey = 'definitely-not-in-cache'; 134 | 135 | const cached = testCache.read(nonExistentKey); 136 | 137 | expect(cached).toBeNull(); 138 | }); 139 | 140 | it('should handle malformed cache data gracefully', () => { 141 | // This test simulates what happens when cache JSON is corrupted 142 | // In the real implementation, readCache has try-catch that returns null 143 | 144 | const prompt = 'Test'; 145 | const skills = { test: {} }; 146 | const key = generateCacheKey(prompt, skills); 147 | 148 | // Simulating the behavior - corrupted data returns null 149 | const result = testCache.read(key); 150 | 151 | // Non-existent key returns null (graceful handling) 152 | expect(result).toBeNull(); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /.claude/hooks/lib/anthropic-client.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Anthropic API client for intent analysis 3 | * 4 | * Handles communication with Claude API for AI-powered skill intent analysis. 5 | * Includes prompt template loading, API calls, and response parsing. 6 | */ 7 | 8 | import Anthropic from '@anthropic-ai/sdk'; 9 | import { readFileSync } from 'fs'; 10 | import { join } from 'path'; 11 | import type { IntentAnalysis, SkillRule } from './types.js'; 12 | 13 | // Load intent analysis prompt template 14 | const INTENT_PROMPT_TEMPLATE = readFileSync( 15 | join( 16 | process.env.CLAUDE_PROJECT_DIR || process.cwd(), 17 | '.claude', 18 | 'hooks', 19 | 'config', 20 | 'intent-analysis-prompt.txt' 21 | ), 22 | 'utf-8' 23 | ); 24 | 25 | /** 26 | * Call Anthropic API for AI-powered intent analysis 27 | * 28 | * Uses Claude to analyze user prompts and determine skill relevance. 29 | * Model is configurable via CLAUDE_SKILLS_MODEL env var (defaults to claude-haiku-4-5). 30 | * Applies template substitutions and parses JSON response. 31 | * 32 | * @param prompt - The user's input prompt to analyze 33 | * @param skills - Available skills configuration from skill-rules.json 34 | * @returns Parsed intent analysis with skill confidence scores 35 | * @throws Error if ANTHROPIC_API_KEY is not configured or API call fails 36 | * 37 | * @example 38 | * ```typescript 39 | * const analysis = await callAnthropicAPI("Fix the authentication service", skillRules); 40 | * // Returns: { primary_intent: "...", skills: [{ name: "service-layer-development", confidence: 0.90, ...}] } 41 | * ``` 42 | */ 43 | export async function callAnthropicAPI( 44 | prompt: string, 45 | skills: Record 46 | ): Promise { 47 | const apiKey = process.env.ANTHROPIC_API_KEY; 48 | 49 | if (!apiKey) { 50 | throw new Error( 51 | '\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' + 52 | '❌ ANTHROPIC_API_KEY not found\n' + 53 | '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n' + 54 | 'AI-powered skill intent analysis requires an Anthropic API key.\n\n' + 55 | 'Setup instructions:\n' + 56 | '1. Go to https://console.anthropic.com/\n' + 57 | '2. Navigate to API Keys section\n' + 58 | '3. Create a new API key\n' + 59 | '4. Create .claude/hooks/.env file:\n' + 60 | ' cp .claude/hooks/.env.example .claude/hooks/.env\n' + 61 | '5. Add your key:\n' + 62 | ' ANTHROPIC_API_KEY=sk-ant-your-key-here\n\n' + 63 | 'Cost: ~$0.0003 per analysis (~$1/month at 100 prompts/day)\n' + 64 | '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' 65 | ); 66 | } 67 | 68 | const client = new Anthropic({ apiKey }); 69 | 70 | // Build skill descriptions for prompt 71 | const skillDescriptions = Object.entries(skills) 72 | .map(([name, config]) => `- ${name}: ${config.description || 'No description'}`) 73 | .join('\n'); 74 | 75 | // Apply template substitutions 76 | const analysisPrompt = INTENT_PROMPT_TEMPLATE.replace('{{USER_PROMPT}}', prompt).replace( 77 | '{{SKILL_DESCRIPTIONS}}', 78 | skillDescriptions 79 | ); 80 | 81 | // Call Claude API (model configurable via CLAUDE_SKILLS_MODEL env var) 82 | const model = process.env.CLAUDE_SKILLS_MODEL || 'claude-haiku-4-5'; 83 | const response = await client.messages.create({ 84 | model, 85 | max_tokens: 500, 86 | temperature: 0.1, 87 | messages: [ 88 | { 89 | role: 'user', 90 | content: analysisPrompt, 91 | }, 92 | ], 93 | }); 94 | 95 | // Extract text content 96 | const content = response.content[0]; 97 | if (content.type !== 'text') { 98 | throw new Error('Unexpected response type from Anthropic API'); 99 | } 100 | 101 | // Parse JSON response (with markdown fence handling) 102 | return parseApiResponse(content.text); 103 | } 104 | 105 | /** 106 | * Parse Anthropic API response text to IntentAnalysis 107 | * 108 | * Handles JSON responses that may be wrapped in markdown code fences. 109 | * Extracts JSON object even if surrounded by extra text. 110 | * 111 | * @param responseText - Raw text response from API 112 | * @returns Parsed intent analysis 113 | * @throws Error if JSON parsing fails 114 | */ 115 | function parseApiResponse(responseText: string): IntentAnalysis { 116 | let jsonText = responseText.trim(); 117 | 118 | // Strip markdown code fences if present (```json ... ```) 119 | if (jsonText.startsWith('```')) { 120 | // Remove opening fence (```json or ```JSON or just ```) 121 | jsonText = jsonText.replace(/^```(?:json|JSON)?\s*\n/, ''); 122 | // Remove closing fence and anything after it 123 | jsonText = jsonText.replace(/\n```.*$/s, ''); 124 | } 125 | 126 | // Find the JSON object boundaries (handles extra text before/after) 127 | const jsonMatch = jsonText.match(/\{[\s\S]*\}/); 128 | if (jsonMatch) { 129 | jsonText = jsonMatch[0]; 130 | } 131 | 132 | return JSON.parse(jsonText); 133 | } 134 | -------------------------------------------------------------------------------- /.claude/hooks/lib/__tests__/keyword-fallback.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { SHORT_PROMPT_WORD_THRESHOLD } from '../constants'; 3 | 4 | /** 5 | * Tests for keyword fallback logic 6 | * 7 | * Mirrors the algorithm in intent-analyzer.ts lines 314-329 8 | */ 9 | 10 | interface PromptTriggers { 11 | keywords?: string[]; 12 | } 13 | 14 | interface SkillConfig { 15 | promptTriggers?: PromptTriggers; 16 | } 17 | 18 | /** 19 | * Fallback skill detection using simple keyword matching 20 | * (Mirrors intent-analyzer.ts lines 314-329) 21 | */ 22 | function fallbackToKeywords( 23 | prompt: string, 24 | skills: Record 25 | ): { required: string[]; suggested: string[] } { 26 | const promptLower = prompt.toLowerCase(); 27 | const detected: string[] = []; 28 | 29 | for (const [name, config] of Object.entries(skills)) { 30 | const keywords = config.promptTriggers?.keywords || []; 31 | if (keywords.some((kw: string) => promptLower.includes(kw.toLowerCase()))) { 32 | detected.push(name); 33 | } 34 | } 35 | 36 | return { required: detected, suggested: [] }; 37 | } 38 | 39 | /** 40 | * Check if prompt should use keyword fallback 41 | */ 42 | function shouldUseKeywordFallback(prompt: string): boolean { 43 | const wordCount = prompt.trim().split(/\s+/).length; 44 | return wordCount <= SHORT_PROMPT_WORD_THRESHOLD; 45 | } 46 | 47 | describe('Keyword Fallback Logic', () => { 48 | it('should use keyword matching for short prompts', () => { 49 | const shortPrompt = 'fix component bug'; // 3 words < threshold 50 | 51 | const shouldUse = shouldUseKeywordFallback(shortPrompt); 52 | 53 | expect(shouldUse).toBe(true); 54 | }); 55 | 56 | it('should NOT use keyword matching for long prompts', () => { 57 | const longPrompt = 58 | 'Please help me fix the component bug that is causing issues with the API integration'; 59 | 60 | const shouldUse = shouldUseKeywordFallback(longPrompt); 61 | 62 | expect(shouldUse).toBe(false); 63 | }); 64 | 65 | it('should perform case-insensitive keyword matching', () => { 66 | const prompt = 'FIX THE COMPONENT BUG'; 67 | const skills = { 68 | 'frontend-framework': { 69 | promptTriggers: { 70 | keywords: ['component', 'debug component'], 71 | }, 72 | }, 73 | }; 74 | 75 | const result = fallbackToKeywords(prompt, skills); 76 | 77 | expect(result.required).toContain('frontend-framework'); 78 | }); 79 | 80 | it('should detect multiple keywords triggering same skill', () => { 81 | const prompt = 'debug component issue'; 82 | const skills = { 83 | 'frontend-framework': { 84 | promptTriggers: { 85 | keywords: ['component', 'debug component', 'React'], 86 | }, 87 | }, 88 | }; 89 | 90 | const result = fallbackToKeywords(prompt, skills); 91 | 92 | // Both 'component' and 'debug component' match, but skill only added once 93 | expect(result.required).toEqual(['frontend-framework']); 94 | }); 95 | 96 | it('should match partial keywords (substring includes)', () => { 97 | const prompt = 'debugging components for Python'; 98 | const skills = { 99 | 'frontend-framework': { 100 | promptTriggers: { 101 | keywords: ['component'], 102 | }, 103 | }, 104 | }; 105 | 106 | const result = fallbackToKeywords(prompt, skills); 107 | 108 | // 'component' matches 'components' via includes() 109 | expect(result.required).toContain('frontend-framework'); 110 | }); 111 | 112 | it('should return all detected skills as required (none as suggested)', () => { 113 | const prompt = 'fix component and test'; 114 | const skills = { 115 | 'frontend-framework': { 116 | promptTriggers: { 117 | keywords: ['component'], 118 | }, 119 | }, 120 | 'testing-strategy': { 121 | promptTriggers: { 122 | keywords: ['test', 'testing'], 123 | }, 124 | }, 125 | }; 126 | 127 | const result = fallbackToKeywords(prompt, skills); 128 | 129 | expect(result.required).toHaveLength(2); 130 | expect(result.required).toContain('frontend-framework'); 131 | expect(result.required).toContain('testing-strategy'); 132 | expect(result.suggested).toEqual([]); // Fallback always returns empty suggested 133 | }); 134 | 135 | it('should return empty arrays when no keywords match', () => { 136 | const prompt = 'completely unrelated topic'; 137 | const skills = { 138 | 'frontend-framework': { 139 | promptTriggers: { 140 | keywords: ['component', 'React'], 141 | }, 142 | }, 143 | }; 144 | 145 | const result = fallbackToKeywords(prompt, skills); 146 | 147 | expect(result.required).toEqual([]); 148 | expect(result.suggested).toEqual([]); 149 | }); 150 | 151 | it('should handle skills with empty keywords array', () => { 152 | const prompt = 'test prompt'; 153 | const skills = { 154 | 'skill-without-keywords': { 155 | promptTriggers: { 156 | keywords: [], 157 | }, 158 | }, 159 | }; 160 | 161 | const result = fallbackToKeywords(prompt, skills); 162 | 163 | expect(result.required).toEqual([]); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /.claude/hooks/lib/output-formatter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Output formatting for skill activation hook 3 | * 4 | * Handles all display formatting including skill injection banners, 5 | * already-loaded sections, recommended skills, and manual load reminders. 6 | */ 7 | 8 | import { existsSync, readFileSync } from 'fs'; 9 | import { join } from 'path'; 10 | 11 | /** 12 | * Inject skill content into system context 13 | * 14 | * Reads skill files and formats them with XML tags for Claude to process. 15 | * Returns formatted string with banner and skill content. 16 | * 17 | * @param skillNames - Names of skills to inject 18 | * @param projectDir - Project root directory 19 | * @returns Formatted skill injection output 20 | */ 21 | export function injectSkillContent(skillNames: string[], projectDir: string): string { 22 | if (skillNames.length === 0) return ''; 23 | 24 | let output = '\n'; 25 | output += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'; 26 | output += '📚 AUTO-LOADED SKILLS\n'; 27 | output += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n'; 28 | 29 | for (const skillName of skillNames) { 30 | const skillPath = join(projectDir, '.claude', 'skills', skillName, 'SKILL.md'); 31 | 32 | if (existsSync(skillPath)) { 33 | try { 34 | const skillContent = readFileSync(skillPath, 'utf-8'); 35 | 36 | output += `\n`; 37 | output += skillContent; 38 | output += `\n\n\n`; 39 | } catch (err) { 40 | console.error(`⚠️ Failed to load skill ${skillName}:`, err); 41 | } 42 | } else { 43 | console.warn(`⚠️ Skill file not found: ${skillPath}`); 44 | } 45 | } 46 | 47 | output += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'; 48 | output += `Loaded ${skillNames.length} skill(s): ${skillNames.join(', ')}\n`; 49 | output += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'; 50 | 51 | return output; 52 | } 53 | 54 | /** 55 | * Format skill activation check banner 56 | * 57 | * Shows header banner for skill activation check section with decorator lines. 58 | * 59 | * @returns Formatted banner string 60 | */ 61 | export function formatActivationBanner(): string { 62 | let output = ''; 63 | output += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'; 64 | output += '🎯 SKILL ACTIVATION CHECK\n'; 65 | output += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n'; 66 | return output; 67 | } 68 | 69 | /** 70 | * Format just-injected skills section 71 | * 72 | * Shows skills that were just loaded in this turn with their injection type. 73 | * 74 | * @param injectedSkills - Skills that were just injected 75 | * @param criticalSkills - Skills injected as critical 76 | * @param affinitySkills - Skills injected via affinity 77 | * @param promotedSkills - Skills promoted from suggested 78 | * @returns Formatted section string 79 | */ 80 | export function formatJustInjectedSection( 81 | injectedSkills: string[], 82 | criticalSkills: string[], 83 | affinitySkills: string[], 84 | promotedSkills: string[] 85 | ): string { 86 | if (injectedSkills.length === 0) return ''; 87 | 88 | let output = '\n📚 JUST LOADED:\n'; 89 | 90 | injectedSkills.forEach((skill) => { 91 | let label = ''; 92 | if (affinitySkills.includes(skill)) { 93 | label = ' (affinity)'; 94 | } else if (promotedSkills.includes(skill)) { 95 | label = ' (promoted)'; 96 | } else if (criticalSkills.includes(skill)) { 97 | label = ' (critical)'; 98 | } 99 | output += ` → ${skill}${label}\n`; 100 | }); 101 | 102 | return output; 103 | } 104 | 105 | /** 106 | * Format already-loaded skills section 107 | * 108 | * Shows skills that were loaded in previous turns (for user awareness). 109 | * Only shown when no new skills are being injected. 110 | * 111 | * @param alreadyLoaded - Skills already acknowledged in this conversation 112 | * @returns Formatted section string 113 | */ 114 | export function formatAlreadyLoadedSection(alreadyLoaded: string[]): string { 115 | if (alreadyLoaded.length === 0) return ''; 116 | 117 | let output = '\n✓ ALREADY LOADED:\n'; 118 | alreadyLoaded.forEach((name) => { 119 | output += ` → ${name}\n`; 120 | }); 121 | return output; 122 | } 123 | 124 | /** 125 | * Format recommended skills section 126 | * 127 | * Shows skills that were suggested but not auto-loaded (available for manual loading). 128 | * 129 | * @param recommendedSkills - Skills in suggested tier (0.50-0.65 confidence) 130 | * @param scores - Optional confidence scores to display 131 | * @returns Formatted section string 132 | */ 133 | export function formatRecommendedSection( 134 | recommendedSkills: string[], 135 | scores?: Record 136 | ): string { 137 | if (recommendedSkills.length === 0) return ''; 138 | 139 | let output = '\n📚 RECOMMENDED SKILLS (not auto-loaded):\n'; 140 | recommendedSkills.forEach((name) => { 141 | output += ` → ${name}`; 142 | if (scores && scores[name]) { 143 | output += ` (${scores[name].toFixed(2)})`; 144 | } 145 | output += '\n'; 146 | }); 147 | output += '\nOptional: Use Skill tool to load if needed\n'; 148 | return output; 149 | } 150 | 151 | /** 152 | * Format manual load section for skills with autoInject: false 153 | * 154 | * Shows skills that were matched but require manual loading via Skill tool. 155 | * 156 | * @param manualSkills - Skills that need manual loading 157 | * @returns Formatted section string 158 | */ 159 | export function formatManualLoadSection(manualSkills: string[]): string { 160 | if (manualSkills.length === 0) return ''; 161 | 162 | let output = '\n📚 MANUAL LOAD REQUIRED (autoInject: false):\n'; 163 | manualSkills.forEach((name) => (output += ` → ${name}\n`)); 164 | output += '\nACTION: Use Skill tool for these skills\n'; 165 | return output; 166 | } 167 | 168 | /** 169 | * Format closing banner for skill activation check 170 | * 171 | * @returns Formatted closing banner 172 | */ 173 | export function formatClosingBanner(): string { 174 | return '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'; 175 | } 176 | -------------------------------------------------------------------------------- /.claude/hooks/lib/__tests__/dependency-resolution.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi } from 'vitest'; 2 | import { DEFAULT_INJECTION_ORDER } from '../constants'; 3 | 4 | /** 5 | * Tests for skill dependency resolution logic 6 | * 7 | * Mirrors the algorithm in skill-activation-prompt.ts lines 45-96 8 | */ 9 | 10 | interface SkillRule { 11 | requiredSkills?: string[]; 12 | injectionOrder?: number; 13 | } 14 | 15 | /** 16 | * Resolve skill dependencies recursively with cycle detection 17 | * (Mirrors skill-activation-prompt.ts lines 45-96) 18 | */ 19 | function resolveSkillDependencies( 20 | skills: string[], 21 | skillRules: Record 22 | ): string[] { 23 | const resolved = new Set(); 24 | const visiting = new Set(); // For cycle detection 25 | 26 | function visit(skillName: string, path: string[] = []): void { 27 | // Cycle detection 28 | if (visiting.has(skillName)) { 29 | console.error(`⚠️ Circular dependency detected: ${[...path, skillName].join(' → ')}`); 30 | return; 31 | } 32 | 33 | // Already resolved 34 | if (resolved.has(skillName)) return; 35 | 36 | const skill = skillRules[skillName]; 37 | if (!skill) { 38 | console.warn(`⚠️ Skill not found: ${skillName}`); 39 | return; 40 | } 41 | 42 | // Mark as visiting 43 | visiting.add(skillName); 44 | path.push(skillName); 45 | 46 | // Visit dependencies first (DFS) 47 | const deps = skill.requiredSkills || []; 48 | deps.forEach((dep) => visit(dep, [...path])); 49 | 50 | // Add to resolved 51 | resolved.add(skillName); 52 | visiting.delete(skillName); 53 | } 54 | 55 | // Visit each root skill 56 | skills.forEach((skill) => visit(skill)); 57 | 58 | // Sort by injection order 59 | return Array.from(resolved).sort((a, b) => { 60 | const orderA = skillRules[a]?.injectionOrder || DEFAULT_INJECTION_ORDER; 61 | const orderB = skillRules[b]?.injectionOrder || DEFAULT_INJECTION_ORDER; 62 | return orderA - orderB; 63 | }); 64 | } 65 | 66 | describe('Dependency Resolution', () => { 67 | it('should resolve simple dependency chain in correct order', () => { 68 | const skillRules = { 69 | 'skill-a': { requiredSkills: ['skill-b'], injectionOrder: 30 }, 70 | 'skill-b': { requiredSkills: ['skill-c'], injectionOrder: 20 }, 71 | 'skill-c': { requiredSkills: [], injectionOrder: 10 }, 72 | }; 73 | 74 | const result = resolveSkillDependencies(['skill-a'], skillRules); 75 | 76 | // Should resolve all dependencies and sort by injectionOrder 77 | expect(result).toEqual(['skill-c', 'skill-b', 'skill-a']); 78 | }); 79 | 80 | it('should detect and handle circular dependencies without crashing', () => { 81 | const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); 82 | const skillRules = { 83 | 'skill-a': { requiredSkills: ['skill-b'], injectionOrder: 1 }, 84 | 'skill-b': { requiredSkills: ['skill-c'], injectionOrder: 2 }, 85 | 'skill-c': { requiredSkills: ['skill-a'], injectionOrder: 3 }, 86 | }; 87 | 88 | const result = resolveSkillDependencies(['skill-a'], skillRules); 89 | 90 | // Should log error about circular dependency 91 | expect(consoleErrorSpy).toHaveBeenCalledWith( 92 | expect.stringContaining('Circular dependency detected') 93 | ); 94 | 95 | // Should still resolve what it can (A and B, but not C due to cycle) 96 | expect(result.length).toBeGreaterThan(0); 97 | expect(result).toContain('skill-a'); 98 | 99 | consoleErrorSpy.mockRestore(); 100 | }); 101 | 102 | it('should resolve multiple dependencies correctly', () => { 103 | const skillRules = { 104 | 'skill-a': { requiredSkills: ['skill-b', 'skill-c', 'skill-d'], injectionOrder: 40 }, 105 | 'skill-b': { requiredSkills: [], injectionOrder: 10 }, 106 | 'skill-c': { requiredSkills: [], injectionOrder: 20 }, 107 | 'skill-d': { requiredSkills: [], injectionOrder: 30 }, 108 | }; 109 | 110 | const result = resolveSkillDependencies(['skill-a'], skillRules); 111 | 112 | // Should resolve all dependencies 113 | expect(result).toHaveLength(4); 114 | expect(result).toEqual(['skill-b', 'skill-c', 'skill-d', 'skill-a']); 115 | }); 116 | 117 | it('should warn about missing dependencies but continue', () => { 118 | const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); 119 | const skillRules = { 120 | 'skill-a': { requiredSkills: ['skill-b', 'non-existent'], injectionOrder: 20 }, 121 | 'skill-b': { requiredSkills: [], injectionOrder: 10 }, 122 | }; 123 | 124 | const result = resolveSkillDependencies(['skill-a'], skillRules); 125 | 126 | // Should warn about non-existent skill 127 | expect(consoleWarnSpy).toHaveBeenCalledWith( 128 | expect.stringContaining('Skill not found: non-existent') 129 | ); 130 | 131 | // Should still resolve existing skills 132 | expect(result).toEqual(['skill-b', 'skill-a']); 133 | 134 | consoleWarnSpy.mockRestore(); 135 | }); 136 | 137 | it('should sort by injectionOrder with default for missing order', () => { 138 | const skillRules = { 139 | 'skill-a': { requiredSkills: ['skill-b', 'skill-c'], injectionOrder: 100 }, 140 | 'skill-b': { requiredSkills: [] }, // No injectionOrder (uses DEFAULT_INJECTION_ORDER) 141 | 'skill-c': { requiredSkills: [], injectionOrder: 1 }, 142 | }; 143 | 144 | const result = resolveSkillDependencies(['skill-a'], skillRules); 145 | 146 | // skill-c (1), skill-b (DEFAULT=50), skill-a (100) 147 | expect(result[0]).toBe('skill-c'); 148 | expect(result[1]).toBe('skill-b'); 149 | expect(result[2]).toBe('skill-a'); 150 | }); 151 | 152 | it('should handle empty requiredSkills array', () => { 153 | const skillRules = { 154 | 'skill-a': { requiredSkills: [], injectionOrder: 1 }, 155 | }; 156 | 157 | const result = resolveSkillDependencies(['skill-a'], skillRules); 158 | 159 | expect(result).toEqual(['skill-a']); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /.claude/hooks/lib/__tests__/skill-state-manager.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest'; 2 | import { mkdirSync, rmSync, writeFileSync, existsSync, chmodSync, readFileSync } from 'fs'; 3 | import { join } from 'path'; 4 | import { tmpdir } from 'os'; 5 | import { readAcknowledgedSkills, writeSessionState } from '../skill-state-manager.js'; 6 | 7 | /** 8 | * Tests for session state management 9 | * 10 | * Validates state persistence, atomic writes, and error handling 11 | * for skill acknowledgment tracking across conversation turns. 12 | */ 13 | 14 | describe('Skill State Manager', () => { 15 | let testStateDir: string; 16 | let testStateId: string; 17 | 18 | beforeEach(() => { 19 | // Create unique temp directory for each test 20 | testStateDir = join(tmpdir(), `skill-state-test-${Date.now()}-${Math.random()}`); 21 | testStateId = 'test-conversation-123'; 22 | }); 23 | 24 | afterEach(() => { 25 | // Clean up temp directory 26 | if (existsSync(testStateDir)) { 27 | try { 28 | chmodSync(testStateDir, 0o755); // Restore permissions if changed 29 | rmSync(testStateDir, { recursive: true, force: true }); 30 | } catch { 31 | // Ignore cleanup errors 32 | } 33 | } 34 | }); 35 | 36 | it('should write and read state successfully (normal flow)', () => { 37 | const acknowledgedSkills = ['frontend-framework', 'testing-strategy']; 38 | const injectedSkills = ['frontend-framework']; 39 | 40 | writeSessionState(testStateDir, testStateId, acknowledgedSkills, injectedSkills); 41 | 42 | const readSkills = readAcknowledgedSkills(testStateDir, testStateId); 43 | 44 | expect(readSkills).toEqual(acknowledgedSkills); 45 | }); 46 | 47 | it('should return empty array when state file does not exist', () => { 48 | // Don't create any state file 49 | const readSkills = readAcknowledgedSkills(testStateDir, testStateId); 50 | 51 | expect(readSkills).toEqual([]); 52 | }); 53 | 54 | it('should return empty array when state file contains corrupted JSON', () => { 55 | // Create directory and write invalid JSON 56 | mkdirSync(testStateDir, { recursive: true }); 57 | const stateFile = join(testStateDir, `${testStateId}-skills-suggested.json`); 58 | writeFileSync(stateFile, '{ this is not valid JSON }'); 59 | 60 | const readSkills = readAcknowledgedSkills(testStateDir, testStateId); 61 | 62 | expect(readSkills).toEqual([]); 63 | }); 64 | 65 | it('should use atomic write pattern (temp file + rename)', () => { 66 | const acknowledgedSkills = ['skill-1', 'skill-2']; 67 | const injectedSkills = ['skill-1']; 68 | 69 | writeSessionState(testStateDir, testStateId, acknowledgedSkills, injectedSkills); 70 | 71 | const stateFile = join(testStateDir, `${testStateId}-skills-suggested.json`); 72 | const tempFile = `${stateFile}.tmp`; 73 | 74 | // After write completes: 75 | // 1. Main state file should exist 76 | expect(existsSync(stateFile)).toBe(true); 77 | 78 | // 2. Temp file should be cleaned up 79 | expect(existsSync(tempFile)).toBe(false); 80 | }); 81 | 82 | it('should handle permission errors gracefully (no crash)', () => { 83 | // Create read-only directory to trigger permission error 84 | mkdirSync(testStateDir, { recursive: true }); 85 | 86 | // Make directory read-only (this will prevent file creation) 87 | try { 88 | chmodSync(testStateDir, 0o444); 89 | } catch { 90 | // If chmod fails (e.g., on some CI systems), skip test 91 | return; 92 | } 93 | 94 | const acknowledgedSkills = ['test-skill']; 95 | const injectedSkills = ['test-skill']; 96 | 97 | // Should not throw - just log warning 98 | expect(() => { 99 | writeSessionState(testStateDir, testStateId, acknowledgedSkills, injectedSkills); 100 | }).not.toThrow(); 101 | 102 | // Restore permissions for cleanup 103 | chmodSync(testStateDir, 0o755); 104 | }); 105 | 106 | it('should create state directory if it does not exist', () => { 107 | // Ensure directory doesn't exist 108 | expect(existsSync(testStateDir)).toBe(false); 109 | 110 | const acknowledgedSkills = ['new-skill']; 111 | const injectedSkills = ['new-skill']; 112 | 113 | writeSessionState(testStateDir, testStateId, acknowledgedSkills, injectedSkills); 114 | 115 | // Directory should be created 116 | expect(existsSync(testStateDir)).toBe(true); 117 | 118 | // State should be readable 119 | const readSkills = readAcknowledgedSkills(testStateDir, testStateId); 120 | expect(readSkills).toEqual(acknowledgedSkills); 121 | }); 122 | 123 | it('should preserve all state fields when updating', () => { 124 | const firstAcknowledged = ['skill-1']; 125 | const firstInjected = ['skill-1']; 126 | 127 | // First write 128 | writeSessionState(testStateDir, testStateId, firstAcknowledged, firstInjected); 129 | 130 | const stateFile = join(testStateDir, `${testStateId}-skills-suggested.json`); 131 | const firstState = JSON.parse(readFileSync(stateFile, 'utf-8')); 132 | 133 | // Verify all fields present 134 | expect(firstState).toHaveProperty('timestamp'); 135 | expect(firstState).toHaveProperty('acknowledgedSkills'); 136 | expect(firstState).toHaveProperty('injectedSkills'); 137 | expect(firstState).toHaveProperty('injectionTimestamp'); 138 | 139 | expect(firstState.acknowledgedSkills).toEqual(firstAcknowledged); 140 | expect(firstState.injectedSkills).toEqual(firstInjected); 141 | }); 142 | 143 | it('should handle empty acknowledged skills array', () => { 144 | const acknowledgedSkills: string[] = []; 145 | const injectedSkills: string[] = []; 146 | 147 | writeSessionState(testStateDir, testStateId, acknowledgedSkills, injectedSkills); 148 | 149 | const readSkills = readAcknowledgedSkills(testStateDir, testStateId); 150 | 151 | expect(readSkills).toEqual([]); 152 | }); 153 | 154 | it('should handle updates to existing state file', () => { 155 | // First write 156 | writeSessionState(testStateDir, testStateId, ['skill-1'], ['skill-1']); 157 | 158 | let readSkills = readAcknowledgedSkills(testStateDir, testStateId); 159 | expect(readSkills).toEqual(['skill-1']); 160 | 161 | // Second write (update) 162 | writeSessionState(testStateDir, testStateId, ['skill-1', 'skill-2'], ['skill-2']); 163 | 164 | readSkills = readAcknowledgedSkills(testStateDir, testStateId); 165 | expect(readSkills).toEqual(['skill-1', 'skill-2']); 166 | }); 167 | 168 | it('should return empty array when state file has malformed structure', () => { 169 | // Create state file with valid JSON but missing acknowledgedSkills field 170 | mkdirSync(testStateDir, { recursive: true }); 171 | const stateFile = join(testStateDir, `${testStateId}-skills-suggested.json`); 172 | writeFileSync( 173 | stateFile, 174 | JSON.stringify({ 175 | timestamp: Date.now(), 176 | // Missing acknowledgedSkills field 177 | injectedSkills: ['skill-1'], 178 | }) 179 | ); 180 | 181 | const readSkills = readAcknowledgedSkills(testStateDir, testStateId); 182 | 183 | // Should return empty array when acknowledgedSkills is missing 184 | expect(readSkills).toEqual([]); 185 | }); 186 | }); 187 | -------------------------------------------------------------------------------- /.claude/hooks/skill-activation-prompt.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Skill Activation Hook - Main Entry Point 4 | * 5 | * Orchestrates skill auto-loading using modular components. 6 | * Analyzes user prompts via AI, filters/promotes skills, and injects 7 | * skill content into the conversation context. 8 | */ 9 | 10 | import { readFileSync } from 'fs'; 11 | import { join } from 'path'; 12 | import { analyzeIntent } from './lib/intent-analyzer.js'; 13 | import { resolveSkillDependencies } from './lib/skill-resolution.js'; 14 | import { filterAndPromoteSkills, findAffinityInjections } from './lib/skill-filtration.js'; 15 | import { readAcknowledgedSkills, writeSessionState } from './lib/skill-state-manager.js'; 16 | import { 17 | injectSkillContent, 18 | formatActivationBanner, 19 | formatJustInjectedSection, 20 | formatAlreadyLoadedSection, 21 | formatRecommendedSection, 22 | formatManualLoadSection, 23 | formatClosingBanner, 24 | } from './lib/output-formatter.js'; 25 | import type { SkillRulesConfig } from './lib/types.js'; 26 | import { debugLog } from './lib/debug-logger.js'; 27 | 28 | /** 29 | * Hook input from Claude 30 | */ 31 | interface HookInput { 32 | session_id: string; 33 | conversation_id?: string; 34 | transcript_path: string; 35 | cwd: string; 36 | permission_mode: string; 37 | prompt: string; 38 | } 39 | 40 | /** 41 | * Main hook execution 42 | */ 43 | async function main(): Promise { 44 | try { 45 | // Read input from stdin 46 | const input = readFileSync(0, 'utf-8'); 47 | const data: HookInput = JSON.parse(input); 48 | 49 | // Load skill rules 50 | const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd(); 51 | const rulesPath = join(projectDir, '.claude', 'skills', 'skill-rules.json'); 52 | const rules: SkillRulesConfig = JSON.parse(readFileSync(rulesPath, 'utf-8')); 53 | 54 | // Analyze user intent with AI 55 | const analysis = await analyzeIntent(data.prompt, rules.skills); 56 | // Filter out non-existent skills (AI could return invalid names) 57 | const requiredDomainSkills = (analysis.required || []).filter((name) => name in rules.skills); 58 | const suggestedDomainSkills = (analysis.suggested || []).filter((name) => name in rules.skills); 59 | 60 | // DEBUG: Log AI analysis results 61 | debugLog('=== NEW PROMPT ==='); 62 | debugLog(`Prompt: ${data.prompt}`); 63 | debugLog('AI Analysis Results:'); 64 | debugLog(` Required (critical): ${JSON.stringify(requiredDomainSkills)}`); 65 | debugLog(` Suggested: ${JSON.stringify(suggestedDomainSkills)}`); 66 | debugLog(` Scores: ${JSON.stringify(analysis.scores || {})}`); 67 | 68 | // Output banner 69 | let output = formatActivationBanner(); 70 | 71 | // Handle skill injection for domain skills only 72 | const hasMatchedSkills = requiredDomainSkills.length > 0 || suggestedDomainSkills.length > 0; 73 | if (hasMatchedSkills) { 74 | // State management 75 | const stateDir = join(projectDir, '.claude', 'hooks', 'state'); 76 | const stateId = data.conversation_id || data.session_id; 77 | const existingAcknowledged = readAcknowledgedSkills(stateDir, stateId); 78 | 79 | // DEBUG: Log session state 80 | debugLog('Session State:'); 81 | debugLog(` Already acknowledged: ${JSON.stringify(existingAcknowledged)}`); 82 | 83 | // Filter and promote skills 84 | const filtration = filterAndPromoteSkills( 85 | requiredDomainSkills, 86 | suggestedDomainSkills, 87 | existingAcknowledged, 88 | rules.skills 89 | ); 90 | 91 | // DEBUG: Log filtration results 92 | debugLog('Filtration Results:'); 93 | debugLog(` To inject: ${JSON.stringify(filtration.toInject)}`); 94 | debugLog(` Promoted: ${JSON.stringify(filtration.promoted)}`); 95 | debugLog(` Remaining suggested: ${JSON.stringify(filtration.remainingSuggested)}`); 96 | 97 | // Find affinity injections (bidirectional, free of slot cost) 98 | const affinitySkills = findAffinityInjections( 99 | filtration.toInject, 100 | existingAcknowledged, 101 | rules.skills 102 | ); 103 | 104 | // DEBUG: Log affinity results 105 | debugLog('Affinity Injection:'); 106 | debugLog(` Affinity skills found: ${JSON.stringify(affinitySkills)}`); 107 | 108 | // Resolve dependencies and inject skills 109 | let injectedSkills: string[] = []; 110 | const allSkillsToInject = [...filtration.toInject, ...affinitySkills]; 111 | 112 | // DEBUG: Log combined skills before dependency resolution 113 | debugLog('Combined Skills (before dependency resolution):'); 114 | debugLog(` All skills to inject: ${JSON.stringify(allSkillsToInject)}`); 115 | 116 | if (allSkillsToInject.length > 0) { 117 | injectedSkills = resolveSkillDependencies(allSkillsToInject, rules.skills); 118 | 119 | // DEBUG: Log final injected skills 120 | debugLog('Final Injection:'); 121 | debugLog(` After dependency resolution: ${JSON.stringify(injectedSkills)}`); 122 | 123 | // Inject skills individually (one console.log per skill) 124 | for (const skillName of injectedSkills) { 125 | const skillPath = join(projectDir, '.claude', 'skills', skillName, 'SKILL.md'); 126 | debugLog(` Injecting skill: ${skillName} from ${skillPath}`); 127 | 128 | const injectionOutput = injectSkillContent([skillName], projectDir); 129 | if (injectionOutput) { 130 | console.log(injectionOutput); 131 | debugLog(` ✓ Injected ${skillName} (${injectionOutput.length} chars)`); 132 | } else { 133 | debugLog(` ✗ Failed to inject ${skillName} - no output generated`); 134 | } 135 | } 136 | } 137 | 138 | // Show just-injected skills in banner 139 | if (injectedSkills.length > 0) { 140 | output += formatJustInjectedSection( 141 | injectedSkills, 142 | filtration.toInject, 143 | affinitySkills, 144 | filtration.promoted 145 | ); 146 | } 147 | 148 | // Show already-loaded skills 149 | const alreadyAcknowledged = [...requiredDomainSkills, ...suggestedDomainSkills].filter( 150 | (skill) => existingAcknowledged.includes(skill) 151 | ); 152 | if (alreadyAcknowledged.length > 0 && injectedSkills.length === 0) { 153 | output += formatAlreadyLoadedSection(alreadyAcknowledged); 154 | } 155 | 156 | // Show remaining recommended skills 157 | output += formatRecommendedSection(filtration.remainingSuggested, analysis.scores); 158 | 159 | // Show manual-load required skills (autoInject: false) 160 | const manualSkills = [...requiredDomainSkills, ...suggestedDomainSkills].filter((skill) => { 161 | const skillRule = rules.skills[skill]; 162 | return !existingAcknowledged.includes(skill) && skillRule?.autoInject === false; 163 | }); 164 | output += formatManualLoadSection(manualSkills); 165 | 166 | output += formatClosingBanner(); 167 | console.log(output); 168 | 169 | // Write session state 170 | if (injectedSkills.length > 0) { 171 | writeSessionState( 172 | stateDir, 173 | stateId, 174 | [...existingAcknowledged, ...injectedSkills], 175 | injectedSkills 176 | ); 177 | } 178 | } 179 | } catch (err) { 180 | console.error('⚠️ Skill activation hook error:', err); 181 | process.exit(0); // Don't fail the conversation on hook errors 182 | } 183 | } 184 | 185 | main(); 186 | -------------------------------------------------------------------------------- /.claude/hooks/lib/skill-filtration.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Skill filtering, promotion, and affinity injection logic 3 | * 4 | * Handles filtering of acknowledged skills, promotion of suggested skills to 5 | * fill the 2-skill target, and bidirectional affinity-based auto-injection. 6 | */ 7 | 8 | import type { SkillRule } from './types.js'; 9 | 10 | /** 11 | * Result of skill filtration and promotion 12 | */ 13 | export interface FiltrationResult { 14 | toInject: string[]; 15 | promoted: string[]; 16 | remainingSuggested: string[]; 17 | } 18 | 19 | /** 20 | * Filter out already acknowledged skills and those with autoInject: false 21 | * 22 | * @param skills - Skills to filter 23 | * @param acknowledged - Previously acknowledged skills 24 | * @param skillRules - Skill configuration 25 | * @returns Filtered list of unacknowledged skills 26 | */ 27 | export function filterUnacknowledgedSkills( 28 | skills: string[], 29 | acknowledged: string[], 30 | skillRules: Record 31 | ): string[] { 32 | return skills.filter( 33 | (skill) => !acknowledged.includes(skill) && skillRules[skill]?.autoInject !== false 34 | ); 35 | } 36 | 37 | /** 38 | * Apply skill injection limits with promotion logic 39 | * 40 | * Promotes suggested skills to fill the 2-skill target. Target calculation 41 | * accounts for critical skills already loaded in the session. 42 | * 43 | * @param criticalSkills - Unacknowledged required skills (confidence > 0.65) 44 | * @param recommendedSkills - Unacknowledged suggested skills (confidence 0.50-0.65) 45 | * @param acknowledgedCriticalCount - Count of critical skills already loaded 46 | * @returns Object with skills to inject, promoted skills, and remaining suggested 47 | */ 48 | export function applyInjectionLimits( 49 | criticalSkills: string[], 50 | recommendedSkills: string[], 51 | acknowledgedCriticalCount: number 52 | ): FiltrationResult { 53 | const TARGET_SLOTS = 2; // Standard 2-skill injection limit 54 | 55 | // Calculate promotion target: 2 total - already loaded critical skills 56 | const promotionTarget = Math.max(0, TARGET_SLOTS - acknowledgedCriticalCount); 57 | 58 | // Start with critical skills (up to promotion target) 59 | const toInject = [...criticalSkills.slice(0, promotionTarget)]; 60 | 61 | // Calculate how many more skills we need to reach target 62 | const needed = Math.max(0, promotionTarget - toInject.length); 63 | 64 | // Promote recommended skills to fill empty slots 65 | const promotedRecommended: string[] = []; 66 | if (needed > 0 && recommendedSkills.length > 0) { 67 | const promoted = recommendedSkills.slice(0, needed); 68 | promotedRecommended.push(...promoted); 69 | toInject.push(...promoted); 70 | } 71 | 72 | // Remaining recommended skills (not promoted) 73 | const remainingSuggested = recommendedSkills.filter((s) => !promotedRecommended.includes(s)); 74 | 75 | return { 76 | toInject, 77 | promoted: promotedRecommended, 78 | remainingSuggested, 79 | }; 80 | } 81 | 82 | /** 83 | * Find skills to auto-inject based on bidirectional affinity 84 | * 85 | * Checks both directions: 86 | * - If injecting skill A with affinity [B, C], inject B and C (parent → child) 87 | * - If any skill lists A in its affinity, inject that skill (child → parent) 88 | * 89 | * Respects acknowledged skills (don't re-inject). 90 | * Free of slot cost (affinity skills don't count toward 2-skill limit). 91 | * 92 | * @param toInject - Skills being injected 93 | * @param acknowledged - Already loaded skills 94 | * @param skillRules - Skill configuration 95 | * @returns Additional skills to inject due to affinity (free of slot cost) 96 | * 97 | * @example 98 | * ```typescript 99 | * // Injecting frontend-framework (has affinity: ["system-architecture", "api-protocols"]) 100 | * const affinities = findAffinityInjections( 101 | * ["frontend-framework"], 102 | * [], 103 | * skillRules 104 | * ); 105 | * // Returns: ["system-architecture", "api-protocols"] 106 | * 107 | * // If architecture already loaded 108 | * const affinities = findAffinityInjections( 109 | * ["frontend-framework"], 110 | * ["system-architecture"], 111 | * skillRules 112 | * ); 113 | * // Returns: ["api-protocols"] (only unloaded affinity) 114 | * ``` 115 | */ 116 | export function findAffinityInjections( 117 | toInject: string[], 118 | acknowledged: string[], 119 | skillRules: Record 120 | ): string[] { 121 | const affinitySet = new Set(); 122 | 123 | for (const skill of toInject) { 124 | const config = skillRules[skill]; 125 | 126 | // Direction 1: This skill lists affinities (parent → child) 127 | // Example: frontend-framework → ["system-architecture", "api-protocols"] 128 | // Enforce max 2 items at runtime (matches schema constraint) 129 | const affinities = (config?.affinity || []).slice(0, 2); 130 | for (const affinity of affinities) { 131 | // Only inject if: 132 | // 1. Not already acknowledged (loaded in session) 133 | // 2. Not already in toInject list 134 | // 3. autoInject is not false 135 | if ( 136 | !acknowledged.includes(affinity) && 137 | !toInject.includes(affinity) && 138 | skillRules[affinity]?.autoInject !== false 139 | ) { 140 | affinitySet.add(affinity); 141 | } 142 | } 143 | 144 | // Direction 2: Other skills list this skill in their affinity (child → parent) 145 | // Example: system-architecture not in toInject, but frontend-framework (which is in toInject) 146 | // is listed in other skills' affinities 147 | for (const [otherSkill, otherConfig] of Object.entries(skillRules)) { 148 | const otherAffinities = otherConfig.affinity || []; 149 | if (otherAffinities.includes(skill)) { 150 | // Only inject if: 151 | // 1. Not already acknowledged 152 | // 2. Not already in toInject 153 | // 3. autoInject is not false 154 | if ( 155 | !acknowledged.includes(otherSkill) && 156 | !toInject.includes(otherSkill) && 157 | otherConfig.autoInject !== false 158 | ) { 159 | affinitySet.add(otherSkill); 160 | } 161 | } 162 | } 163 | } 164 | 165 | return Array.from(affinitySet); 166 | } 167 | 168 | /** 169 | * Complete filtration workflow: filter + promotion + affinity 170 | * 171 | * Combines all filtration steps: 172 | * 1. Filter out acknowledged skills 173 | * 2. Calculate promotion target (2 - acknowledged critical count) 174 | * 3. Apply promotion to reach target 175 | * 176 | * Note: Affinity injection happens separately in the main hook flow 177 | * after this function returns, to maintain clear separation of concerns. 178 | * 179 | * @param requiredSkills - Critical skills from AI analysis 180 | * @param suggestedSkills - Recommended skills from AI analysis 181 | * @param acknowledged - Previously acknowledged skills 182 | * @param skillRules - Skill configuration 183 | * @returns Filtration result with skills to inject and metadata 184 | */ 185 | export function filterAndPromoteSkills( 186 | requiredSkills: string[], 187 | suggestedSkills: string[], 188 | acknowledged: string[], 189 | skillRules: Record 190 | ): FiltrationResult { 191 | // Filter out acknowledged skills 192 | const unacknowledgedCritical = filterUnacknowledgedSkills( 193 | requiredSkills, 194 | acknowledged, 195 | skillRules 196 | ); 197 | const unacknowledgedRecommended = filterUnacknowledgedSkills( 198 | suggestedSkills, 199 | acknowledged, 200 | skillRules 201 | ); 202 | 203 | // Calculate how many critical skills are already loaded 204 | const acknowledgedCriticalCount = requiredSkills.filter((s) => acknowledged.includes(s)).length; 205 | 206 | // Apply promotion to reach 2-skill target 207 | return applyInjectionLimits( 208 | unacknowledgedCritical, 209 | unacknowledgedRecommended, 210 | acknowledgedCriticalCount 211 | ); 212 | } 213 | -------------------------------------------------------------------------------- /.claude/hooks/lib/__tests__/output-formatter.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest'; 2 | import { mkdirSync, rmSync, writeFileSync, existsSync } from 'fs'; 3 | import { join } from 'path'; 4 | import { tmpdir } from 'os'; 5 | import { 6 | injectSkillContent, 7 | formatActivationBanner, 8 | formatAlreadyLoadedSection, 9 | formatRecommendedSection, 10 | formatClosingBanner, 11 | } from '../output-formatter.js'; 12 | 13 | /** 14 | * Tests for output formatting functions 15 | * 16 | * Validates banner generation, skill content injection, and section formatting 17 | * for the skill activation hook display output. 18 | */ 19 | 20 | describe('Output Formatter', () => { 21 | let testProjectDir: string; 22 | 23 | beforeEach(() => { 24 | // Create unique temp directory for each test 25 | testProjectDir = join(tmpdir(), `output-formatter-test-${Date.now()}-${Math.random()}`); 26 | }); 27 | 28 | afterEach(() => { 29 | // Clean up temp directory 30 | if (existsSync(testProjectDir)) { 31 | rmSync(testProjectDir, { recursive: true, force: true }); 32 | } 33 | }); 34 | 35 | describe('injectSkillContent', () => { 36 | it('should read skills and format with XML tags', () => { 37 | // Setup test skill files 38 | const skillsDir = join(testProjectDir, '.claude', 'skills'); 39 | mkdirSync(join(skillsDir, 'test-skill'), { recursive: true }); 40 | writeFileSync( 41 | join(skillsDir, 'test-skill', 'SKILL.md'), 42 | '# Test Skill\n\nThis is a test skill.' 43 | ); 44 | 45 | const output = injectSkillContent(['test-skill'], testProjectDir); 46 | 47 | expect(output).toContain('📚 AUTO-LOADED SKILLS'); 48 | expect(output).toContain(''); 49 | expect(output).toContain('# Test Skill'); 50 | expect(output).toContain('This is a test skill.'); 51 | expect(output).toContain(''); 52 | expect(output).toContain('Loaded 1 skill(s): test-skill'); 53 | }); 54 | 55 | it('should handle missing skill files gracefully', () => { 56 | // Don't create any skill files 57 | const output = injectSkillContent(['non-existent-skill'], testProjectDir); 58 | 59 | // Should still output banner but warn about missing file 60 | expect(output).toContain('📚 AUTO-LOADED SKILLS'); 61 | expect(output).toContain('Loaded 1 skill(s): non-existent-skill'); 62 | // Should not contain skill content tags 63 | expect(output).not.toContain(''); 64 | }); 65 | 66 | it('should return empty string for empty skill array', () => { 67 | const output = injectSkillContent([], testProjectDir); 68 | 69 | expect(output).toBe(''); 70 | }); 71 | 72 | it('should format multiple skills correctly', () => { 73 | const skillsDir = join(testProjectDir, '.claude', 'skills'); 74 | mkdirSync(join(skillsDir, 'skill-1'), { recursive: true }); 75 | mkdirSync(join(skillsDir, 'skill-2'), { recursive: true }); 76 | writeFileSync(join(skillsDir, 'skill-1', 'SKILL.md'), 'Skill 1 content'); 77 | writeFileSync(join(skillsDir, 'skill-2', 'SKILL.md'), 'Skill 2 content'); 78 | 79 | const output = injectSkillContent(['skill-1', 'skill-2'], testProjectDir); 80 | 81 | expect(output).toContain(''); 82 | expect(output).toContain('Skill 1 content'); 83 | expect(output).toContain(''); 84 | expect(output).toContain(''); 85 | expect(output).toContain('Skill 2 content'); 86 | expect(output).toContain('Loaded 2 skill(s): skill-1, skill-2'); 87 | }); 88 | 89 | it('should handle file read errors gracefully', () => { 90 | // Create directory but no file (will trigger file read error) 91 | const skillsDir = join(testProjectDir, '.claude', 'skills'); 92 | mkdirSync(join(skillsDir, 'broken-skill'), { recursive: true }); 93 | // Don't create SKILL.md file 94 | 95 | const output = injectSkillContent(['broken-skill'], testProjectDir); 96 | 97 | // Should complete without throwing 98 | expect(output).toContain('📚 AUTO-LOADED SKILLS'); 99 | }); 100 | }); 101 | 102 | describe('formatActivationBanner', () => { 103 | it('should return expected banner format', () => { 104 | const banner = formatActivationBanner(); 105 | 106 | expect(banner).toContain('🎯 SKILL ACTIVATION CHECK'); 107 | expect(banner).toContain('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); // 39 chars 108 | expect(banner).toMatch(/^━+\n/); // Starts with separator 109 | expect(banner).toMatch(/\n━+\n\n$/); // Ends with separator and newline 110 | }); 111 | }); 112 | 113 | describe('formatAlreadyLoadedSection', () => { 114 | it('should list previously loaded skills', () => { 115 | const alreadyLoaded = ['frontend-framework', 'testing-strategy']; 116 | 117 | const output = formatAlreadyLoadedSection(alreadyLoaded); 118 | 119 | expect(output).toContain('✓ ALREADY LOADED:'); 120 | expect(output).toContain('→ frontend-framework'); 121 | expect(output).toContain('→ testing-strategy'); 122 | }); 123 | 124 | it('should return empty string for empty array', () => { 125 | const output = formatAlreadyLoadedSection([]); 126 | 127 | expect(output).toBe(''); 128 | }); 129 | 130 | it('should format single skill', () => { 131 | const output = formatAlreadyLoadedSection(['single-skill']); 132 | 133 | expect(output).toContain('✓ ALREADY LOADED:'); 134 | expect(output).toContain('→ single-skill'); 135 | }); 136 | }); 137 | 138 | describe('formatRecommendedSection', () => { 139 | it('should show skills with confidence scores when provided', () => { 140 | const skills = ['skill-1', 'skill-2']; 141 | const scores = { 'skill-1': 0.55, 'skill-2': 0.62 }; 142 | 143 | const output = formatRecommendedSection(skills, scores); 144 | 145 | expect(output).toContain('📚 RECOMMENDED SKILLS (not auto-loaded):'); 146 | expect(output).toContain('→ skill-1 (0.55)'); 147 | expect(output).toContain('→ skill-2 (0.62)'); 148 | expect(output).toContain('Optional: Use Skill tool to load if needed'); 149 | }); 150 | 151 | it('should work without confidence scores', () => { 152 | const skills = ['skill-1', 'skill-2']; 153 | 154 | const output = formatRecommendedSection(skills); 155 | 156 | expect(output).toContain('📚 RECOMMENDED SKILLS (not auto-loaded):'); 157 | expect(output).toContain('→ skill-1'); 158 | expect(output).toContain('→ skill-2'); 159 | expect(output).not.toContain('(0.'); 160 | expect(output).toContain('Optional: Use Skill tool to load if needed'); 161 | }); 162 | 163 | it('should return empty string for empty array', () => { 164 | const output = formatRecommendedSection([]); 165 | 166 | expect(output).toBe(''); 167 | }); 168 | 169 | it('should handle partial score data gracefully', () => { 170 | const skills = ['skill-1', 'skill-2', 'skill-3']; 171 | const scores = { 'skill-1': 0.58, 'skill-3': 0.51 }; // Missing skill-2 172 | 173 | const output = formatRecommendedSection(skills, scores); 174 | 175 | expect(output).toContain('→ skill-1 (0.58)'); 176 | expect(output).toContain('→ skill-2\n'); // No score 177 | expect(output).toContain('→ skill-3 (0.51)'); 178 | }); 179 | 180 | it('should format scores with 2 decimal places', () => { 181 | const skills = ['precise-skill']; 182 | const scores = { 'precise-skill': 0.5555555 }; 183 | 184 | const output = formatRecommendedSection(skills, scores); 185 | 186 | expect(output).toContain('(0.56)'); // Rounded to 2 decimals 187 | }); 188 | }); 189 | 190 | describe('formatClosingBanner', () => { 191 | it('should return closing line separator', () => { 192 | const banner = formatClosingBanner(); 193 | 194 | expect(banner).toBe('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'); 195 | expect(banner.length).toBe(40); // 39 separator chars + newline 196 | }); 197 | }); 198 | }); 199 | -------------------------------------------------------------------------------- /.claude/skills/skill-developer/resources/skill-rules-reference.md: -------------------------------------------------------------------------------- 1 | # skill-rules.json - Complete Reference 2 | 3 | Complete schema and configuration reference for `.claude/skills/skill-rules.json`. 4 | 5 | ______________________________________________________________________ 6 | 7 | ## File Location 8 | 9 | **Path:** `.claude/skills/skill-rules.json` 10 | 11 | This JSON file defines all skills and their trigger conditions for the auto-activation system. 12 | 13 | ______________________________________________________________________ 14 | 15 | ## Complete TypeScript Schema 16 | 17 | ```typescript 18 | interface SkillRules { 19 | version: string; 20 | skills: Record; 21 | } 22 | 23 | interface SkillRule { 24 | type: 'guardrail' | 'domain'; 25 | autoInject?: boolean; 26 | requiredSkills?: string[]; 27 | description?: string; 28 | injectionOrder?: number; 29 | 30 | promptTriggers?: { 31 | keywords?: string[]; 32 | }; 33 | 34 | affinity?: string[]; // Bidirectional complementary skills (auto-inject, max 2) 35 | } 36 | ``` 37 | 38 | ______________________________________________________________________ 39 | 40 | ## Field Guide 41 | 42 | ### Top Level 43 | 44 | | Field | Type | Required | Description | 45 | | --------- | ------ | -------- | -------------------------------- | 46 | | `version` | string | Yes | Schema version (currently "1.0") | 47 | | `skills` | object | Yes | Map of skill name → SkillRule | 48 | 49 | ### SkillRule Fields 50 | 51 | | Field | Type | Required | Description | 52 | | ------------------ | -------- | -------- | ---------------------------------------------------------- | 53 | | `type` | string | Yes | "guardrail" (enforced) or "domain" (advisory) | 54 | | `autoInject` | boolean | Optional | Automatically inject this skill when available | 55 | | `requiredSkills` | string[] | Optional | Skills that must be injected before this skill | 56 | | `description` | string | Optional | Human-readable description of the skill | 57 | | `injectionOrder` | number | Optional | Order of injection relative to other skills | 58 | | `promptTriggers` | object | Optional | Triggers based on user prompts | 59 | | `affinity` | string[] | Optional | Complementary skills and bonus injection slots | 60 | 61 | ### promptTriggers Fields 62 | 63 | | Field | Type | Required | Description | 64 | | ---------- | -------- | -------- | ------------------------------------------ | 65 | | `keywords` | string[] | Optional | Exact substring matches (case-insensitive) | 66 | 67 | ### affinity Field 68 | 69 | | Field | Type | Required | Description | 70 | | ---------- | -------- | -------- | --------------------------------------------------------------- | 71 | | `affinity` | string[] | Optional | Bidirectional complementary skills (auto-injected, max 2 items) | 72 | 73 | **How it works (Bidirectional Auto-Injection):** 74 | 75 | - Standard injection limit: 2 skills maximum (critical or promoted) 76 | - Affinity skills auto-inject **bidirectionally** at **no slot cost** (don't count toward 2-skill limit) 77 | - **Direction 1 (Parent→Child):** If skill A is injected and lists `affinity: ["B", "C"]`, both B and C auto-inject 78 | - **Direction 2 (Child→Parent):** If skill A is injected and skill B lists `affinity: ["A"]`, skill B auto-injects 79 | - Affinities respect session state: won't re-inject already-loaded skills 80 | - Max 2 affinities per skill (rare; most have 0-1) 81 | 82 | **Example:** 83 | 84 | ```json 85 | { 86 | "frontend-framework": { 87 | "affinity": ["system-architecture", "api-protocols"] 88 | }, 89 | "api-protocols": { 90 | "affinity": ["system-architecture"] 91 | }, 92 | "integration-tools": { 93 | "affinity": ["system-architecture"] 94 | }, 95 | "system-architecture": { 96 | // Root skill - no affinities 97 | } 98 | } 99 | ``` 100 | 101 | **Scenario:** User asks "Fix the frontend component" 102 | 103 | - AI detects: `frontend-framework` (critical) 104 | - System injects: `frontend-framework` (1 critical, counts toward limit) 105 | - Affinity triggers: `system-architecture` + `api-protocols` (2 affinity, free) 106 | - **Total: 3 skills injected** (1 critical + 2 affinity) 107 | 108 | ______________________________________________________________________ 109 | 110 | ## Example: Guardrail Skill 111 | 112 | Complete example of a guardrail skill: 113 | 114 | ```json 115 | { 116 | "api-security": { 117 | "type": "guardrail", 118 | "description": "Ensures secure API design and implementation", 119 | "requiredSkills": ["security-basics"], 120 | "autoInject": true, 121 | "injectionOrder": 1, 122 | 123 | "promptTriggers": { 124 | "keywords": [ 125 | "api", 126 | "endpoint", 127 | "authentication", 128 | "authorization", 129 | "jwt", 130 | "oauth", 131 | "password", 132 | "token", 133 | "security" 134 | ] 135 | }, 136 | 137 | "affinity": ["security-basics"] 138 | } 139 | } 140 | ``` 141 | 142 | ### Key Points for Guardrails 143 | 144 | 1. **type**: Must be "guardrail" 145 | 1. **autoInject**: Consider auto-injecting for critical security skills 146 | 1. **requiredSkills**: List dependent skills 147 | 1. **description**: Clear explanation of the skill's purpose 148 | 1. **promptTriggers**: Keywords for detecting relevant prompts 149 | 1. **affinity**: Related complementary skills 150 | 151 | ______________________________________________________________________ 152 | 153 | ## Example: Domain Skill 154 | 155 | Complete example of a domain-specific skill: 156 | 157 | ```json 158 | { 159 | "python-best-practices": { 160 | "type": "domain", 161 | "description": "Python development best practices including PEP 8, type hints, and docstrings", 162 | "injectionOrder": 2, 163 | 164 | "promptTriggers": { 165 | "keywords": [ 166 | "python", 167 | "django", 168 | "flask", 169 | "pytest", 170 | "type hints", 171 | "docstring", 172 | "pep 8", 173 | "refactor", 174 | "testing" 175 | ] 176 | }, 177 | 178 | "affinity": [] 179 | } 180 | } 181 | ``` 182 | 183 | ### Key Points for Domain Skills 184 | 185 | 1. **type**: Must be "domain" 186 | 1. **description**: Clear explanation of domain expertise 187 | 1. **promptTriggers**: Keywords for detecting relevant prompts 188 | 1. **requiredSkills**: Dependencies on other skills 189 | 1. **affinity**: Related complementary skills 190 | 191 | ______________________________________________________________________ 192 | 193 | ## Validation 194 | 195 | ### Check JSON Syntax 196 | 197 | ```bash 198 | cat .claude/skills/skill-rules.json | jq . 199 | ``` 200 | 201 | If valid, jq will pretty-print the JSON. If invalid, it will show the error. 202 | 203 | ### Common JSON Errors 204 | 205 | **Trailing comma:** 206 | 207 | ```json 208 | { 209 | "keywords": ["one", "two",] // ❌ Trailing comma 210 | } 211 | ``` 212 | 213 | **Missing quotes:** 214 | 215 | ```json 216 | { 217 | type: "guardrail" // ❌ Missing quotes on key 218 | } 219 | ``` 220 | 221 | **Single quotes (invalid JSON):** 222 | 223 | ```json 224 | { 225 | 'type': 'guardrail' // ❌ Must use double quotes 226 | } 227 | ``` 228 | 229 | ### Validation Checklist 230 | 231 | - [ ] JSON syntax valid (use `jq`) 232 | - [ ] All skill names match SKILL.md filenames 233 | - [ ] No duplicate skill names 234 | - [ ] Keywords are meaningful and case-insensitive 235 | - [ ] requiredSkills references exist in the skills map 236 | - [ ] affinity lists reference valid skill names 237 | - [ ] injectionOrder is consistent across skills 238 | 239 | ______________________________________________________________________ 240 | 241 | **Related Files:** 242 | 243 | - [SKILL.md](../SKILL.md) - Main skill guide 244 | - [trigger-types.md](trigger-types.md) - Complete trigger documentation 245 | -------------------------------------------------------------------------------- /.claude/skills/README.md: -------------------------------------------------------------------------------- 1 | # Claude Code Skills 2 | 3 | This directory contains skills that provide comprehensive, context-aware guidance for your project. Skills automatically activate based on your prompts and file edits. 4 | 5 | ## What Are Skills? 6 | 7 | Skills are modular knowledge bases that Claude loads when needed. They provide: 8 | 9 | - Domain-specific best practices and patterns 10 | - Code quality guidance and style rules 11 | - Common pitfalls and how to avoid them 12 | - Working code examples and templates 13 | - Security best practices and guardrails 14 | 15 | ## How Skills Work 16 | 17 | The skills system uses an AI-powered hook (`skill-activation-prompt`) that: 18 | 19 | 1. Analyzes your prompt using Claude Haiku 4.5 20 | 2. Assigns confidence scores to each skill (0.0-1.0) 21 | 3. Automatically injects high-confidence skills (>0.65) 22 | 4. Suggests medium-confidence skills (0.50-0.65) 23 | 5. Tracks skills loaded per conversation (no duplicates) 24 | 6. Caches analysis results for performance (1-hour TTL) 25 | 26 | ## Installed Skills 27 | 28 | ### 1. skill-developer 29 | 30 | **Type:** Domain skill 31 | **Purpose:** Explains and maintains the skills system itself 32 | 33 | **When it activates:** 34 | 35 | - Keywords matching: "Claude Code skill", "skill system", "skill triggers", "skill rules", etc. 36 | 37 | **What it covers:** 38 | 39 | - Skill structure and YAML frontmatter 40 | - Keyword-based trigger activation 41 | - Hook architecture (UserPromptSubmit workflow) 42 | - The 500-line rule and progressive disclosure 43 | - Skill types (domain and guardrail) 44 | - Automatic injection and session tracking 45 | 46 | **Enforcement:** Automatically injected when detected (autoInject: true) 47 | 48 | **Main file:** `skill-developer/SKILL.md` 49 | **Resources:** 8 files covering skill creation, triggers, troubleshooting 50 | 51 | ### 2. python-best-practices (DOMAIN SKILL) 52 | 53 | **Type:** Domain skill 54 | **Purpose:** Python development best practices 55 | 56 | **When it activates:** 57 | 58 | - Keywords matching: "python", "PEP 8", "type hints", "docstring", etc. 59 | - Writing or refactoring Python code 60 | 61 | **What it covers:** 62 | 63 | - PEP 8 style guidelines 64 | - Type hints and modern Python syntax (3.9+) 65 | - NumPy-style docstrings 66 | - Error handling best practices 67 | - Common patterns (dataclasses, enums, pathlib) 68 | - Testing with pytest 69 | 70 | **Main file:** `python-best-practices/SKILL.md` (484 lines) 71 | 72 | ### 3. git-workflow (DOMAIN SKILL) 73 | 74 | **Type:** Domain skill 75 | **Purpose:** Git workflow and version control best practices 76 | 77 | **When it activates:** 78 | 79 | - Keywords matching: "git", "commit", "branch", "merge", "pull request", etc. 80 | 81 | **What it covers:** 82 | 83 | - Conventional Commits format 84 | - Branch naming conventions 85 | - Git Flow strategy 86 | - Pull request best practices 87 | - Common Git operations (rebase, stash, cherry-pick) 88 | - Security best practices (signing commits, avoiding secrets) 89 | 90 | **Main file:** `git-workflow/SKILL.md` (423 lines) 91 | 92 | ### 4. api-security (GUARDRAIL SKILL) 93 | 94 | **Type:** Guardrail skill (enforces security) 95 | **Purpose:** API security and vulnerability prevention 96 | 97 | **When it activates:** 98 | 99 | - Keywords matching: "api", "endpoint", "route", "authentication", "security", "SQL injection", "XSS", "CORS", etc. 100 | 101 | **What it covers:** 102 | 103 | - Authentication and authorization patterns 104 | - Input validation and output sanitization 105 | - SQL injection prevention 106 | - Cross-Site Scripting (XSS) prevention 107 | - HTTPS and transport security 108 | - CORS configuration 109 | - Sensitive data handling 110 | - OWASP Top 10 vulnerabilities 111 | 112 | **Main file:** `api-security/SKILL.md` (427 lines) 113 | 114 | ## How Skills Activate 115 | 116 | Skills automatically activate via the `skill-activation-prompt` hook, which uses `skill-rules.json` configuration: 117 | 118 | ### Trigger Types 119 | 120 | Skills are activated based on keywords in prompts: 121 | 122 | ```json 123 | "promptTriggers": { 124 | "keywords": ["python", "PEP 8", "type hints"] 125 | } 126 | ``` 127 | 128 | Keywords are matched directly against user prompts for skill activation. 129 | 130 | ### Skill Types 131 | 132 | 1. **Domain Skills** - Provide comprehensive guidance for specific areas 133 | 134 | - `type: "domain"` 135 | - Example: `python-best-practices`, `git-workflow`, `skill-developer` 136 | 137 | 2. **Guardrail Skills** - Enforce critical best practices 138 | 139 | - `type: "guardrail"` 140 | - Example: `api-security` 141 | 142 | 143 | ## Configuration 144 | 145 | Skills are configured in `skill-rules.json`: 146 | 147 | ```json 148 | { 149 | "version": "1.0", 150 | "skills": { 151 | "python-best-practices": { 152 | "type": "domain", 153 | "autoInject": true, 154 | "requiredSkills": [], 155 | "description": "Python development best practices...", 156 | "promptTriggers": { 157 | "keywords": ["python", "PEP 8", "type hints"] 158 | } 159 | } 160 | } 161 | } 162 | ``` 163 | 164 | ## Adding New Skills 165 | 166 | To add a new skill to your project: 167 | 168 | 1. **Create skill directory:** 169 | 170 | ```bash 171 | mkdir -p .claude/skills/my-skill/resources 172 | ``` 173 | 174 | 2. **Create SKILL.md with YAML frontmatter:** 175 | 176 | ```markdown 177 | --- 178 | name: my-skill 179 | description: Brief description of the skill 180 | --- 181 | 182 | # My Skill 183 | 184 | ## Purpose 185 | [Explain what this skill does...] 186 | 187 | ## When to Use This Skill 188 | [When it should activate...] 189 | 190 | [Content under 500 lines...] 191 | ``` 192 | 193 | 3. **Add to skill-rules.json:** 194 | 195 | ```json 196 | { 197 | "my-skill": { 198 | "type": "domain", 199 | "autoInject": true, 200 | "requiredSkills": [], 201 | "description": "Brief description", 202 | "promptTriggers": { 203 | "keywords": ["keyword1", "keyword2"] 204 | } 205 | } 206 | } 207 | ``` 208 | 209 | 4. **Test activation:** 210 | 211 | ```bash 212 | # Manual test 213 | echo '{"session_id":"test","prompt":"mention keywords here"}' | \ 214 | npx tsx .claude/hooks/skill-activation-prompt.ts 215 | ``` 216 | 217 | 5. **Verify in conversation:** 218 | 219 | - Mention trigger keywords in a prompt 220 | - Check for skill in "AUTO-LOADED SKILLS" banner 221 | 222 | **For detailed guidance**, see `skill-developer/SKILL.md` - the skill that guides the skills system. 223 | 224 | ## Skill Best Practices 225 | 226 | ### The 500-Line Rule 227 | 228 | **ALL skill markdown files must stay under 500 lines:** 229 | 230 | - Target: Under 500 lines 231 | - Warning: Only 5 files maximum can breach 500 lines across entire system 232 | - Hard limit: NO file should EVER breach 600 lines 233 | 234 | **Why:** Agents process files linearly. Long files waste tokens and context. Use progressive disclosure instead. 235 | 236 | ### Progressive Disclosure 237 | 238 | Keep main SKILL.md concise, extract details to `resources/`: 239 | 240 | ``` 241 | my-skill/ 242 | ├── SKILL.md # < 500 lines, high-level guidance 243 | └── resources/ 244 | ├── patterns.md # Detailed patterns and practices 245 | └── examples.md # Detailed code examples 246 | ``` 247 | 248 | ### No TOCs or Line Numbers 249 | 250 | **Never include:** 251 | 252 | - ❌ Table of Contents (TOC) - Agents don't benefit, just bloat 253 | - ❌ Line number references - Change too frequently 254 | - ❌ Heading navigation links - Agents scan headings natively 255 | 256 | ### Clear Trigger Keywords 257 | 258 | Define specific, non-overlapping keywords that users naturally mention: 259 | 260 | - **Keywords**: Exact terms users mention (e.g., "python", "git", "api security") 261 | 262 | ### Actionable Content 263 | 264 | Provide practical, actionable guidance: 265 | 266 | - ✅ Working code examples 267 | - ✅ Do's and don'ts 268 | - ✅ Common pitfalls 269 | - ✅ Step-by-step workflows 270 | - ❌ Avoid abstract theory without examples 271 | 272 | ## Troubleshooting 273 | 274 | ### Skills not activating 275 | 276 | 1. **Check trigger configuration in skill-rules.json** 277 | 278 | ```bash 279 | cat .claude/skills/skill-rules.json 280 | ``` 281 | 282 | 2. **Test hook manually:** 283 | 284 | ```bash 285 | echo '{"session_id":"test","prompt":"your test prompt"}' | \ 286 | npx tsx .claude/hooks/skill-activation-prompt.ts 287 | ``` 288 | 289 | 3. **Enable debug mode:** 290 | 291 | ```bash 292 | export CLAUDE_SKILLS_DEBUG=1 293 | ``` 294 | 295 | 4. **Check debug log:** 296 | 297 | ```bash 298 | tail -f .claude/hooks/skill-injection-debug.log 299 | ``` 300 | 301 | ### Skills loading but not helping 302 | 303 | 1. **Check skill content quality** - Is it specific and actionable? 304 | 2. **Check file length** - Under 500 lines? 305 | 3. **Check examples** - Are there working code examples? 306 | 4. **Check structure** - Clear sections with focused guidance? 307 | 308 | ### Performance issues 309 | 310 | 1. **Check cache** - Is intent analysis caching working? 311 | 312 | ```bash 313 | ls -la .cache/intent-analysis/ 314 | ``` 315 | 316 | 2. **Check API key** - Is Anthropic API responding? 317 | 3. **Check file sizes** - Are skill files under 500 lines? 318 | 319 | ### False positives/negatives 320 | 321 | 1. **Adjust confidence thresholds** in `.claude/hooks/lib/constants.ts`: 322 | 323 | ```typescript 324 | export const CONFIDENCE_THRESHOLD = 0.65; 325 | export const SUGGESTED_THRESHOLD = 0.50; 326 | ``` 327 | 328 | 2. **Refine triggers** in skill-rules.json - More specific keywords for better matching 329 | 330 | ## Related Documentation 331 | 332 | - Hook system: `.claude/hooks/README.md` 333 | - Skill creation guide: `skill-developer/resources/skill-creation-guide.md` 334 | - Architecture documentation: `../docs/ARCHITECTURE.md` 335 | - Getting started: `../docs/GETTING-STARTED.md` 336 | - Main project README: `../README.md` 337 | -------------------------------------------------------------------------------- /.claude/hooks/lib/__tests__/skill-filtering.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { 3 | filterUnacknowledgedSkills, 4 | applyInjectionLimits, 5 | filterAndPromoteSkills, 6 | } from '../skill-filtration.js'; 7 | import type { SkillRule } from '../types.js'; 8 | 9 | /** 10 | * Tests for skill filtering and promotion logic 11 | * 12 | * Validates filtering of acknowledged skills, promotion to fill 2-skill target, 13 | * and integration with the acknowledgment system. 14 | */ 15 | 16 | describe('Skill Filtering', () => { 17 | describe('filterUnacknowledgedSkills', () => { 18 | it('should filter out already acknowledged skills', () => { 19 | const skills = ['python-best-practices', 'git-workflow', 'api-security']; 20 | const acknowledged = ['python-best-practices', 'git-workflow']; 21 | const skillRules: Record = { 22 | 'python-best-practices': { type: 'domain' }, 23 | 'git-workflow': { type: 'domain' }, 24 | 'api-security': { type: 'guardrail' }, 25 | }; 26 | 27 | const unacknowledged = filterUnacknowledgedSkills(skills, acknowledged, skillRules); 28 | 29 | expect(unacknowledged).toEqual(['api-security']); 30 | }); 31 | 32 | it('should filter out skills with autoInject: false', () => { 33 | const skills = ['python-best-practices', 'skill-developer', 'api-security']; 34 | const acknowledged: string[] = []; 35 | const skillRules: Record = { 36 | 'python-best-practices': { type: 'domain' }, 37 | 'skill-developer': { 38 | type: 'domain', 39 | autoInject: false, 40 | }, 41 | 'api-security': { type: 'guardrail' }, 42 | }; 43 | 44 | const unacknowledged = filterUnacknowledgedSkills(skills, acknowledged, skillRules); 45 | 46 | expect(unacknowledged).toEqual(['python-best-practices', 'api-security']); 47 | }); 48 | }); 49 | 50 | describe('applyInjectionLimits', () => { 51 | it('should inject up to 2 critical skills when acknowledgedCriticalCount = 0', () => { 52 | const critical = ['skill-a', 'skill-b', 'skill-c']; 53 | const recommended: string[] = []; 54 | const acknowledgedCriticalCount = 0; // No critical skills loaded yet 55 | 56 | const { toInject } = applyInjectionLimits(critical, recommended, acknowledgedCriticalCount); 57 | 58 | expect(toInject).toHaveLength(2); 59 | expect(toInject).toEqual(['skill-a', 'skill-b']); 60 | }); 61 | 62 | it('should promote recommended skills to fill empty slots', () => { 63 | const critical = ['skill-a']; // 1 critical 64 | const recommended = ['skill-b', 'skill-c', 'skill-d']; 65 | const acknowledgedCriticalCount = 0; 66 | 67 | const { toInject, promoted } = applyInjectionLimits( 68 | critical, 69 | recommended, 70 | acknowledgedCriticalCount 71 | ); 72 | 73 | // Should inject 1 critical + 1 promoted = 2 total 74 | expect(toInject).toHaveLength(2); 75 | expect(toInject).toEqual(['skill-a', 'skill-b']); 76 | expect(promoted).toEqual(['skill-b']); 77 | }); 78 | 79 | it('should promote 2 recommended when no critical skills (target = 2)', () => { 80 | const critical: string[] = []; 81 | const recommended = ['skill-a', 'skill-b', 'skill-c']; 82 | const acknowledgedCriticalCount = 0; 83 | 84 | const { toInject, promoted } = applyInjectionLimits( 85 | critical, 86 | recommended, 87 | acknowledgedCriticalCount 88 | ); 89 | 90 | // Should promote 2 recommended to reach target 91 | expect(toInject).toHaveLength(2); 92 | expect(toInject).toEqual(['skill-a', 'skill-b']); 93 | expect(promoted).toEqual(['skill-a', 'skill-b']); 94 | }); 95 | 96 | it('should reduce target when critical skills already acknowledged', () => { 97 | const critical = ['skill-a']; // 1 unacknowledged critical 98 | const recommended = ['skill-b', 'skill-c']; 99 | const acknowledgedCriticalCount = 1; // 1 critical already loaded 100 | 101 | // Target = 2 - 1 = 1 slot available 102 | const { toInject, promoted } = applyInjectionLimits( 103 | critical, 104 | recommended, 105 | acknowledgedCriticalCount 106 | ); 107 | 108 | // Should inject only 1 skill (target = 1) 109 | expect(toInject).toHaveLength(1); 110 | expect(toInject).toEqual(['skill-a']); 111 | expect(promoted).toEqual([]); 112 | }); 113 | 114 | it('should inject 0 skills when 2 critical skills already acknowledged', () => { 115 | const critical: string[] = []; // No unacknowledged critical 116 | const recommended = ['skill-a', 'skill-b']; 117 | const acknowledgedCriticalCount = 2; // 2 critical already loaded (target met) 118 | 119 | // Target = 2 - 2 = 0 slots available 120 | const { toInject, promoted } = applyInjectionLimits( 121 | critical, 122 | recommended, 123 | acknowledgedCriticalCount 124 | ); 125 | 126 | expect(toInject).toEqual([]); 127 | expect(promoted).toEqual([]); 128 | }); 129 | 130 | it('should separate promoted from remaining recommended skills', () => { 131 | const critical = ['skill-a']; 132 | const recommended = ['skill-b', 'skill-c', 'skill-d', 'skill-e']; 133 | const acknowledgedCriticalCount = 0; 134 | 135 | const { toInject, promoted, remainingSuggested } = applyInjectionLimits( 136 | critical, 137 | recommended, 138 | acknowledgedCriticalCount 139 | ); 140 | 141 | expect(toInject).toEqual(['skill-a', 'skill-b']); 142 | expect(promoted).toEqual(['skill-b']); 143 | expect(remainingSuggested).toEqual(['skill-c', 'skill-d', 'skill-e']); 144 | }); 145 | }); 146 | 147 | describe('filterAndPromoteSkills (Integration)', () => { 148 | it('should filter + promote when 1 critical already loaded', () => { 149 | const requiredSkills = ['python-best-practices', 'api-security']; // api-security already loaded 150 | const suggestedSkills = ['git-workflow', 'skill-developer']; 151 | const acknowledged = ['api-security']; 152 | const skillRules: Record = { 153 | 'python-best-practices': { type: 'domain' }, 154 | 'api-security': { type: 'guardrail' }, 155 | 'git-workflow': { type: 'domain' }, 156 | 'skill-developer': { type: 'domain' }, 157 | }; 158 | 159 | const result = filterAndPromoteSkills( 160 | requiredSkills, 161 | suggestedSkills, 162 | acknowledged, 163 | skillRules 164 | ); 165 | 166 | // Target = 2 - 1 (acknowledged critical) = 1 slot 167 | // Should inject python-best-practices only (fills the 1 slot) 168 | expect(result.toInject).toEqual(['python-best-practices']); 169 | expect(result.promoted).toEqual([]); 170 | expect(result.remainingSuggested).toEqual(['git-workflow', 'skill-developer']); 171 | }); 172 | 173 | it('should promote when all critical skills already loaded', () => { 174 | const requiredSkills = ['python-best-practices', 'api-security']; // Both already loaded 175 | const suggestedSkills = ['git-workflow', 'skill-developer']; 176 | const acknowledged = ['python-best-practices', 'api-security']; 177 | const skillRules: Record = { 178 | 'python-best-practices': { type: 'domain' }, 179 | 'api-security': { type: 'guardrail' }, 180 | 'git-workflow': { type: 'domain' }, 181 | 'skill-developer': { type: 'domain' }, 182 | }; 183 | 184 | const result = filterAndPromoteSkills( 185 | requiredSkills, 186 | suggestedSkills, 187 | acknowledged, 188 | skillRules 189 | ); 190 | 191 | // Target = 2 - 2 (acknowledged) = 0 slots 192 | // Should inject nothing (target met) 193 | expect(result.toInject).toEqual([]); 194 | expect(result.promoted).toEqual([]); 195 | expect(result.remainingSuggested).toEqual(['git-workflow', 'skill-developer']); 196 | }); 197 | 198 | it('should promote 2 suggested when no critical skills', () => { 199 | const requiredSkills: string[] = []; 200 | const suggestedSkills = ['python-best-practices', 'api-security', 'git-workflow']; 201 | const acknowledged: string[] = []; 202 | const skillRules: Record = { 203 | 'python-best-practices': { type: 'domain' }, 204 | 'api-security': { type: 'guardrail' }, 205 | 'git-workflow': { type: 'domain' }, 206 | }; 207 | 208 | const result = filterAndPromoteSkills( 209 | requiredSkills, 210 | suggestedSkills, 211 | acknowledged, 212 | skillRules 213 | ); 214 | 215 | // Target = 2, no critical → promote 2 suggested 216 | expect(result.toInject).toEqual(['python-best-practices', 'api-security']); 217 | expect(result.promoted).toEqual(['python-best-practices', 'api-security']); 218 | expect(result.remainingSuggested).toEqual(['git-workflow']); 219 | }); 220 | 221 | it('should handle skills with autoInject: false correctly', () => { 222 | const requiredSkills = ['python-best-practices']; 223 | const suggestedSkills = ['skill-developer', 'git-workflow']; 224 | const acknowledged: string[] = []; 225 | const skillRules: Record = { 226 | 'python-best-practices': { type: 'domain' }, 227 | 'skill-developer': { 228 | type: 'domain', 229 | autoInject: false, // Manual load only 230 | }, 231 | 'git-workflow': { type: 'domain' }, 232 | }; 233 | 234 | const result = filterAndPromoteSkills( 235 | requiredSkills, 236 | suggestedSkills, 237 | acknowledged, 238 | skillRules 239 | ); 240 | 241 | // Should skip skill-developer (autoInject: false) and promote git-workflow 242 | expect(result.toInject).toEqual(['python-best-practices', 'git-workflow']); 243 | expect(result.promoted).toEqual(['git-workflow']); 244 | expect(result.remainingSuggested).toEqual([]); 245 | }); 246 | }); 247 | }); 248 | -------------------------------------------------------------------------------- /.claude/hooks/README.md: -------------------------------------------------------------------------------- 1 | # Claude Code Skills System - Hooks 2 | 3 | This directory contains the hook implementation that enables AI-powered skill auto-activation for Claude Code. 4 | 5 | ## What Are Hooks? 6 | 7 | Hooks are scripts that execute at specific points in Claude Code's workflow: 8 | 9 | - **UserPromptSubmit** - Before Claude sees your prompt 10 | - **PostToolUse** - After Edit/Write/MultiEdit tools complete 11 | - **Stop** - When you stop Claude's response 12 | 13 | ## The Skill Activation System 14 | 15 | ### skill-activation-prompt (UserPromptSubmit Hook) 16 | 17 | **Purpose:** Automatically inject relevant skills into Claude's context based on your prompt using AI-powered intent analysis. 18 | 19 | **How it works:** 20 | 21 | 1. Reads your prompt and conversation history 22 | 1. Uses Claude (configurable model, defaults to Haiku 4.5) to analyze PRIMARY task intent 23 | 1. Assigns confidence scores (0.0-1.0) to each skill 24 | 1. Automatically injects high-confidence skills (>0.65) into context 25 | 1. Suggests medium-confidence skills (0.50-0.65) as optional 26 | 1. Falls back to keyword matching if AI analysis fails 27 | 1. Caches analysis results for 1 hour to improve performance 28 | 29 | **Example:** 30 | 31 | ``` 32 | User: "I need to add a new REST API endpoint" 33 | 34 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 35 | 📚 AUTO-LOADED SKILLS 36 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 37 | 38 | 39 | [Skill content automatically injected...] 40 | 41 | 42 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 43 | ``` 44 | 45 | **Key Features:** 46 | 47 | - **AI-Powered Intent Analysis**: Claude analyzes prompts for accurate skill detection (model configurable via env var) 48 | - **Smart Caching**: 1-hour TTL reduces API costs and improves response time 49 | - **Bidirectional Affinity**: Related skills automatically loaded together 50 | - **Session Tracking**: Skills only injected once per conversation 51 | - **Progressive Disclosure**: Resources separated to maintain 500-line limit 52 | - **Fallback to Keywords**: Keyword matching if AI analysis fails 53 | 54 | **Setup:** 55 | 56 | 1. Install dependencies: 57 | 58 | ```bash 59 | cd .claude/hooks 60 | npm install 61 | ``` 62 | 63 | 2. Create `.env` file with your Anthropic API key: 64 | 65 | ```bash 66 | cp .env.example .env 67 | # Edit .env and add: ANTHROPIC_API_KEY=sk-ant-your-key-here 68 | ``` 69 | 70 | 3. Get API key from https://console.anthropic.com/ 71 | 72 | **Performance:** 73 | 74 | - AI analysis: ~200ms (first call), <10ms (cached) 75 | - Cost: ~$1-2/month at 100 prompts/day (using default Claude Haiku model) 76 | - Model: Configurable via CLAUDE_SKILLS_MODEL env var (defaults to claude-haiku-4-5) 77 | - Cache TTL: 1 hour 78 | - Cache location: `.cache/intent-analysis/` 79 | 80 | **Files:** 81 | 82 | - `skill-activation-prompt.ts` - Main TypeScript orchestration logic 83 | - `lib/` - Core library modules: 84 | - `intent-analyzer.ts` - AI-powered intent analysis coordinator 85 | - `anthropic-client.ts` - Claude API integration 86 | - `intent-scorer.ts` - Confidence scoring and categorization 87 | - `keyword-matcher.ts` - Keyword-based fallback detection 88 | - `skill-filtration.ts` - Filtering and affinity injection 89 | - `skill-resolution.ts` - Dependency resolution 90 | - `skill-state-manager.ts` - Session state persistence 91 | - `output-formatter.ts` - Banner and output formatting 92 | - `cache-manager.ts` - Intent analysis caching 93 | - `constants.ts` - Configuration constants 94 | - `debug-logger.ts` - Debug logging 95 | - `schema-validator.ts` - Runtime validation 96 | - `types.ts` - TypeScript type definitions 97 | - `config/intent-analysis-prompt.txt` - AI analysis prompt template 98 | - `package.json` - NPM dependencies 99 | - `.env.example` - API key template 100 | 101 | ## Architecture 102 | 103 | The skill activation system uses a sophisticated multi-stage pipeline: 104 | 105 | ``` 106 | User Prompt 107 | ↓ 108 | ┌─────────────────────────────────────┐ 109 | │ 1. Intent Analysis │ 110 | │ - AI analysis (Claude) │ 111 | │ - Keyword fallback │ 112 | │ - Cache hit/miss │ 113 | └─────────────────────────────────────┘ 114 | ↓ 115 | ┌─────────────────────────────────────┐ 116 | │ 2. Confidence Scoring │ 117 | │ - Required (>0.65) │ 118 | │ - Suggested (0.50-0.65) │ 119 | └─────────────────────────────────────┘ 120 | ↓ 121 | ┌─────────────────────────────────────┐ 122 | │ 3. Skill Filtration │ 123 | │ - Filter acknowledged skills │ 124 | │ - Apply 2-skill injection limit │ 125 | │ - Promote suggested to fill slots│ 126 | └─────────────────────────────────────┘ 127 | ↓ 128 | ┌─────────────────────────────────────┐ 129 | │ 4. Affinity Injection │ 130 | │ - Load complementary skills │ 131 | │ - Bidirectional (parent↔child) │ 132 | │ - Free of slot cost │ 133 | └─────────────────────────────────────┘ 134 | ↓ 135 | ┌─────────────────────────────────────┐ 136 | │ 5. Dependency Resolution │ 137 | │ - Depth-first search │ 138 | │ - Cycle detection │ 139 | │ - Sort by injectionOrder │ 140 | └─────────────────────────────────────┘ 141 | ↓ 142 | ┌─────────────────────────────────────┐ 143 | │ 6. Skill Injection │ 144 | │ - Read SKILL.md files │ 145 | │ - Wrap in XML tags │ 146 | │ - Output to console │ 147 | └─────────────────────────────────────┘ 148 | ↓ 149 | ┌─────────────────────────────────────┐ 150 | │ 7. State Management │ 151 | │ - Track acknowledged skills │ 152 | │ - Prevent duplicate injection │ 153 | └─────────────────────────────────────┘ 154 | ``` 155 | 156 | ## Hook Configuration 157 | 158 | Hooks are configured through Claude Code's settings. The skill activation hook runs on the UserPromptSubmit event to automatically inject relevant skills based on prompt analysis. 159 | 160 | ## Dependencies 161 | 162 | Install hook dependencies: 163 | 164 | ```bash 165 | cd .claude/hooks 166 | npm install 167 | ``` 168 | 169 | Dependencies (from `package.json`): 170 | 171 | - `@anthropic-ai/sdk` (^0.68.0) - Anthropic API client for AI-powered intent analysis 172 | - `typescript` (^5.3.3) - TypeScript compiler 173 | - `tsx` (^4.7.0) - TypeScript execution 174 | - `@types/node` (^24.10.0) - Node.js type definitions 175 | - `vitest` (^4.0.8) - Testing framework (dev dependency) 176 | 177 | ## Troubleshooting 178 | 179 | ### Hooks not running 180 | 181 | 1. Check hook is executable (if using .sh wrapper): 182 | 183 | ```bash 184 | ls -la .claude/hooks/skill-activation-prompt.* 185 | ``` 186 | 187 | 2. Verify dependencies: 188 | 189 | ```bash 190 | cd .claude/hooks && npm install 191 | ``` 192 | 193 | ### TypeScript errors 194 | 195 | Check TypeScript compilation: 196 | 197 | ```bash 198 | cd .claude/hooks 199 | npx tsc --noEmit 200 | ``` 201 | 202 | ### Skill activation not working 203 | 204 | 1. Verify `skill-rules.json` exists: 205 | 206 | ```bash 207 | cat .claude/skills/skill-rules.json 208 | ``` 209 | 210 | 2. Test hook manually: 211 | 212 | ```bash 213 | echo '{"session_id":"test","prompt":"write python code"}' | \ 214 | npx tsx .claude/hooks/skill-activation-prompt.ts 215 | ``` 216 | 217 | 3. Check for errors in debug log: 218 | 219 | ```bash 220 | cat .claude/hooks/skill-injection-debug.log 221 | ``` 222 | 223 | 4. Enable debug mode: 224 | 225 | ```bash 226 | export CLAUDE_SKILLS_DEBUG=1 227 | ``` 228 | 229 | ### API key issues 230 | 231 | 1. Verify `.env` file exists: 232 | 233 | ```bash 234 | cat .claude/hooks/.env 235 | ``` 236 | 237 | 2. Check API key format starts with `sk-ant-` 238 | 239 | 3. Test API key: 240 | 241 | ```bash 242 | curl -H "x-api-key: $ANTHROPIC_API_KEY" \ 243 | https://api.anthropic.com/v1/messages 244 | ``` 245 | 246 | ### Cache issues 247 | 248 | Clear intent analysis cache: 249 | 250 | ```bash 251 | rm -rf .cache/intent-analysis/ 252 | ``` 253 | 254 | ### Session state issues 255 | 256 | Clear session state: 257 | 258 | ```bash 259 | rm -rf .claude/hooks/state/ 260 | ``` 261 | 262 | ## Performance Tuning 263 | 264 | ### Adjust Confidence Thresholds 265 | 266 | Edit `.claude/hooks/lib/constants.ts`: 267 | 268 | ```typescript 269 | export const CONFIDENCE_THRESHOLD = 0.65; // Skills auto-injected 270 | export const SUGGESTED_THRESHOLD = 0.50; // Skills suggested 271 | ``` 272 | 273 | ### Adjust Injection Limits 274 | 275 | ```typescript 276 | export const MAX_REQUIRED_SKILLS = 2; // Max required skills per prompt 277 | export const MAX_SUGGESTED_SKILLS = 2; // Max suggested skills per prompt 278 | ``` 279 | 280 | ### Adjust Cache TTL 281 | 282 | ```typescript 283 | export const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour 284 | ``` 285 | 286 | ## Testing 287 | 288 | Run the test suite: 289 | 290 | ```bash 291 | cd .claude/hooks 292 | npm test 293 | ``` 294 | 295 | Test coverage includes: 296 | 297 | - Intent analysis with caching 298 | - Skill filtration and affinity injection 299 | - Dependency resolution 300 | - Session state management 301 | - Output formatting 302 | - Schema validation 303 | 304 | ## Debug Mode 305 | 306 | Enable detailed logging: 307 | 308 | ```bash 309 | export CLAUDE_SKILLS_DEBUG=1 310 | ``` 311 | 312 | Logs are written to `.claude/hooks/skill-injection-debug.log` with: 313 | 314 | - Prompt analysis results 315 | - Confidence scores per skill 316 | - Filtration and affinity decisions 317 | - Dependency resolution steps 318 | - Injection outcomes 319 | 320 | ## Adding Custom Hooks 321 | 322 | To add additional hooks for your workflow: 323 | 324 | 1. Create hook script in this directory 325 | 2. Make it executable (if shell script): `chmod +x new-hook.sh` 326 | 3. Configure the hook through Claude Code's settings 327 | 4. Test manually before relying on it 328 | 329 | Example PostToolUse hook to track file edits: 330 | 331 | ```typescript 332 | // post-tool-use-tracker.ts 333 | import * as fs from "fs"; 334 | 335 | const input = JSON.parse(fs.readFileSync(0, "utf-8")); 336 | const filePath = input.arguments?.file_path; 337 | 338 | if (filePath) { 339 | fs.appendFileSync(".claude/cache/edited-files.log", `${filePath}\n`); 340 | } 341 | ``` 342 | 343 | ## Related Documentation 344 | 345 | - Skills system overview: `.claude/skills/README.md` 346 | - Skill rules configuration: `.claude/skills/skill-rules.json` 347 | - Skill creation guide: `.claude/skills/skill-developer/SKILL.md` 348 | - Main project README: `../README.md` 349 | - Architecture documentation: `docs/ARCHITECTURE.md` 350 | -------------------------------------------------------------------------------- /.claude/skills/git-workflow/SKILL.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: git-workflow 3 | description: Git workflow best practices including commit messages, branching strategies, pull requests, and collaboration patterns. Use when working with Git version control. 4 | --- 5 | 6 | # Git Workflow Best Practices 7 | 8 | ## Purpose 9 | 10 | This skill provides guidance on Git workflow best practices to ensure clean commit history, effective collaboration, and maintainable version control in your projects. 11 | 12 | ## When to Use This Skill 13 | 14 | Auto-activates when: 15 | 16 | - Mentions of "git", "commit", "branch", "merge", "pull request" 17 | - Working with version control operations 18 | - Creating or reviewing commits 19 | - Managing branches and releases 20 | 21 | ## Commit Messages 22 | 23 | ### Conventional Commits Format 24 | 25 | Follow the Conventional Commits specification: 26 | 27 | ``` 28 | (): 29 | 30 | 31 | 32 |