Initial commit
This commit is contained in:
53
hooks/utils/context-query.sh
Executable file
53
hooks/utils/context-query.sh
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTEXT_DIR="$(dirname "$0")/../context"
|
||||
LOG_FILE="$CONTEXT_DIR/edit-log.txt"
|
||||
|
||||
# Get files edited since timestamp
|
||||
get_recent_edits() {
|
||||
local since="${1:-}"
|
||||
|
||||
if [ ! -f "$LOG_FILE" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -z "$since" ]; then
|
||||
cat "$LOG_FILE" 2>/dev/null || true
|
||||
else
|
||||
awk -v since="$since" -F '|' '$1 >= since' "$LOG_FILE" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# Get unique files edited in current session
|
||||
get_session_files() {
|
||||
local session_start="${1:-}"
|
||||
|
||||
get_recent_edits "$session_start" | \
|
||||
awk -F '|' '{gsub(/^[ \t]+|[ \t]+$/, "", $4); print $4}' | \
|
||||
sort -u
|
||||
}
|
||||
|
||||
# Check if specific file was edited
|
||||
was_file_edited() {
|
||||
local file_path="$1"
|
||||
local since="${2:-}"
|
||||
|
||||
get_recent_edits "$since" | grep -q "$(printf '%q' "$file_path")" 2>/dev/null
|
||||
}
|
||||
|
||||
# Get edit count by repo
|
||||
get_repo_stats() {
|
||||
local since="${1:-}"
|
||||
|
||||
get_recent_edits "$since" | \
|
||||
awk -F '|' '{gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2}' | \
|
||||
sort | uniq -c | sort -rn
|
||||
}
|
||||
|
||||
# Clear log (for testing)
|
||||
clear_log() {
|
||||
if [ -f "$LOG_FILE" ]; then
|
||||
> "$LOG_FILE"
|
||||
fi
|
||||
}
|
||||
105
hooks/utils/format-output.sh
Executable file
105
hooks/utils/format-output.sh
Executable file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
check_dependencies() {
|
||||
local missing=()
|
||||
command -v jq >/dev/null 2>&1 || missing+=("jq")
|
||||
|
||||
if [ ${#missing[@]} -gt 0 ]; then
|
||||
echo "ERROR: Missing required dependencies: ${missing[*]}" >&2
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
check_dependencies || exit 1
|
||||
|
||||
# Get priority emoji for visual distinction
|
||||
get_priority_emoji() {
|
||||
local priority="$1"
|
||||
case "$priority" in
|
||||
"critical") echo "🔴" ;;
|
||||
"high") echo "⭐" ;;
|
||||
"medium") echo "📌" ;;
|
||||
"low") echo "💡" ;;
|
||||
*) echo "•" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Format skill activation reminder
|
||||
# Usage: format_skill_reminder <rules_path> <skill_name1> [<skill_name2> ...]
|
||||
format_skill_reminder() {
|
||||
local rules_path="$1"
|
||||
shift
|
||||
local skills=("$@")
|
||||
|
||||
if [ ${#skills[@]} -eq 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "⚠️ SKILL ACTIVATION REMINDER"
|
||||
echo ""
|
||||
echo "The following skills may apply to your current task:"
|
||||
echo ""
|
||||
|
||||
for skill in "${skills[@]}"; do
|
||||
local priority=$(jq -r --arg skill "$skill" '.[$skill].priority // "medium"' "$rules_path")
|
||||
local emoji=$(get_priority_emoji "$priority")
|
||||
local skill_type=$(jq -r --arg skill "$skill" '.[$skill].type // "workflow"' "$rules_path")
|
||||
|
||||
echo "$emoji $skill ($skill_type, $priority priority)"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "📖 Use the Skill tool to activate: Skill command=\"hyperpowers:$skill\""
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Format gentle reminders for common workflow steps
|
||||
format_gentle_reminder() {
|
||||
local reminder_type="$1"
|
||||
|
||||
case "$reminder_type" in
|
||||
"tdd")
|
||||
cat <<'EOF'
|
||||
💭 Remember: Test-Driven Development (TDD)
|
||||
|
||||
Before writing implementation code:
|
||||
1. RED: Write the test first, watch it fail
|
||||
2. GREEN: Write minimal code to pass
|
||||
3. REFACTOR: Clean up while keeping tests green
|
||||
|
||||
Why? The failure proves your test actually tests something!
|
||||
EOF
|
||||
;;
|
||||
|
||||
"verification")
|
||||
cat <<'EOF'
|
||||
✅ Before claiming work is complete:
|
||||
|
||||
1. Run verification commands (tests, lints, builds)
|
||||
2. Capture output as evidence
|
||||
3. Only claim success if verification passes
|
||||
|
||||
Evidence before assertions, always.
|
||||
EOF
|
||||
;;
|
||||
|
||||
"testing-anti-patterns")
|
||||
cat <<'EOF'
|
||||
⚠️ Common Testing Anti-Patterns:
|
||||
|
||||
• Testing mock behavior instead of real behavior
|
||||
• Adding test-only methods to production code
|
||||
• Mocking without understanding dependencies
|
||||
|
||||
Test the real thing, not the test double!
|
||||
EOF
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unknown reminder type: $reminder_type"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
142
hooks/utils/skill-matcher.sh
Executable file
142
hooks/utils/skill-matcher.sh
Executable file
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
check_dependencies() {
|
||||
local missing=()
|
||||
command -v jq >/dev/null 2>&1 || missing+=("jq")
|
||||
command -v grep >/dev/null 2>&1 || missing+=("grep")
|
||||
|
||||
if [ ${#missing[@]} -gt 0 ]; then
|
||||
echo "ERROR: Missing required dependencies: ${missing[*]}" >&2
|
||||
echo "Please install missing tools and try again." >&2
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
check_dependencies || exit 1
|
||||
|
||||
# Load and validate skill-rules.json
|
||||
load_skill_rules() {
|
||||
local rules_path="$1"
|
||||
|
||||
if [ -z "$rules_path" ]; then
|
||||
echo "ERROR: No rules path provided" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$rules_path" ]; then
|
||||
echo "ERROR: Rules file not found: $rules_path" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! jq . "$rules_path" 2>/dev/null; then
|
||||
echo "ERROR: Invalid JSON in $rules_path" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Match keywords (case-insensitive substring matching)
|
||||
match_keywords() {
|
||||
local text="$1"
|
||||
local keywords="$2"
|
||||
|
||||
if [ -z "$text" ] || [ -z "$keywords" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local lower_text=$(echo "$text" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
IFS=',' read -ra KEYWORD_ARRAY <<< "$keywords"
|
||||
for keyword in "${KEYWORD_ARRAY[@]}"; do
|
||||
local lower_keyword=$(echo "$keyword" | tr '[:upper:]' '[:lower:]' | xargs)
|
||||
if [[ "$lower_text" == *"$lower_keyword"* ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Match regex patterns (case-insensitive)
|
||||
match_patterns() {
|
||||
local text="$1"
|
||||
local patterns="$2"
|
||||
|
||||
if [ -z "$text" ] || [ -z "$patterns" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Use bash regex matching for performance (no external process spawning)
|
||||
local lower_text=$(echo "$text" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
IFS=',' read -ra PATTERN_ARRAY <<< "$patterns"
|
||||
for pattern in "${PATTERN_ARRAY[@]}"; do
|
||||
pattern=$(echo "$pattern" | xargs | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
# Use bash's built-in regex matching (much faster than spawning grep)
|
||||
if [[ "$lower_text" =~ $pattern ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Find matching skills from prompt
|
||||
# Returns JSON array of skill names, sorted by priority
|
||||
find_matching_skills() {
|
||||
local prompt="$1"
|
||||
local rules_path="$2"
|
||||
local max_skills="${3:-3}"
|
||||
|
||||
if [ -z "$prompt" ] || [ -z "$rules_path" ]; then
|
||||
echo "[]"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! load_skill_rules "$rules_path" >/dev/null; then
|
||||
echo "[]"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Load all skill data in one jq call for performance
|
||||
local skill_data=$(jq -r '
|
||||
to_entries |
|
||||
map(select(.key != "_comment" and .key != "_schema")) |
|
||||
map({
|
||||
name: .key,
|
||||
priority: .value.priority,
|
||||
keywords: (.value.promptTriggers.keywords | join(",")),
|
||||
patterns: (.value.promptTriggers.intentPatterns | join(","))
|
||||
}) |
|
||||
.[] |
|
||||
"\(.name)|\(.priority)|\(.keywords)|\(.patterns)"
|
||||
' "$rules_path")
|
||||
|
||||
local matches=()
|
||||
|
||||
while IFS='|' read -r skill priority keywords patterns; do
|
||||
# Check if keywords or patterns match
|
||||
if match_keywords "$prompt" "$keywords" || match_patterns "$prompt" "$patterns"; then
|
||||
matches+=("$priority:$skill")
|
||||
fi
|
||||
done <<< "$skill_data"
|
||||
|
||||
# Sort by priority (critical > high > medium > low) and limit to max_skills
|
||||
if [ ${#matches[@]} -eq 0 ]; then
|
||||
echo "[]"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Sort and format as JSON array
|
||||
printf '%s\n' "${matches[@]}" | \
|
||||
sed 's/^critical:/0:/; s/^high:/1:/; s/^medium:/2:/; s/^low:/3:/' | \
|
||||
sort -t: -k1,1n | \
|
||||
head -n "$max_skills" | \
|
||||
cut -d: -f2- | \
|
||||
jq -R . | \
|
||||
jq -s .
|
||||
}
|
||||
60
hooks/utils/test-performance.sh
Executable file
60
hooks/utils/test-performance.sh
Executable file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
source utils/skill-matcher.sh
|
||||
|
||||
echo "=== Performance Tests ==="
|
||||
echo ""
|
||||
|
||||
# Test 1: match_keywords performance (<50ms)
|
||||
echo "Test 1: match_keywords performance"
|
||||
prompt="I want to write a test for the login function"
|
||||
keywords="test,testing,TDD,spec,unit test"
|
||||
|
||||
start=$(date +%s%N)
|
||||
for i in {1..10}; do
|
||||
match_keywords "$prompt" "$keywords" >/dev/null
|
||||
done
|
||||
end=$(date +%s%N)
|
||||
|
||||
duration_ns=$((end - start))
|
||||
duration_ms=$((duration_ns / 1000000 / 10))
|
||||
|
||||
echo " Duration: ${duration_ms}ms (target: <50ms)"
|
||||
if [ $duration_ms -lt 50 ]; then
|
||||
echo " ✓ PASS"
|
||||
else
|
||||
echo " ✗ FAIL"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Test 2: find_matching_skills performance (<1000ms acceptable for 113 patterns)
|
||||
echo "Test 2: find_matching_skills performance"
|
||||
prompt="I want to implement a new feature with TDD"
|
||||
rules_path="skill-rules.json"
|
||||
|
||||
start=$(date +%s%N)
|
||||
result=$(find_matching_skills "$prompt" "$rules_path" 3)
|
||||
end=$(date +%s%N)
|
||||
|
||||
duration_ns=$((end - start))
|
||||
duration_ms=$((duration_ns / 1000000))
|
||||
|
||||
echo " Duration: ${duration_ms}ms (target: <1000ms for 19 skills, 113 patterns)"
|
||||
echo " Matches found: $(echo "$result" | jq 'length')"
|
||||
if [ $duration_ms -lt 1000 ]; then
|
||||
echo " ✓ PASS"
|
||||
else
|
||||
echo " ✗ FAIL - Performance degradation detected"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Note: 113 regex patterns × 19 skills with bash regex matching
|
||||
# Typical user prompts are 10-50 words, matching completes in <600ms
|
||||
# This is acceptable for a user-prompt-submit hook (runs once per prompt)
|
||||
|
||||
echo ""
|
||||
echo "=== All Performance Tests Passed ==="
|
||||
Reference in New Issue
Block a user