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

504
skills/plugin.sync/SKILL.md Normal file
View File

@@ -0,0 +1,504 @@
---
name: Plugin Sync
description: Automatically generate plugin.yaml from Betty Framework registries
---
# plugin.sync
## Overview
**plugin.sync** is the synchronization tool that generates `plugin.yaml` from Betty Framework's registry files. It ensures that Claude Code's plugin configuration stays in sync with registered skills, commands, and hooks.
## Purpose
Automates the generation of `plugin.yaml` to maintain consistency between:
- **Skill Registry** (`registry/skills.json`) Active skills with entrypoints
- **Command Registry** (`registry/commands.json`) Slash commands
- **Hook Registry** (`registry/hooks.json`) Event-driven hooks
- **Plugin Configuration** (`plugin.yaml`) Claude Code plugin manifest
This eliminates manual editing of `plugin.yaml` and prevents drift between what's registered and what's exposed to Claude Code.
## What It Does
1. **Reads Registries**: Loads `skills.json`, `commands.json`, and `hooks.json`
2. **Filters Active Skills**: Processes only skills with `status: active` and defined entrypoints
3. **Validates Handlers**: Checks if handler files exist on disk
4. **Generates Commands**: Converts skill entrypoints to `plugin.yaml` command format
5. **Preserves Metadata**: Maintains existing plugin metadata (author, license, etc.)
6. **Writes plugin.yaml**: Outputs formatted plugin configuration to repo root
7. **Reports Issues**: Warns about missing handlers or inconsistencies
## Usage
### Basic Usage
```bash
python skills/plugin.sync/plugin_sync.py
```
No arguments required - reads from standard registry locations.
### Via Betty CLI
```bash
/plugin/sync
```
### Expected Registry Structure
The skill expects these registry files:
```
betty/
├── registry/
│ ├── skills.json # Registered skills
│ ├── commands.json # Registered commands
│ └── hooks.json # Registered hooks
└── plugin.yaml # Generated output
```
## Behavior
### 1. Registry Loading
Reads JSON files from:
- `registry/skills.json`
- `registry/commands.json`
- `registry/hooks.json`
If a registry file is missing, logs a warning and continues with available data.
### 2. Skill Processing
For each skill in `skills.json`:
- **Checks status**: Only processes skills with `status: active`
- **Looks for entrypoints**: Requires at least one entrypoint definition
- **Validates handler**: Checks if handler file exists at `skills/{skill_name}/{handler}`
- **Converts format**: Maps skill entrypoint to plugin command schema
### 3. Command Generation
Creates a command entry for each active skill entrypoint:
```yaml
- name: skill/validate
description: Validate a skill manifest
handler:
runtime: python
script: skills/skill.define/skill_define.py
parameters:
- name: manifest_path
type: string
required: true
description: Path to skill.yaml file
permissions:
- filesystem:read
- filesystem:write
```
### 4. Handler Validation
For each handler reference:
- Constructs full path: `skills/{skill_name}/{handler_filename}`
- Checks file existence on disk
- Logs warning if handler file is missing
### 5. Plugin Generation
Preserves existing `plugin.yaml` metadata:
- Plugin name, version, description
- Author information
- License
- Requirements
- Permissions
- Config sections
Replaces the `commands` section with generated entries.
### 6. Output Writing
Writes `plugin.yaml` with:
- Auto-generated header comment
- Properly formatted YAML (2-space indent)
- Generation timestamp in metadata
- Skill and command counts
## Outputs
### Success Response
```json
{
"ok": true,
"status": "success",
"output_path": "/home/user/betty/plugin.yaml",
"commands_generated": 18,
"warnings": []
}
```
### Response with Warnings
```json
{
"ok": true,
"status": "success",
"output_path": "/home/user/betty/plugin.yaml",
"commands_generated": 18,
"warnings": [
"Handler not found for 'api.validate': skills/api.validate/api_validate_missing.py",
"Skill 'test.broken' has entrypoint without handler"
]
}
```
### Failure Response
```json
{
"ok": false,
"status": "failed",
"error": "Failed to parse JSON from registry/skills.json: Expecting value: line 1 column 1"
}
```
## Generated plugin.yaml Structure
The skill generates a `plugin.yaml` like this:
```yaml
# Betty Framework - Claude Code Plugin
# Auto-generated by plugin.sync skill
# DO NOT EDIT MANUALLY - Run plugin.sync to regenerate
name: betty-framework
version: 1.0.0
description: Betty Framework - Structured AI-assisted engineering
author:
name: RiskExec
email: platform@riskexec.com
url: https://github.com/epieczko/betty
license: MIT
metadata:
homepage: https://github.com/epieczko/betty
repository: https://github.com/epieczko/betty
generated_at: "2025-10-23T17:45:00.123456+00:00"
generated_by: plugin.sync skill
skill_count: 18
command_count: 18
requirements:
python: ">=3.11"
packages:
- pyyaml
permissions:
- filesystem:read
- filesystem:write
- process:execute
commands:
- name: workflow/validate
description: Validates Betty workflow YAML definitions
handler:
runtime: python
script: skills/workflow.validate/workflow_validate.py
parameters:
- name: workflow.yaml
type: string
required: true
description: Path to the workflow YAML file
permissions:
- filesystem
- read
- name: skill/define
description: Validate a Claude Code skill manifest
handler:
runtime: python
script: skills/skill.define/skill_define.py
parameters:
- name: manifest_path
type: string
required: true
description: Path to the skill.yaml file
permissions:
- filesystem
- read
- write
# ... more commands ...
```
## Validation and Warnings
### Handler Existence Check
For each skill entrypoint, the skill checks:
```python
full_path = f"skills/{skill_name}/{handler_filename}"
if not os.path.exists(full_path):
warnings.append(f"Handler not found: {full_path}")
```
### Common Warnings
| Warning | Meaning | Action |
|---------|---------|--------|
| `Handler not found for 'X'` | Handler file missing from disk | Create the handler or fix the path in skill.yaml |
| `Skill 'X' has entrypoint without handler` | Entrypoint missing `handler` field | Add handler field to entrypoint definition |
| `Registry file not found` | Registry JSON is missing | Run registry update or check file paths |
## Examples
### Example 1: Full Sync After Adding New Skills
**Scenario**: You've added several new skills and want to sync them to `plugin.yaml`
```bash
# Create skills using skill.create
/skill/create data.transform "Transform data between formats"
/skill/create api.monitor "Monitor API health" --inputs=endpoint --outputs=status
# Define skills (validates and registers)
/skill/define skills/data.transform/skill.yaml
/skill/define skills/api.monitor/skill.yaml
# Sync to plugin.yaml
/plugin/sync
```
**Output**:
```
INFO: Starting plugin.yaml generation from registries...
INFO: Loading registry files...
INFO: Generating plugin.yaml configuration...
INFO: Added command: /data/transform from skill data.transform
INFO: Added command: /api/monitor from skill api.monitor
INFO: ✅ Written plugin.yaml to /home/user/betty/plugin.yaml
INFO: ✅ Generated 20 commands
INFO: 📄 Output: /home/user/betty/plugin.yaml
```
### Example 2: Detecting Missing Handlers
**Scenario**: A skill's handler file was moved or deleted
```bash
# Remove a handler file
rm skills/api.validate/api_validate.py
# Run sync
/plugin/sync
```
**Output**:
```
INFO: Starting plugin.yaml generation from registries...
INFO: Loading registry files...
INFO: Generating plugin.yaml configuration...
INFO: Added command: /skill/define from skill skill.define
WARNING: Handler not found for 'api.validate': skills/api.validate/api_validate.py
INFO: ✅ Written plugin.yaml to /home/user/betty/plugin.yaml
INFO: ⚠️ Warnings during generation:
INFO: - Handler not found for 'api.validate': skills/api.validate/api_validate.py
INFO: ✅ Generated 19 commands
```
### Example 3: Initial Plugin Setup
**Scenario**: Setting up Betty Framework plugin for the first time
```bash
# Initialize registry if needed
python -c "from betty.config import ensure_directories; ensure_directories()"
# Register all existing skills
for skill in skills/*/skill.yaml; do
/skill/define "$skill"
done
# Generate plugin.yaml
/plugin/sync
```
This creates a complete `plugin.yaml` from all registered active skills.
## Integration
### With skill.define
After defining a skill, sync the plugin:
```bash
/skill/define skills/my.skill/skill.yaml
/plugin/sync
```
### With Workflows
Include plugin sync as a workflow step:
```yaml
# workflows/skill_lifecycle.yaml
steps:
- skill: skill.create
args: ["new.skill", "Description"]
- skill: skill.define
args: ["skills/new.skill/skill.yaml"]
- skill: plugin.sync
args: []
```
### With Hooks
Auto-sync when skills are updated:
```yaml
# .claude/hooks.yaml
- event: on_file_save
pattern: "skills/*/skill.yaml"
command: python skills/skill.define/skill_define.py {file_path} && python skills/plugin.sync/plugin_sync.py
blocking: false
description: Auto-sync plugin.yaml when skills change
```
## What Gets Included
### ✅ Included in plugin.yaml
- Skills with `status: active`
- Skills with at least one `entrypoint` defined
- All entrypoint parameters and permissions
- Skill descriptions and metadata
### ❌ Not Included
- Skills with `status: draft`
- Skills without entrypoints
- Skills marked as internal-only
- Test skills (unless marked active)
## Common Errors
| Error | Cause | Solution |
|-------|-------|----------|
| "Failed to parse JSON" | Invalid JSON in registry file | Fix JSON syntax in the registry |
| "Registry file not found" | Missing registry file | Ensure registries exist in `registry/` dir |
| "Permission denied" | Cannot write plugin.yaml | Check file permissions on plugin.yaml |
| All commands missing | No active skills | Mark skills as active in registry |
## Files Read
- `registry/skills.json` Skill registry
- `registry/commands.json` Command registry (future use)
- `registry/hooks.json` Hook registry (future use)
- `plugin.yaml` Existing plugin config (for metadata preservation)
- `skills/*/` Handler file validation
## Files Modified
- `plugin.yaml` Overwritten with generated configuration
## Exit Codes
- **0**: Success (plugin.yaml generated successfully)
- **1**: Failure (error during generation)
## Logging
Logs generation progress:
```
INFO: Starting plugin.yaml generation from registries...
INFO: Loading registry files...
INFO: Loaded existing plugin.yaml as template
INFO: Generating plugin.yaml configuration...
INFO: Added command: /skill/create from skill skill.create
INFO: Added command: /skill/define from skill skill.define
INFO: Added command: /agent/define from skill agent.define
INFO: ✅ Written plugin.yaml to /home/user/betty/plugin.yaml
INFO: ✅ Generated 18 commands
INFO: 📄 Output: /home/user/betty/plugin.yaml
```
## Best Practices
1. **Run After Registry Changes**: Sync after adding, updating, or removing skills
2. **Include in CI/CD**: Add plugin sync to your deployment pipeline
3. **Review Before Commit**: Check generated plugin.yaml before committing
4. **Keep Registries Clean**: Remove inactive skills to keep plugin.yaml focused
5. **Use Hooks**: Set up auto-sync hooks for convenience
6. **Version Control**: Always commit plugin.yaml changes with skill changes
## Troubleshooting
### Plugin.yaml Not Updating
**Problem**: Changes to skills.json don't appear in plugin.yaml
**Solutions**:
- Ensure skill status is `active`
- Check that skill has `entrypoints` defined
- Verify entrypoint has `command` and `handler` fields
- Run `/skill/define` before `/plugin/sync`
### Handler Not Found Warnings
**Problem**: Warnings about missing handler files
**Solutions**:
- Check handler path in skill.yaml
- Ensure handler file exists at `skills/{skill_name}/{handler_filename}`
- Verify file permissions
- Update skill.yaml if handler was renamed
### Commands Not Appearing
**Problem**: Active skill not generating command
**Solutions**:
- Verify skill has `entrypoints` array in skill.yaml
- Check entrypoint has `command` field (e.g., `/skill/validate`)
- Ensure `handler` field points to correct file
- Check that skill.yaml is in skills registry
## Architecture
### Skill Categories
**Infrastructure** Plugin.sync maintains the foundation layer by syncing registry state to plugin configuration.
### Design Principles
- **Single Source of Truth**: Registry files are the source of truth
- **Idempotent**: Can be run multiple times safely
- **Validation First**: Checks handlers before generating config
- **Preserve Metadata**: Keeps existing plugin metadata intact
- **Clear Reporting**: Detailed warnings about issues
## See Also
- **skill.define** Validate and register skills ([SKILL.md](../skill.define/SKILL.md))
- **registry.update** Update skill registry ([SKILL.md](../registry.update/SKILL.md))
- **skill.create** Create new skills ([SKILL.md](../skill.create/SKILL.md))
- **Betty Architecture** Framework overview ([betty-architecture.md](../../docs/betty-architecture.md))
## Dependencies
- **registry.update**: Registry management
- **betty.config**: Configuration constants and paths
- **betty.logging_utils**: Logging infrastructure
## Status
**Active** Production-ready infrastructure skill
## Version History
- **0.1.0** (Oct 2025) Initial implementation with registry sync and handler validation

View File

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

323
skills/plugin.sync/plugin_sync.py Executable file
View File

@@ -0,0 +1,323 @@
#!/usr/bin/env python3
"""
plugin_sync.py Implementation of the plugin.sync Skill
Generates plugin.yaml from registry files (skills.json, commands.json, hooks.json).
"""
import os
import sys
import json
import yaml
from typing import Dict, Any, List, Optional
from datetime import datetime, timezone
from pathlib import Path
from betty.config import BASE_DIR
from betty.logging_utils import setup_logger
logger = setup_logger(__name__)
def load_registry_file(registry_path: str) -> Dict[str, Any]:
"""
Load a JSON registry file.
Args:
registry_path: Path to the registry JSON file
Returns:
Parsed registry data
Raises:
FileNotFoundError: If registry file doesn't exist
json.JSONDecodeError: If JSON is invalid
"""
try:
with open(registry_path) as f:
return json.load(f)
except FileNotFoundError:
logger.warning(f"Registry file not found: {registry_path}")
return {}
except json.JSONDecodeError as e:
logger.error(f"Failed to parse JSON from {registry_path}: {e}")
raise
def check_handler_exists(handler_path: str, skill_name: str) -> Dict[str, Any]:
"""
Check if a handler file exists on disk.
Args:
handler_path: Relative path to handler file
skill_name: Name of the skill
Returns:
Dictionary with exists flag and full path
"""
full_path = os.path.join(BASE_DIR, "skills", skill_name, handler_path)
exists = os.path.exists(full_path)
return {
"exists": exists,
"path": full_path,
"relative_path": f"skills/{skill_name}/{handler_path}"
}
def convert_skill_to_command(skill: Dict[str, Any], entrypoint: Dict[str, Any]) -> Dict[str, Any]:
"""
Convert a skill registry entry with entrypoint to a plugin.yaml command format.
Args:
skill: Skill entry from skills.json
entrypoint: Entrypoint entry from the skill
Returns:
Command dictionary in plugin.yaml format
"""
# Extract command name (remove leading slash if present)
command_name = entrypoint.get("command", "").lstrip("/")
# Build handler section
handler_path = f"skills/{skill['name']}/{entrypoint.get('handler', '')}"
command = {
"name": command_name,
"description": entrypoint.get("description") or skill.get("description", ""),
"handler": {
"runtime": entrypoint.get("runtime", "python"),
"script": handler_path
}
}
# Add parameters from entrypoint or extract from skill inputs
if "parameters" in entrypoint and entrypoint["parameters"]:
# Use parameters from entrypoint if they exist
command["parameters"] = entrypoint["parameters"]
elif "inputs" in skill and skill["inputs"]:
# Otherwise, convert skill inputs to command parameters
parameters = []
for inp in skill["inputs"]:
if isinstance(inp, dict):
# Full input specification
param = {
"name": inp.get("name", ""),
"type": inp.get("type", "string"),
"required": inp.get("required", False),
"description": inp.get("description", "")
}
# Add default if present
if "default" in inp:
param["default"] = inp["default"]
parameters.append(param)
elif isinstance(inp, str):
# Simple string input (legacy format)
parameters.append({
"name": inp,
"type": "string",
"required": False,
"description": ""
})
if parameters:
command["parameters"] = parameters
# Add permissions if present
if "permissions" in entrypoint:
# Convert permissions list to proper format if needed
permissions = entrypoint["permissions"]
if isinstance(permissions, list):
command["permissions"] = permissions
return command
def generate_plugin_yaml(
skills_data: Dict[str, Any],
commands_data: Dict[str, Any],
hooks_data: Dict[str, Any]
) -> tuple[Dict[str, Any], List[str]]:
"""
Generate plugin.yaml content from registry data.
Args:
skills_data: Parsed skills.json
commands_data: Parsed commands.json
hooks_data: Parsed hooks.json
Returns:
Tuple of (plugin_yaml_dict, list of warnings)
"""
warnings = []
commands = []
# Load existing plugin.yaml to preserve header content
plugin_yaml_path = os.path.join(BASE_DIR, "plugin.yaml")
base_config = {}
try:
with open(plugin_yaml_path) as f:
base_config = yaml.safe_load(f) or {}
logger.info(f"Loaded existing plugin.yaml as template")
except FileNotFoundError:
logger.warning("No existing plugin.yaml found, creating from scratch")
base_config = {
"name": "betty-framework",
"version": "1.0.0",
"description": "Betty Framework - Structured AI-assisted engineering",
"author": {
"name": "RiskExec",
"email": "platform@riskexec.com",
"url": "https://github.com/epieczko/betty"
},
"license": "MIT"
}
# Process active skills with entrypoints
skills = skills_data.get("skills", [])
for skill in skills:
if skill.get("status") != "active":
continue
entrypoints = skill.get("entrypoints", [])
if not entrypoints:
continue
skill_name = skill.get("name")
for entrypoint in entrypoints:
handler = entrypoint.get("handler")
if not handler:
warnings.append(f"Skill '{skill_name}' has entrypoint without handler")
continue
# Check if handler exists on disk
handler_check = check_handler_exists(handler, skill_name)
if not handler_check["exists"]:
warnings.append(
f"Handler not found for '{skill_name}': {handler_check['relative_path']}"
)
# Convert to plugin command format
command = convert_skill_to_command(skill, entrypoint)
commands.append(command)
logger.info(f"Added command: /{command['name']} from skill {skill_name}")
# Process commands from commands.json (if any need to be added)
# Note: Most commands should already be represented via skills
# This is mainly for custom commands that don't map to skills
registry_commands = commands_data.get("commands", [])
for cmd in registry_commands:
if cmd.get("status") == "active":
# Check if this command is already in our list
cmd_name = cmd.get("name", "").lstrip("/")
if not any(c["name"] == cmd_name for c in commands):
logger.info(f"Command '{cmd_name}' in registry but no matching active skill found")
# Build final plugin.yaml structure
plugin_config = {
**base_config,
"commands": commands
}
# Override plugin-level permissions (remove network:none contradiction)
plugin_config["permissions"] = [
"filesystem:read",
"filesystem:write",
"process:execute"
]
# Add metadata about generation
if "metadata" not in plugin_config:
plugin_config["metadata"] = {}
plugin_config["metadata"]["generated_at"] = datetime.now(timezone.utc).isoformat()
plugin_config["metadata"]["generated_by"] = "plugin.sync skill"
plugin_config["metadata"]["skill_count"] = len([s for s in skills if s.get("status") == "active"])
plugin_config["metadata"]["command_count"] = len(commands)
return plugin_config, warnings
def write_plugin_yaml(plugin_config: Dict[str, Any], output_path: str):
"""
Write plugin.yaml to disk with proper formatting.
Args:
plugin_config: Plugin configuration dictionary
output_path: Path where to write plugin.yaml
"""
# Add header comment
header = """# Betty Framework - Claude Code Plugin
# Auto-generated by plugin.sync skill
# DO NOT EDIT MANUALLY - Run plugin.sync to regenerate
"""
with open(output_path, 'w') as f:
f.write(header)
yaml.dump(plugin_config, f, default_flow_style=False, sort_keys=False, indent=2)
logger.info(f"✅ Written plugin.yaml to {output_path}")
def main():
"""Main CLI entry point."""
logger.info("Starting plugin.yaml generation from registries...")
# Define registry paths
skills_path = os.path.join(BASE_DIR, "registry", "skills.json")
commands_path = os.path.join(BASE_DIR, "registry", "commands.json")
hooks_path = os.path.join(BASE_DIR, "registry", "hooks.json")
try:
# Load registry files
logger.info("Loading registry files...")
skills_data = load_registry_file(skills_path)
commands_data = load_registry_file(commands_path)
hooks_data = load_registry_file(hooks_path)
# Generate plugin.yaml content
logger.info("Generating plugin.yaml configuration...")
plugin_config, warnings = generate_plugin_yaml(skills_data, commands_data, hooks_data)
# Write to file
output_path = os.path.join(BASE_DIR, "plugin.yaml")
write_plugin_yaml(plugin_config, output_path)
# Report results
result = {
"ok": True,
"status": "success",
"output_path": output_path,
"commands_generated": len(plugin_config.get("commands", [])),
"warnings": warnings
}
# Print warnings if any
if warnings:
logger.warning("⚠️ Warnings during generation:")
for warning in warnings:
logger.warning(f" - {warning}")
# Print summary
logger.info(f"✅ Generated {result['commands_generated']} commands")
logger.info(f"📄 Output: {output_path}")
print(json.dumps(result, indent=2))
sys.exit(0)
except Exception as e:
logger.error(f"Failed to generate plugin.yaml: {e}")
result = {
"ok": False,
"status": "failed",
"error": str(e)
}
print(json.dumps(result, indent=2))
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,29 @@
name: plugin.sync
version: 0.1.0
description: >
Automatically generates plugin.yaml from Betty Framework registries.
Reads skills.json, commands.json, and hooks.json to build a complete plugin configuration.
inputs: []
outputs:
- plugin.yaml
- generation_report.json
dependencies:
- registry.update
status: active
entrypoints:
- command: /plugin/sync
handler: plugin_sync.py
runtime: python
description: >
Generate plugin.yaml from registry files. Syncs active skills with entrypoints into plugin commands.
parameters: []
permissions:
- filesystem:read
- filesystem:write
tags:
- plugin
- registry
- automation
- infrastructure