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

View File

@@ -0,0 +1,751 @@
---
name: Plugin Manifest Sync
description: Reconcile plugin.yaml with Betty Framework registries
---
# docs.sync.plugin_manifest
## Overview
**docs.sync.plugin_manifest** is a validation and reconciliation tool that compares `plugin.yaml` against Betty Framework's registry files to ensure consistency and completeness. It identifies missing commands, orphaned entries, metadata mismatches, and suggests corrections.
## Purpose
Ensures synchronization between:
- **Skill Registry** (`registry/skills.json`) Active skills with entrypoints
- **Command Registry** (`registry/commands.json`) Slash commands
- **Plugin Configuration** (`plugin.yaml`) Claude Code plugin manifest
This skill helps maintain plugin.yaml accuracy by detecting:
- Active skills missing from plugin.yaml
- Orphaned commands in plugin.yaml not found in registries
- Metadata inconsistencies (permissions, runtime, handlers)
- Missing metadata that should be added
## What It Does
1. **Loads Registries**: Reads `skills.json` and `commands.json`
2. **Loads Plugin**: Reads current `plugin.yaml`
3. **Builds Indexes**: Creates lookup tables for both registries and plugin
4. **Compares Entries**: Identifies missing, orphaned, and mismatched commands
5. **Analyzes Metadata**: Checks permissions, runtime, handlers, descriptions
6. **Generates Preview**: Creates `plugin.preview.yaml` with suggested updates
7. **Creates Report**: Outputs `plugin_manifest_diff.md` with detailed analysis
8. **Provides Summary**: Displays key findings and recommendations
## Usage
### Basic Usage
```bash
python skills/docs.sync.plugin_manifest/plugin_manifest_sync.py
```
No arguments required - reads from standard locations.
### Via Betty CLI
```bash
/docs/sync/plugin-manifest
```
### Expected File Structure
```
betty/
├── registry/
│ ├── skills.json # Source of truth for skills
│ └── commands.json # Source of truth for commands
├── plugin.yaml # Current plugin manifest
├── plugin.preview.yaml # Generated preview (output)
└── plugin_manifest_diff.md # Generated report (output)
```
## Behavior
### 1. Registry Loading
Reads and parses:
- `registry/skills.json` All registered skills
- `registry/commands.json` All registered commands
Only processes entries with `status: active`.
### 2. Plugin Loading
Reads and parses:
- `plugin.yaml` Current plugin configuration
Extracts all command definitions.
### 3. Index Building
**Registry Index**: Maps command names to their registry sources
```python
{
"skill/define": {
"type": "skill",
"source": "skill.define",
"skill": {...},
"entrypoint": {...}
},
"api/validate": {
"type": "skill",
"source": "api.validate",
"skill": {...},
"entrypoint": {...}
}
}
```
**Plugin Index**: Maps command names to plugin entries
```python
{
"skill/define": {
"name": "skill/define",
"handler": {...},
"permissions": [...]
}
}
```
### 4. Comparison Analysis
Performs four types of checks:
#### Missing Commands
Commands in registry but not in plugin.yaml:
```
- skill/create (active in registry, missing from plugin)
- api/validate (active in registry, missing from plugin)
```
#### Orphaned Commands
Commands in plugin.yaml but not in registry:
```
- old/deprecated (in plugin but not registered)
- test/removed (in plugin but removed from registry)
```
#### Metadata Mismatches
Commands present in both but with different metadata:
**Runtime Mismatch**:
```
- skill/define:
- Registry: python
- Plugin: node
```
**Permission Mismatch**:
```
- api/validate:
- Missing: filesystem:read
- Extra: network:write
```
**Handler Mismatch**:
```
- skill/create:
- Registry: skills/skill.create/skill_create.py
- Plugin: skills/skill.create/old_handler.py
```
**Description Mismatch**:
```
- agent/run:
- Registry: "Execute a Betty agent..."
- Plugin: "Run agent"
```
#### Missing Metadata Suggestions
Identifies registry entries missing recommended metadata:
```
- hook/define: Consider adding permissions metadata
- test/skill: Consider adding description
```
### 5. Preview Generation
Creates `plugin.preview.yaml` by:
- Taking all active commands from registries
- Converting to plugin.yaml format
- Including all metadata from registries
- Adding generation timestamp
- Preserving existing plugin metadata (author, license, etc.)
### 6. Report Generation
Creates `plugin_manifest_diff.md` with:
- Executive summary
- Lists of missing commands
- Lists of orphaned commands
- Detailed metadata issues
- Metadata suggestions
## Outputs
### Success Response
```json
{
"ok": true,
"status": "success",
"preview_path": "/home/user/betty/plugin.preview.yaml",
"report_path": "/home/user/betty/plugin_manifest_diff.md",
"reconciliation": {
"missing_commands": [...],
"orphaned_commands": [...],
"metadata_issues": [...],
"metadata_suggestions": [...],
"total_registry_commands": 19,
"total_plugin_commands": 18
}
}
```
### Console Output
```
============================================================
PLUGIN MANIFEST RECONCILIATION COMPLETE
============================================================
📊 Summary:
- Commands in registry: 19
- Commands in plugin.yaml: 18
- Missing from plugin.yaml: 2
- Orphaned in plugin.yaml: 1
- Metadata issues: 3
- Metadata suggestions: 2
📄 Output files:
- Preview: /home/user/betty/plugin.preview.yaml
- Diff report: /home/user/betty/plugin_manifest_diff.md
⚠️ 2 command(s) missing from plugin.yaml:
- registry/query (registry.query)
- hook/simulate (hook.simulate)
⚠️ 1 orphaned command(s) in plugin.yaml:
- old/deprecated
✅ Review plugin_manifest_diff.md for full details
============================================================
```
### Failure Response
```json
{
"ok": false,
"status": "failed",
"error": "Failed to parse JSON from registry/skills.json"
}
```
## Generated Files
### plugin.preview.yaml
Updated plugin manifest with all active registry commands:
```yaml
# Betty Framework - Claude Code Plugin (Preview)
# Generated by docs.sync.plugin_manifest skill
# Review changes before applying to plugin.yaml
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:
generated_at: "2025-10-23T20:00:00.000000+00:00"
generated_by: docs.sync.plugin_manifest skill
command_count: 19
commands:
- 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 skill.yaml file
permissions:
- filesystem:read
- filesystem:write
# ... more commands ...
```
### plugin_manifest_diff.md
Detailed reconciliation report:
```markdown
# Plugin Manifest Reconciliation Report
Generated: 2025-10-23T20:00:00.000000+00:00
## Summary
- Total commands in registry: 19
- Total commands in plugin.yaml: 18
- Missing from plugin.yaml: 2
- Orphaned in plugin.yaml: 1
- Metadata issues: 3
- Metadata suggestions: 2
## Missing Commands (in registry but not in plugin.yaml)
- **registry/query** (skill: registry.query)
- **hook/simulate** (skill: hook.simulate)
## Orphaned Commands (in plugin.yaml but not in registry)
- **old/deprecated**
## Metadata Issues
- **skill/create**: Permissions Mismatch
- Missing: process:execute
- Extra: network:http
- **api/validate**: Handler Mismatch
- Registry: `skills/api.validate/api_validate.py`
- Plugin: `skills/api.validate/validator.py`
- **agent/run**: Runtime Mismatch
- Registry: `python`
- Plugin: `node`
## Metadata Suggestions
- **hook/define** (permissions): Consider adding permissions metadata
- **test/skill** (description): Consider adding description
```
## Examples
### Example 1: Routine Sync Check
**Scenario**: Regular validation after making registry changes
```bash
# Make some registry updates
/skill/define skills/new.skill/skill.yaml
# Check for discrepancies
/docs/sync/plugin-manifest
# Review the report
cat plugin_manifest_diff.md
# If changes look good, apply them
cp plugin.preview.yaml plugin.yaml
```
**Output**:
```
============================================================
PLUGIN MANIFEST RECONCILIATION COMPLETE
============================================================
📊 Summary:
- Commands in registry: 20
- Commands in plugin.yaml: 19
- Missing from plugin.yaml: 1
- Orphaned in plugin.yaml: 0
- Metadata issues: 0
- Metadata suggestions: 0
⚠️ 1 command(s) missing from plugin.yaml:
- new/skill (new.skill)
✅ Review plugin_manifest_diff.md for full details
```
### Example 2: Detecting Orphaned Commands
**Scenario**: A skill was removed from registry but command remains in plugin.yaml
```bash
# Remove skill from registry
rm -rf skills/deprecated.skill/
# Run reconciliation
/docs/sync/plugin-manifest
# Check report
cat plugin_manifest_diff.md
```
**Output**:
```
============================================================
PLUGIN MANIFEST RECONCILIATION COMPLETE
============================================================
📊 Summary:
- Commands in registry: 18
- Commands in plugin.yaml: 19
- Missing from plugin.yaml: 0
- Orphaned in plugin.yaml: 1
- Metadata issues: 0
- Metadata suggestions: 0
⚠️ 1 orphaned command(s) in plugin.yaml:
- deprecated/skill
✅ Review plugin_manifest_diff.md for full details
```
### Example 3: Finding Metadata Mismatches
**Scenario**: Registry was updated but plugin.yaml wasn't synced
```bash
# Update skill permissions in registry
/skill/define skills/api.validate/skill.yaml
# Check for differences
/docs/sync/plugin-manifest
# Review specific mismatches
grep -A 5 "Metadata Issues" plugin_manifest_diff.md
```
**Report Output**:
```markdown
## Metadata Issues
- **api/validate**: Permissions Mismatch
- Missing: network:http
- Extra: filesystem:write
```
### Example 4: Pre-Commit Validation
**Scenario**: Validate plugin.yaml before committing changes
```bash
# Before committing
/docs/sync/plugin-manifest
# If discrepancies found, fix them
if [ $? -eq 0 ]; then
# Review and apply changes
diff plugin.yaml plugin.preview.yaml
cp plugin.preview.yaml plugin.yaml
fi
# Commit changes
git add plugin.yaml
git commit -m "Sync plugin.yaml with registries"
```
### Example 5: CI/CD Integration
**Scenario**: Automated validation in CI pipeline
```yaml
# .github/workflows/validate-plugin.yml
name: Validate Plugin Manifest
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Reconcile Plugin Manifest
run: |
python skills/docs.sync.plugin_manifest/plugin_manifest_sync.py
# Check if there are discrepancies
if grep -q "Missing from plugin.yaml: [1-9]" plugin_manifest_diff.md; then
echo "❌ Plugin manifest has missing commands"
cat plugin_manifest_diff.md
exit 1
fi
if grep -q "Orphaned in plugin.yaml: [1-9]" plugin_manifest_diff.md; then
echo "❌ Plugin manifest has orphaned commands"
cat plugin_manifest_diff.md
exit 1
fi
echo "✅ Plugin manifest is in sync"
```
## Integration
### With plugin.sync
Use reconciliation to verify before syncing:
```bash
# Check current state
/docs/sync/plugin-manifest
# Review differences
cat plugin_manifest_diff.md
# If satisfied, run full sync
/plugin/sync
```
### With skill.define
Validate after defining skills:
```bash
# Define new skill
/skill/define skills/my.skill/skill.yaml
# Check plugin consistency
/docs/sync/plugin-manifest
# Apply changes if needed
cp plugin.preview.yaml plugin.yaml
```
### With Hooks
Auto-check on registry changes:
```yaml
# .claude/hooks.yaml
- event: on_file_save
pattern: "registry/*.json"
command: python skills/docs.sync.plugin_manifest/plugin_manifest_sync.py
blocking: false
description: Check plugin manifest sync when registries change
```
### With Workflows
Include in skill lifecycle workflow:
```yaml
# workflows/update_plugin.yaml
steps:
- skill: skill.define
args: ["skills/new.skill/skill.yaml"]
- skill: docs.sync.plugin_manifest
args: []
- skill: plugin.sync
args: []
```
## What Gets Reported
### ✅ Detected Issues
- Active skills missing from plugin.yaml
- Orphaned commands in plugin.yaml
- Runtime mismatches (python vs node)
- Permission mismatches (missing or extra)
- Handler path mismatches
- Description mismatches
- Missing metadata (permissions, descriptions)
### ❌ Not Detected
- Draft/inactive skills (intentionally excluded)
- Malformed YAML syntax (causes failure)
- Handler file existence (use plugin.sync for that)
- Parameter schema validation
## Common Use Cases
| Use Case | When to Use |
|----------|-------------|
| **Pre-commit check** | Before committing plugin.yaml changes |
| **Post-registry update** | After adding/updating skills in registry |
| **CI/CD validation** | Automated pipeline checks |
| **Manual audit** | Periodic manual review of plugin state |
| **Debugging** | When commands aren't appearing as expected |
| **Migration** | After major registry restructuring |
## Common Errors
| Error | Cause | Solution |
|-------|-------|----------|
| "Failed to parse JSON" | Invalid JSON in registry | Fix JSON syntax in registry files |
| "Failed to parse YAML" | Invalid YAML in plugin.yaml | Fix YAML syntax in plugin.yaml |
| "Registry file not found" | Missing registry files | Ensure registries exist in registry/ |
| "Permission denied" | Cannot write output files | Check write permissions on directory |
| All commands missing | Empty or invalid registries | Verify registry files are populated |
## Files Read
- `registry/skills.json` Skill registry (source of truth)
- `registry/commands.json` Command registry (source of truth)
- `plugin.yaml` Current plugin manifest (for comparison)
## Files Generated
- `plugin.preview.yaml` Updated plugin manifest preview
- `plugin_manifest_diff.md` Detailed reconciliation report
## Exit Codes
- **0**: Success (reconciliation completed successfully)
- **1**: Failure (error during reconciliation)
Note: Discrepancies found are reported but don't cause failure (exit 0). Only parsing errors or system failures cause exit 1.
## Logging
Logs reconciliation progress:
```
INFO: Starting plugin manifest reconciliation...
INFO: Loading registry files...
INFO: Loading plugin.yaml...
INFO: Building registry index...
INFO: Building plugin index...
INFO: Comparing registries with plugin.yaml...
INFO: Reconciling registries with plugin.yaml...
INFO: Generating updated plugin.yaml...
INFO: ✅ Written file to /home/user/betty/plugin.preview.yaml
INFO: Generating diff report...
INFO: ✅ Written diff report to /home/user/betty/plugin_manifest_diff.md
```
## Best Practices
1. **Run Before Committing**: Always check sync status before committing plugin.yaml
2. **Review Diff Report**: Read the full report to understand all changes
3. **Validate Preview**: Review plugin.preview.yaml before applying
4. **Include in CI**: Add validation to your CI/CD pipeline
5. **Regular Audits**: Run periodic checks even without changes
6. **Address Orphans**: Remove orphaned commands promptly
7. **Fix Mismatches**: Resolve metadata mismatches to maintain consistency
8. **Keep Registries Clean**: Mark inactive skills as draft instead of deleting
## Workflow Integration
### Recommended Workflow
```bash
# 1. Define or update skills
/skill/define skills/my.skill/skill.yaml
# 2. Check for discrepancies
/docs/sync/plugin-manifest
# 3. Review the report
cat plugin_manifest_diff.md
# 4. Review the preview
diff plugin.yaml plugin.preview.yaml
# 5. Apply changes if satisfied
cp plugin.preview.yaml plugin.yaml
# 6. Commit changes
git add plugin.yaml registry/
git commit -m "Update plugin manifest"
```
### Alternative: Auto-Sync Workflow
```bash
# 1. Define or update skills
/skill/define skills/my.skill/skill.yaml
# 2. Run full sync (overwrites plugin.yaml)
/plugin/sync
# 3. Validate the result
/docs/sync/plugin-manifest
# 4. If clean, commit
git add plugin.yaml registry/
git commit -m "Update plugin manifest"
```
## Troubleshooting
### Plugin.yaml Shows as Out of Sync
**Problem**: Reconciliation reports missing or orphaned commands
**Solutions**:
1. Run `/plugin/sync` to regenerate plugin.yaml from registries
2. Review and apply `plugin.preview.yaml` manually
3. Check if skills are marked as `active` in registry
4. Verify skills have `entrypoints` defined
### Metadata Mismatches Reported
**Problem**: Registry and plugin have different permissions/runtime/handlers
**Solutions**:
1. Update skill.yaml with correct metadata
2. Run `/skill/define` to register changes
3. Run `/docs/sync/plugin-manifest` to verify
4. Apply plugin.preview.yaml or run `/plugin/sync`
### Orphaned Commands Found
**Problem**: Commands in plugin.yaml not found in registry
**Solutions**:
1. Check if skill was removed from registry
2. Verify skill status is `active` in registry
3. Re-register the skill if it should exist
4. Remove from plugin.yaml if intentionally deprecated
### Preview File Not Generated
**Problem**: plugin.preview.yaml missing after running skill
**Solutions**:
1. Check write permissions on betty/ directory
2. Verify registries are readable
3. Check logs for errors
4. Ensure plugin.yaml exists and is valid
## Architecture
### Skill Category
**Documentation & Infrastructure** Maintains consistency between registry and plugin configuration layers.
### Design Principles
- **Non-Destructive**: Never modifies plugin.yaml directly
- **Comprehensive**: Reports all types of discrepancies
- **Actionable**: Provides preview file ready to apply
- **Transparent**: Detailed report explains all findings
- **Idempotent**: Can be run multiple times safely
## See Also
- **plugin.sync** Generate plugin.yaml from registries ([SKILL.md](../plugin.sync/SKILL.md))
- **skill.define** Validate and register skills ([SKILL.md](../skill.define/SKILL.md))
- **registry.update** Update skill registry ([SKILL.md](../registry.update/SKILL.md))
- **Betty Architecture** Framework overview ([betty-architecture.md](../../docs/betty-architecture.md))
## Dependencies
- **plugin.sync**: Plugin generation infrastructure
- **registry.update**: Registry management
- **betty.config**: Configuration constants and paths
- **betty.logging_utils**: Logging infrastructure
## Status
**Active** Production-ready documentation and validation skill
## Version History
- **0.1.0** (Oct 2025) Initial implementation with full reconciliation, preview generation, and diff reporting

View File

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

View File

@@ -0,0 +1,609 @@
#!/usr/bin/env python3
"""
plugin_manifest_sync.py Implementation of the docs.sync.plugin_manifest Skill
Reconciles plugin.yaml with registry files to ensure consistency and completeness.
"""
import os
import sys
import json
import yaml
from typing import Dict, Any, List, Tuple, Optional
from datetime import datetime, timezone
from pathlib import Path
from collections import defaultdict
from betty.config import BASE_DIR
from betty.logging_utils import setup_logger
logger = setup_logger(__name__)
def load_json_file(file_path: str) -> Dict[str, Any]:
"""
Load a JSON file.
Args:
file_path: Path to the JSON file
Returns:
Parsed JSON data
Raises:
FileNotFoundError: If file doesn't exist
json.JSONDecodeError: If JSON is invalid
"""
try:
with open(file_path) as f:
return json.load(f)
except FileNotFoundError:
logger.warning(f"File not found: {file_path}")
return {}
except json.JSONDecodeError as e:
logger.error(f"Failed to parse JSON from {file_path}: {e}")
raise
def load_yaml_file(file_path: str) -> Dict[str, Any]:
"""
Load a YAML file.
Args:
file_path: Path to the YAML file
Returns:
Parsed YAML data
Raises:
FileNotFoundError: If file doesn't exist
yaml.YAMLError: If YAML is invalid
"""
try:
with open(file_path) as f:
return yaml.safe_load(f) or {}
except FileNotFoundError:
logger.warning(f"File not found: {file_path}")
return {}
except yaml.YAMLError as e:
logger.error(f"Failed to parse YAML from {file_path}: {e}")
raise
def normalize_command_name(name: str) -> str:
"""
Normalize command name by removing leading slash and converting to consistent format.
Args:
name: Command name (e.g., "/skill/define" or "skill/define")
Returns:
Normalized command name (e.g., "skill/define")
"""
return name.lstrip("/")
def build_registry_index(skills_data: Dict[str, Any], commands_data: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
"""
Build an index of all active entrypoints from registries.
Args:
skills_data: Parsed skills.json
commands_data: Parsed commands.json
Returns:
Dictionary mapping command names to their source data
"""
index = {}
# Index skills with entrypoints
for skill in skills_data.get("skills", []):
if skill.get("status") != "active":
continue
skill_name = skill.get("name")
entrypoints = skill.get("entrypoints", [])
for entrypoint in entrypoints:
command = normalize_command_name(entrypoint.get("command", ""))
if command:
index[command] = {
"type": "skill",
"source": skill_name,
"skill": skill,
"entrypoint": entrypoint
}
# Index commands
for command in commands_data.get("commands", []):
if command.get("status") != "active":
continue
command_name = normalize_command_name(command.get("name", ""))
if command_name and command_name not in index:
index[command_name] = {
"type": "command",
"source": command_name,
"command": command
}
return index
def build_plugin_index(plugin_data: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
"""
Build an index of all commands in plugin.yaml.
Args:
plugin_data: Parsed plugin.yaml
Returns:
Dictionary mapping command names to their plugin data
"""
index = {}
for command in plugin_data.get("commands", []):
command_name = normalize_command_name(command.get("name", ""))
if command_name:
index[command_name] = command
return index
def compare_permissions(registry_perms: List[str], plugin_perms: List[str]) -> Tuple[bool, List[str]]:
"""
Compare permissions between registry and plugin.
Args:
registry_perms: Permissions from registry
plugin_perms: Permissions from plugin
Returns:
Tuple of (match, differences)
"""
if not registry_perms and not plugin_perms:
return True, []
registry_set = set(registry_perms or [])
plugin_set = set(plugin_perms or [])
if registry_set == plugin_set:
return True, []
differences = []
missing = registry_set - plugin_set
extra = plugin_set - registry_set
if missing:
differences.append(f"Missing: {', '.join(sorted(missing))}")
if extra:
differences.append(f"Extra: {', '.join(sorted(extra))}")
return False, differences
def analyze_command_metadata(
command_name: str,
registry_entry: Dict[str, Any],
plugin_entry: Optional[Dict[str, Any]]
) -> List[Dict[str, Any]]:
"""
Analyze metadata differences between registry and plugin entries.
Args:
command_name: Name of the command
registry_entry: Entry from registry index
plugin_entry: Entry from plugin index (if exists)
Returns:
List of metadata issues
"""
issues = []
if not plugin_entry:
return issues
# Extract registry metadata based on type
if registry_entry["type"] == "skill":
entrypoint = registry_entry["entrypoint"]
registry_runtime = entrypoint.get("runtime", "python")
registry_perms = entrypoint.get("permissions", [])
registry_handler = entrypoint.get("handler", "")
registry_desc = entrypoint.get("description") or registry_entry["skill"].get("description", "")
else:
command = registry_entry["command"]
registry_runtime = command.get("execution", {}).get("runtime", "python")
registry_perms = command.get("permissions", [])
registry_handler = None
registry_desc = command.get("description", "")
# Extract plugin metadata
plugin_runtime = plugin_entry.get("handler", {}).get("runtime", "python")
plugin_perms = plugin_entry.get("permissions", [])
plugin_handler = plugin_entry.get("handler", {}).get("script", "")
plugin_desc = plugin_entry.get("description", "")
# Check runtime
if registry_runtime != plugin_runtime:
issues.append({
"type": "runtime_mismatch",
"command": command_name,
"registry_value": registry_runtime,
"plugin_value": plugin_runtime
})
# Check permissions
perms_match, perms_diff = compare_permissions(registry_perms, plugin_perms)
if not perms_match:
issues.append({
"type": "permissions_mismatch",
"command": command_name,
"differences": perms_diff,
"registry_value": registry_perms,
"plugin_value": plugin_perms
})
# Check handler path (for skills only)
if registry_handler and registry_entry["type"] == "skill":
expected_handler = f"skills/{registry_entry['source']}/{registry_handler}"
if plugin_handler != expected_handler:
issues.append({
"type": "handler_mismatch",
"command": command_name,
"registry_value": expected_handler,
"plugin_value": plugin_handler
})
# Check description
if registry_desc and plugin_desc and registry_desc.strip() != plugin_desc.strip():
issues.append({
"type": "description_mismatch",
"command": command_name,
"registry_value": registry_desc,
"plugin_value": plugin_desc
})
return issues
def reconcile_registries_with_plugin(
skills_data: Dict[str, Any],
commands_data: Dict[str, Any],
plugin_data: Dict[str, Any]
) -> Dict[str, Any]:
"""
Compare registries with plugin.yaml and identify discrepancies.
Args:
skills_data: Parsed skills.json
commands_data: Parsed commands.json
plugin_data: Parsed plugin.yaml
Returns:
Dictionary containing analysis results
"""
logger.info("Building registry index...")
registry_index = build_registry_index(skills_data, commands_data)
logger.info("Building plugin index...")
plugin_index = build_plugin_index(plugin_data)
logger.info("Comparing registries with plugin.yaml...")
# Find missing commands (in registry but not in plugin)
missing_commands = []
for cmd_name, registry_entry in registry_index.items():
if cmd_name not in plugin_index:
missing_commands.append({
"command": cmd_name,
"type": registry_entry["type"],
"source": registry_entry["source"],
"registry_entry": registry_entry
})
# Find orphaned commands (in plugin but not in registry)
orphaned_commands = []
for cmd_name, plugin_entry in plugin_index.items():
if cmd_name not in registry_index:
orphaned_commands.append({
"command": cmd_name,
"plugin_entry": plugin_entry
})
# Find metadata mismatches
metadata_issues = []
for cmd_name, registry_entry in registry_index.items():
if cmd_name in plugin_index:
issues = analyze_command_metadata(cmd_name, registry_entry, plugin_index[cmd_name])
metadata_issues.extend(issues)
# Check for missing metadata suggestions
metadata_suggestions = []
for cmd_name, registry_entry in registry_index.items():
if registry_entry["type"] == "skill":
entrypoint = registry_entry["entrypoint"]
if not entrypoint.get("permissions"):
metadata_suggestions.append({
"command": cmd_name,
"field": "permissions",
"suggestion": "Consider adding permissions metadata"
})
if not entrypoint.get("description"):
metadata_suggestions.append({
"command": cmd_name,
"field": "description",
"suggestion": "Consider adding description"
})
return {
"missing_commands": missing_commands,
"orphaned_commands": orphaned_commands,
"metadata_issues": metadata_issues,
"metadata_suggestions": metadata_suggestions,
"total_registry_commands": len(registry_index),
"total_plugin_commands": len(plugin_index)
}
def generate_updated_plugin_yaml(
plugin_data: Dict[str, Any],
registry_index: Dict[str, Dict[str, Any]],
reconciliation: Dict[str, Any]
) -> Dict[str, Any]:
"""
Generate an updated plugin.yaml based on reconciliation results.
Args:
plugin_data: Current plugin.yaml data
registry_index: Index of registry entries
reconciliation: Reconciliation results
Returns:
Updated plugin.yaml data
"""
updated_plugin = {**plugin_data}
# Build new commands list
commands = []
plugin_index = build_plugin_index(plugin_data)
# Add all commands from registry
for cmd_name, registry_entry in registry_index.items():
if registry_entry["type"] == "skill":
skill = registry_entry["skill"]
entrypoint = registry_entry["entrypoint"]
command = {
"name": cmd_name,
"description": entrypoint.get("description") or skill.get("description", ""),
"handler": {
"runtime": entrypoint.get("runtime", "python"),
"script": f"skills/{skill['name']}/{entrypoint.get('handler', '')}"
}
}
# Add parameters if present
if "parameters" in entrypoint:
command["parameters"] = entrypoint["parameters"]
# Add permissions if present
if "permissions" in entrypoint:
command["permissions"] = entrypoint["permissions"]
commands.append(command)
elif registry_entry["type"] == "command":
# Convert command registry format to plugin format
cmd = registry_entry["command"]
command = {
"name": cmd_name,
"description": cmd.get("description", ""),
"handler": {
"runtime": cmd.get("execution", {}).get("runtime", "python"),
"script": cmd.get("execution", {}).get("target", "")
}
}
if "parameters" in cmd:
command["parameters"] = cmd["parameters"]
if "permissions" in cmd:
command["permissions"] = cmd["permissions"]
commands.append(command)
updated_plugin["commands"] = commands
# Update metadata
if "metadata" not in updated_plugin:
updated_plugin["metadata"] = {}
updated_plugin["metadata"]["updated_at"] = datetime.now(timezone.utc).isoformat()
updated_plugin["metadata"]["updated_by"] = "docs.sync.plugin_manifest skill"
updated_plugin["metadata"]["command_count"] = len(commands)
return updated_plugin
def write_yaml_file(data: Dict[str, Any], file_path: str, header: Optional[str] = None):
"""
Write data to YAML file with optional header.
Args:
data: Dictionary to write
file_path: Path to write to
header: Optional header comment
"""
with open(file_path, 'w') as f:
if header:
f.write(header)
yaml.dump(data, f, default_flow_style=False, sort_keys=False, indent=2)
logger.info(f"✅ Written file to {file_path}")
def generate_diff_report(reconciliation: Dict[str, Any]) -> str:
"""
Generate a human-readable diff report.
Args:
reconciliation: Reconciliation results
Returns:
Formatted report string
"""
lines = []
lines.append("# Plugin Manifest Reconciliation Report")
lines.append(f"Generated: {datetime.now(timezone.utc).isoformat()}\n")
# Summary
lines.append("## Summary")
lines.append(f"- Total commands in registry: {reconciliation['total_registry_commands']}")
lines.append(f"- Total commands in plugin.yaml: {reconciliation['total_plugin_commands']}")
lines.append(f"- Missing from plugin.yaml: {len(reconciliation['missing_commands'])}")
lines.append(f"- Orphaned in plugin.yaml: {len(reconciliation['orphaned_commands'])}")
lines.append(f"- Metadata issues: {len(reconciliation['metadata_issues'])}")
lines.append(f"- Metadata suggestions: {len(reconciliation['metadata_suggestions'])}\n")
# Missing commands
if reconciliation['missing_commands']:
lines.append("## Missing Commands (in registry but not in plugin.yaml)")
for item in reconciliation['missing_commands']:
lines.append(f"- **{item['command']}** ({item['type']}: {item['source']})")
lines.append("")
# Orphaned commands
if reconciliation['orphaned_commands']:
lines.append("## Orphaned Commands (in plugin.yaml but not in registry)")
for item in reconciliation['orphaned_commands']:
lines.append(f"- **{item['command']}**")
lines.append("")
# Metadata issues
if reconciliation['metadata_issues']:
lines.append("## Metadata Issues")
for issue in reconciliation['metadata_issues']:
issue_type = issue['type'].replace('_', ' ').title()
lines.append(f"- **{issue['command']}**: {issue_type}")
if 'differences' in issue:
for diff in issue['differences']:
lines.append(f" - {diff}")
elif 'registry_value' in issue and 'plugin_value' in issue:
lines.append(f" - Registry: `{issue['registry_value']}`")
lines.append(f" - Plugin: `{issue['plugin_value']}`")
lines.append("")
# Suggestions
if reconciliation['metadata_suggestions']:
lines.append("## Metadata Suggestions")
for suggestion in reconciliation['metadata_suggestions']:
lines.append(f"- **{suggestion['command']}** ({suggestion['field']}): {suggestion['suggestion']}")
lines.append("")
return "\n".join(lines)
def main():
"""Main CLI entry point."""
logger.info("Starting plugin manifest reconciliation...")
# Define file paths
skills_path = os.path.join(BASE_DIR, "registry", "skills.json")
commands_path = os.path.join(BASE_DIR, "registry", "commands.json")
plugin_path = os.path.join(BASE_DIR, "plugin.yaml")
preview_path = os.path.join(BASE_DIR, "plugin.preview.yaml")
report_path = os.path.join(BASE_DIR, "plugin_manifest_diff.md")
try:
# Load files
logger.info("Loading registry files...")
skills_data = load_json_file(skills_path)
commands_data = load_json_file(commands_path)
logger.info("Loading plugin.yaml...")
plugin_data = load_yaml_file(plugin_path)
# Reconcile
logger.info("Reconciling registries with plugin.yaml...")
reconciliation = reconcile_registries_with_plugin(skills_data, commands_data, plugin_data)
# Generate updated plugin.yaml
logger.info("Generating updated plugin.yaml...")
registry_index = build_registry_index(skills_data, commands_data)
updated_plugin = generate_updated_plugin_yaml(plugin_data, registry_index, reconciliation)
# Write preview file
header = """# Betty Framework - Claude Code Plugin (Preview)
# Generated by docs.sync.plugin_manifest skill
# Review changes before applying to plugin.yaml
"""
write_yaml_file(updated_plugin, preview_path, header)
# Generate diff report
logger.info("Generating diff report...")
diff_report = generate_diff_report(reconciliation)
with open(report_path, 'w') as f:
f.write(diff_report)
logger.info(f"✅ Written diff report to {report_path}")
# Print summary
print("\n" + "="*60)
print("PLUGIN MANIFEST RECONCILIATION COMPLETE")
print("="*60)
print(f"\n📊 Summary:")
print(f" - Commands in registry: {reconciliation['total_registry_commands']}")
print(f" - Commands in plugin.yaml: {reconciliation['total_plugin_commands']}")
print(f" - Missing from plugin.yaml: {len(reconciliation['missing_commands'])}")
print(f" - Orphaned in plugin.yaml: {len(reconciliation['orphaned_commands'])}")
print(f" - Metadata issues: {len(reconciliation['metadata_issues'])}")
print(f" - Metadata suggestions: {len(reconciliation['metadata_suggestions'])}")
print(f"\n📄 Output files:")
print(f" - Preview: {preview_path}")
print(f" - Diff report: {report_path}")
if reconciliation['missing_commands']:
print(f"\n⚠️ {len(reconciliation['missing_commands'])} command(s) missing from plugin.yaml:")
for item in reconciliation['missing_commands'][:5]:
print(f" - {item['command']} ({item['source']})")
if len(reconciliation['missing_commands']) > 5:
print(f" ... and {len(reconciliation['missing_commands']) - 5} more")
if reconciliation['orphaned_commands']:
print(f"\n⚠️ {len(reconciliation['orphaned_commands'])} orphaned command(s) in plugin.yaml:")
for item in reconciliation['orphaned_commands'][:5]:
print(f" - {item['command']}")
if len(reconciliation['orphaned_commands']) > 5:
print(f" ... and {len(reconciliation['orphaned_commands']) - 5} more")
print(f"\n✅ Review {report_path} for full details")
print("="*60 + "\n")
# Return result
result = {
"ok": True,
"status": "success",
"preview_path": preview_path,
"report_path": report_path,
"reconciliation": reconciliation
}
print(json.dumps(result, indent=2))
sys.exit(0)
except Exception as e:
logger.error(f"Failed to reconcile plugin manifest: {e}")
import traceback
traceback.print_exc()
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,33 @@
name: docs.sync.pluginmanifest
version: 0.1.0
description: >
Reconciles plugin.yaml with Betty Framework registries to ensure consistency.
Identifies missing, orphaned, and mismatched command entries and suggests corrections.
inputs: []
outputs:
- plugin.preview.yaml
- plugin_manifest_diff.md
dependencies:
- plugin.sync
- registry.update
status: active
entrypoints:
- command: /docs/sync/plugin-manifest
handler: plugin_manifest_sync.py
runtime: python
description: >
Reconcile plugin.yaml with registry files. Identifies discrepancies and generates
plugin.preview.yaml with suggested updates and a detailed diff report.
parameters: []
permissions:
- filesystem:read
- filesystem:write
tags:
- docs
- plugin
- registry
- validation
- reconciliation
- infrastructure