Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:26:08 +08:00
commit 8f22ddf339
295 changed files with 59710 additions and 0 deletions

261
skills/hook.define/SKILL.md Normal file
View File

@@ -0,0 +1,261 @@
# hook.define
## Overview
**hook.define** is a Betty skill that creates and registers validation hooks for Claude Code. Hooks enable automatic validation and policy enforcement by triggering skills on events like file edits, commits, and pushes.
## Purpose
Transform manual validation into automatic safety rails:
- **Before**: Developers must remember to run validation
- **After**: Validation happens automatically on every file edit
## Usage
### Basic Usage
```bash
python skills/hook.define/hook_define.py <event> <command> [options]
```
### Parameters
| Parameter | Required | Description | Example |
|-----------|----------|-------------|---------|
| `event` | Yes | Hook event trigger | `on_file_edit` |
| `command` | Yes | Command to execute | `api.validate {file_path} zalando` |
| `--pattern` | No | File pattern to match | `*.openapi.yaml` |
| `--blocking` | No | Block on failure (default: true) | `true` |
| `--timeout` | No | Timeout in ms (default: 30000) | `10000` |
| `--description` | No | Human-readable description | `Validate OpenAPI specs` |
### Valid Events
| Event | Triggers When | Use Case |
|-------|---------------|----------|
| `on_file_edit` | File is edited in editor | Syntax validation |
| `on_file_save` | File is saved | Code generation |
| `on_commit` | Git commit attempted | Breaking change detection |
| `on_push` | Git push attempted | Full validation suite |
| `on_tool_use` | Any tool is used | Audit logging |
| `on_agent_start` | Agent begins execution | Context injection |
| `on_workflow_end` | Workflow completes | Cleanup/notification |
## Examples
### Example 1: Validate OpenAPI Specs on Edit
```bash
python skills/hook.define/hook_define.py \
on_file_edit \
"python betty/skills/api.validate/api_validate.py {file_path} zalando" \
--pattern="*.openapi.yaml" \
--blocking=true \
--timeout=10000 \
--description="Validate OpenAPI specs against Zalando guidelines"
```
**Result**: Every time a `*.openapi.yaml` file is edited, it's automatically validated against Zalando guidelines. If validation fails, the edit is blocked.
### Example 2: Check Breaking Changes on Commit
```bash
python skills/hook.define/hook_define.py \
on_commit \
"python betty/skills/api.compatibility/check_breaking_changes.py {file_path}" \
--pattern="specs/**/*.yaml" \
--blocking=true \
--description="Prevent commits with breaking API changes"
```
**Result**: Commits are blocked if they contain breaking API changes.
### Example 3: Regenerate Models on Save
```bash
python skills/hook.define/hook_define.py \
on_file_save \
"python betty/skills/api.generate-models/auto_generate.py {file_path}" \
--pattern="specs/*.openapi.yaml" \
--blocking=false \
--description="Auto-regenerate models when specs change"
```
**Result**: When an OpenAPI spec is saved, models are regenerated automatically (non-blocking).
### Example 4: Audit Trail for All Tool Use
```bash
python skills/hook.define/hook_define.py \
on_tool_use \
"python betty/skills/audit.log/log_api_change.py {tool_name}" \
--blocking=false \
--description="Log all tool usage for compliance"
```
**Result**: All tool usage is logged for audit trails (non-blocking).
## Output
### Success Response
```json
{
"status": "success",
"data": {
"hook_config": {
"name": "api-validate-all-openapi-yaml",
"command": "python betty/skills/api.validate/api_validate.py {file_path} zalando",
"blocking": true,
"timeout": 10000,
"when": {
"pattern": "*.openapi.yaml"
},
"description": "Validate OpenAPI specs against Zalando guidelines"
},
"hooks_file_path": "/home/user/betty/.claude/hooks.yaml",
"event": "on_file_edit",
"total_hooks": 1
}
}
```
### Generated Hooks File
The skill creates/updates `.claude/hooks.yaml`:
```yaml
hooks:
on_file_edit:
- name: api-validate-all-openapi-yaml
command: python betty/skills/api.validate/api_validate.py {file_path} zalando
blocking: true
timeout: 10000
when:
pattern: "*.openapi.yaml"
description: Validate OpenAPI specs against Zalando guidelines
```
## How It Works
1. **Load Existing Hooks**: Reads `.claude/hooks.yaml` if it exists
2. **Create Hook Config**: Builds configuration from parameters
3. **Add or Update**: Adds new hook or updates existing one with same name
4. **Save**: Writes updated configuration back to `.claude/hooks.yaml`
## Benefits
### For Developers
-**Instant feedback**: Errors caught immediately, not at commit time
-**No discipline required**: Validation happens automatically
-**Consistent quality**: Standards enforced, not suggested
### For Teams
-**Enforced standards**: Can't save non-compliant code
-**Reduced review time**: Automated checks before human review
-**Onboarding**: New developers can't accidentally break standards
### For Organizations
-**Compliance**: Policies enforced at tool level
-**Audit trail**: Every validation is logged
-**Risk reduction**: Catch issues early, not in production
## Integration with Betty
### Use in Workflows
```yaml
# workflows/setup_api_validation.yaml
steps:
- skill: hook.define
args:
- "on_file_edit"
- "api.validate {file_path} zalando"
- "--pattern=*.openapi.yaml"
- "--blocking=true"
```
### Use in Agents
Agents can dynamically create hooks based on project needs:
```python
# Agent detects OpenAPI specs in project
# Automatically sets up validation hooks
```
## File Pattern Examples
| Pattern | Matches |
|---------|---------|
| `*.openapi.yaml` | All OpenAPI files in current directory |
| `*.asyncapi.yaml` | All AsyncAPI files in current directory |
| `specs/**/*.yaml` | All YAML files in specs/ and subdirectories |
| `src/**/*.ts` | All TypeScript files in src/ and subdirectories |
| `**/*.py` | All Python files anywhere |
## Blocking vs Non-Blocking
### Blocking Hooks (blocking: true)
- Operation is **paused** until hook completes
- If hook **fails**, operation is **aborted**
- Use for: Validation, compliance checks, breaking change detection
### Non-Blocking Hooks (blocking: false)
- Hook runs **asynchronously**
- Operation **continues** regardless of hook result
- Use for: Logging, notifications, background tasks
## Timeout Considerations
| Operation | Suggested Timeout | Reasoning |
|-----------|-------------------|-----------|
| Syntax validation | 5,000 - 10,000 ms | Fast, local checks |
| Zally API call | 10,000 - 30,000 ms | Network latency |
| Model generation | 30,000 - 60,000 ms | Compilation time |
| Full test suite | 300,000 ms (5 min) | Comprehensive testing |
## Error Handling
### Hook Execution Failed
If a blocking hook fails:
```
❌ Hook 'validate-openapi' failed:
- Missing required field: info.x-api-id
- Property 'userId' should use snake_case
Operation blocked. Fix errors and try again.
```
### Hook Timeout
If a hook exceeds timeout:
```
⚠️ Hook 'validate-openapi' timed out after 10000ms
Operation blocked for safety.
```
## Dependencies
- **PyYAML**: Required for YAML file handling
```bash
pip install pyyaml
```
- **context.schema**: For validation rule definitions
## Files Created
- `.claude/hooks.yaml` - Hook configurations for Claude Code
## See Also
- [api.validate](../api.validate/SKILL.md) - API validation skill
- [Betty Architecture](../../docs/betty-architecture.md) - Five-layer model
- [API-Driven Development](../../docs/api-driven-development.md) - Complete guide
- [Claude Code Hooks Documentation](https://docs.claude.com/en/docs/claude-code/hooks)
## Version
**0.1.0** - Initial implementation with basic hook definition support

View File

@@ -0,0 +1 @@
# Auto-generated package initializer for skills.

310
skills/hook.define/hook_define.py Executable file
View File

@@ -0,0 +1,310 @@
#!/usr/bin/env python3
"""
Define and register validation hooks for Claude Code.
This skill creates hook configurations in .claude/hooks.yaml that automatically
trigger validation skills on events like file edits, commits, and pushes.
"""
import sys
import json
import argparse
import os
from pathlib import Path
from typing import Dict, Any, Optional
# Add betty module to path
from betty.logging_utils import setup_logger
from betty.errors import format_error_response, BettyError
from betty.validation import validate_path
from betty.file_utils import safe_read_json, safe_write_json
logger = setup_logger(__name__)
# Valid hook events
VALID_EVENTS = [
"on_file_edit",
"on_file_save",
"on_commit",
"on_push",
"on_tool_use",
"on_agent_start",
"on_workflow_end"
]
def create_hooks_directory() -> Path:
"""
Create .claude directory if it doesn't exist.
Returns:
Path to .claude directory
"""
claude_dir = Path.cwd() / ".claude"
claude_dir.mkdir(exist_ok=True)
logger.info(f"Ensured .claude directory exists at {claude_dir}")
return claude_dir
def load_existing_hooks(hooks_file: Path) -> Dict[str, Any]:
"""
Load existing hooks configuration if it exists.
Args:
hooks_file: Path to hooks.yaml file
Returns:
Existing hooks configuration or empty structure
"""
if hooks_file.exists():
try:
import yaml
with open(hooks_file, 'r') as f:
config = yaml.safe_load(f) or {}
logger.info(f"Loaded existing hooks from {hooks_file}")
return config
except Exception as e:
logger.warning(f"Could not load existing hooks: {e}")
return {"hooks": {}}
else:
logger.info(f"No existing hooks file found at {hooks_file}")
return {"hooks": {}}
def create_hook_config(
event: str,
command: str,
pattern: Optional[str] = None,
blocking: bool = True,
timeout: int = 30000,
description: Optional[str] = None
) -> Dict[str, Any]:
"""
Create a hook configuration object.
Args:
event: Hook event trigger
command: Command to execute
pattern: File pattern to match (optional)
blocking: Whether hook should block on failure
timeout: Timeout in milliseconds
description: Human-readable description
Returns:
Hook configuration dictionary
"""
# Generate a name from the command and pattern
if pattern:
name = f"{command.replace('/', '-').replace('.', '-')}-{pattern.replace('*', 'all').replace('/', '-')}"
else:
name = command.replace('/', '-').replace('.', '-')
hook_config = {
"name": name,
"command": command,
"blocking": blocking,
"timeout": timeout
}
if pattern:
hook_config["when"] = {"pattern": pattern}
if description:
hook_config["description"] = description
else:
hook_config["description"] = f"Auto-generated hook for {command}"
return hook_config
def add_hook_to_config(
config: Dict[str, Any],
event: str,
hook_config: Dict[str, Any]
) -> Dict[str, Any]:
"""
Add a hook to the configuration, avoiding duplicates.
Args:
config: Existing hooks configuration
event: Event to add hook to
hook_config: Hook configuration to add
Returns:
Updated configuration
"""
if "hooks" not in config:
config["hooks"] = {}
if event not in config["hooks"]:
config["hooks"][event] = []
# Check if hook with same name already exists
existing_names = [h.get("name") for h in config["hooks"][event]]
if hook_config["name"] in existing_names:
# Update existing hook
for i, hook in enumerate(config["hooks"][event]):
if hook.get("name") == hook_config["name"]:
config["hooks"][event][i] = hook_config
logger.info(f"Updated existing hook: {hook_config['name']}")
break
else:
# Add new hook
config["hooks"][event].append(hook_config)
logger.info(f"Added new hook: {hook_config['name']}")
return config
def save_hooks_config(hooks_file: Path, config: Dict[str, Any]) -> None:
"""
Save hooks configuration to YAML file.
Args:
hooks_file: Path to hooks.yaml file
config: Configuration to save
"""
try:
import yaml
with open(hooks_file, 'w') as f:
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
logger.info(f"Saved hooks configuration to {hooks_file}")
except Exception as e:
raise BettyError(f"Failed to save hooks configuration: {e}")
def define_hook(
event: str,
command: str,
pattern: Optional[str] = None,
blocking: bool = True,
timeout: int = 30000,
description: Optional[str] = None
) -> Dict[str, Any]:
"""
Define a new hook or update an existing one.
Args:
event: Hook event trigger
command: Command to execute
pattern: File pattern to match
blocking: Whether hook should block on failure
timeout: Timeout in milliseconds
description: Human-readable description
Returns:
Result dictionary with hook config and file path
"""
# Validate event
if event not in VALID_EVENTS:
raise BettyError(
f"Invalid event '{event}'. Valid events: {', '.join(VALID_EVENTS)}"
)
# Create .claude directory
claude_dir = create_hooks_directory()
hooks_file = claude_dir / "hooks.yaml"
# Load existing hooks
config = load_existing_hooks(hooks_file)
# Create new hook config
hook_config = create_hook_config(
event=event,
command=command,
pattern=pattern,
blocking=blocking,
timeout=timeout,
description=description
)
# Add to configuration
config = add_hook_to_config(config, event, hook_config)
# Save configuration
save_hooks_config(hooks_file, config)
return {
"hook_config": hook_config,
"hooks_file_path": str(hooks_file),
"event": event,
"total_hooks": len(config.get("hooks", {}).get(event, []))
}
def main():
parser = argparse.ArgumentParser(
description="Define and register validation hooks for Claude Code"
)
parser.add_argument(
"event",
type=str,
choices=VALID_EVENTS,
help="Hook event trigger"
)
parser.add_argument(
"command",
type=str,
help="Command to execute when hook triggers"
)
parser.add_argument(
"--pattern",
type=str,
help="File pattern to match (e.g., '*.openapi.yaml')"
)
parser.add_argument(
"--blocking",
type=lambda x: x.lower() in ['true', '1', 'yes'],
default=True,
help="Whether hook should block on failure (default: true)"
)
parser.add_argument(
"--timeout",
type=int,
default=30000,
help="Timeout in milliseconds (default: 30000)"
)
parser.add_argument(
"--description",
type=str,
help="Human-readable description of what the hook does"
)
args = parser.parse_args()
try:
# Check if PyYAML is installed
try:
import yaml
except ImportError:
raise BettyError(
"PyYAML is required for hook.define. Install with: pip install pyyaml"
)
# Define the hook
logger.info(f"Defining hook for event '{args.event}' with command '{args.command}'")
result = define_hook(
event=args.event,
command=args.command,
pattern=args.pattern,
blocking=args.blocking,
timeout=args.timeout,
description=args.description
)
# Return structured result
output = {
"status": "success",
"data": result
}
print(json.dumps(output, indent=2))
except Exception as e:
logger.error(f"Failed to define hook: {e}")
print(json.dumps(format_error_response(e), indent=2))
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,60 @@
name: hook.define
version: 0.1.0
description: Create and register validation hooks for Claude Code
inputs:
- name: event
type: string
required: true
description: Hook event trigger (on_file_edit, on_file_save, on_commit, on_push, on_tool_use)
- name: pattern
type: string
required: false
description: File pattern to match (e.g., "*.openapi.yaml", "specs/**/*.yaml")
- name: command
type: string
required: true
description: Command to execute when hook triggers (skill name or full command)
- name: blocking
type: boolean
required: false
default: true
description: Whether hook should block operation if it fails
- name: timeout
type: number
required: false
default: 30000
description: Timeout in milliseconds (default 30 seconds)
- name: description
type: string
required: false
description: Human-readable description of what the hook does
outputs:
- name: hook_config
type: object
description: Generated hook configuration
- name: hooks_file_path
type: string
description: Path to .claude/hooks.yaml file
dependencies:
- context.schema
entrypoints:
- command: /skill/hook/define
handler: hook_define.py
runtime: python
permissions:
- filesystem:read
- filesystem:write
status: active
tags: [hooks, validation, automation, claude-code]