Initial commit
This commit is contained in:
751
skills/docs.sync.pluginmanifest/SKILL.md
Normal file
751
skills/docs.sync.pluginmanifest/SKILL.md
Normal 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
|
||||
1
skills/docs.sync.pluginmanifest/__init__.py
Normal file
1
skills/docs.sync.pluginmanifest/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Auto-generated package initializer for skills.
|
||||
609
skills/docs.sync.pluginmanifest/plugin_manifest_sync.py
Executable file
609
skills/docs.sync.pluginmanifest/plugin_manifest_sync.py
Executable 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()
|
||||
33
skills/docs.sync.pluginmanifest/skill.yaml
Normal file
33
skills/docs.sync.pluginmanifest/skill.yaml
Normal 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
|
||||
Reference in New Issue
Block a user