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,245 @@
---
name: Command Define
description: Validates and registers command manifest files (YAML) to integrate new slash commands into Betty.
---
# command.define Skill
Validates and registers command manifest files (YAML) to integrate new slash commands into Betty.
## Purpose
The `command.define` skill acts as the "compiler" for Betty Commands. It ensures a command manifest meets all schema requirements and then updates the Command Registry (`/registry/commands.json`) with the new command.
This skill is part of Betty's Layer 1 (Commands) infrastructure, enabling developers to create user-facing slash commands that delegate to agents, workflows, or skills.
## Usage
```bash
python skills/command.define/command_define.py <path_to_command.yaml>
```
### Arguments
| Argument | Type | Required | Description |
|----------|------|----------|-------------|
| manifest_path | string | Yes | Path to the command manifest YAML to validate and register |
## Behavior
1. **Schema Validation** Checks that required fields (`name`, `version`, `description`, `execution`) are present and correctly formatted (e.g., name must start with `/`).
2. **Parameter Verification** Verifies each parameter in the manifest has `name`, `type`, and `description`, and that the execution target (agent/skill/workflow) actually exists in the system.
3. **Registry Update** On success, adds the command entry to `/registry/commands.json` with status `active`.
## Validation Rules
### Required Fields
- **name**: Command name (must start with `/`, e.g., `/api-design`)
- **version**: Semantic version (e.g., `0.1.0`)
- **description**: Human-readable description of what the command does
- **execution**: Object specifying how to execute the command
### Execution Configuration
The `execution` field must contain:
- **type**: One of `skill`, `agent`, or `workflow`
- **target**: Name of the skill/agent/workflow to invoke
- For skills: Must exist in `/registry/skills.json`
- For agents: Must exist in `/registry/agents.json`
- For workflows: File must exist at `/workflows/{target}.yaml`
### Optional Fields
- **parameters**: Array of parameter objects, each with:
- `name` (required): Parameter name
- `type` (required): Parameter type (string, number, boolean, etc.)
- `required` (optional): Whether parameter is required
- `description` (optional): Parameter description
- `default` (optional): Default value
- **status**: Command status (`draft` or `active`, defaults to `draft`)
- **tags**: Array of tags for categorization
## Outputs
### Success Response
```json
{
"ok": true,
"status": "registered",
"errors": [],
"path": "commands/hello.yaml",
"details": {
"valid": true,
"status": "registered",
"registry_updated": true,
"manifest": {
"name": "/hello",
"version": "0.1.0",
"description": "Prints Hello World",
"execution": {
"type": "skill",
"target": "test.hello"
}
}
}
}
```
### Failure Response
```json
{
"ok": false,
"status": "failed",
"errors": [
"Skill 'test.hello' not found in skill registry"
],
"path": "commands/hello.yaml",
"details": {
"valid": false,
"errors": [
"Skill 'test.hello' not found in skill registry"
],
"path": "commands/hello.yaml"
}
}
```
## Example
### Valid Command Manifest
```yaml
# commands/api-design.yaml
name: /api-design
version: 0.1.0
description: "Design a new API following enterprise guidelines"
parameters:
- name: service_name
type: string
required: true
description: "Name of the service/API"
- name: spec_type
type: string
required: false
default: openapi
description: "Type of API specification (openapi or asyncapi)"
execution:
type: agent
target: api.designer
status: active
tags: [api, design, enterprise]
```
### Running the Validator
```bash
$ python skills/command.define/command_define.py commands/api-design.yaml
{
"ok": true,
"status": "registered",
"errors": [],
"path": "commands/api-design.yaml",
"details": {
"valid": true,
"status": "registered",
"registry_updated": true
}
}
```
### Invalid Command Example
If the target agent doesn't exist:
```bash
$ python skills/command.define/command_define.py commands/hello.yaml
{
"ok": false,
"status": "failed",
"errors": [
"Agent 'api.designer' not found in agent registry"
],
"path": "commands/hello.yaml"
}
```
## Integration
### With Workflows
Commands can be validated as part of a workflow:
```yaml
# workflows/register_command.yaml
steps:
- skill: command.define
args:
- "commands/my-command.yaml"
required: true
```
### With Hooks
Validate commands automatically when they're edited:
```bash
# Create a hook that validates command manifests on save
python skills/hook.define/hook_define.py \
--event on_file_save \
--pattern "commands/**/*.yaml" \
--command "python skills/command.define/command_define.py" \
--blocking true
```
## Common Errors
| Error | Cause | Solution |
|-------|-------|----------|
| "Missing required fields: name" | Command manifest missing `name` field | Add `name` field with value starting with `/` |
| "Invalid name: Command name must start with /" | Name doesn't start with `/` | Update name to start with `/` (e.g., `/api-design`) |
| "Skill 'X' not found in skill registry" | Referenced skill doesn't exist | Register the skill first using `skill.define` or fix the target name |
| "Agent 'X' not found in agent registry" | Referenced agent doesn't exist | Register the agent first using `agent.define` or fix the target name |
| "Workflow file not found" | Referenced workflow file doesn't exist | Create the workflow file at `/workflows/{target}.yaml` |
| "execution.type is required" | Missing execution type | Add `execution.type` field with value `skill`, `agent`, or `workflow` |
## See Also
- **Command Manifest Schema** documented in [Command and Hook Infrastructure](../../docs/COMMAND_HOOK_INFRASTRUCTURE.md)
- **Slash Commands Usage** overview in [.claude/commands/README.md](../../.claude/commands/README.md)
- **Betty Architecture** [Five-Layer Model](../../docs/betty-architecture.md) for understanding how commands fit into the framework
- **agent.define** for validating and registering agents that commands can invoke
- **hook.define** for creating validation hooks that can trigger command validation
## Exit Codes
- **0**: Success (manifest valid and registered)
- **1**: Failure (validation errors or registry update failed)
## Files Modified
- **Registry**: `/registry/commands.json` updated with new or modified command entry
- **Logs**: Command validation and registration logged to Betty's logging system
## Dependencies
- **Skill Registry** (`/registry/skills.json`) for validating skill targets
- **Agent Registry** (`/registry/agents.json`) for validating agent targets
- **Workflow Files** (`/workflows/*.yaml`) for validating workflow targets
## Status
**Active** This skill is production-ready and actively used in Betty's command infrastructure.
## Version History
- **0.1.0** (Oct 2025) Initial implementation with full validation and registry management

View File

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

View File

@@ -0,0 +1,493 @@
#!/usr/bin/env python3
"""
command_define.py Implementation of the command.define Skill
Validates command manifests and registers them in the Command Registry.
"""
import os
import sys
import json
import yaml
from typing import Dict, Any, List, Optional
from datetime import datetime, timezone
from pydantic import ValidationError as PydanticValidationError
from betty.config import (
BASE_DIR,
REQUIRED_COMMAND_FIELDS,
COMMANDS_REGISTRY_FILE,
REGISTRY_FILE,
AGENTS_REGISTRY_FILE,
)
from betty.enums import CommandExecutionType, CommandStatus
from betty.validation import (
validate_path,
validate_manifest_fields,
validate_command_name,
validate_version,
validate_command_execution_type
)
from betty.logging_utils import setup_logger
from betty.errors import format_error_response
from betty.models import CommandManifest
from betty.file_utils import atomic_write_json
logger = setup_logger(__name__)
class CommandValidationError(Exception):
"""Raised when command validation fails."""
pass
class CommandRegistryError(Exception):
"""Raised when command registry operations fail."""
pass
def build_response(ok: bool, path: str, errors: Optional[List[str]] = None, details: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""
Build standardized response dictionary.
Args:
ok: Whether operation succeeded
path: Path to command manifest
errors: List of error messages
details: Additional details
Returns:
Response dictionary
"""
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_command_manifest(path: str) -> Dict[str, Any]:
"""
Load and parse a command manifest from YAML file.
Args:
path: Path to command manifest file
Returns:
Parsed manifest dictionary
Raises:
CommandValidationError: If manifest cannot be loaded or parsed
"""
try:
with open(path) as f:
manifest = yaml.safe_load(f)
return manifest
except FileNotFoundError:
raise CommandValidationError(f"Manifest file not found: {path}")
except yaml.YAMLError as e:
raise CommandValidationError(f"Failed to parse YAML: {e}")
def load_skill_registry() -> Dict[str, Any]:
"""
Load skill registry for validation.
Returns:
Skill registry dictionary
Raises:
CommandValidationError: If registry cannot be loaded
"""
try:
with open(REGISTRY_FILE) as f:
return json.load(f)
except FileNotFoundError:
raise CommandValidationError(f"Skill registry not found: {REGISTRY_FILE}")
except json.JSONDecodeError as e:
raise CommandValidationError(f"Failed to parse skill registry: {e}")
def load_agent_registry() -> Dict[str, Any]:
"""
Load agent registry for validation.
Returns:
Agent registry dictionary
Raises:
CommandValidationError: If registry cannot be loaded
"""
try:
with open(AGENTS_REGISTRY_FILE) as f:
return json.load(f)
except FileNotFoundError:
raise CommandValidationError(f"Agent registry not found: {AGENTS_REGISTRY_FILE}")
except json.JSONDecodeError as e:
raise CommandValidationError(f"Failed to parse agent registry: {e}")
def validate_command_schema(manifest: Dict[str, Any]) -> List[str]:
"""
Validate command manifest using Pydantic schema.
Args:
manifest: Command manifest dictionary
Returns:
List of validation errors (empty if valid)
"""
errors: List[str] = []
try:
CommandManifest.model_validate(manifest)
logger.info("Pydantic schema validation passed for command manifest")
except PydanticValidationError as exc:
logger.warning("Pydantic schema validation failed for command 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_execution_target(execution: Dict[str, Any]) -> List[str]:
"""
Validate that the execution target exists in the appropriate registry.
Args:
execution: Execution configuration from manifest
Returns:
List of validation errors (empty if valid)
"""
errors = []
exec_type = execution.get("type")
target = execution.get("target")
if not target:
errors.append("execution.target is required")
return errors
try:
if exec_type == "skill":
# Validate skill exists
skill_registry = load_skill_registry()
registered_skills = {skill["name"] for skill in skill_registry.get("skills", [])}
if target not in registered_skills:
errors.append(f"Skill '{target}' not found in skill registry")
elif exec_type == "agent":
# Validate agent exists
agent_registry = load_agent_registry()
registered_agents = {agent["name"] for agent in agent_registry.get("agents", [])}
if target not in registered_agents:
errors.append(f"Agent '{target}' not found in agent registry")
elif exec_type == "workflow":
# Validate workflow file exists
workflow_path = os.path.join(BASE_DIR, "workflows", f"{target}.yaml")
if not os.path.exists(workflow_path):
errors.append(f"Workflow file not found: {workflow_path}")
except CommandValidationError as e:
errors.append(f"Could not validate target: {str(e)}")
return errors
def validate_manifest(path: str) -> Dict[str, Any]:
"""
Validate that a command manifest meets all requirements.
Validation checks:
1. Required fields are present
2. Name format is valid
3. Version format is valid
4. Execution type is valid
5. Execution target exists in appropriate registry
6. Parameters are properly formatted (if present)
Args:
path: Path to command manifest file
Returns:
Dictionary with validation results:
- valid: Boolean indicating if manifest is valid
- errors: List of validation errors (if any)
- manifest: The parsed manifest (if valid)
- path: Path to the manifest file
"""
validate_path(path, must_exist=True)
logger.info(f"Validating command manifest: {path}")
errors = []
# Load manifest
try:
manifest = load_command_manifest(path)
except CommandValidationError as e:
return {
"valid": False,
"errors": [str(e)],
"path": path
}
# Check required fields first so the message appears before schema errors
missing = validate_manifest_fields(manifest, REQUIRED_COMMAND_FIELDS)
if missing:
missing_message = f"Missing required fields: {', '.join(missing)}"
errors.append(missing_message)
logger.warning(f"Missing required fields: {missing}")
# Validate with Pydantic schema (keep going to surface custom errors too)
schema_errors = validate_command_schema(manifest)
errors.extend(schema_errors)
name = manifest.get("name")
if name is not None:
try:
validate_command_name(name)
except Exception as e:
errors.append(f"Invalid name: {str(e)}")
logger.warning(f"Invalid name: {e}")
version = manifest.get("version")
if version is not None:
try:
validate_version(version)
except Exception as e:
errors.append(f"Invalid version: {str(e)}")
logger.warning(f"Invalid version: {e}")
execution = manifest.get("execution")
if execution is None:
if "execution" not in missing:
errors.append("execution must be provided")
logger.warning("Execution configuration missing")
elif not isinstance(execution, dict):
errors.append("execution must be an object")
logger.warning("Execution configuration is not a dictionary")
else:
exec_type = execution.get("type")
if not exec_type:
errors.append("execution.type is required")
else:
try:
validate_command_execution_type(exec_type)
except Exception as e:
errors.append(f"Invalid execution.type: {str(e)}")
logger.warning(f"Invalid execution type: {e}")
if exec_type:
target_errors = validate_execution_target(execution)
errors.extend(target_errors)
# Validate status if present
if "status" in manifest:
valid_statuses = [s.value for s in CommandStatus]
if manifest["status"] not in valid_statuses:
errors.append(f"Invalid status: '{manifest['status']}'. Must be one of: {', '.join(valid_statuses)}")
logger.warning(f"Invalid status: {manifest['status']}")
# Validate parameters if present
if "parameters" in manifest:
params = manifest["parameters"]
if not isinstance(params, list):
errors.append("parameters must be an array")
else:
for i, param in enumerate(params):
if not isinstance(param, dict):
errors.append(f"parameters[{i}] must be an object")
continue
if "name" not in param:
errors.append(f"parameters[{i}] missing required field: name")
if "type" not in param:
errors.append(f"parameters[{i}] missing required field: type")
if errors:
logger.warning(f"Validation failed with {len(errors)} error(s)")
return {
"valid": False,
"errors": errors,
"path": path
}
logger.info("✅ Command manifest validation passed")
return {
"valid": True,
"errors": [],
"path": path,
"manifest": manifest
}
def load_command_registry() -> Dict[str, Any]:
"""
Load existing command registry.
Returns:
Command registry dictionary, or new empty registry if file doesn't exist
"""
if not os.path.exists(COMMANDS_REGISTRY_FILE):
logger.info("Command registry not found, creating new registry")
return {
"registry_version": "1.0.0",
"generated_at": datetime.now(timezone.utc).isoformat(),
"commands": []
}
try:
with open(COMMANDS_REGISTRY_FILE) as f:
registry = json.load(f)
logger.info(f"Loaded command registry with {len(registry.get('commands', []))} command(s)")
return registry
except json.JSONDecodeError as e:
raise CommandRegistryError(f"Failed to parse command registry: {e}")
def update_command_registry(manifest: Dict[str, Any]) -> bool:
"""
Add or update command in the command registry.
Args:
manifest: Validated command manifest
Returns:
True if registry was updated successfully
Raises:
CommandRegistryError: If registry update fails
"""
logger.info(f"Updating command registry for: {manifest['name']}")
# Load existing registry
registry = load_command_registry()
# Create registry entry
entry = {
"name": manifest["name"],
"version": manifest["version"],
"description": manifest["description"],
"execution": manifest["execution"],
"parameters": manifest.get("parameters", []),
"status": manifest.get("status", "draft"),
"tags": manifest.get("tags", [])
}
# Check if command already exists
commands = registry.get("commands", [])
existing_index = None
for i, command in enumerate(commands):
if command["name"] == manifest["name"]:
existing_index = i
break
if existing_index is not None:
# Update existing command
commands[existing_index] = entry
logger.info(f"Updated existing command: {manifest['name']}")
else:
# Add new command
commands.append(entry)
logger.info(f"Added new command: {manifest['name']}")
registry["commands"] = commands
registry["generated_at"] = datetime.now(timezone.utc).isoformat()
# Write registry back to disk atomically
try:
atomic_write_json(COMMANDS_REGISTRY_FILE, registry)
logger.info(f"Command registry updated successfully")
return True
except Exception as e:
raise CommandRegistryError(f"Failed to write command registry: {e}")
def main():
"""Main CLI entry point."""
if len(sys.argv) < 2:
message = "Usage: command_define.py <path_to_command.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:
# Validate manifest
validation = validate_manifest(path)
details = dict(validation)
if validation.get("valid"):
# Update registry
try:
registry_updated = update_command_registry(validation["manifest"])
details["status"] = "registered"
details["registry_updated"] = registry_updated
except CommandRegistryError as e:
logger.error(f"Registry update failed: {e}")
details["status"] = "validated"
details["registry_updated"] = False
details["registry_error"] = str(e)
else:
# 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": "Command manifest schema validation failed",
"details": {"errors": validation.get("errors", [])}
}
# Build response
response = build_response(
bool(validation.get("valid")),
path=path,
errors=validation.get("errors", []),
details=details,
)
print(json.dumps(response, indent=2))
sys.exit(0 if response["ok"] else 1)
except CommandValidationError 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()

View File

@@ -0,0 +1,36 @@
name: command.define
version: 0.1.0
description: "Validate and register command manifests in the Command Registry"
inputs:
- name: manifest_path
type: string
required: true
description: "Path to the command manifest file (YAML)"
outputs:
- name: validation_result
type: object
description: "Validation results and registration status"
schema:
properties:
ok: boolean
status: string
errors: array
path: string
details: object
dependencies:
- None
entrypoints:
- command: /skill/command/define
handler: command_define.py
runtime: python
permissions:
- filesystem:read
- filesystem:write
status: active
tags: [command, registry, validation, infrastructure]