Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:37:11 +08:00
commit 20b36ca9b1
56 changed files with 14530 additions and 0 deletions

185
hooks/claude-md-reminder.sh Executable file
View File

@@ -0,0 +1,185 @@
#!/usr/bin/env bash
# UserPromptSubmit hook to periodically re-inject instruction files
# Combats context drift in long-running sessions by re-surfacing project instructions
# Supports: CLAUDE.md, AGENTS.md, RULES.md
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
# Configuration
THROTTLE_INTERVAL=3 # Re-inject every N prompts
INSTRUCTION_FILES=("CLAUDE.md" "AGENTS.md" "RULES.md") # File types to discover
# Use session-specific state file (per-session, not persistent)
# CLAUDE_SESSION_ID should be provided by Claude Code, fallback to PPID for session isolation
SESSION_ID="${CLAUDE_SESSION_ID:-$PPID}"
STATE_FILE="/tmp/claude-instruction-reminder-${SESSION_ID}.state"
CACHE_FILE="/tmp/claude-instruction-reminder-${SESSION_ID}.cache"
# Initialize or read state
if [ -f "$STATE_FILE" ]; then
PROMPT_COUNT=$(cat "$STATE_FILE")
else
PROMPT_COUNT=0
fi
# Increment prompt count
PROMPT_COUNT=$((PROMPT_COUNT + 1))
echo "$PROMPT_COUNT" > "$STATE_FILE"
# Check if we should inject (every THROTTLE_INTERVAL prompts)
if [ $((PROMPT_COUNT % THROTTLE_INTERVAL)) -ne 0 ]; then
# Not time to inject, return empty
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit"
}
}
EOF
exit 0
fi
# Time to inject! Find all instruction files
# Array to store all instruction file paths
declare -a instruction_files=()
# For each file type, discover global, project root, and subdirectories
for file_name in "${INSTRUCTION_FILES[@]}"; do
# 1. Global file (~/.claude/CLAUDE.md, ~/.claude/AGENTS.md, etc.)
global_file="${HOME}/.claude/${file_name}"
if [ -f "$global_file" ]; then
instruction_files+=("$global_file")
fi
# 2. Project root file
if [ -f "${PROJECT_DIR}/${file_name}" ]; then
instruction_files+=("${PROJECT_DIR}/${file_name}")
fi
# 3. All subdirectory files
# Use find to discover files in project tree (exclude hidden dirs and common ignores)
while IFS= read -r -d '' file; do
instruction_files+=("$file")
done < <(find "$PROJECT_DIR" \
-type f -not -type l \
-name "$file_name" \
-not -path "*/\.*" \
-not -path "*/node_modules/*" \
-not -path "*/vendor/*" \
-not -path "*/.venv/*" \
-not -path "*/dist/*" \
-not -path "*/build/*" \
-print0 2>/dev/null)
done
# Remove duplicates (project root might be found twice)
# Use sort -u with proper handling of paths containing spaces/newlines
if [ "${#instruction_files[@]}" -gt 0 ]; then
# Create a temporary file to store paths (one per line)
tmp_file=$(mktemp)
printf '%s\n' "${instruction_files[@]}" | sort -u > "$tmp_file"
# Read back into array
instruction_files=()
while IFS= read -r file; do
[ -n "$file" ] && instruction_files+=("$file")
done < "$tmp_file"
rm -f "$tmp_file"
fi
# Build reminder context
reminder="<instruction-files-reminder>\n"
reminder="${reminder}Re-reading instruction files to combat context drift (prompt ${PROMPT_COUNT}):\n\n"
for file in "${instruction_files[@]}"; do
# Get relative path for display
file_name=$(basename "$file")
if [[ "$file" == "${HOME}/.claude/"* ]]; then
display_path="~/.claude/${file_name} (global)"
else
# Create relative path (cross-platform compatible)
display_path="${file#$PROJECT_DIR/}"
# If the file IS the project dir (no relative path created), just show filename
if [[ "$display_path" == "$file" ]]; then
display_path="$file_name"
fi
fi
# Choose emoji based on file type
case "$file_name" in
CLAUDE.md)
emoji="📋"
;;
AGENTS.md)
emoji="🤖"
;;
RULES.md)
emoji="📜"
;;
*)
emoji="📄"
;;
esac
reminder="${reminder}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
reminder="${reminder}${emoji} ${display_path}\n"
reminder="${reminder}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
# Read entire file content and escape for JSON
# Proper JSON string escaping for all control characters (RFC 8259)
# Uses gsub for reliable cross-platform escaping (works on BSD and GNU awk)
escaped_content=$(awk '
BEGIN { ORS="" }
{
# Order matters: backslash must be escaped first
gsub(/\\/, "\\\\")
gsub(/"/, "\\\"")
gsub(/\t/, "\\t")
gsub(/\r/, "\\r")
gsub(/\f/, "\\f")
# Note: \b (backspace) rarely appears in text files, skip to avoid regex issues
# Add escaped newline between lines
if (NR > 1) printf "\\n"
printf "%s", $0
}
END { printf "\\n" }
' "$file")
reminder="${reminder}${escaped_content}\n\n"
done
reminder="${reminder}</instruction-files-reminder>\n"
# Add agent usage reminder (compact, ~200 tokens)
agent_reminder="<agent-usage-reminder>\n"
agent_reminder="${agent_reminder}CONTEXT CHECK: Before using Glob/Grep/Read chains, consider agents:\n\n"
agent_reminder="${agent_reminder}| Task | Agent |\n"
agent_reminder="${agent_reminder}|------|-------|\n"
agent_reminder="${agent_reminder}| Explore codebase | Explore |\n"
agent_reminder="${agent_reminder}| Multi-file search | Explore |\n"
agent_reminder="${agent_reminder}| Complex research | general-purpose |\n"
agent_reminder="${agent_reminder}| Code review | ring-default:code-reviewer + ring-default:business-logic-reviewer + ring-default:security-reviewer (PARALLEL) |\n"
agent_reminder="${agent_reminder}| Implementation plan | ring-default:write-plan |\n"
agent_reminder="${agent_reminder}| Deep architecture | ring-default:codebase-explorer |\n\n"
agent_reminder="${agent_reminder}**3-File Rule:** If reading >3 files, use an agent instead. 15x more context-efficient.\n"
agent_reminder="${agent_reminder}</agent-usage-reminder>\n"
reminder="${reminder}${agent_reminder}"
# Output hook response with injected context
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "${reminder}"
}
}
EOF
exit 0

83
hooks/detect-solution.sh Executable file
View File

@@ -0,0 +1,83 @@
#!/usr/bin/env bash
# detect-solution.sh - Detect solution confirmation phrases and suggest codify skill
# Called by UserPromptSubmit hook to auto-suggest documentation after fixes
#
# Hook Order: This hook runs AFTER claude-md-reminder.sh in UserPromptSubmit sequence
# See hooks.json for the full hook chain configuration
set -euo pipefail
# Read hook input from stdin (contains user's message)
HOOK_INPUT=$(cat)
# Defense in depth: limit input size to prevent resource exhaustion
MAX_INPUT_SIZE=100000
if [ ${#HOOK_INPUT} -gt $MAX_INPUT_SIZE ]; then
exit 0
fi
# Extract user message from JSON input with proper error handling
USER_MESSAGE=$(echo "$HOOK_INPUT" | jq -r '.userMessage // empty' 2>/dev/null)
JQ_EXIT_CODE=$?
if [[ $JQ_EXIT_CODE -ne 0 ]]; then
echo '{"error": "jq parsing failed", "exitCode": '$JQ_EXIT_CODE'}' >&2
exit 0
fi
# Exit silently if no message
[[ -z "$USER_MESSAGE" ]] && exit 0
# Convert to lowercase for case-insensitive matching
MSG_LOWER=$(echo "$USER_MESSAGE" | tr '[:upper:]' '[:lower:]')
# Detection patterns for solution confirmation
# These phrases typically indicate a problem was just solved
# NOTE: Single-word patterns removed to reduce false positives (M4 fix)
PATTERNS=(
# Explicit confirmation phrases (high confidence)
"that worked"
"it's fixed"
"its fixed"
"fixed it"
"working now"
"problem solved"
"solved it"
"that did it"
"all good now"
"issue resolved"
"finally working"
"works now"
"that was it"
"got it working"
"issue fixed"
# Gratitude with confirmation (high confidence)
"thank you, it works"
"thanks, working"
"thanks, that fixed"
# Note: Removed single-word patterns ("perfect", "awesome", "excellent", "nice", "great")
# and ambiguous short phrases ("looks good", "all good", "good to go")
# to reduce false positives from general positive feedback
)
# Check if any pattern matches
MATCHED=false
for pattern in "${PATTERNS[@]}"; do
if [[ "$MSG_LOWER" == *"$pattern"* ]]; then
MATCHED=true
break
fi
done
# If matched, output suggestion via additionalContext
if [[ "$MATCHED" == "true" ]]; then
cat <<'EOF'
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "<codify-suggestion>\nIt looks like you've solved a problem! Would you like to document this solution for future reference?\n\nRun: /ring-default:codify\nOr say: 'yes, document this' to capture the solution.\n\nThis builds a searchable knowledge base in docs/solutions/ that helps future debugging.\n</codify-suggestion>"
}
}
EOF
fi
exit 0

308
hooks/generate-skills-ref.py Executable file
View File

@@ -0,0 +1,308 @@
#!/usr/bin/env python3
"""
Generate skills quick reference from skill frontmatter.
Scans skills/ directory and extracts metadata from SKILL.md files.
New schema fields:
- name: Skill identifier
- description: WHAT the skill does (method/technique)
- trigger: WHEN to use (specific conditions) - primary decision field
- skip_when: WHEN NOT to use (exclusions) - differentiation field
- sequence.after: Skills that should come before
- sequence.before: Skills that typically follow
- related.similar: Skills that seem similar but differ
- related.complementary: Skills that pair well
"""
import os
import re
import sys
from pathlib import Path
from typing import Dict, List, Optional, Any
# Category patterns for grouping skills
CATEGORIES = {
'Pre-Dev Workflow': [r'^pre-dev-'],
'Testing & Debugging': [r'^test-', r'-debugging$', r'^condition-', r'^defense-', r'^root-cause'],
'Collaboration': [r'-review$', r'^dispatching-', r'^sharing-'],
'Planning & Execution': [r'^brainstorming$', r'^writing-plans$', r'^executing-plans$', r'-worktrees$', r'^subagent-driven'],
'Meta Skills': [r'^using-', r'^writing-skills$', r'^testing-skills', r'^testing-agents'],
}
try:
import yaml
YAML_AVAILABLE = True
except ImportError:
YAML_AVAILABLE = False
print("Warning: pyyaml not installed, using fallback parser", file=sys.stderr)
class Skill:
"""Represents a skill with its metadata."""
def __init__(self, name: str, description: str, directory: str,
trigger: str = "", skip_when: str = "",
sequence: Optional[Dict[str, List[str]]] = None,
related: Optional[Dict[str, List[str]]] = None):
self.name = name
self.description = description
self.directory = directory
self.trigger = trigger
self.skip_when = skip_when
self.sequence = sequence or {}
self.related = related or {}
self.category = self._categorize()
def _categorize(self) -> str:
"""Determine skill category based on directory name."""
for category, patterns in CATEGORIES.items():
for pattern in patterns:
if re.search(pattern, self.directory):
return category
return 'Other'
def __repr__(self):
return f"Skill(name={self.name}, category={self.category})"
def first_line(text: str) -> str:
"""Extract first meaningful line from multi-line text."""
if not text:
return ""
# Remove leading/trailing whitespace, take first line
lines = text.strip().split('\n')
for line in lines:
line = line.strip()
# Skip list markers and empty lines
if line and not line.startswith('-'):
return line
elif line.startswith('- '):
return line[2:] # Return first list item without marker
return lines[0].strip() if lines else ""
def parse_frontmatter_yaml(content: str) -> Optional[Dict[str, Any]]:
"""Parse YAML frontmatter using pyyaml library."""
if not YAML_AVAILABLE:
return None
# Extract frontmatter between --- delimiters
match = re.match(r'^---\s*\n(.*?)\n---\s*\n', content, re.DOTALL)
if not match:
return None
try:
frontmatter = yaml.safe_load(match.group(1))
return frontmatter if isinstance(frontmatter, dict) else None
except yaml.YAMLError as e:
print(f"Warning: YAML parse error: {e}", file=sys.stderr)
return None
def parse_frontmatter_fallback(content: str) -> Optional[Dict[str, Any]]:
"""Fallback parser using regex when pyyaml unavailable.
Handles:
- Simple scalar fields: name, description, trigger, skip_when, when_to_use
- Multi-line block scalars (|) - extracts first meaningful line
- Nested structures: sequence, related - parses sub-fields with arrays
"""
match = re.match(r'^---\s*\n(.*?)\n---\s*\n', content, re.DOTALL)
if not match:
return None
frontmatter_text = match.group(1)
result = {}
# Extract simple/block scalar fields
# Known top-level field names (prevents false matches on "error:" etc in values)
simple_fields = ['name', 'description', 'trigger', 'skip_when', 'when_to_use']
all_fields = simple_fields + ['sequence', 'related']
fields_pattern = '|'.join(all_fields)
for field in simple_fields:
# Match field: value OR field: | followed by indented content
# Capture until next known top-level field or end of frontmatter
# Using explicit field list prevents matching "error:" inside values
pattern = rf'^{field}:\s*\|?\s*\n?(.*?)(?=^(?:{fields_pattern}):|\Z)'
field_match = re.search(pattern, frontmatter_text, re.MULTILINE | re.DOTALL)
if field_match:
raw_value = field_match.group(1).strip()
if raw_value:
# Extract lines, clean indentation
lines = []
for line in raw_value.split('\n'):
cleaned = line.strip()
# Remove list marker prefix for cleaner display
if cleaned.startswith('- '):
cleaned = cleaned[2:]
if cleaned and not cleaned.startswith('#'):
lines.append(cleaned)
if lines:
# For quick reference, use first meaningful line
result[field] = lines[0]
# Handle nested structures: sequence and related
for nested_field in ['sequence', 'related']:
# Match the nested block (indented content under field:)
pattern = rf'^{nested_field}:\s*\n((?:[ \t]+[^\n]*\n?)+)'
nested_match = re.search(pattern, frontmatter_text, re.MULTILINE)
if nested_match:
nested_text = nested_match.group(1)
result[nested_field] = {}
# Parse sub-fields: after, before, similar, complementary
# Format: subfield: [item1, item2] or subfield: [item1]
subfields = ['after', 'before', 'similar', 'complementary']
for subfield in subfields:
# Match: subfield: [contents]
sub_pattern = rf'^\s*{subfield}:\s*\[([^\]]*)\]'
sub_match = re.search(sub_pattern, nested_text, re.MULTILINE)
if sub_match:
items_str = sub_match.group(1)
# Parse comma-separated items, strip whitespace
items = [s.strip() for s in items_str.split(',') if s.strip()]
if items:
result[nested_field][subfield] = items
# Remove empty nested dicts
if not result[nested_field]:
del result[nested_field]
return result if result else None
def parse_skill_file(skill_path: Path) -> Optional[Skill]:
"""Parse a SKILL.md file and extract metadata."""
try:
with open(skill_path, 'r', encoding='utf-8') as f:
content = f.read()
# Try YAML parser first, fall back to regex
frontmatter = parse_frontmatter_yaml(content)
if not frontmatter:
frontmatter = parse_frontmatter_fallback(content)
if not frontmatter or 'name' not in frontmatter:
print(f"Warning: Missing name in {skill_path}", file=sys.stderr)
return None
# Handle backward compatibility: use when_to_use as trigger if trigger not set
trigger = frontmatter.get('trigger', '')
if not trigger:
trigger = frontmatter.get('when_to_use', '')
if not trigger:
# Fall back to description for old-style skills
trigger = frontmatter.get('description', '')
# Get description - prefer dedicated description field
description = frontmatter.get('description', '')
directory = skill_path.parent.name
return Skill(
name=frontmatter['name'],
description=description,
directory=directory,
trigger=trigger,
skip_when=frontmatter.get('skip_when', ''),
sequence=frontmatter.get('sequence', {}),
related=frontmatter.get('related', {})
)
except Exception as e:
print(f"Warning: Error parsing {skill_path}: {e}", file=sys.stderr)
return None
def scan_skills_directory(skills_dir: Path) -> List[Skill]:
"""Scan skills directory and parse all SKILL.md files."""
skills = []
if not skills_dir.exists():
print(f"Error: Skills directory not found: {skills_dir}", file=sys.stderr)
return skills
for skill_dir in sorted(skills_dir.iterdir()):
if not skill_dir.is_dir():
continue
skill_file = skill_dir / 'SKILL.md'
if not skill_file.exists():
print(f"Warning: No SKILL.md in {skill_dir.name}", file=sys.stderr)
continue
skill = parse_skill_file(skill_file)
if skill:
skills.append(skill)
return skills
def generate_markdown(skills: List[Skill]) -> str:
"""Generate markdown quick reference from skills list.
New format is decision-focused:
- Shows trigger (WHEN to use) as primary decision criteria
- Shows skip_when to differentiate from similar skills
- Shows sequence for workflow ordering
"""
if not skills:
return "# Ring Skills Quick Reference\n\n**No skills found.**\n"
# Group skills by category
categorized: Dict[str, List[Skill]] = {}
for skill in skills:
category = skill.category
if category not in categorized:
categorized[category] = []
categorized[category].append(skill)
# Sort categories (predefined order, then Other)
category_order = list(CATEGORIES.keys()) + ['Other']
sorted_categories = [cat for cat in category_order if cat in categorized]
# Build markdown
lines = ['# Ring Skills Quick Reference\n']
for category in sorted_categories:
category_skills = categorized[category]
lines.append(f'## {category} ({len(category_skills)} skills)\n')
for skill in sorted(category_skills, key=lambda s: s.name):
# Skill name and description
lines.append(f'- **{skill.name}**: {first_line(skill.description)}')
lines.append('') # Blank line between categories
# Add usage section
lines.append('## Usage\n')
lines.append('To use a skill: Use the Skill tool with skill name')
lines.append('Example: `ring-default:brainstorming`')
return '\n'.join(lines)
def main():
"""Main entry point."""
# Determine plugin root (parent of hooks directory)
script_dir = Path(__file__).parent.resolve()
plugin_root = script_dir.parent
skills_dir = plugin_root / 'skills'
# Scan and parse skills
skills = scan_skills_directory(skills_dir)
if not skills:
print("Error: No valid skills found", file=sys.stderr)
sys.exit(1)
# Generate and output markdown
markdown = generate_markdown(skills)
print(markdown)
# Report statistics to stderr
print(f"Generated reference for {len(skills)} skills", file=sys.stderr)
if __name__ == '__main__':
main()

207
hooks/generate-skills-ref.sh Executable file
View File

@@ -0,0 +1,207 @@
#!/usr/bin/env bash
# Fallback skill reference generator when Python is unavailable
# Requires bash 3.2+ (uses [[ ]], ${BASH_SOURCE}, ${var:0:n})
# Tools used: sed, awk, grep (standard on macOS/Linux/Git Bash)
#
# This script provides a degraded but functional skills quick reference
# when Python or PyYAML are not available on the system.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
SKILLS_DIR="${PLUGIN_ROOT}/skills"
# Parse a single field from YAML frontmatter
# Uses the proven sed pattern from ralph-wiggum/hooks/stop-hook.sh
extract_field() {
local frontmatter="$1"
local field="$2"
# For simple fields: fieldname: value
# For block scalars: fieldname: | followed by indented lines
echo "$frontmatter" | awk -v field="$field" '
BEGIN { found = 0; value = "" }
# Match the field we want
$0 ~ "^" field ":" {
found = 1
# Check for inline value (not block scalar)
sub("^" field ":[[:space:]]*\\|?[[:space:]]*", "")
if (length($0) > 0 && $0 !~ /^\|[[:space:]]*$/) {
value = $0
exit
}
next
}
# If we found our field and this line is indented, capture it
found && /^[[:space:]]+[^[:space:]]/ {
gsub(/^[[:space:]]+/, "")
gsub(/[[:space:]]+$/, "")
# Skip list markers for cleaner output
gsub(/^-[[:space:]]+/, "")
if (length($0) > 0 && value == "") {
value = $0
exit
}
}
# If we hit another field definition, stop
found && /^[a-z_]+:/ && $0 !~ "^" field ":" {
exit
}
END { print value }
'
}
# Parse YAML frontmatter from SKILL.md
parse_skill() {
local skill_file="$1"
local skill_dir
skill_dir=$(basename "$(dirname "$skill_file")")
# Skip shared-patterns directory
if [[ "$skill_dir" == "shared-patterns" ]]; then
return
fi
# Extract frontmatter between --- delimiters
# Pattern proven portable in ralph-wiggum/hooks/stop-hook.sh
local frontmatter
frontmatter=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$skill_file" 2>/dev/null) || return
if [[ -z "$frontmatter" ]]; then
echo "Warning: No frontmatter in $skill_file" >&2
return
fi
# Extract fields
local name description trigger
name=$(extract_field "$frontmatter" "name")
description=$(extract_field "$frontmatter" "description")
trigger=$(extract_field "$frontmatter" "trigger")
# Fallback: use when_to_use if trigger not set (backward compat)
if [[ -z "$trigger" ]]; then
trigger=$(extract_field "$frontmatter" "when_to_use")
fi
# Use directory name if name field missing
if [[ -z "$name" ]]; then
name="$skill_dir"
fi
# Default description if missing
if [[ -z "$description" ]]; then
description="(no description)"
fi
# Truncate long descriptions for quick reference
if [[ ${#description} -gt 100 ]]; then
description="${description:0:97}..."
fi
# Output as TSV for reliable parsing (dir, name, description, trigger)
printf '%s\t%s\t%s\t%s\n' "$skill_dir" "$name" "$description" "$trigger"
}
# Categorize skill based on directory name
categorize_skill() {
local dir="$1"
case "$dir" in
pre-dev-*) echo "Pre-Dev Workflow" ;;
test-*|*-debugging|condition-*|defense-*|root-cause*) echo "Testing & Debugging" ;;
*-review|dispatching-*|sharing-*) echo "Collaboration" ;;
brainstorming|writing-plans|executing-plans|*-worktrees|subagent-driven*) echo "Planning & Execution" ;;
using-*|writing-skills|testing-skills*|testing-agents*) echo "Meta Skills" ;;
*) echo "Other" ;;
esac
}
# Generate markdown output
generate_markdown() {
echo "# Ring Skills Quick Reference"
echo ""
echo "> **Note:** Python unavailable. Using bash fallback parser."
echo "> Install Python + PyYAML for full output with categories."
echo ""
local skill_count=0
local current_category=""
# Sort by category, then by name
while IFS=$'\t' read -r dir name desc trigger; do
local category
category=$(categorize_skill "$dir")
# Print category header if changed
if [[ "$category" != "$current_category" ]]; then
if [[ -n "$current_category" ]]; then
echo ""
fi
echo "## $category"
echo ""
current_category="$category"
fi
# Combine description with trigger hint if available
local display_desc="$desc"
if [[ -n "$trigger" && "$trigger" != "$desc" ]]; then
display_desc="$trigger"
fi
echo "- **${name}**: ${display_desc}"
skill_count=$((skill_count + 1))
done
echo ""
echo "## Usage"
echo ""
echo "To use a skill: Use the Skill tool with skill name"
echo "Example: \`ring-default:brainstorming\`"
# Output stats to stderr (like Python version)
echo "" >&2
echo "Generated reference for ${skill_count} skills (bash fallback)" >&2
}
# Main execution
main() {
if [[ ! -d "$SKILLS_DIR" ]]; then
echo "Error: Skills directory not found: $SKILLS_DIR" >&2
exit 1
fi
# Collect all skills with categories, then sort and generate markdown
local tmpfile
tmpfile=$(mktemp)
chmod 600 "$tmpfile" # Restrict permissions for security
trap "rm -f '$tmpfile'" EXIT INT TERM HUP
for skill_dir in "$SKILLS_DIR"/*/; do
# Skip if not a directory
[[ -d "$skill_dir" ]] || continue
local skill_file="${skill_dir}SKILL.md"
if [[ -f "$skill_file" ]]; then
local skill_line
skill_line=$(parse_skill "$skill_file")
if [[ -n "$skill_line" ]]; then
# Add category as first field for sorting
local dir name desc trigger cat
IFS=$'\t' read -r dir name desc trigger <<< "$skill_line"
cat=$(categorize_skill "$dir")
printf '%s\t%s\t%s\t%s\t%s\n' "$cat" "$dir" "$name" "$desc" "$trigger" >> "$tmpfile"
fi
else
echo "Warning: No SKILL.md in $(basename "$skill_dir")" >&2
fi
done
# Sort by category, then by name, remove category column, generate markdown
sort -t$'\t' -k1,1 -k3,3 "$tmpfile" | cut -f2- | generate_markdown
}
main "$@"

42
hooks/hooks.json Normal file
View File

@@ -0,0 +1,42 @@
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh"
}
]
},
{
"matcher": "clear|compact",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh"
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/claude-md-reminder.sh"
}
]
},
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/detect-solution.sh"
}
]
}
]
}
}

171
hooks/session-start.sh Executable file
View File

@@ -0,0 +1,171 @@
#!/usr/bin/env bash
# Enhanced SessionStart hook for ring plugin
# Provides comprehensive skill overview and status
set -euo pipefail
# Determine plugin root directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# Auto-update Ring marketplace and plugins
marketplace_updated="false"
if command -v claude &> /dev/null && command -v git &> /dev/null; then
# Detect marketplace path (common locations)
marketplace_path=""
for path in ~/.claude/plugins/marketplaces/ring ~/.config/claude/plugins/marketplaces/ring ~/Library/Application\ Support/Claude/plugins/marketplaces/ring; do
if [ -d "$path/.git" ]; then
marketplace_path="$path"
break
fi
done
if [ -n "$marketplace_path" ]; then
# Get current commit hash before update
before_hash=$(git -C "$marketplace_path" rev-parse HEAD 2>/dev/null || echo "none")
# Update marketplace
claude plugin marketplace update ring &> /dev/null || true
# Get commit hash after update
after_hash=$(git -C "$marketplace_path" rev-parse HEAD 2>/dev/null || echo "none")
# If hashes differ, marketplace was actually updated
if [ "$before_hash" != "$after_hash" ] && [ "$after_hash" != "none" ]; then
marketplace_updated="true"
# Reinstall all plugins to get new versions
claude plugin install ring-default &> /dev/null || true
claude plugin install ring-dev-team &> /dev/null || true
claude plugin install ring-finops-team &> /dev/null || true
claude plugin install ring-pm-team &> /dev/null || true
claude plugin install ralph-wiggum &> /dev/null || true
fi
else
# Marketplace not found, just run updates silently
claude plugin marketplace update ring &> /dev/null || true
claude plugin install ring-default &> /dev/null || true
claude plugin install ring-dev-team &> /dev/null || true
claude plugin install ring-finops-team &> /dev/null || true
claude plugin install ralph-wiggum &> /dev/null || true
fi
fi
# Auto-install PyYAML if Python is available but PyYAML is not
if command -v python3 &> /dev/null; then
if ! python3 -c "import yaml" &> /dev/null 2>&1; then
# PyYAML not installed, try to install it
# Try different pip commands (pip3 preferred, then pip)
for pip_cmd in pip3 pip; do
if command -v "$pip_cmd" &> /dev/null; then
# Strategy: Try --user first, then --user --break-system-packages
# (--break-system-packages only exists in pip 22.1+, needed for PEP 668)
if "$pip_cmd" install --quiet --user 'PyYAML>=6.0,<7.0' &> /dev/null 2>&1; then
echo "PyYAML installed successfully" >&2
break
elif "$pip_cmd" install --quiet --user --break-system-packages 'PyYAML>=6.0,<7.0' &> /dev/null 2>&1; then
echo "PyYAML installed successfully (with --break-system-packages)" >&2
break
fi
fi
done
# If all installation attempts fail, generate-skills-ref.py will use fallback parser
# (No error message needed - the Python script already warns about missing PyYAML)
fi
fi
# Critical rules that MUST survive compact (injected directly, not via skill file)
# These are the most-violated rules that need to be in immediate context
CRITICAL_RULES='## ⛔ ORCHESTRATOR CRITICAL RULES (SURVIVE COMPACT)
**3-FILE RULE: HARD GATE**
DO NOT read/edit >3 files directly. This is a PROHIBITION.
- >3 files → STOP. Launch specialist agent. DO NOT proceed manually.
- Already touched 3 files? → At gate. Dispatch agent NOW.
**AUTO-TRIGGER PHRASES → MANDATORY AGENT:**
- "fix issues/remaining/findings" → Launch specialist agent
- "apply fixes", "fix the X issues" → Launch specialist agent
- "find where", "search for", "understand how" → Launch Explore agent
**If you think "this task is small" or "I can handle 5 files":**
WRONG. Count > 3 = agent. No exceptions. Task size is irrelevant.
**Full rules:** Use Skill tool with "ring-default:using-ring" if needed.
'
# Generate skills overview with cascading fallback
# Priority: Python+PyYAML > Python regex > Bash fallback > Error message
generate_skills_overview() {
local python_cmd=""
# Try python3 first, then python
for cmd in python3 python; do
if command -v "$cmd" &> /dev/null; then
python_cmd="$cmd"
break
fi
done
if [[ -n "$python_cmd" ]]; then
# Python available - use Python script (handles PyYAML fallback internally)
"$python_cmd" "${SCRIPT_DIR}/generate-skills-ref.py" 2>&1
return $?
fi
# Python not available - try bash fallback
if [[ -x "${SCRIPT_DIR}/generate-skills-ref.sh" ]]; then
echo "Note: Python unavailable, using bash fallback" >&2
"${SCRIPT_DIR}/generate-skills-ref.sh" 2>&1
return $?
fi
# Ultimate fallback - minimal useful output
echo "# Ring Skills Quick Reference"
echo ""
echo "**Note:** Neither Python nor bash fallback available."
echo "Skills are still accessible via the Skill tool."
echo ""
echo "Run: \`Skill tool: ring-default:using-ring\` to see available workflows."
echo ""
echo "To fix: Install Python 3.x or ensure generate-skills-ref.sh is executable."
}
skills_overview=$(generate_skills_overview || echo "Error generating skills quick reference")
# Check jq availability (required for JSON escaping)
if ! command -v jq &>/dev/null; then
echo "Error: jq is required for JSON escaping but not found" >&2
echo "Install with: brew install jq (macOS) or apt install jq (Linux)" >&2
exit 1
fi
# Escape outputs for JSON using jq for RFC 8259 compliant escaping
# Note: jq is required - commonly pre-installed on macOS/Linux, install via package manager if missing
# The -Rs flags: -R (raw input, don't parse as JSON), -s (slurp entire input into single string)
# jq -Rs outputs a properly quoted JSON string including surrounding quotes, so we strip them
# Note: using-ring content is already included in skills_overview via generate-skills-ref.py
overview_escaped=$(echo "$skills_overview" | jq -Rs . | sed 's/^"//;s/"$//' || echo "$skills_overview")
critical_rules_escaped=$(echo "$CRITICAL_RULES" | jq -Rs . | sed 's/^"//;s/"$//' || echo "$CRITICAL_RULES")
# Build JSON output - include update notification if marketplace was updated
if [ "$marketplace_updated" = "true" ]; then
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "<ring-marketplace-updated>\nThe Ring marketplace was just updated to a new version. New skills and agents have been installed but won't be available until the session is restarted. Inform the user they should restart their session (type 'clear' or restart Claude Code) to load the new capabilities.\n</ring-marketplace-updated>\n\n<ring-critical-rules>\n${critical_rules_escaped}\n</ring-critical-rules>\n\n<ring-skills-system>\n${overview_escaped}\n</ring-skills-system>"
}
}
EOF
else
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "<ring-critical-rules>\n${critical_rules_escaped}\n</ring-critical-rules>\n\n<ring-skills-system>\n${overview_escaped}\n</ring-skills-system>"
}
}
EOF
fi
exit 0