Initial commit
This commit is contained in:
328
skills/skill.define/SKILL.md
Normal file
328
skills/skill.define/SKILL.md
Normal file
@@ -0,0 +1,328 @@
|
||||
---
|
||||
name: Skill Define
|
||||
description: Validate and register new Claude Code Skill manifests (.skill.yaml) to ensure structure, inputs/outputs, and dependencies are correct.
|
||||
---
|
||||
|
||||
# skill.define
|
||||
|
||||
## Overview
|
||||
|
||||
**skill.define** is the compiler and registrar for Betty Framework skills. It ensures each `skill.yaml` conforms to schema and governance rules before registration.
|
||||
|
||||
## Purpose
|
||||
|
||||
Acts as the quality gate for all skills in the Betty ecosystem:
|
||||
- **Schema Validation**: Ensures all required fields are present
|
||||
- **Manifest Parsing**: Validates YAML structure and syntax
|
||||
- **Registry Integration**: Delegates to `registry.update` for registration
|
||||
- **Error Reporting**: Provides detailed validation errors for troubleshooting
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```bash
|
||||
python skills/skill.define/skill_define.py <path_to_skill.yaml>
|
||||
```
|
||||
|
||||
### Arguments
|
||||
|
||||
| Argument | Type | Required | Description |
|
||||
|----------|------|----------|-------------|
|
||||
| manifest_path | string | Yes | Path to the skill manifest file (skill.yaml) |
|
||||
|
||||
## Required Skill Manifest Fields
|
||||
|
||||
A valid skill manifest must include:
|
||||
|
||||
| Field | Type | Description | Example |
|
||||
|-------|------|-------------|---------|
|
||||
| `name` | string | Unique skill identifier | `api.validate` |
|
||||
| `version` | string | Semantic version | `0.1.0` |
|
||||
| `description` | string | What the skill does | `Validates API specifications` |
|
||||
| `inputs` | array | Input parameters | `["spec_path", "guideline_set"]` |
|
||||
| `outputs` | array | Output artifacts | `["validation_report"]` |
|
||||
| `dependencies` | array | Required skills/deps | `["context.schema"]` |
|
||||
| `status` | string | Skill status | `active` or `draft` |
|
||||
|
||||
### Optional Fields
|
||||
|
||||
- **entrypoints**: CLI command definitions
|
||||
- **tags**: Categorization tags
|
||||
- **permissions**: Required filesystem/network permissions
|
||||
|
||||
## Behavior
|
||||
|
||||
1. **Load Manifest**: Reads and parses the YAML file
|
||||
2. **Validate Structure**: Checks for all required fields
|
||||
3. **Validate Format**: Ensures field types and values are correct
|
||||
4. **Delegate Registration**: Calls `registry.update` to add skill to registry
|
||||
5. **Return Results**: Provides JSON response with validation status
|
||||
|
||||
## Outputs
|
||||
|
||||
### Success Response
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"status": "registered",
|
||||
"errors": [],
|
||||
"path": "skills/workflow.validate/skill.yaml",
|
||||
"details": {
|
||||
"valid": true,
|
||||
"missing": [],
|
||||
"path": "skills/workflow.validate/skill.yaml",
|
||||
"manifest": {
|
||||
"name": "workflow.validate",
|
||||
"version": "0.1.0",
|
||||
"description": "Validates workflow YAML definitions",
|
||||
"inputs": ["workflow.yaml"],
|
||||
"outputs": ["validation_result.json"],
|
||||
"dependencies": ["context.schema"],
|
||||
"status": "active"
|
||||
},
|
||||
"status": "registered",
|
||||
"registry_updated": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Failure Response (Missing Fields)
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"status": "failed",
|
||||
"errors": [
|
||||
"Missing required fields: version, outputs"
|
||||
],
|
||||
"path": "skills/my-skill/skill.yaml",
|
||||
"details": {
|
||||
"valid": false,
|
||||
"missing": ["version", "outputs"],
|
||||
"path": "skills/my-skill/skill.yaml"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Failure Response (Invalid YAML)
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"status": "failed",
|
||||
"errors": [
|
||||
"Failed to parse YAML: mapping values are not allowed here"
|
||||
],
|
||||
"path": "skills/broken/skill.yaml",
|
||||
"details": {
|
||||
"valid": false,
|
||||
"error": "Failed to parse YAML: mapping values are not allowed here",
|
||||
"path": "skills/broken/skill.yaml"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Validate a Complete Skill
|
||||
|
||||
**Skill Manifest** (`skills/api.validate/skill.yaml`):
|
||||
|
||||
```yaml
|
||||
name: api.validate
|
||||
version: 0.1.0
|
||||
description: "Validate OpenAPI and AsyncAPI specifications against enterprise guidelines"
|
||||
|
||||
inputs:
|
||||
- name: spec_path
|
||||
type: string
|
||||
required: true
|
||||
description: "Path to the API specification file"
|
||||
- name: guideline_set
|
||||
type: string
|
||||
required: false
|
||||
default: zalando
|
||||
description: "Which API guidelines to validate against"
|
||||
|
||||
outputs:
|
||||
- name: validation_report
|
||||
type: object
|
||||
description: "Detailed validation results"
|
||||
- name: valid
|
||||
type: boolean
|
||||
description: "Whether the spec is valid"
|
||||
|
||||
dependencies:
|
||||
- context.schema
|
||||
|
||||
status: active
|
||||
tags: [api, validation, openapi]
|
||||
```
|
||||
|
||||
**Validation Command**:
|
||||
|
||||
```bash
|
||||
$ python skills/skill.define/skill_define.py skills/api.validate/skill.yaml
|
||||
{
|
||||
"ok": true,
|
||||
"status": "registered",
|
||||
"errors": [],
|
||||
"path": "skills/api.validate/skill.yaml",
|
||||
"details": {
|
||||
"valid": true,
|
||||
"status": "registered",
|
||||
"registry_updated": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: Detect Missing Fields
|
||||
|
||||
**Incomplete Manifest** (`skills/incomplete/skill.yaml`):
|
||||
|
||||
```yaml
|
||||
name: incomplete.skill
|
||||
description: "This skill is missing required fields"
|
||||
inputs: []
|
||||
```
|
||||
|
||||
**Validation Result**:
|
||||
|
||||
```bash
|
||||
$ python skills/skill.define/skill_define.py skills/incomplete/skill.yaml
|
||||
{
|
||||
"ok": false,
|
||||
"status": "failed",
|
||||
"errors": [
|
||||
"Missing required fields: version, outputs, dependencies, status"
|
||||
],
|
||||
"path": "skills/incomplete/skill.yaml",
|
||||
"details": {
|
||||
"valid": false,
|
||||
"missing": ["version", "outputs", "dependencies", "status"],
|
||||
"path": "skills/incomplete/skill.yaml"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Integration
|
||||
|
||||
### With skill.create
|
||||
|
||||
The `skill.create` skill automatically generates a valid manifest and runs `skill.define` to validate it:
|
||||
|
||||
```bash
|
||||
python skills/skill.create/skill_create.py \
|
||||
my.skill \
|
||||
"Does something useful" \
|
||||
--inputs input1,input2 \
|
||||
--outputs output1
|
||||
# Internally runs skill.define on the generated manifest
|
||||
```
|
||||
|
||||
### With Workflows
|
||||
|
||||
Skills can be validated as part of a workflow:
|
||||
|
||||
```yaml
|
||||
# workflows/create_and_register.yaml
|
||||
steps:
|
||||
- skill: skill.create
|
||||
args: ["workflow.validate", "Validates workflow definitions"]
|
||||
|
||||
- skill: skill.define
|
||||
args: ["skills/workflow.validate/skill.yaml"]
|
||||
required: true
|
||||
|
||||
- skill: registry.update
|
||||
args: ["skills/workflow.validate/skill.yaml"]
|
||||
```
|
||||
|
||||
### With Hooks
|
||||
|
||||
Automatically validate skill manifests when they're edited:
|
||||
|
||||
```bash
|
||||
# Create a hook to validate on save
|
||||
python skills/hook.define/hook_define.py \
|
||||
--event on_file_save \
|
||||
--pattern "skills/*/skill.yaml" \
|
||||
--command "python skills/skill.define/skill_define.py {file_path}" \
|
||||
--blocking true
|
||||
```
|
||||
|
||||
## Common Errors
|
||||
|
||||
| Error | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| "Manifest file not found" | File path is incorrect | Check the path and ensure file exists |
|
||||
| "Failed to parse YAML" | Invalid YAML syntax | Fix YAML syntax errors (indentation, quotes, etc.) |
|
||||
| "Missing required fields: X" | Manifest missing required field(s) | Add the missing field(s) to the manifest |
|
||||
| "registry.update skill not found" | Registry updater not available | Ensure `registry.update` skill exists in `skills/` directory |
|
||||
|
||||
## Relationship with registry.update
|
||||
|
||||
`skill.define` **validates** manifests but **delegates registration** to `registry.update`:
|
||||
|
||||
1. **skill.define**: Validates the manifest structure
|
||||
2. **registry.update**: Updates `/registry/skills.json` with the validated skill
|
||||
|
||||
This separation of concerns follows Betty's single-responsibility principle.
|
||||
|
||||
## Files Read
|
||||
|
||||
- **Input**: Skill manifest at specified path (e.g., `skills/my.skill/skill.yaml`)
|
||||
- **Registry**: May read existing `/registry/skills.json` via delegation to `registry.update`
|
||||
|
||||
## Files Modified
|
||||
|
||||
- **None directly** – Registry updates are delegated to `registry.update` skill
|
||||
- **Indirectly**: `/registry/skills.json` updated via `registry.update`
|
||||
|
||||
## Exit Codes
|
||||
|
||||
- **0**: Success (manifest valid, registration attempted)
|
||||
- **1**: Failure (validation errors or file not found)
|
||||
|
||||
## Logging
|
||||
|
||||
Logs validation steps using Betty's logging infrastructure:
|
||||
|
||||
```
|
||||
INFO: Validating manifest: skills/api.validate/skill.yaml
|
||||
INFO: ✅ Manifest validation passed
|
||||
INFO: 🔁 Delegating registry update to registry.update skill...
|
||||
INFO: Registry update succeeded
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Run Before Commit**: Validate skill manifests before committing changes
|
||||
2. **Use with skill.create**: Let `skill.create` generate manifests to ensure correct structure
|
||||
3. **Check Dependencies**: Ensure any skills listed in `dependencies` exist in the registry
|
||||
4. **Version Properly**: Follow semantic versioning for skill versions
|
||||
5. **Complete Descriptions**: Write clear descriptions for inputs, outputs, and the skill itself
|
||||
6. **Set Status Appropriately**: Use `draft` for development, `active` for production-ready skills
|
||||
|
||||
## See Also
|
||||
|
||||
- **skill.create** – Generate new skill scaffolding with valid manifest ([skill.create SKILL.md](../skill.create/SKILL.md))
|
||||
- **registry.update** – Update the skill registry ([registry.update SKILL.md](../registry.update/SKILL.md))
|
||||
- **Betty Architecture** – Understanding the skill layer ([Five-Layer Model](../../docs/betty-architecture.md))
|
||||
- **Skill Framework** – Overview of skill categories and design ([Skill Framework](../../docs/skills-framework.md))
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **registry.update**: For updating the skill registry (delegated call)
|
||||
- **betty.validation**: Validation utility functions
|
||||
- **betty.config**: Configuration constants
|
||||
|
||||
## Status
|
||||
|
||||
**Active** – This skill is production-ready and core to Betty's skill infrastructure.
|
||||
|
||||
## Version History
|
||||
|
||||
- **0.1.0** (Oct 2025) – Initial implementation with manifest validation and registry delegation
|
||||
1
skills/skill.define/__init__.py
Normal file
1
skills/skill.define/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Auto-generated package initializer for skills.
|
||||
28
skills/skill.define/skill.yaml
Normal file
28
skills/skill.define/skill.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
name: skill.define
|
||||
version: 0.1.0
|
||||
description: >
|
||||
Validates and registers skill manifests (.skill.yaml) for the Betty Framework.
|
||||
Ensures schema compliance and updates the Skill Registry.
|
||||
inputs:
|
||||
- manifest_path
|
||||
outputs:
|
||||
- validation_result.json
|
||||
- updated_registry.json
|
||||
dependencies: []
|
||||
status: active
|
||||
|
||||
entrypoints:
|
||||
- command: /skill/define
|
||||
handler: skill_define.py
|
||||
runtime: python
|
||||
description: >
|
||||
Validate a Claude Code skill manifest and register it in the Betty Skill Registry.
|
||||
parameters:
|
||||
- name: manifest_path
|
||||
type: string
|
||||
required: true
|
||||
description: Path to the skill.yaml file to validate.
|
||||
permissions:
|
||||
- filesystem
|
||||
- read
|
||||
- write
|
||||
258
skills/skill.define/skill_define.py
Normal file
258
skills/skill.define/skill_define.py
Normal file
@@ -0,0 +1,258 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
skill_define.py – Implementation of the skill.define Skill
|
||||
Validates skill manifests (.skill.yaml) and registers them in the Skill Registry.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import yaml
|
||||
import subprocess
|
||||
from typing import Dict, Any, List, Optional
|
||||
from pydantic import ValidationError as PydanticValidationError
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
from betty.config import BASE_DIR, REQUIRED_SKILL_FIELDS
|
||||
from betty.validation import validate_path, validate_manifest_fields
|
||||
from betty.logging_utils import setup_logger
|
||||
from betty.errors import SkillValidationError, format_error_response
|
||||
from betty.models import SkillManifest
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
def build_response(ok: bool, path: str, errors: Optional[List[str]] = None, details: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
response: Dict[str, Any] = {
|
||||
"ok": ok,
|
||||
"status": "success" if ok else "failed",
|
||||
"errors": errors or [],
|
||||
"path": path,
|
||||
}
|
||||
|
||||
if details is not None:
|
||||
response["details"] = details
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def load_skill_manifest(path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Load and parse a skill manifest from YAML file.
|
||||
|
||||
Args:
|
||||
path: Path to skill manifest file
|
||||
|
||||
Returns:
|
||||
Parsed manifest dictionary
|
||||
|
||||
Raises:
|
||||
SkillValidationError: If manifest cannot be loaded or parsed
|
||||
"""
|
||||
try:
|
||||
with open(path) as f:
|
||||
manifest = yaml.safe_load(f)
|
||||
return manifest
|
||||
except FileNotFoundError:
|
||||
raise SkillValidationError(f"Manifest file not found: {path}")
|
||||
except yaml.YAMLError as e:
|
||||
raise SkillValidationError(f"Failed to parse YAML: {e}")
|
||||
|
||||
|
||||
def validate_skill_schema(manifest: Dict[str, Any]) -> List[str]:
|
||||
"""
|
||||
Validate skill manifest using Pydantic schema.
|
||||
|
||||
Args:
|
||||
manifest: Skill manifest dictionary
|
||||
|
||||
Returns:
|
||||
List of validation errors (empty if valid)
|
||||
"""
|
||||
errors: List[str] = []
|
||||
|
||||
try:
|
||||
SkillManifest.model_validate(manifest)
|
||||
logger.info("Pydantic schema validation passed for skill manifest")
|
||||
except PydanticValidationError as exc:
|
||||
logger.warning("Pydantic schema validation failed for skill manifest")
|
||||
for error in exc.errors():
|
||||
field = ".".join(str(loc) for loc in error["loc"])
|
||||
message = error["msg"]
|
||||
error_type = error["type"]
|
||||
errors.append(f"Schema validation error at '{field}': {message} (type: {error_type})")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def validate_manifest(path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Validate that required fields exist in a skill manifest.
|
||||
|
||||
Args:
|
||||
path: Path to skill manifest file
|
||||
|
||||
Returns:
|
||||
Dictionary with validation results:
|
||||
- valid: Boolean indicating if manifest is valid
|
||||
- missing: List of missing required fields (if any)
|
||||
- manifest: The parsed manifest (if valid)
|
||||
- path: Path to the manifest file
|
||||
|
||||
Raises:
|
||||
SkillValidationError: If validation fails
|
||||
"""
|
||||
validate_path(path, must_exist=True)
|
||||
|
||||
logger.info(f"Validating manifest: {path}")
|
||||
|
||||
try:
|
||||
manifest = load_skill_manifest(path)
|
||||
except SkillValidationError as e:
|
||||
return {
|
||||
"valid": False,
|
||||
"error": str(e),
|
||||
"path": path
|
||||
}
|
||||
|
||||
# Validate with Pydantic schema first
|
||||
schema_errors = validate_skill_schema(manifest)
|
||||
if schema_errors:
|
||||
return {
|
||||
"valid": False,
|
||||
"errors": schema_errors,
|
||||
"path": path
|
||||
}
|
||||
|
||||
# Validate required fields
|
||||
missing = validate_manifest_fields(manifest, REQUIRED_SKILL_FIELDS)
|
||||
|
||||
if missing:
|
||||
logger.warning(f"Missing required fields: {missing}")
|
||||
return {
|
||||
"valid": False,
|
||||
"missing": missing,
|
||||
"path": path
|
||||
}
|
||||
|
||||
logger.info("✅ Manifest validation passed")
|
||||
return {
|
||||
"valid": True,
|
||||
"missing": [],
|
||||
"path": path,
|
||||
"manifest": manifest
|
||||
}
|
||||
|
||||
|
||||
def delegate_to_registry_update(manifest_path: str) -> bool:
|
||||
"""
|
||||
Delegate registry update to registry.update skill.
|
||||
|
||||
Args:
|
||||
manifest_path: Path to skill manifest
|
||||
|
||||
Returns:
|
||||
True if registry update succeeded, False otherwise
|
||||
"""
|
||||
registry_updater = os.path.join(BASE_DIR, "skills", "registry.update", "registry_update.py")
|
||||
|
||||
if not os.path.exists(registry_updater):
|
||||
logger.warning("registry.update skill not found - skipping registry update")
|
||||
return False
|
||||
|
||||
logger.info("🔁 Delegating registry update to registry.update skill...")
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, registry_updater, manifest_path],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
logger.error(f"Registry update failed: {result.stderr}")
|
||||
return False
|
||||
|
||||
logger.info("Registry update succeeded")
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
"""Main CLI entry point."""
|
||||
if len(sys.argv) < 2:
|
||||
message = "Usage: skill_define.py <path_to_skill.yaml>"
|
||||
response = build_response(
|
||||
False,
|
||||
path="",
|
||||
errors=[message],
|
||||
details={"error": {"error": "UsageError", "message": message, "details": {}}},
|
||||
)
|
||||
print(json.dumps(response, indent=2))
|
||||
sys.exit(1)
|
||||
|
||||
path = sys.argv[1]
|
||||
|
||||
try:
|
||||
validation = validate_manifest(path)
|
||||
details = dict(validation)
|
||||
|
||||
if validation.get("valid"):
|
||||
registry_updated = delegate_to_registry_update(path)
|
||||
details["status"] = "registered" if registry_updated else "validated"
|
||||
details["registry_updated"] = registry_updated
|
||||
|
||||
errors: List[str] = []
|
||||
if not validation.get("valid"):
|
||||
if validation.get("missing"):
|
||||
errors.append("Missing required fields: " + ", ".join(validation["missing"]))
|
||||
if validation.get("error"):
|
||||
errors.append(str(validation["error"]))
|
||||
if validation.get("errors"):
|
||||
errors.extend(validation.get("errors"))
|
||||
|
||||
# Check if there are schema validation errors
|
||||
has_schema_errors = any("Schema validation error" in err for err in validation.get("errors", []))
|
||||
if has_schema_errors:
|
||||
details["error"] = {
|
||||
"type": "SchemaError",
|
||||
"error": "SchemaError",
|
||||
"message": "Skill manifest schema validation failed",
|
||||
"details": {"errors": validation.get("errors", [])}
|
||||
}
|
||||
|
||||
response = build_response(
|
||||
bool(validation.get("valid")),
|
||||
path=path,
|
||||
errors=errors,
|
||||
details=details,
|
||||
)
|
||||
print(json.dumps(response, indent=2))
|
||||
sys.exit(0 if response["ok"] else 1)
|
||||
|
||||
except SkillValidationError as e:
|
||||
logger.error(str(e))
|
||||
error_info = format_error_response(e)
|
||||
response = build_response(
|
||||
False,
|
||||
path=path,
|
||||
errors=[error_info.get("message", str(e))],
|
||||
details={"error": error_info},
|
||||
)
|
||||
print(json.dumps(response, indent=2))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error: {e}")
|
||||
error_info = format_error_response(e, include_traceback=True)
|
||||
response = build_response(
|
||||
False,
|
||||
path=path,
|
||||
errors=[error_info.get("message", str(e))],
|
||||
details={"error": error_info},
|
||||
)
|
||||
print(json.dumps(response, indent=2))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user